DBHelper::getSortedComicsFromParent(qulonglong parentId, QSqlData
}
std::sort(list.begin(), list.end(), [](const ComicDB &c1, const ComicDB &c2) {
- if (c1.info.number.isNull() && c2.info.number.isNull()) {
- return naturalSortLessThanCI(c1.name, c2.name);
- } else {
- if (c1.info.number.isNull() == false && c2.info.number.isNull() == false) {
- return naturalSortLessThanCI(c1.info.number.toString(), c2.info.number.toString());
- } else {
- return c2.info.number.isNull();
- }
- }
+ return comicNumberLessThan(c1.info.number, c1.name, c2.info.number, c2.name);
});
// selectQuery.finish();
diff --git a/YACReaderLibrary/grid_comics_view.cpp b/YACReaderLibrary/grid_comics_view.cpp
index ff25d753b..970b66d63 100644
--- a/YACReaderLibrary/grid_comics_view.cpp
+++ b/YACReaderLibrary/grid_comics_view.cpp
@@ -358,11 +358,13 @@ void GridComicsView::updateCoversSizeInContext(int width, QQmlContext *ctxt)
{
int cellBottomMarging = 8 * (1 + 2 * (1 - (float(YACREADER_MAX_GRID_ZOOM_WIDTH - width) / (YACREADER_MAX_GRID_ZOOM_WIDTH - YACREADER_MIN_GRID_ZOOM_WIDTH))));
- ctxt->setContextProperty("cellCustomHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + 51 + cellBottomMarging);
+ int infoHeight = 56;
+
+ ctxt->setContextProperty("cellCustomHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + infoHeight + cellBottomMarging);
ctxt->setContextProperty("cellCustomWidth", (width * YACREADER_MIN_CELL_CUSTOM_WIDTH) / YACREADER_MIN_COVER_WIDTH);
ctxt->setContextProperty("itemWidth", width);
- ctxt->setContextProperty("itemHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + 51);
+ ctxt->setContextProperty("itemHeight", ((width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH) + infoHeight);
ctxt->setContextProperty("coverWidth", width);
ctxt->setContextProperty("coverHeight", (width * YACREADER_MAX_COVER_HEIGHT) / YACREADER_MIN_COVER_WIDTH);
diff --git a/YACReaderLibrary/import_widget.cpp b/YACReaderLibrary/import_widget.cpp
index 522e46568..335ccda12 100644
--- a/YACReaderLibrary/import_widget.cpp
+++ b/YACReaderLibrary/import_widget.cpp
@@ -358,6 +358,18 @@ void ImportWidget::setXMLScanLook()
hideButton->setVisible(false);
}
+void ImportWidget::setRepairLook()
+{
+ iconLabel->setPixmap(theme.importWidget.updatingIcon);
+ text->setText(QCoreApplication::translate("LibraryWindowActions", "Repair covers and comic info"));
+ textDescription->setText(tr("The current library is being checked for missing covers and incomplete comic information.
This can take several minutes. You can stop the process and run it again later.
"));
+
+ stopButton->setVisible(true);
+ coversLabel->setVisible(false);
+ coversViewContainer->setVisible(false);
+ hideButton->setVisible(false);
+}
+
void ImportWidget::clearScene()
{
}
diff --git a/YACReaderLibrary/import_widget.h b/YACReaderLibrary/import_widget.h
index 9da45b84f..6579a8b27 100644
--- a/YACReaderLibrary/import_widget.h
+++ b/YACReaderLibrary/import_widget.h
@@ -37,6 +37,7 @@ public slots:
void setUpdateLook();
void setUpgradeLook();
void setXMLScanLook();
+ void setRepairLook();
void showCovers(bool hide);
private:
diff --git a/YACReaderLibrary/initial_comic_info_extractor.cpp b/YACReaderLibrary/initial_comic_info_extractor.cpp
index 9cdef9189..5b9b47da2 100644
--- a/YACReaderLibrary/initial_comic_info_extractor.cpp
+++ b/YACReaderLibrary/initial_comic_info_extractor.cpp
@@ -3,17 +3,20 @@
#include "comic.h"
#include "compressed_archive.h"
#include "cover_utils.h"
+#include "epub_page_index.h"
#include "pdf_comic.h"
#include "qnaturalsorting.h"
#include
+#include
+
using namespace YACReader;
bool InitialComicInfoExtractor::crash = false;
InitialComicInfoExtractor::InitialComicInfoExtractor(QString fileSource, QString target, int coverPage, bool getXMLMetadata)
- : _fileSource(fileSource), _target(target), _numPages(0), _coverPage(coverPage), getXMLMetadata(getXMLMetadata), _xmlInfoData()
+ : _fileSource(fileSource), _target(target), _numPages(0), _coverSize(0, 0), _coverExtracted(false), _coverPage(coverPage), getXMLMetadata(getXMLMetadata), _xmlInfoData()
{
if (coverPage <= 0) {
_coverPage = 1;
@@ -62,13 +65,16 @@ void InitialComicInfoExtractor::extract()
#else
QImage p = pdfComic->page(_coverPage - 1)->renderToImage(72, 72);
#endif //
- _cover = p;
- _coverSize = QPair(p.width(), p.height());
- if (_target != "") {
- saveCover(_target, p);
- } else if (_target != "") {
- QLOG_WARN() << "Extracting cover: requested cover index greater than numPages " << _fileSource;
+ if (!p.isNull()) {
+ _cover = p;
+ _coverSize = QPair(p.width(), p.height());
+ _coverExtracted = true;
+ if (_target != "") {
+ saveCover(_target, p);
+ }
}
+ } else {
+ QLOG_WARN() << "Extracting cover: requested cover index greater than numPages " << _fileSource;
}
return;
}
@@ -89,12 +95,14 @@ void InitialComicInfoExtractor::extract()
}
QList order = archive.getFileNames();
+ const bool isEpub = Comic::fileIsEpub(_fileSource);
if (getXMLMetadata) {
// Try to find embeded XML info (ComicRack or ComicTagger)
auto infoIndex = 0;
- for (auto &fileName : order) {
- if (fileName.endsWith(".xml", Qt::CaseInsensitive)) {
+ for (const QString &fileName : std::as_const(order)) {
+ const bool isComicInfo = QFileInfo(fileName).fileName().compare(QStringLiteral("ComicInfo.xml"), Qt::CaseInsensitive) == 0;
+ if (isComicInfo) {
_xmlInfoData = archive.getRawDataAtIndex(infoIndex);
break;
}
@@ -110,8 +118,26 @@ void InitialComicInfoExtractor::extract()
}
// se filtran para obtener sólo los formatos soportados
- QList fileNames = FileComic::filter(order);
- _numPages = fileNames.size();
+ int coverArchiveIndex = -1;
+ if (isEpub) {
+ const auto epub = FileComic::epubScanInfo(order, archive, _coverPage);
+ if (!epub.isValid()) {
+ QLOG_WARN() << "Extracting cover: unsupported EPUB" << _fileSource << epub.error;
+ }
+ _numPages = epub.pageCount;
+ coverArchiveIndex = epub.coverArchiveIndex;
+ } else {
+ QList fileNames = FileComic::filter(order);
+ std::sort(fileNames.begin(), fileNames.end(), naturalSortLessThanCI);
+ _numPages = fileNames.size();
+ if (_coverPage > _numPages) {
+ _coverPage = 1;
+ }
+ if (_numPages > 0) {
+ coverArchiveIndex = order.indexOf(fileNames.at(_coverPage - 1));
+ }
+ }
+
if (_numPages == 0) {
QLOG_WARN() << "Extracting cover: empty comic " << _fileSource;
_cover.load(":/images/notCover.png");
@@ -119,21 +145,26 @@ void InitialComicInfoExtractor::extract()
_cover.save(_target);
}
} else {
- if (_coverPage > _numPages) {
- _coverPage = 1;
+ if (coverArchiveIndex < 0) {
+ QLOG_WARN() << "Extracting cover: unable to resolve cover image " << _fileSource;
+ _cover.load(":/images/notCover.png");
+ return;
}
- std::sort(fileNames.begin(), fileNames.end(), naturalSortLessThanCI);
- int index = order.indexOf(fileNames.at(_coverPage - 1));
if (_target == "") {
- if (!_cover.loadFromData(archive.getRawDataAtIndex(index))) {
+ if (_cover.loadFromData(archive.getRawDataAtIndex(coverArchiveIndex))) {
+ _coverSize = QPair(_cover.width(), _cover.height());
+ _coverExtracted = true;
+ } else {
QLOG_WARN() << "Extracting cover: unable to load image from extracted cover " << _fileSource;
_cover.load(":/images/notCover.png");
}
} else {
QImage p;
- if (p.loadFromData(archive.getRawDataAtIndex(index))) {
+ if (p.loadFromData(archive.getRawDataAtIndex(coverArchiveIndex))) {
+ _cover = p;
_coverSize = QPair(p.width(), p.height());
+ _coverExtracted = true;
saveCover(_target, p);
} else {
QLOG_WARN() << "Extracting cover: unable to load image from extracted cover " << _fileSource;
diff --git a/YACReaderLibrary/initial_comic_info_extractor.h b/YACReaderLibrary/initial_comic_info_extractor.h
index 3e8f99cea..e02ca6cd5 100644
--- a/YACReaderLibrary/initial_comic_info_extractor.h
+++ b/YACReaderLibrary/initial_comic_info_extractor.h
@@ -22,6 +22,7 @@ class InitialComicInfoExtractor : public QObject
int _numPages;
QPair _coverSize;
QImage _cover;
+ bool _coverExtracted;
int _coverPage;
int getXMLMetadata;
static bool crash;
@@ -32,6 +33,8 @@ public slots:
void extract();
int getNumPages() { return _numPages; }
QPixmap getCover() { return QPixmap::fromImage(_cover); }
+ QImage getCoverImage() const { return _cover; }
+ bool hasValidCover() const { return _coverExtracted; }
QPair getOriginalCoverSize() { return _coverSize; }
QByteArray getXMLInfoRawData();
signals:
diff --git a/YACReaderLibrary/libraries_update_coordinator.cpp b/YACReaderLibrary/libraries_update_coordinator.cpp
index 30de3d677..77aea39ab 100644
--- a/YACReaderLibrary/libraries_update_coordinator.cpp
+++ b/YACReaderLibrary/libraries_update_coordinator.cpp
@@ -2,6 +2,7 @@
#include "libraries_update_coordinator.h"
#include "library_creator.h"
+#include "xml_info_library_scanner.h"
#include "yacreader_global.h"
#include "yacreader_libraries.h"
@@ -119,6 +120,24 @@ LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::requ
return startUpdate({ path });
}
+LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::requestSingleLibraryXmlRescan(int id)
+{
+ if (isRunning()) {
+ return UpdateRequestResult::AlreadyRunning;
+ }
+
+ const QString path = libraries.getPath(id);
+ if (path.isEmpty()) {
+ return UpdateRequestResult::LibraryNotFound;
+ }
+
+ if (!canStartUpdateProvider()) {
+ return UpdateRequestResult::NotAllowed;
+ }
+
+ return startXmlRescan(path);
+}
+
bool LibrariesUpdateCoordinator::isRunning() const
{
QMutexLocker locker(&futureMutex);
@@ -155,6 +174,26 @@ LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::star
return UpdateRequestResult::Started;
}
+LibrariesUpdateCoordinator::UpdateRequestResult LibrariesUpdateCoordinator::startXmlRescan(const QString &path)
+{
+ QMutexLocker locker(&futureMutex);
+
+ if (updateFuture.valid() && updateFuture.wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
+ return UpdateRequestResult::AlreadyRunning;
+ }
+
+ canceled = false;
+ updateFuture = std::async(std::launch::async, [this, path] {
+ emit updateStarted();
+ if (!canceled) {
+ rescanLibraryXml(path);
+ }
+ emit updateEnded();
+ });
+
+ return UpdateRequestResult::Started;
+}
+
void LibrariesUpdateCoordinator::updateLibrary(const QString &path)
{
QDir pathDir(path);
@@ -177,6 +216,25 @@ void LibrariesUpdateCoordinator::updateLibrary(const QString &path)
eventLoop.exec();
}
+void LibrariesUpdateCoordinator::rescanLibraryXml(const QString &path)
+{
+ QDir pathDir(path);
+ if (!pathDir.exists()) {
+ return;
+ }
+
+ QEventLoop eventLoop;
+ auto scanner = new XMLInfoLibraryScanner();
+ std::shared_ptr sharedPtr(scanner);
+ currentXmlInfoLibraryScanner = sharedPtr;
+
+ const QString cleanPath = QDir::cleanPath(pathDir.absolutePath());
+ connect(scanner, &XMLInfoLibraryScanner::finished, &eventLoop, &QEventLoop::quit);
+
+ scanner->scanLibrary(cleanPath, LibraryPaths::libraryDataPath(cleanPath));
+ eventLoop.exec();
+}
+
void LibrariesUpdateCoordinator::stop()
{
canceled = true;
@@ -184,6 +242,10 @@ void LibrariesUpdateCoordinator::stop()
if (auto libraryCreator = currentLibraryCreator.lock()) {
libraryCreator->stop();
}
+
+ if (auto scanner = currentXmlInfoLibraryScanner.lock()) {
+ scanner->stop();
+ }
}
void LibrariesUpdateCoordinator::cancel()
@@ -193,4 +255,8 @@ void LibrariesUpdateCoordinator::cancel()
if (auto libraryCreator = currentLibraryCreator.lock()) {
libraryCreator->cancel();
}
+
+ if (auto scanner = currentXmlInfoLibraryScanner.lock()) {
+ scanner->stop();
+ }
}
diff --git a/YACReaderLibrary/libraries_update_coordinator.h b/YACReaderLibrary/libraries_update_coordinator.h
index 814e3a06b..e48d7a080 100644
--- a/YACReaderLibrary/libraries_update_coordinator.h
+++ b/YACReaderLibrary/libraries_update_coordinator.h
@@ -6,6 +6,9 @@
class YACReaderLibraries;
class LibraryCreator;
+namespace YACReader {
+class XMLInfoLibraryScanner;
+}
class LibrariesUpdateCoordinator : public QObject
{
@@ -24,6 +27,7 @@ class LibrariesUpdateCoordinator : public QObject
bool isRunning() const;
UpdateRequestResult requestLibrariesUpdate();
UpdateRequestResult requestSingleLibraryUpdate(int id);
+ UpdateRequestResult requestSingleLibraryXmlRescan(int id);
public slots:
void updateLibraries();
@@ -40,7 +44,9 @@ private slots:
private:
UpdateRequestResult startUpdate(const QStringList &paths);
+ UpdateRequestResult startXmlRescan(const QString &path);
void updateLibrary(const QString &path);
+ void rescanLibraryXml(const QString &path);
QSettings *settings;
YACReaderLibraries &libraries;
@@ -50,6 +56,7 @@ private slots:
mutable QMutex futureMutex;
bool canceled;
std::weak_ptr currentLibraryCreator;
+ std::weak_ptr currentXmlInfoLibraryScanner;
std::function canStartUpdateProvider;
};
diff --git a/YACReaderLibrary/library_creator.cpp b/YACReaderLibrary/library_creator.cpp
index 3b55c3af3..ca578aced 100644
--- a/YACReaderLibrary/library_creator.cpp
+++ b/YACReaderLibrary/library_creator.cpp
@@ -46,7 +46,7 @@ Folder rootFolder(QSqlDatabase &db)
LibraryCreator::LibraryCreator(QSettings *settings)
: creation(false), partialUpdate(false), settings(settings)
{
- _nameFilter << Comic::comicExtensions;
+ _nameFilter = Comic::comicExtensions;
}
void LibraryCreator::createLibrary(const QString &source, const QString &target)
@@ -57,6 +57,7 @@ void LibraryCreator::createLibrary(const QString &source, const QString &target)
void LibraryCreator::updateLibrary(const QString &source, const QString &target)
{
+ creation = false;
checkModifiedDatesOnUpdate = settings->value(COMPARE_MODIFIED_DATE_ON_LIBRARY_UPDATES, false).toBool();
partialUpdate = false;
_source = source;
@@ -66,6 +67,7 @@ void LibraryCreator::updateLibrary(const QString &source, const QString &target)
void LibraryCreator::updateFolder(const QString &source, const QString &target, const QString &sourceFolder, qulonglong folderId)
{
+ creation = false;
checkModifiedDatesOnUpdate = settings->value(COMPARE_MODIFIED_DATE_ON_LIBRARY_UPDATES, false).toBool();
partialUpdate = true;
_folderDestinationId = folderId;
@@ -124,10 +126,7 @@ void LibraryCreator::processLibrary(const QString &source, const QString &target
_source = source;
_target = target;
if (DataBaseManagement::checkValidDB(target + "/library.ydb") == "") {
- // se limpia el directorio ./yacreaderlibrary
- QDir d(target);
- d.removeRecursively();
- _mode = CREATOR;
+ _mode = creation ? CREATOR : UPDATER;
} else { //
_mode = UPDATER;
}
@@ -148,8 +147,37 @@ void LibraryCreator::run()
}
sevenzLib->deleteLater();
#endif
+ if (_mode == CREATOR)
+ QDir().mkpath(_target);
+
+ LibraryMaintenanceLock maintenanceLock(_source);
+ if (!maintenanceLock.tryLock()) {
+ const auto error = maintenanceLock.errorString();
+ QLOG_ERROR() << error;
+ if (_mode == CREATOR)
+ emit failedCreatingDB(error);
+ else
+ emit failedOpeningDB(error);
+ return;
+ }
+
+ if (_mode == UPDATER) {
+ QString recoveryError;
+ if (!DataBaseManagement::recoverInterruptedRestore(_source, &recoveryError, true)) {
+ QLOG_ERROR() << recoveryError;
+ emit failedOpeningDB(recoveryError);
+ return;
+ }
+ }
+
if (_mode == CREATOR) {
QLOG_INFO() << "Starting to create new library ( " << _source << "," << _target << ")";
+ QString cleanupError;
+ if (!DataBaseManagement::prepareForRecreation(_source, &cleanupError, true)) {
+ QLOG_ERROR() << cleanupError;
+ emit failedCreatingDB(cleanupError);
+ return;
+ }
_currentPathFolders.clear();
// se crean los directorios .yacreaderlibrary y .yacreaderlibrary/covers
QDir dir;
@@ -183,6 +211,13 @@ void LibraryCreator::run()
QLOG_INFO() << "Create library END";
} else {
QLOG_INFO() << "Starting to update folder" << _sourceFolder << "in library ( " << _source << "," << _target << ")";
+ QString backupError;
+ if (!DataBaseManagement::backupLibrary(_source, DatabaseBackupReason::AutoUpdate, &backupError)) {
+ const auto error = QString("Unable to back up library database: %1").arg(backupError);
+ QLOG_ERROR() << error;
+ emit failedOpeningDB(error);
+ return;
+ }
{
auto _database = DataBaseManagement::loadDatabase(_target);
diff --git a/YACReaderLibrary/library_window.cpp b/YACReaderLibrary/library_window.cpp
index d8af4810a..49f0ebf7e 100644
--- a/YACReaderLibrary/library_window.cpp
+++ b/YACReaderLibrary/library_window.cpp
@@ -41,6 +41,7 @@
#include "api_key_dialog.h"
#include "comic_db.h"
#include "comic_files_manager.h"
+#include "comic_info_repairer.h"
#include "comic_model.h"
#include "comic_vine_dialog.h"
#include "comics_remover.h"
@@ -69,6 +70,7 @@
#include "reading_list_model.h"
#include "recent_visibility_coordinator.h"
#include "rename_library_dialog.h"
+#include "search_syntax_dialog.h"
#include "server_config_dialog.h"
#include "shortcuts_manager.h"
#include "static.h"
@@ -214,6 +216,7 @@ void LibraryWindow::setupUI()
libraryCreator = new LibraryCreator(settings);
packageManager = new PackageManager();
xmlInfoLibraryScanner = new XMLInfoLibraryScanner();
+ comicInfoRepairer = new ComicInfoRepairer(settings);
historyController = new YACReaderHistoryController(this);
@@ -498,6 +501,13 @@ void LibraryWindow::createToolBars()
libraryToolBar->setSearchWidget(searchEdit);
#endif
+ auto *searchMenu = createSearchMenu();
+#ifdef Y_MAC_UI
+ libraryToolBar->setSearchMenu(searchMenu);
+#else
+ searchEdit->setSearchMenu(searchMenu);
+#endif
+
editInfoToolBar->setIconSize(QSize(18, 18));
editInfoToolBar->addAction(actions.openComicAction);
editInfoToolBar->addSeparator();
@@ -540,6 +550,94 @@ void LibraryWindow::createToolBars()
contentViewsManager->comicsView->setToolBar(editInfoToolBar);
}
+QMenu *LibraryWindow::createSearchMenu()
+{
+ auto *menu = new QMenu(tr("Search filters"), this);
+ menu->setMinimumWidth(190);
+
+ auto addFilter = [this, menu](const QString &label, const QString &query) {
+ auto *action = menu->addAction(label);
+ connect(action, &QAction::triggered, this, [this, query] {
+ applySearchQuery(query);
+ });
+ };
+
+ addFilter(tr("Unread"), QStringLiteral("read:false"));
+ addFilter(
+ tr("In progress"),
+ QStringLiteral("hasBeenOpened:true AND read:false"));
+ addFilter(tr("Highly rated"), QStringLiteral("rating>=4"));
+
+ auto *recentlyAdded = menu->addAction(tr("Recently added"));
+ connect(recentlyAdded, &QAction::triggered, this, [this] {
+ const int days = settings->value(NUM_DAYS_TO_CONSIDER_RECENT, 1).toInt();
+ applySearchQuery(QStringLiteral("added>%1").arg(days));
+ });
+
+ menu->addSeparator();
+ auto *syntaxAction = menu->addAction(tr("Search syntax…"));
+ connect(syntaxAction, &QAction::triggered, this, &LibraryWindow::showSearchSyntax);
+
+ return menu;
+}
+
+void LibraryWindow::applySearchQuery(const QString &query)
+{
+#ifdef Y_MAC_UI
+ libraryToolBar->setSearchText(query);
+ libraryToolBar->focusSearch();
+#else
+ searchEdit->setText(query);
+ searchEdit->setFocus(Qt::ShortcutFocusReason);
+#endif
+}
+
+void LibraryWindow::setSearchInputEnabled(bool enabled)
+{
+#ifdef Y_MAC_UI
+ libraryToolBar->setSearchEnabled(enabled);
+#else
+ searchEdit->setEnabled(enabled);
+#endif
+}
+
+void LibraryWindow::clearSearchInput(bool notify)
+{
+#ifdef Y_MAC_UI
+ libraryToolBar->clearSearchText(notify);
+#else
+ if (notify)
+ searchEdit->clear();
+ else
+ searchEdit->clearText();
+#endif
+}
+
+void LibraryWindow::focusSearchInput()
+{
+#ifdef Y_MAC_UI
+ libraryToolBar->focusSearch();
+#else
+ searchEdit->setFocus(Qt::ShortcutFocusReason);
+#endif
+}
+
+QString LibraryWindow::searchText() const
+{
+#ifdef Y_MAC_UI
+ return libraryToolBar->searchText();
+#else
+ return searchEdit->text();
+#endif
+}
+
+void LibraryWindow::showSearchSyntax()
+{
+ auto *dialog = new SearchSyntaxDialog(this);
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+ dialog->open();
+}
+
void LibraryWindow::createMenus()
{
foldersView->addAction(actions.addFolderAction);
@@ -642,6 +740,11 @@ void LibraryWindow::createMenus()
typeMenu->addAction(setYonkomaAction);
selectedLibrary->addAction(actions.rescanLibraryForXMLInfoAction);
+ selectedLibrary->addAction(actions.repairLibraryAction);
+ YACReader::addSperator(selectedLibrary);
+
+ selectedLibrary->addAction(actions.backupLibraryAction);
+ selectedLibrary->addAction(actions.restoreLibraryAction);
YACReader::addSperator(selectedLibrary);
selectedLibrary->addAction(actions.exportComicsInfoAction);
@@ -652,6 +755,7 @@ void LibraryWindow::createMenus()
selectedLibrary->addAction(actions.importLibraryAction);
YACReader::addSperator(selectedLibrary);
+ selectedLibrary->addAction(actions.openLibraryFolderAction);
selectedLibrary->addAction(actions.showLibraryInfo);
// MacOSX app menus
@@ -672,6 +776,11 @@ void LibraryWindow::createMenus()
libraryMenu->addSeparator();
libraryMenu->addAction(actions.rescanLibraryForXMLInfoAction);
+ libraryMenu->addAction(actions.repairLibraryAction);
+ libraryMenu->addSeparator();
+
+ libraryMenu->addAction(actions.backupLibraryAction);
+ libraryMenu->addAction(actions.restoreLibraryAction);
libraryMenu->addSeparator();
libraryMenu->addAction(actions.exportComicsInfoAction);
@@ -684,6 +793,7 @@ void LibraryWindow::createMenus()
libraryMenu->addSeparator();
+ libraryMenu->addAction(actions.openLibraryFolderAction);
libraryMenu->addAction(actions.showLibraryInfo);
// folder
@@ -733,7 +843,7 @@ void LibraryWindow::createConnections()
optionsDialog,
serverConfigDialog,
recentVisibilityCoordinator);
- QObject::connect(actions.focusSearchLineAction, &QAction::triggered, searchEdit, [this] { searchEdit->setFocus(Qt::ShortcutFocusReason); });
+ connect(actions.focusSearchLineAction, &QAction::triggered, this, &LibraryWindow::focusSearchInput);
// libraryCreator connections
connect(createLibraryDialog, &CreateLibraryDialog::createLibrary, this, QOverload::of(&LibraryWindow::create));
@@ -749,15 +859,80 @@ void LibraryWindow::createConnections()
connect(libraryCreator, &LibraryCreator::comicAdded, importWidget, &ImportWidget::newComic);
// libraryCreator errors
connect(libraryCreator, &LibraryCreator::failedCreatingDB, this, &LibraryWindow::manageCreatingError);
- // connect(libraryCreator, SIGNAL(failedUpdatingDB(QString)), this, SLOT(manageUpdatingError(QString))); // TODO: implement failedUpdatingDB
+ connect(libraryCreator, &LibraryCreator::failedOpeningDB, this, [this](const QString &error) {
+ showRootWidget();
+ const auto libraryName = selectedLibrary->currentText();
+ const auto libraryPath = libraries.getPath(libraryName);
+ if (!libraryPath.isEmpty() && QFile::exists(LibraryPaths::libraryDatabasePath(libraryPath)) && !DataBaseManagement::isLibraryDatabaseValid(libraryPath)) {
+ offerDatabaseRecovery(libraryName);
+ return;
+ }
+ manageUpdatingError(error);
+ });
connect(xmlInfoLibraryScanner, &QThread::finished, this, &LibraryWindow::showRootWidget);
connect(xmlInfoLibraryScanner, &QThread::finished, this, &LibraryWindow::reloadCurrentFolderComicsContent);
connect(xmlInfoLibraryScanner, &XMLInfoLibraryScanner::comicScanned, importWidget, &ImportWidget::newComic);
+ connect(comicInfoRepairer, &QThread::finished, this, [this]() {
+ const auto summary = comicInfoRepairer->summary();
+ showRootWidget();
+ reloadCurrentLibrary();
+
+ if (summary.lockedByAnotherProcess) {
+ if (summary.lockHolderIsRunningLocally) {
+ QMessageBox::information(this,
+ actions.repairLibraryAction->text(),
+ tr("A repair of this library is already running (%1). Wait for it to finish.").arg(summary.lockHolderInfo));
+ return;
+ }
+
+ auto text = summary.lockHolderInfo.isEmpty()
+ ? tr("The library is locked by a repair that did not finish.")
+ : tr("The library is locked by a repair started by %1.").arg(summary.lockHolderInfo);
+ text += "\n\n";
+ text += tr("If you are sure that no other repair is running, the lock can be removed. Remove the lock and continue?");
+
+ const auto answer = QMessageBox::question(this,
+ actions.repairLibraryAction->text(),
+ text,
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No);
+ if (answer == QMessageBox::Yes) {
+ startLibraryRepair(true);
+ }
+ return;
+ }
+
+ if (summary.canceled || !summary.error.isEmpty()) {
+ return;
+ }
+
+ QMessageBox messageBox(QMessageBox::Information,
+ actions.repairLibraryAction->text(),
+ tr("Repaired: %1\nFailed: %2\nMissing files: %3").arg(summary.repaired).arg(summary.failed).arg(summary.missingFiles),
+ QMessageBox::Ok,
+ this);
+ if (!summary.failedFilePaths.isEmpty()) {
+ messageBox.setDetailedText(summary.failedFilePaths.join('\n'));
+ }
+ messageBox.exec();
+ });
+ connect(comicInfoRepairer, &ComicInfoRepairer::comicProcessed, importWidget, &ImportWidget::newComic);
+ connect(comicInfoRepairer, &ComicInfoRepairer::failed, this, [this](const QString &error) {
+ const auto libraryName = selectedLibrary->currentText();
+ const auto libraryPath = libraries.getPath(libraryName);
+ if (!libraryPath.isEmpty() && QFile::exists(LibraryPaths::libraryDatabasePath(libraryPath)) && !DataBaseManagement::isLibraryDatabaseValid(libraryPath)) {
+ offerDatabaseRecovery(libraryName);
+ return;
+ }
+ QMessageBox::critical(this, actions.repairLibraryAction->text(), error);
+ });
+
// new import widget
connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopLibraryCreator);
connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopXMLScanning);
+ connect(importWidget, &ImportWidget::stop, this, &LibraryWindow::stopComicInfoRepair);
// packageManager connections
connect(exportLibraryDialog, &ExportLibraryDialog::exportPath, this, &LibraryWindow::exportLibrary);
@@ -769,6 +944,9 @@ void LibraryWindow::createConnections()
connect(importLibraryDialog, &ImportLibraryDialog::libraryExists, this, &LibraryWindow::libraryAlreadyExists);
connect(packageManager, &PackageManager::imported, importLibraryDialog, &QWidget::hide);
connect(packageManager, &PackageManager::imported, this, &LibraryWindow::openLastCreated);
+ connect(packageManager, &PackageManager::failed, this, [this](const QString &error) {
+ QMessageBox::critical(this, tr("Package operation failed"), error.isEmpty() ? tr("The covers package operation could not be completed.") : error);
+ });
// create and update dialogs
connect(createLibraryDialog, &CreateLibraryDialog::cancelCreate, this, &LibraryWindow::cancelCreating);
@@ -853,6 +1031,11 @@ void LibraryWindow::loadLibrary(const QString &name)
showRootWidget();
QString rootPath = libraries.getPath(name);
+ QString recoveryError;
+ if (!DataBaseManagement::recoverInterruptedRestore(rootPath, &recoveryError)) {
+ QMessageBox::critical(this, tr("Restore recovery failed"), recoveryError);
+ return;
+ }
QString path = LibraryPaths::libraryDataPath(rootPath);
QString customFolderCoversPath = LibraryPaths::libraryCustomFoldersCoverPath(rootPath);
QString databasePath = LibraryPaths::libraryDatabasePath(rootPath);
@@ -866,6 +1049,20 @@ void LibraryWindow::loadLibrary(const QString &name)
int comparation = DataBaseManagement::compareVersions(dbVersion, DB_VERSION);
if (comparation < 0) {
+ // a database that fails validation would block the upgrade backup and
+ // trap the user in the update-needed/upgrade-failed dialog cycle;
+ // offer recovery instead of the upgrade question
+ if (!DataBaseManagement::isLibraryDatabaseValid(rootPath)) {
+ contentViewsManager->comicsView->setModel(NULL);
+ foldersView->setModel(NULL);
+ listsView->setModel(NULL);
+ actions.disableAllActions();
+ actions.renameLibraryAction->setEnabled(true);
+ actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
+ offerDatabaseRecovery(name);
+ return;
+ }
int ret = QMessageBox::question(this, tr("Update needed"), tr("This library was created with a previous version of YACReaderLibrary. It needs to be updated. Update now?"), QMessageBox::Yes, QMessageBox::No);
if (ret == QMessageBox::Yes) {
importWidget->setUpgradeLook();
@@ -889,6 +1086,7 @@ void LibraryWindow::loadLibrary(const QString &name)
// será possible renombrar y borrar estas bibliotecas
actions.renameLibraryAction->setEnabled(true);
actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
}
}
@@ -914,6 +1112,7 @@ void LibraryWindow::loadLibrary(const QString &name)
{
actions.disableLibrariesActions(false);
actions.updateLibraryAction->setDisabled(true);
+ actions.repairLibraryAction->setDisabled(true);
actions.openContainingFolderAction->setDisabled(true);
actions.rescanLibraryForXMLInfoAction->setDisabled(true);
@@ -931,7 +1130,7 @@ void LibraryWindow::loadLibrary(const QString &name)
setRootIndex();
- searchEdit->clear();
+ clearSearchInput(true);
} else if (comparation > 0) {
int ret = QMessageBox::question(this, tr("Download new version"), tr("This library was created with a newer version of YACReaderLibrary. Download the new version now?"), QMessageBox::Yes, QMessageBox::No);
if (ret == QMessageBox::Yes)
@@ -944,6 +1143,7 @@ void LibraryWindow::loadLibrary(const QString &name)
// será possible renombrar y borrar estas bibliotecas
actions.renameLibraryAction->setEnabled(true);
actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
}
} else {
contentViewsManager->comicsView->setModel(NULL);
@@ -960,6 +1160,7 @@ void LibraryWindow::loadLibrary(const QString &name)
// será possible renombrar y borrar estas bibliotecas
actions.renameLibraryAction->setEnabled(true);
actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
} else // si existe el path, puede ser que la librería sea alguna versión pre-5.0 ó que esté corrupta o que no haya drivers sql
{
@@ -970,17 +1171,17 @@ void LibraryWindow::loadLibrary(const QString &name)
// será possible renombrar y borrar estas bibliotecas
actions.renameLibraryAction->setEnabled(true);
actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
} else {
QString currentLibrary = selectedLibrary->currentText();
QString path = libraries.getPath(selectedLibrary->currentText());
if (QMessageBox::question(this, tr("Old library"), tr("Library '%1' has been created with an older version of YACReaderLibrary. It must be created again. Do you want to create the library now?").arg(currentLibrary), QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) {
- QDir d(LibraryPaths::libraryDataPath(path));
- d.removeRecursively();
createLibraryDialog->setDataAndStart(currentLibrary, path);
}
// será possible renombrar y borrar estas bibliotecas
actions.renameLibraryAction->setEnabled(true);
actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
}
}
}
@@ -1879,12 +2080,256 @@ void LibraryWindow::updateLibrary()
libraryCreator->start();
}
+void LibraryWindow::backupLibrary()
+{
+ const auto path = libraries.getPath(selectedLibrary->currentText());
+ if (path.isEmpty())
+ return;
+
+ auto version = DataBaseManagement::checkValidDB(LibraryPaths::libraryDatabasePath(path));
+ if (version.isEmpty())
+ version = "unknown";
+ const auto suggestedName = QString("library-%1-db-%2-manual.ydb")
+ .arg(QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss"), version);
+ const auto destination = QFileDialog::getSaveFileName(this,
+ actions.backupLibraryAction->text(),
+ QDir::home().filePath(suggestedName),
+ tr("YACReader library database (*.ydb)"));
+ if (destination.isEmpty())
+ return;
+
+ struct BackupResult {
+ bool success { false };
+ QString error;
+ };
+
+ auto result = std::make_shared();
+ auto worker = QThread::create([path, destination, result] {
+ result->success = DataBaseManagement::backupLibrary(path, DatabaseBackupReason::Manual, &result->error, destination);
+ });
+
+ actions.backupLibraryAction->setDisabled(true);
+ connect(worker, &QThread::finished, this, [this, destination, result] {
+ actions.backupLibraryAction->setDisabled(false);
+ if (result->success) {
+ QMessageBox::information(this,
+ actions.backupLibraryAction->text(),
+ tr("The library database backup was created at:\n%1").arg(destination));
+ } else {
+ QMessageBox::critical(this,
+ actions.backupLibraryAction->text(),
+ tr("Unable to create the library database backup:\n%1").arg(result->error));
+ }
+ });
+ connect(worker, &QThread::finished, worker, &QObject::deleteLater);
+ worker->start();
+}
+
+void LibraryWindow::restoreLibrary()
+{
+ const auto libraryPath = libraries.getPath(selectedLibrary->currentText());
+ if (libraryPath.isEmpty())
+ return;
+
+ const auto backupPath = QFileDialog::getOpenFileName(this,
+ actions.restoreLibraryAction->text(),
+ QDir(LibraryPaths::libraryDataPath(libraryPath)).filePath("backups"),
+ tr("YACReader library database (*.ydb)"));
+ if (backupPath.isEmpty())
+ return;
+
+ const auto answer = QMessageBox::warning(this,
+ actions.restoreLibraryAction->text(),
+ tr("Close YACReaderLibraryServer and any other YACReader application using this library before restoring. Continue?"),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ if (answer == QMessageBox::Yes)
+ startLibraryRestore(backupPath);
+}
+
+void LibraryWindow::startLibraryRestore(const QString &backupPath, bool allowInvalidCurrent, bool removeStaleLock)
+{
+ const auto libraryName = selectedLibrary->currentText();
+ const auto libraryPath = libraries.getPath(libraryName);
+ auto result = std::make_shared();
+ auto progress = new QProgressDialog(tr("Restoring library database..."), QString(), 0, 0, this);
+ progress->setCancelButton(nullptr);
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(0);
+
+ contentViewsManager->comicsView->setModel(nullptr);
+ foldersView->setModel(nullptr);
+ listsView->setModel(nullptr);
+ actions.disableAllActions();
+
+ auto worker = QThread::create([libraryPath, backupPath, allowInvalidCurrent, removeStaleLock, result] {
+ *result = DataBaseManagement::restoreLibrary(libraryPath, backupPath, allowInvalidCurrent, removeStaleLock);
+ });
+ connect(worker, &QThread::finished, this, [this, libraryName, backupPath, allowInvalidCurrent, result, progress] {
+ progress->deleteLater();
+
+ if (result->status == DatabaseRestoreStatus::InvalidCurrentDatabase && !allowInvalidCurrent) {
+ const auto answer = QMessageBox::warning(this,
+ actions.restoreLibraryAction->text(),
+ tr("The current library database is invalid. Restore the selected backup anyway?"),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ if (answer == QMessageBox::Yes) {
+ startLibraryRestore(backupPath, true);
+ return;
+ }
+ actions.renameLibraryAction->setEnabled(true);
+ actions.removeLibraryAction->setEnabled(true);
+ actions.restoreLibraryAction->setEnabled(true);
+ return;
+ } else if (result->status == DatabaseRestoreStatus::LockFailed && !result->lockHolderIsRunningLocally) {
+ const auto answer = QMessageBox::warning(this,
+ actions.restoreLibraryAction->text(),
+ tr("The library maintenance lock may be stale. Remove it and retry?"),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ if (answer == QMessageBox::Yes) {
+ startLibraryRestore(backupPath, allowInvalidCurrent, true);
+ return;
+ }
+ loadLibrary(libraryName);
+ return;
+ }
+
+ if (!result->success()) {
+ auto error = result->error;
+ if (result->status == DatabaseRestoreStatus::RollbackFailed)
+ error += tr("\n\nRestart YACReaderLibrary before attempting recovery again.");
+ QMessageBox::critical(this, actions.restoreLibraryAction->text(), error);
+ if (result->status != DatabaseRestoreStatus::RollbackFailed) {
+ loadLibrary(libraryName);
+ } else {
+ actions.restoreLibraryAction->setEnabled(true);
+ actions.removeLibraryAction->setEnabled(true);
+ }
+ return;
+ }
+
+ loadLibrary(libraryName);
+ const auto answer = QMessageBox::question(this,
+ actions.restoreLibraryAction->text(),
+ tr("The library database was restored successfully. Update the library now?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes);
+ if (answer == QMessageBox::Yes)
+ updateLibrary();
+ });
+ connect(worker, &QThread::finished, worker, &QObject::deleteLater);
+ worker->start();
+}
+
+void LibraryWindow::offerDatabaseRecovery(const QString &libraryName)
+{
+ QMessageBox messageBox(QMessageBox::Warning,
+ tr("Library database damaged"),
+ tr("The database of library '%1' is damaged, so normal updates, maintenance, and backups are unavailable. YACReader can attempt to repair the database. Some damaged data may not be recoverable. Existing backups will not be changed.").arg(libraryName),
+ QMessageBox::NoButton,
+ this);
+ const auto repairButton = messageBox.addButton(tr("Attempt repair"), QMessageBox::AcceptRole);
+ const auto restoreButton = messageBox.addButton(tr("Restore a backup..."), QMessageBox::ActionRole);
+ messageBox.addButton(QMessageBox::Cancel);
+ messageBox.setWindowModality(Qt::WindowModal);
+ messageBox.exec();
+
+ if (messageBox.clickedButton() == repairButton)
+ startDatabaseSalvage(libraryName);
+ else if (messageBox.clickedButton() == restoreButton)
+ restoreLibrary();
+}
+
+void LibraryWindow::startDatabaseSalvage(const QString &libraryName, bool removeStaleLock)
+{
+ const auto libraryPath = libraries.getPath(libraryName);
+ if (libraryPath.isEmpty())
+ return;
+
+ auto result = std::make_shared();
+ auto progress = new QProgressDialog(tr("Repairing library database..."), QString(), 0, 0, this);
+ progress->setCancelButton(nullptr);
+ progress->setWindowModality(Qt::WindowModal);
+ progress->setMinimumDuration(0);
+
+ auto worker = QThread::create([libraryPath, removeStaleLock, result] {
+ *result = DataBaseManagement::salvageLibrary(libraryPath, removeStaleLock);
+ });
+ connect(worker, &QThread::finished, this, [this, libraryName, result, progress] {
+ progress->deleteLater();
+
+ if (result->status == DatabaseSalvageStatus::LockFailed) {
+ if (!result->lockHolderIsRunningLocally) {
+ const auto answer = QMessageBox::warning(this,
+ tr("Library database repair"),
+ tr("The library maintenance lock may be stale. Remove it and retry?"),
+ QMessageBox::Yes | QMessageBox::Cancel,
+ QMessageBox::Cancel);
+ if (answer == QMessageBox::Yes)
+ startDatabaseSalvage(libraryName, true);
+ } else {
+ QMessageBox::warning(this,
+ tr("Library database repair"),
+ tr("Another maintenance operation is currently using this library. Try again after it finishes."));
+ }
+ return;
+ }
+
+ if (result->success()) {
+ loadLibrary(libraryName);
+ if (result->status == DatabaseSalvageStatus::AlreadyValid) {
+ QMessageBox::information(this,
+ tr("Library database repair"),
+ tr("The library database is already valid."));
+ } else if (result->status == DatabaseSalvageStatus::Reindexed) {
+ QMessageBox::information(this,
+ tr("Library database repaired"),
+ tr("The library database was repaired by rebuilding its indexes. The damaged original was preserved at:\n%1").arg(result->preservedDatabasePath));
+ } else {
+ const auto answer = QMessageBox::question(this,
+ tr("Library database rebuilt"),
+ tr("The library database was rebuilt successfully. The damaged original was preserved at:\n%1\n\nUpdate the library now?").arg(result->preservedDatabasePath),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::Yes);
+ if (answer == QMessageBox::Yes)
+ updateLibrary();
+ }
+ } else {
+ auto recovery = result->preservedDatabasePath.isEmpty()
+ ? QString()
+ : tr("\n\nThe damaged original was preserved at:\n%1").arg(result->preservedDatabasePath);
+ QMessageBox::critical(this,
+ tr("Library database repair failed"),
+ tr("The library database could not be repaired:\n%1%2\n\nYou can restore a backup from the Library menu or recreate the library.").arg(result->error, recovery));
+ actions.restoreLibraryAction->setEnabled(true);
+ }
+ });
+ connect(worker, &QThread::finished, worker, &QObject::deleteLater);
+ worker->start();
+}
+
+void LibraryWindow::repairLibrary()
+{
+ startLibraryRepair(false);
+}
+
+void LibraryWindow::startLibraryRepair(bool removeStaleLock)
+{
+ importWidget->setRepairLook();
+ showImportingWidget();
+
+ const auto path = libraries.getPath(selectedLibrary->currentText());
+ comicInfoRepairer->repairLibrary(path, LibraryPaths::libraryDataPath(path), removeStaleLock);
+}
+
void LibraryWindow::deleteCurrentLibrary()
{
QString path = libraries.getPath(selectedLibrary->currentText());
libraries.remove(selectedLibrary->currentText());
selectedLibrary->removeItem(selectedLibrary->currentIndex());
- path = LibraryPaths::libraryDatabasePath(path);
+ path = LibraryPaths::libraryDataPath(path);
QDir d(path);
d.removeRecursively();
@@ -1908,7 +2353,7 @@ void LibraryWindow::removeLibrary()
tr("Do you want remove ") + currentLibrary + tr(" library?"),
QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No,
this);
- messageBox->button(QMessageBox::YesToAll)->setText(tr("Remove and delete metadata"));
+ messageBox->button(QMessageBox::YesToAll)->setText(tr("Remove and delete metadata and backups"));
messageBox->setWindowModality(Qt::WindowModal);
int ret = messageBox->exec();
if (ret == QMessageBox::Yes) {
@@ -1987,6 +2432,13 @@ void LibraryWindow::showLibraryInfo()
msgBox.exec();
}
+void LibraryWindow::openLibraryFolder()
+{
+ const auto path = libraries.getPath(selectedLibrary->currentText());
+ if (!path.isEmpty())
+ QDesktopServices::openUrl(QUrl::fromLocalFile(QDir::cleanPath(path)));
+}
+
void LibraryWindow::rescanCurrentFolderForXMLInfo()
{
rescanFolderForXMLInfo(getCurrentFolderIndex());
@@ -2021,6 +2473,12 @@ void LibraryWindow::stopXMLScanning()
xmlInfoLibraryScanner->wait();
}
+void LibraryWindow::stopComicInfoRepair()
+{
+ comicInfoRepairer->stop();
+ comicInfoRepairer->wait();
+}
+
void LibraryWindow::setRootIndex()
{
if (!libraries.isEmpty()) {
@@ -2236,17 +2694,12 @@ void LibraryWindow::openContainingFolderComic()
#endif
#ifdef Q_OS_MACOS
- QString filePath = file.absoluteFilePath();
+ // `open -R` reveals and selects the file in Finder without sending an Apple
+ // Event, so it doesn't trigger the macOS automation permission prompt.
QStringList args;
- args << "-e";
- args << "tell application \"Finder\"";
- args << "-e";
- args << "activate";
- args << "-e";
- args << "select POSIX file \"" + filePath + "\"";
- args << "-e";
- args << "end tell";
- QProcess::startDetached("osascript", args);
+ args << "-R";
+ args << file.absoluteFilePath();
+ QProcess::startDetached("open", args);
#endif
#ifdef Q_OS_WIN
@@ -2390,7 +2843,7 @@ void LibraryWindow::showExportComicsInfo()
void LibraryWindow::showImportComicsInfo()
{
- importComicsInfoDialog->dest = currentPath() + LibraryPaths::libraryDatabasePath(currentPath());
+ importComicsInfoDialog->dest = LibraryPaths::libraryDatabasePath(currentPath());
importComicsInfoDialog->open();
}
@@ -2408,6 +2861,7 @@ void LibraryWindow::prepareToCloseApp()
libraryCreator->stop();
librariesUpdateCoordinator->stop();
+ stopComicInfoRepair();
settings->setValue(MAIN_WINDOW_GEOMETRY, saveGeometry());
settings->setValue(MAIN_WINDOW_STATE, saveState());
@@ -2428,7 +2882,7 @@ void LibraryWindow::closeApp()
void LibraryWindow::showNoLibrariesWidget()
{
actions.disableAllActions();
- searchEdit->setDisabled(true);
+ setSearchInputEnabled(false);
mainWidget->setCurrentIndex(1);
}
@@ -2437,7 +2891,7 @@ void LibraryWindow::showRootWidget()
#ifndef Y_MAC_UI
libraryToolBar->setDisabled(false);
#endif
- searchEdit->setEnabled(true);
+ setSearchInputEnabled(true);
mainWidget->setCurrentIndex(0);
}
@@ -2448,7 +2902,7 @@ void LibraryWindow::showImportingWidget()
#ifndef Y_MAC_UI
libraryToolBar->setDisabled(true);
#endif
- searchEdit->setDisabled(true);
+ setSearchInputEnabled(false);
mainWidget->setCurrentIndex(2);
}
@@ -2703,7 +3157,7 @@ bool LibraryWindow::exitSearchMode()
{
if (status != LibraryWindow::Searching)
return false;
- searchEdit->clearText();
+ clearSearchInput(false);
clearSearchFilter();
return true;
}
diff --git a/YACReaderLibrary/library_window.h b/YACReaderLibrary/library_window.h
index af31dc430..c418d745d 100644
--- a/YACReaderLibrary/library_window.h
+++ b/YACReaderLibrary/library_window.h
@@ -28,6 +28,7 @@
class QTreeView;
class QDirModel;
class QAction;
+class QMenu;
class QToolBar;
class QComboBox;
class QThread;
@@ -87,6 +88,7 @@ class RecentVisibilityCoordinator;
namespace YACReader {
class TrayIconController;
class XMLInfoLibraryScanner;
+class ComicInfoRepairer;
}
#include "comic_db.h"
@@ -110,6 +112,7 @@ class LibraryWindow : public QMainWindow, protected Themable
AddLibraryDialog *addLibraryDialog;
LibraryCreator *libraryCreator;
XMLInfoLibraryScanner *xmlInfoLibraryScanner;
+ ComicInfoRepairer *comicInfoRepairer;
HelpAboutDialog *had;
RenameLibraryDialog *renameLibraryDialog;
PropertiesDialog *propertiesDialog;
@@ -197,6 +200,12 @@ class LibraryWindow : public QMainWindow, protected Themable
void doModels();
void setupCoordinators();
bool hasLoadedLibraryModels() const;
+ QMenu *createSearchMenu();
+ void applySearchQuery(const QString &query);
+ void setSearchInputEnabled(bool enabled);
+ void clearSearchInput(bool notify);
+ void focusSearchInput();
+ void showSearchSyntax();
QString currentPath();
QString currentFolderPath();
@@ -220,6 +229,7 @@ class LibraryWindow : public QMainWindow, protected Themable
public:
LibraryWindow();
+ QString searchText() const;
signals:
void libraryUpgraded(const QString &libraryName);
@@ -239,6 +249,13 @@ public slots:
void reloadCurrentLibrary();
void openLastCreated();
void updateLibrary();
+ void backupLibrary();
+ void restoreLibrary();
+ void startLibraryRestore(const QString &backupPath, bool allowInvalidCurrent = false, bool removeStaleLock = false);
+ void offerDatabaseRecovery(const QString &libraryName);
+ void startDatabaseSalvage(const QString &libraryName, bool removeStaleLock = false);
+ void repairLibrary();
+ void startLibraryRepair(bool removeStaleLock);
// void deleteLibrary();
void openContainingFolder();
void setFolderAsNotCompleted();
@@ -256,12 +273,14 @@ public slots:
void renameLibrary();
void rescanLibraryForXMLInfo();
void showLibraryInfo();
+ void openLibraryFolder();
void rescanCurrentFolderForXMLInfo();
void rescanFolderForXMLInfo(QModelIndex modelIndex);
void rename(QString newName);
void cancelCreating();
void stopLibraryCreator();
void stopXMLScanning();
+ void stopComicInfoRepair();
void setRootIndex();
void toggleFullScreen();
void toNormal();
diff --git a/YACReaderLibrary/library_window_actions.cpp b/YACReaderLibrary/library_window_actions.cpp
index 76957dcbd..d6b634d20 100644
--- a/YACReaderLibrary/library_window_actions.cpp
+++ b/YACReaderLibrary/library_window_actions.cpp
@@ -68,6 +68,21 @@ void LibraryWindowActions::createActions(LibraryWindow *window, QSettings *setti
updateLibraryAction->setData(UPDATE_LIBRARY_ACTION_YL);
updateLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(UPDATE_LIBRARY_ACTION_YL));
+ backupLibraryAction = new QAction(tr("Back up library database"), window);
+ backupLibraryAction->setToolTip(tr("Create a backup of the current library database"));
+ backupLibraryAction->setData(BACKUP_LIBRARY_ACTION_YL);
+ backupLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(BACKUP_LIBRARY_ACTION_YL));
+
+ restoreLibraryAction = new QAction(tr("Restore library database backup"), window);
+ restoreLibraryAction->setToolTip(tr("Restore the current library database from a backup"));
+ restoreLibraryAction->setData(RESTORE_LIBRARY_ACTION_YL);
+ restoreLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(RESTORE_LIBRARY_ACTION_YL));
+
+ repairLibraryAction = new QAction(tr("Repair covers and comic info"), window);
+ repairLibraryAction->setToolTip(tr("Retry comics with missing covers or incomplete information"));
+ repairLibraryAction->setData(REPAIR_LIBRARY_ACTION_YL);
+ repairLibraryAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(REPAIR_LIBRARY_ACTION_YL));
+
renameLibraryAction = new QAction(tr("Rename library"), window);
renameLibraryAction->setToolTip(tr("Rename current library"));
renameLibraryAction->setData(RENAME_LIBRARY_ACTION_YL);
@@ -83,6 +98,11 @@ void LibraryWindowActions::createActions(LibraryWindow *window, QSettings *setti
rescanLibraryForXMLInfoAction->setData(RESCAN_LIBRARY_XML_INFO_ACTION_YL);
rescanLibraryForXMLInfoAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(RESCAN_LIBRARY_XML_INFO_ACTION_YL));
+ openLibraryFolderAction = new QAction(tr("Open library folder..."), window);
+ openLibraryFolderAction->setToolTip(tr("Open the root folder of the current library"));
+ openLibraryFolderAction->setData(OPEN_LIBRARY_FOLDER_ACTION_YL);
+ openLibraryFolderAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(OPEN_LIBRARY_FOLDER_ACTION_YL));
+
showLibraryInfo = new QAction(tr("Show library info"), window);
showLibraryInfo->setToolTip(tr("Show information about the current library"));
showLibraryInfo->setData(SHOW_LIBRARY_INFO_ACTION_YL);
@@ -511,10 +531,14 @@ void LibraryWindowActions::createConnections(
QObject::connect(renameListAction, &QAction::triggered, window, &LibraryWindow::showRenameCurrentList);
QObject::connect(updateLibraryAction, &QAction::triggered, window, &LibraryWindow::updateLibrary);
+ QObject::connect(backupLibraryAction, &QAction::triggered, window, &LibraryWindow::backupLibrary);
+ QObject::connect(restoreLibraryAction, &QAction::triggered, window, &LibraryWindow::restoreLibrary);
+ QObject::connect(repairLibraryAction, &QAction::triggered, window, &LibraryWindow::repairLibrary);
QObject::connect(renameLibraryAction, &QAction::triggered, window, &LibraryWindow::renameLibrary);
// connect(deleteLibraryAction,SIGNAL(triggered()),window,SLOT(deleteLibrary()));
QObject::connect(removeLibraryAction, &QAction::triggered, window, &LibraryWindow::removeLibrary);
QObject::connect(rescanLibraryForXMLInfoAction, &QAction::triggered, window, &LibraryWindow::rescanLibraryForXMLInfo);
+ QObject::connect(openLibraryFolderAction, &QAction::triggered, window, &LibraryWindow::openLibraryFolder);
QObject::connect(showLibraryInfo, &QAction::triggered, window, &LibraryWindow::showLibraryInfo);
QObject::connect(openComicAction, &QAction::triggered, window, QOverload<>::of(&LibraryWindow::openComic));
@@ -629,9 +653,13 @@ void LibraryWindowActions::setUpShortcutsManagement(EditShortcutsDialog *editSho
<< exportLibraryAction
<< importLibraryAction
<< updateLibraryAction
+ << backupLibraryAction
+ << restoreLibraryAction
+ << repairLibraryAction
<< renameLibraryAction
<< removeLibraryAction
<< rescanLibraryForXMLInfoAction
+ << openLibraryFolderAction
<< showLibraryInfo);
allActions << tmpList;
@@ -684,6 +712,9 @@ void LibraryWindowActions::disableComicsActions(bool disabled)
void LibraryWindowActions::disableLibrariesActions(bool disabled)
{
updateLibraryAction->setDisabled(disabled);
+ backupLibraryAction->setDisabled(disabled);
+ restoreLibraryAction->setDisabled(disabled);
+ repairLibraryAction->setDisabled(disabled);
renameLibraryAction->setDisabled(disabled);
removeLibraryAction->setDisabled(disabled);
exportComicsInfoAction->setDisabled(disabled);
@@ -696,6 +727,9 @@ void LibraryWindowActions::disableLibrariesActions(bool disabled)
void LibraryWindowActions::disableNoUpdatedLibrariesActions(bool disabled)
{
updateLibraryAction->setDisabled(disabled);
+ backupLibraryAction->setDisabled(disabled);
+ restoreLibraryAction->setDisabled(disabled);
+ repairLibraryAction->setDisabled(disabled);
exportComicsInfoAction->setDisabled(disabled);
importComicsInfoAction->setDisabled(disabled);
exportLibraryAction->setDisabled(disabled);
@@ -777,6 +811,7 @@ void LibraryWindowActions::updateTheme(const Theme &theme)
updateLibraryAction->setIcon(menuIcons.updateLibraryIcon);
renameLibraryAction->setIcon(menuIcons.renameLibraryIcon);
removeLibraryAction->setIcon(menuIcons.removeLibraryIcon);
+ openLibraryFolderAction->setIcon(menuIcons.openContainingFolderIcon);
openContainingFolderAction->setIcon(menuIcons.openContainingFolderIcon);
openContainingFolderComicAction->setIcon(menuIcons.openContainingFolderIcon);
updateFolderAction->setIcon(menuIcons.updateCurrentFolderIcon);
diff --git a/YACReaderLibrary/library_window_actions.h b/YACReaderLibrary/library_window_actions.h
index 0cffabdbb..4c60580ff 100644
--- a/YACReaderLibrary/library_window_actions.h
+++ b/YACReaderLibrary/library_window_actions.h
@@ -37,10 +37,14 @@ class LibraryWindowActions
QAction *rescanLibraryForXMLInfoAction;
QAction *updateLibraryAction;
+ QAction *backupLibraryAction;
+ QAction *restoreLibraryAction;
+ QAction *repairLibraryAction;
QAction *removeLibraryAction;
QAction *helpAboutAction;
QAction *renameLibraryAction;
+ QAction *openLibraryFolderAction;
QAction *showLibraryInfo;
#ifndef Q_OS_MACOS
diff --git a/YACReaderLibrary/options_dialog.cpp b/YACReaderLibrary/options_dialog.cpp
index ff37d8859..b3c68b757 100644
--- a/YACReaderLibrary/options_dialog.cpp
+++ b/YACReaderLibrary/options_dialog.cpp
@@ -7,6 +7,7 @@
#include "theme_manager.h"
#include "yacreader_3d_flow_config_widget.h"
#include "yacreader_global_gui.h"
+#include "yacreader_settings_widget.h"
#include
#include
@@ -14,7 +15,6 @@
#include
#include
#include
-#include
#include
FlowType flowType = Strip;
@@ -29,12 +29,13 @@ OptionsDialog::OptionsDialog(QWidget *parent)
auto appearanceW = createAppearanceTab();
- auto tabWidget = new QTabWidget();
- tabWidget->addTab(generalW, tr("General"));
- tabWidget->addTab(librariesW, tr("Libraries"));
- tabWidget->addTab(comicFlowW, tr("Comic Flow"));
- tabWidget->addTab(gridViewW, tr("Grid view"));
- tabWidget->addTab(appearanceW, tr("Appearance"));
+ auto settingsWidget = new YACReaderSettingsWidget();
+ settingsWidget->addPage(generalW, tr("General"));
+ settingsWidget->addPage(librariesW, tr("Libraries"));
+ settingsWidget->addPage(comicFlowW, tr("Comic Flow"));
+ settingsWidget->addPage(gridViewW, tr("Grid view"));
+ settingsWidget->addPage(appearanceW, tr("Appearance"));
+ settingsWidget->addPage(shortcutsPage, shortcutsPage->windowTitle());
auto buttons = new QHBoxLayout();
buttons->addStretch();
@@ -43,13 +44,11 @@ OptionsDialog::OptionsDialog(QWidget *parent)
buttons->addWidget(cancel);
auto layout = new QVBoxLayout(this);
- layout->addWidget(tabWidget);
+ layout->addWidget(settingsWidget);
layout->addLayout(buttons);
setLayout(layout);
setModal(true);
setWindowTitle(tr("Options"));
-
- this->layout()->setSizeConstraint(QLayout::SetFixedSize);
}
void OptionsDialog::editApiKey()
@@ -253,7 +252,6 @@ QWidget *OptionsDialog::createGeneralTab()
auto generalLayout = new QVBoxLayout();
generalLayout->addWidget(languageBox);
generalLayout->addWidget(trayIconBox);
- generalLayout->addWidget(shortcutsBox);
generalLayout->addWidget(apiKeyBox);
generalLayout->addWidget(comicInfoXMLBox);
generalLayout->addWidget(recentlyAddedBox);
@@ -332,10 +330,14 @@ QWidget *OptionsDialog::createLibrariesTab()
librariesBoxLayout->addWidget(updateLibrariesAtCertainTimeCheck);
librariesBoxLayout->addLayout(updateLibrariesAtCertainTimeLayout);
- librariesBoxLayout->addWidget(new QLabel(tr("WARNING! During library updates writes to the database are disabled!\n"
- "Don't schedule updates while you may be using the app actively.\n"
- "During automatic updates the app will block some of the actions until the update is finished.\n"
- "To stop an automatic update tap on the loading indicator next to the Libraries title.")));
+ // Without word wrapping this label is the widest widget in the whole dialog, and because
+ // QStackedWidget hints at the width of its widest page it would size every other section too.
+ auto updatesWarningLabel = new QLabel(tr("WARNING! During library updates writes to the database are disabled!\n"
+ "Don't schedule updates while you may be using the app actively.\n"
+ "During automatic updates the app will block some of the actions until the update is finished.\n"
+ "To stop an automatic update tap on the loading indicator next to the Libraries title."));
+ updatesWarningLabel->setWordWrap(true);
+ librariesBoxLayout->addWidget(updatesWarningLabel);
auto librariesBox = new QGroupBox(tr("Libraries"));
librariesBox->setLayout(librariesBoxLayout);
diff --git a/YACReaderLibrary/package_manager.cpp b/YACReaderLibrary/package_manager.cpp
index 2c8278d84..5f05f3b46 100644
--- a/YACReaderLibrary/package_manager.cpp
+++ b/YACReaderLibrary/package_manager.cpp
@@ -3,41 +3,44 @@
#include