Too often I got hangs because of the nested event loop started by exec(): therefore everything is moved to a QPointer-guarded QFileDialog pointer which is then used asynchronously.
581 lines
18 KiB
C++
581 lines
18 KiB
C++
/*
|
|
* Copyright 2015 Luca Beltrame <lbeltrame@kde.org>
|
|
*
|
|
* This file is part of Danbooru Client.
|
|
*
|
|
* Danbooru Client 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
|
|
* (at your option) any later version.
|
|
*
|
|
* Danbooru Client 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 Danbooru Client. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// Qt
|
|
|
|
#include <QAction>
|
|
#include <QQuickWidget>
|
|
#include <QQmlContext>
|
|
#include <QApplication>
|
|
#include <QGuiApplication>
|
|
#include <QStandardPaths>
|
|
#include <QUrl>
|
|
#include "danbooru_client_debug.h"
|
|
#include <QDockWidget>
|
|
#include <QQuickItem>
|
|
#include <QFileDialog>
|
|
#include <QPointer>
|
|
#include <QSortFilterProxyModel>
|
|
#include <QStatusBar>
|
|
|
|
// KF5
|
|
|
|
#include <kactioncollection.h>
|
|
#include <KStandardAction>
|
|
#include <KLocalizedString>
|
|
#include <KDeclarative/KDeclarative>
|
|
#include <KConfigDialog>
|
|
#include <KToggleAction>
|
|
#include <KDualAction>
|
|
#include <KIO/FileCopyJob>
|
|
#include <KIO/MimetypeJob>
|
|
|
|
#ifdef WITH_KFILEMETADATA
|
|
#include <KFileMetaData/kfilemetadata/usermetadata.h>
|
|
#endif
|
|
|
|
// Own
|
|
|
|
#include "libdanbooru/danbooruservice.h"
|
|
#include "libdanbooru/danboorupost.h"
|
|
#include "libdanbooru/danboorupool.h"
|
|
#include "model/danboorupostmodel.h"
|
|
#include "model/danboorupoolmodel.h"
|
|
#include "model/danboorutagmodel.h"
|
|
#include "mainwindow.h"
|
|
#include "danbooruconnectwidget.h"
|
|
#include "danboorusearchwidget.h"
|
|
#include "danboorutagwidget.h"
|
|
#include "danboorusettings.h"
|
|
#include "generalpage.h"
|
|
#include "blacklistpage.h"
|
|
|
|
namespace Danbooru
|
|
{
|
|
|
|
QHash<int, DanbooruPost::Rating> DanbooruMainWindow::ratingMap = {
|
|
{0, DanbooruPost::Safe},
|
|
{1, DanbooruPost::Questionable},
|
|
{2, DanbooruPost::Explicit}
|
|
};
|
|
|
|
DanbooruMainWindow::DanbooruMainWindow(QWidget *parent)
|
|
: KXmlGuiWindow(parent),
|
|
m_view(new QQuickWidget(this)),
|
|
m_model(new DanbooruPostModel(this)),
|
|
m_poolModel(new DanbooruPoolModel(this)),
|
|
m_tagModel(new DanbooruTagModel(this)),
|
|
m_service(new DanbooruService()),
|
|
m_connectWidget(Q_NULLPTR),
|
|
m_searchWidget(new DanbooruSearchWidget(this)),
|
|
m_tagWidget(new DanbooruTagWidget(this)),
|
|
m_proxyModel(new QSortFilterProxyModel(this)),
|
|
m_tableView(new QTableView(this)),
|
|
m_cache(Q_NULLPTR)
|
|
{
|
|
|
|
m_service->setParent(this);
|
|
setCentralWidget(m_view);
|
|
|
|
qmlRegisterType<Danbooru::DanbooruPost>("DanbooruClient", 1, 0, "DanbooruPost");
|
|
qmlRegisterType<Danbooru::DanbooruService>("DanbooruClient", 1, 0, "DanbooruService");
|
|
qRegisterMetaType<DanbooruPost::Rating>();
|
|
|
|
loadSettings();
|
|
|
|
m_cache = new KImageCache(qApp->applicationName(), DanbooruSettings::self()->cacheSize());
|
|
m_service->setImageCache(m_cache);
|
|
|
|
m_proxyModel->setSortRole(DanbooruTagModel::TagCountRole);
|
|
m_proxyModel->setSourceModel(m_tagModel);
|
|
m_proxyModel->setDynamicSortFilter(true);
|
|
|
|
m_tableView->setModel(m_poolModel);
|
|
m_tagWidget->setModel(m_proxyModel);
|
|
|
|
// Set up declarative bindings for the QQuickWidget
|
|
|
|
m_view->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
KDeclarative::KDeclarative declarative;
|
|
declarative.setDeclarativeEngine(m_view->engine());
|
|
declarative.setupBindings();
|
|
|
|
auto qmlViewPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
|
|
qApp->applicationName() + QLatin1String("/danbooruimageview.qml"));
|
|
|
|
QQmlContext *ctxt = m_view->rootContext();
|
|
ctxt->setContextProperty("danbooruModel", m_model);
|
|
ctxt->setContextProperty("danbooruService", m_service);
|
|
m_view->setSource(QUrl::fromLocalFile(qmlViewPath));
|
|
m_view->rootObject()->setProperty("poolMode", QVariant(false));
|
|
ctxt->setContextProperty("infiniteScroll", DanbooruSettings::self()->infiniteScrolling());
|
|
|
|
auto rootObj = m_view->rootObject();
|
|
connect(m_service, SIGNAL(postDownloadFinished()), rootObj, SIGNAL(downloadFinished()));
|
|
|
|
statusBar()->addPermanentWidget(m_connectWidget);
|
|
statusBar()->hide();
|
|
|
|
setupDockWidgets();
|
|
|
|
// then, setup our actions
|
|
setupActions();
|
|
setupGUI(KXmlGuiWindow::ToolBar | Keys | Save | Create, "danbooru-clientui.rc");
|
|
|
|
// connections
|
|
|
|
connect(m_connectWidget, &DanbooruConnectWidget::accepted, [this]() {
|
|
|
|
m_service->setBoardUrl(m_connectWidget->boardUrl());
|
|
if (!m_connectWidget->isAnonymous() && !m_connectWidget->username().isEmpty()
|
|
&& !m_connectWidget->password().isEmpty()) {
|
|
|
|
m_service->setUserName(m_connectWidget->username());
|
|
m_service->setPassword(m_connectWidget->password());
|
|
}
|
|
|
|
actionCollection()->action(QLatin1String("fetch"))->setEnabled(true);
|
|
actionCollection()->action(QLatin1String("find"))->setEnabled(true);
|
|
actionCollection()->action(QLatin1String("poolDownload"))->setEnabled(true);
|
|
actionCollection()->action(QLatin1String("tags"))->setEnabled(true);
|
|
actionCollection()->action(QLatin1String("morePosts"))->setEnabled(true);
|
|
|
|
if (DanbooruSettings::self()->autoDownload()) {
|
|
|
|
handlePostDownload(QStringList(), false);
|
|
|
|
}
|
|
m_connectWidget->hide();
|
|
statusBar()->hide();
|
|
|
|
});
|
|
|
|
connect(m_connectWidget, &DanbooruConnectWidget::rejected, [this]() {
|
|
m_connectWidget->hide();
|
|
});
|
|
|
|
connect(m_service, &Danbooru::DanbooruService::postDownloaded, m_model,
|
|
&Danbooru::DanbooruPostModel::addPost);
|
|
connect(m_service, &Danbooru::DanbooruService::poolDownloaded, m_poolModel,
|
|
&DanbooruPoolModel::addPool);
|
|
connect(m_service, &Danbooru::DanbooruService::tagDownloaded, m_tagModel,
|
|
&DanbooruTagModel::addTag);
|
|
connect(m_service, &Danbooru::DanbooruService::poolDownloadFinished, [this]() {
|
|
m_tableView->resizeColumnsToContents();
|
|
}
|
|
);
|
|
|
|
connect(m_tableView, &QTableView::doubleClicked, [this](QModelIndex index) {
|
|
auto pool = m_poolModel->poolAt(index.row());
|
|
clearModels();
|
|
m_view->rootObject()->setProperty("poolMode", QVariant(true));
|
|
QMetaObject::invokeMethod(m_view->rootObject(), "downloadStarted");
|
|
m_service->getPool(pool->id());
|
|
|
|
});
|
|
|
|
connect(m_searchWidget, &DanbooruSearchWidget::accepted, [this]() {
|
|
|
|
QDockWidget *searchDockWidget = findChild<QDockWidget *>(QLatin1String("SearchView"));
|
|
searchDockWidget->hide();
|
|
|
|
handlePostDownload(m_searchWidget->selectedTags(), true /* relatedTags */);
|
|
|
|
});
|
|
|
|
connect(m_searchWidget, &DanbooruSearchWidget::rejected, [this]() {
|
|
QDockWidget *searchDockWidget = findChild<QDockWidget *>(QLatin1String("SearchView"));
|
|
searchDockWidget->hide();
|
|
});
|
|
|
|
connect(m_service, &DanbooruService::postDownloadFinished, [this]() {
|
|
|
|
if (m_tagModel->rowCount() == 0) {
|
|
// Only get tags if we don't have any already
|
|
for (auto tag : m_model->postTags()) {
|
|
m_service->getTagList(1, tag);
|
|
}
|
|
}
|
|
m_proxyModel->sort(0, Qt::DescendingOrder);
|
|
|
|
});
|
|
|
|
connect(m_view->rootObject(), SIGNAL(downloadRequested(QUrl,QVariant)), this,
|
|
SLOT(slotHandleDownload(QUrl,QVariant)));
|
|
|
|
connect(m_tagWidget, &QListView::doubleClicked, this, &DanbooruMainWindow::searchTag);
|
|
|
|
}
|
|
|
|
DanbooruMainWindow::~DanbooruMainWindow()
|
|
{
|
|
}
|
|
|
|
void DanbooruMainWindow::loadSettings()
|
|
{
|
|
m_service->setBlacklist(DanbooruSettings::self()->tagBlacklist());
|
|
m_service->setMaxPosts(DanbooruSettings::self()->maxPosts());
|
|
|
|
m_service->setMaximumAllowedRating(ratingMap.value(DanbooruSettings::self()->maxRating()));
|
|
|
|
m_service->setMaxPosts(DanbooruSettings::self()->maxPosts());
|
|
|
|
QVector<QUrl> boardsList;
|
|
|
|
QStringList::const_iterator it;
|
|
|
|
for (it = DanbooruSettings::self()->boards().constBegin();
|
|
it != DanbooruSettings::self()->boards().constEnd();
|
|
++it) {
|
|
|
|
boardsList.append(QUrl::fromUserInput(*it));
|
|
|
|
}
|
|
|
|
if (!m_connectWidget) {
|
|
m_connectWidget = new DanbooruConnectWidget(boardsList, this);
|
|
} else {
|
|
m_connectWidget->setBoards(boardsList);
|
|
}
|
|
|
|
m_tagWidget->setBlackList(DanbooruSettings::self()->tagBlacklist());
|
|
|
|
m_view->rootContext()->setContextProperty("infiniteScroll",
|
|
DanbooruSettings::self()->infiniteScrolling());
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::setupActions()
|
|
{
|
|
|
|
QAction *connectAction = new QAction(
|
|
QIcon::fromTheme(QLatin1String("document-open-remote")),
|
|
i18n("Connect..."),
|
|
this);
|
|
|
|
QAction *fetchAction = new QAction(QIcon::fromTheme(QLatin1String("download")),
|
|
i18n("Download"), this);
|
|
KToggleAction *findAction = new KToggleAction(QIcon::fromTheme(QLatin1String("edit-find")),
|
|
i18n("Search"), this);
|
|
KToggleAction *poolAction = new KToggleAction(QIcon::fromTheme(QLatin1String("image-x-generic")),
|
|
i18n("Pools"), this);
|
|
QAction *nextPageAction = new QAction(QIcon::fromTheme(QLatin1String("go-next")),
|
|
i18n("More posts"), this);
|
|
QAction *nextPoolAction = new QAction(QIcon::fromTheme(QLatin1String("go-next")),
|
|
i18n("More pools"), this);
|
|
|
|
KDualAction *tagAction = new KDualAction(i18n("Show tags"), i18n("Hide tags"), this);
|
|
tagAction->setIconForStates(QIcon::fromTheme(QLatin1String("tag")));
|
|
|
|
fetchAction->setEnabled(false);
|
|
findAction->setEnabled(false);
|
|
poolAction->setEnabled(false);
|
|
nextPageAction->setEnabled(false);
|
|
nextPoolAction->setEnabled(false);
|
|
|
|
poolAction->setChecked(false);
|
|
findAction->setChecked(false);
|
|
tagAction->setEnabled(false);
|
|
|
|
actionCollection()->addAction(QLatin1String("connect"), connectAction);
|
|
actionCollection()->addAction(QLatin1String("fetch"), fetchAction);
|
|
actionCollection()->addAction(QLatin1String("find"), findAction);
|
|
actionCollection()->addAction(QLatin1String("poolDownload"), poolAction);
|
|
actionCollection()->addAction(QLatin1String("tags"), tagAction);
|
|
actionCollection()->addAction(QLatin1String("morePosts"), nextPageAction);
|
|
actionCollection()->addAction(QLatin1String("morePools"), nextPoolAction);
|
|
|
|
actionCollection()->setDefaultShortcut(connectAction, KStandardShortcut::Open);
|
|
actionCollection()->setDefaultShortcut(findAction, KStandardShortcut::Find);
|
|
|
|
KStandardAction::quit(qApp, SLOT(quit()), actionCollection());
|
|
KStandardAction::preferences(this, SLOT(optionsPreferences()), actionCollection());
|
|
|
|
connect(connectAction, &QAction::triggered, this, &DanbooruMainWindow::connectToBoard);
|
|
connect(fetchAction, &QAction::triggered, this, &DanbooruMainWindow::downloadPosts);
|
|
|
|
connect(poolAction, &KToggleAction::toggled, [this](bool checked) {
|
|
if (!m_service) {
|
|
return;
|
|
}
|
|
|
|
QDockWidget *poolDockWidget = findChild<QDockWidget *>(QLatin1String("PoolView"));
|
|
|
|
if (checked) {
|
|
|
|
if (m_poolModel->rowCount() == 0) {
|
|
m_service->getPoolList();
|
|
|
|
}
|
|
|
|
poolDockWidget->show();
|
|
actionCollection()->action(QLatin1String("morePools"))->setEnabled(true);
|
|
m_tableView->show();
|
|
|
|
} else {
|
|
poolDockWidget->hide();
|
|
actionCollection()->action(QLatin1String("morePools"))->setEnabled(false);
|
|
m_tableView->hide();
|
|
}
|
|
});
|
|
|
|
connect(findAction, &KToggleAction::toggled, [this](bool checked) {
|
|
|
|
QDockWidget *searchDockWidget = findChild<QDockWidget *>(QLatin1String("SearchView"));
|
|
|
|
if (checked) {
|
|
searchDockWidget->show();
|
|
m_searchWidget->show();
|
|
} else {
|
|
searchDockWidget->hide();
|
|
m_searchWidget->hide();
|
|
}
|
|
|
|
});
|
|
|
|
connect(tagAction, &KDualAction::activeChanged, [this](bool checked) {
|
|
|
|
QDockWidget *tagDockWidget = findChild<QDockWidget *>(QLatin1String("TagView"));
|
|
|
|
if (checked) {
|
|
tagDockWidget->show();
|
|
m_tagWidget->show();
|
|
} else {
|
|
tagDockWidget->hide();
|
|
m_tagWidget->hide();
|
|
}
|
|
|
|
});
|
|
|
|
connect(nextPageAction, &QAction::triggered, [this]() {
|
|
|
|
if (m_model->rowCount() == 0) {
|
|
return;
|
|
}
|
|
|
|
QMetaObject::invokeMethod(m_view->rootObject(), "downloadStarted");
|
|
m_service->nextPostPage();
|
|
}
|
|
);
|
|
|
|
connect(nextPoolAction, &QAction::triggered, [this]() {
|
|
m_service->nextPoolPage();
|
|
}
|
|
);
|
|
}
|
|
|
|
void DanbooruMainWindow::setupDockWidgets()
|
|
{
|
|
|
|
// Set up PoolWidget
|
|
|
|
QDockWidget *poolDockWidget = new QDockWidget(i18n("Pools"), this);
|
|
poolDockWidget->setAllowedAreas(Qt::BottomDockWidgetArea);
|
|
poolDockWidget->setWidget(m_tableView);
|
|
poolDockWidget->setObjectName("PoolView");
|
|
|
|
// Prevent the use of winId() when detached, leads to QQuickWidget bugs
|
|
poolDockWidget->setFeatures(QDockWidget::DockWidgetClosable);
|
|
addDockWidget(Qt::BottomDockWidgetArea, poolDockWidget);
|
|
m_tableView->hide();
|
|
poolDockWidget->hide();
|
|
|
|
// Find widget
|
|
QDockWidget *searchDockWidget = new QDockWidget(QLatin1String(""), this);
|
|
searchDockWidget->setAllowedAreas(Qt::TopDockWidgetArea);
|
|
searchDockWidget->setWidget(m_searchWidget);
|
|
searchDockWidget->setObjectName("SearchView");
|
|
|
|
searchDockWidget->setFeatures(QDockWidget::NoDockWidgetFeatures);
|
|
addDockWidget(Qt::TopDockWidgetArea, searchDockWidget);
|
|
// No title wanted
|
|
searchDockWidget->setTitleBarWidget(new QWidget(this));
|
|
|
|
searchDockWidget->hide();
|
|
m_searchWidget->hide();
|
|
|
|
QDockWidget *tagDockWidget = new QDockWidget(QLatin1String("Tags"), this);
|
|
tagDockWidget->setAllowedAreas(Qt::RightDockWidgetArea);
|
|
tagDockWidget->setWidget(m_tagWidget);
|
|
tagDockWidget->setObjectName("TagView");
|
|
tagDockWidget->setFeatures(QDockWidget::DockWidgetClosable);
|
|
addDockWidget(Qt::RightDockWidgetArea, tagDockWidget);
|
|
|
|
// tagDockWidget->hide();
|
|
// m_tagWidget->hide();
|
|
|
|
// Connections
|
|
|
|
connect(poolDockWidget, &QDockWidget::visibilityChanged, [this](bool visible) {
|
|
actionCollection()->action(QLatin1String("poolDownload"))->setChecked(visible);
|
|
});
|
|
|
|
connect(searchDockWidget, &QDockWidget::visibilityChanged, [this](bool visible) {
|
|
actionCollection()->action(QLatin1String("find"))->setChecked(visible);
|
|
});
|
|
|
|
connect(tagDockWidget, &QDockWidget::visibilityChanged, [this](bool visible) {
|
|
qobject_cast<KDualAction *>(actionCollection()->action(QLatin1String("tags")))->setActive(visible);
|
|
});
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::connectToBoard()
|
|
{
|
|
|
|
if (!m_view) {
|
|
return;
|
|
}
|
|
|
|
clearModels();
|
|
m_poolModel->clear();
|
|
m_service->reset();
|
|
statusBar()->show();
|
|
m_connectWidget->show();
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::downloadPosts()
|
|
{
|
|
if (!m_service) {
|
|
return;
|
|
}
|
|
|
|
handlePostDownload(QStringList(), false);
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::optionsPreferences()
|
|
{
|
|
KConfigDialog *dialog = new KConfigDialog(this, "danboorusettings",
|
|
DanbooruSettings::self());
|
|
|
|
dialog->addPage(new GeneralPage(DanbooruSettings::self(), this), i18n("General"),
|
|
"table");
|
|
dialog->addPage(new BlacklistPage(DanbooruSettings::self(), this), i18n("Tag blacklist"),
|
|
"configure");
|
|
connect(dialog, &KConfigDialog::settingsChanged, this, &DanbooruMainWindow::loadSettings);
|
|
dialog->show();
|
|
}
|
|
|
|
void DanbooruMainWindow::slotHandleDownload(const QUrl &url, const QVariant tags)
|
|
{
|
|
|
|
QStringList tagList = tags.toStringList();
|
|
|
|
|
|
KIO::MimetypeJob *mimeJob = KIO::mimetype(url, KIO::HideProgressInfo);
|
|
|
|
QString remoteFile = url.fileName();
|
|
|
|
QPointer<QFileDialog> saveDialog = new QFileDialog(this, i18n("Save image"));
|
|
|
|
saveDialog->setAcceptMode(QFileDialog::AcceptSave);
|
|
saveDialog->setFileMode(QFileDialog::AnyFile);
|
|
saveDialog->setDirectory(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
|
|
saveDialog->setOption(QFileDialog::DontConfirmOverwrite, false);
|
|
|
|
QStringList filters;
|
|
|
|
if (mimeJob->exec()) {
|
|
filters << mimeJob->mimetype();
|
|
saveDialog->setMimeTypeFilters(filters);
|
|
} else {
|
|
filters.reserve(2);
|
|
filters << "Images (*.png *.gif *.jpg)" << "All files (*.*)";
|
|
saveDialog->setNameFilters(filters);
|
|
}
|
|
|
|
// Prevent invalid characters (":" can be a tag in Danbooru)
|
|
if (remoteFile.contains(":")) {
|
|
remoteFile.replace(":", "_");
|
|
}
|
|
|
|
saveDialog->selectFile(remoteFile);
|
|
saveDialog->open();
|
|
|
|
connect(saveDialog, &QFileDialog::finished, [this, tagList, saveDialog, url](int result) {
|
|
|
|
if (result) {
|
|
|
|
QUrl localFile = saveDialog->selectedUrls().at(0);
|
|
if (!localFile.isEmpty()) {
|
|
|
|
KIO::FileCopyJob *job = KIO::file_copy(url, localFile, -1, KIO::DefaultFlags);
|
|
|
|
connect(job, &KIO::Job::result, [this, localFile, tagList](KJob * job) {
|
|
|
|
if (job->error()) {
|
|
qCDebug(DANBOORU_CLIENT) << "Error while downloading " << job->errorString();
|
|
return;
|
|
}
|
|
|
|
#ifdef WITH_KFILEMETADATA
|
|
// qCDebug(DANBOORU_CLIENT) << "Local file" << localFile.toLocalFile();
|
|
KFileMetaData::UserMetaData meta(localFile.toLocalFile());
|
|
meta.setTags(tagList);
|
|
#endif
|
|
});
|
|
|
|
}
|
|
}
|
|
saveDialog->deleteLater();
|
|
});
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::searchTag(const QModelIndex &index)
|
|
{
|
|
|
|
if (!index.isValid()) {
|
|
return;
|
|
}
|
|
|
|
QString tagName = m_tagModel->itemAt(index.row())->name();
|
|
QStringList tags = {tagName};
|
|
|
|
handlePostDownload(tags, true /* relatedTags */);
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::clearModels()
|
|
{
|
|
m_model->clear();
|
|
m_tagModel->clear();
|
|
// m_poolModel->clear();
|
|
m_service->reset();
|
|
|
|
}
|
|
|
|
void DanbooruMainWindow::handlePostDownload(const QStringList &tags, bool relatedTags)
|
|
{
|
|
clearModels();
|
|
m_view->rootObject()->setProperty("poolMode", QVariant(false));
|
|
m_service->setPostTags(tags);
|
|
|
|
if (relatedTags) {
|
|
m_service->getRelatedTags(tags);
|
|
}
|
|
|
|
// Call the downloadStarted signal from QML
|
|
QMetaObject::invokeMethod(m_view->rootObject(), "downloadStarted");
|
|
m_service->getPostList();
|
|
}
|
|
|
|
} // namespace Danbooru
|