diff --git a/CHANGELOG.md b/CHANGELOG.md index be96ea5f..2220344f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Version counting is based on semantic versioning (Major.Feature.Patch) ### YACReaderLibrary * Avoid showing stale information in the server config dialog by updating the connection information when the dialog is opened. +* Add new metadata support, it improves compatibility with ComicInfo.xml +* Add support for showing a "recently added/updated" indicator. +* Improved comic metadata dialog. +* Add textual tags support that can be queried through the search engine. +* Make = in the search engine work as : does. +* Add new operators to the search engine: exact match ==, <, >, <=, >=. ## 9.12 diff --git a/YACReader/main_window_viewer.cpp b/YACReader/main_window_viewer.cpp index b5df0749..179540e4 100644 --- a/YACReader/main_window_viewer.cpp +++ b/YACReader/main_window_viewer.cpp @@ -1575,7 +1575,7 @@ void MainWindowViewer::sendComic() auto client = new YACReaderLocalClient; connect(client, &YACReaderLocalClient::finished, client, &YACReaderLocalClient::deleteLater); - currentComicDB.info.lastTimeOpened = QDateTime::currentMSecsSinceEpoch() / 1000; + currentComicDB.info.lastTimeOpened = QDateTime::currentSecsSinceEpoch(); viewer->updateComic(currentComicDB); diff --git a/YACReaderLibrary/YACReaderLibrary.pro b/YACReaderLibrary/YACReaderLibrary.pro index d454d218..4ac749b7 100644 --- a/YACReaderLibrary/YACReaderLibrary.pro +++ b/YACReaderLibrary/YACReaderLibrary.pro @@ -85,6 +85,7 @@ HEADERS += comic_flow.h \ library_creator.h \ library_window.h \ add_library_dialog.h \ + recent_visibility_coordinator.h \ rename_library_dialog.h \ properties_dialog.h \ options_dialog.h \ @@ -171,6 +172,7 @@ SOURCES += comic_flow.cpp \ library_window.cpp \ main.cpp \ add_library_dialog.cpp \ + recent_visibility_coordinator.cpp \ rename_library_dialog.cpp \ properties_dialog.cpp \ options_dialog.cpp \ diff --git a/YACReaderLibrary/classic_comics_view.cpp b/YACReaderLibrary/classic_comics_view.cpp index e772875b..1121074a 100644 --- a/YACReaderLibrary/classic_comics_view.cpp +++ b/YACReaderLibrary/classic_comics_view.cpp @@ -90,7 +90,7 @@ ClassicComicsView::ClassicComicsView(QWidget *parent) hideFlowViewAction->setText(tr("Hide comic flow")); hideFlowViewAction->setData(HIDE_COMIC_VIEW_ACTION_YL); hideFlowViewAction->setShortcut(ShortcutsManager::getShortcutsManager().getShortcut(HIDE_COMIC_VIEW_ACTION_YL)); - hideFlowViewAction->setIcon(QIcon(":/images/comics_view_toolbar/hideComicFlow.png")); + hideFlowViewAction->setIcon(QIcon(":/images/comics_view_toolbar/hideComicFlow.svg")); hideFlowViewAction->setCheckable(true); hideFlowViewAction->setChecked(false); @@ -120,9 +120,7 @@ void ClassicComicsView::setToolBar(QToolBar *toolBar) static_cast(comics->layout())->insertWidget(0, toolBar); this->toolbar = toolBar; - toolBarStretch = new YACReaderToolBarStretch(this); - - toolBarStretchAction = toolBar->addWidget(toolBarStretch); + startSeparatorAction = toolBar->addSeparator(); toolBar->addAction(hideFlowViewAction); } @@ -343,7 +341,7 @@ void ClassicComicsView::removeItemsFromFlow(const QModelIndex &parent, int from, void ClassicComicsView::closeEvent(QCloseEvent *event) { - toolbar->removeAction(toolBarStretchAction); + toolbar->removeAction(startSeparatorAction); toolbar->removeAction(hideFlowViewAction); saveTableHeadersStatus(); diff --git a/YACReaderLibrary/classic_comics_view.h b/YACReaderLibrary/classic_comics_view.h index 98bb02ef..6307b4bb 100644 --- a/YACReaderLibrary/classic_comics_view.h +++ b/YACReaderLibrary/classic_comics_view.h @@ -54,8 +54,6 @@ protected slots: private: YACReaderTableView *tableView; - YACReaderToolBarStretch *toolBarStretch; - QAction *toolBarStretchAction; QToolBar *toolbar; QWidget *comics; QSplitter *sVertical; @@ -63,6 +61,7 @@ private: QSettings *settings; void closeEvent(QCloseEvent *event) override; QAction *hideFlowViewAction; + QAction *startSeparatorAction; QStackedWidget *stack; diff --git a/YACReaderLibrary/comic_vine/comic_vine.pri b/YACReaderLibrary/comic_vine/comic_vine.pri index 85232a53..1c84f938 100644 --- a/YACReaderLibrary/comic_vine/comic_vine.pri +++ b/YACReaderLibrary/comic_vine/comic_vine.pri @@ -1,5 +1,6 @@ HEADERS += \ + $$PWD/comic_vine_json_parser.h \ comic_vine/comic_vine_dialog.h \ comic_vine/comic_vine_client.h \ comic_vine/scraper_lineedit.h \ @@ -24,6 +25,7 @@ HEADERS += \ $$PWD/comic_vine_all_volume_comics_retriever.h SOURCES += \ + $$PWD/comic_vine_json_parser.cpp \ comic_vine/comic_vine_dialog.cpp \ comic_vine/comic_vine_client.cpp \ comic_vine/scraper_lineedit.cpp \ diff --git a/YACReaderLibrary/comic_vine/comic_vine_dialog.cpp b/YACReaderLibrary/comic_vine/comic_vine_dialog.cpp index 28781ada..025b9b76 100644 --- a/YACReaderLibrary/comic_vine/comic_vine_dialog.cpp +++ b/YACReaderLibrary/comic_vine/comic_vine_dialog.cpp @@ -10,12 +10,11 @@ #include #include #include "data_base_management.h" -#include -#include #include #include "yacreader_busy_widget.h" #include "comic_vine_client.h" +#include "comic_vine_json_parser.h" #include "scraper_lineedit.h" #include "title_header.h" #include "series_question.h" @@ -461,7 +460,7 @@ void ComicVineDialog::getComicsInfo(QList> matchingInfo, QByteArray result = comicVineClient->getComicDetail(p.second, error, timeout); // TODO check timeOut or Connection error if (error || timeout) continue; // TODO - ComicDB comic = parseComicInfo(p.first, result, count, publisher); // TODO check result error + ComicDB comic = YACReader::parseCVJSONComicInfo(p.first, result, count, publisher); // TODO check result error comic.info.comicVineID = p.second; comics.push_back(comic); @@ -489,7 +488,7 @@ void ComicVineDialog::getComicInfo(const QString &comicId, int count, const QStr } } - ComicDB comic = parseComicInfo(comics[currentIndex], result, count, publisher); // TODO check result error + ComicDB comic = YACReader::parseCVJSONComicInfo(comics[currentIndex], result, count, publisher); // TODO check result error comic.info.comicVineID = comicId; setLoadingMessage(tr("Retrieving tags for : %1").arg(comics[currentIndex].getFileName())); QString connectionName = ""; @@ -512,196 +511,6 @@ void ComicVineDialog::getComicInfo(const QString &comicId, int count, const QStr } } -ComicDB ComicVineDialog::parseComicInfo(ComicDB &comic, const QString &json, int count, const QString &publisher) -{ - QJsonParseError Err; - - QVariantMap sc = QJsonDocument::fromJson(json.toUtf8(), &Err).toVariant().toMap(); - if (Err.error != QJsonParseError::NoError) { - qDebug("Error detected"); - return comic; - } - - int numResults = sc.value("number_of_total_results").toInt(); // fix to weird behaviour using hasNext - - if (numResults > 0) { - QVariantMap result = sc.value("results").toMap(); - comic.info.title = result.value("name"); - comic.info.number = result.value("issue_number"); - comic.info.volume = result.value("volume").toMap().value("name"); - - if (result.contains("person_credits") && !result.value("person_credits").isNull()) { - auto authors = getAuthors(result.value("person_credits")); - - QString writer = authors.values("writer").join("\n"); - QString penciller = authors.values("penciller").join("\n"); - QString inker = authors.values("inker").join("\n"); - QString colorist = authors.values("colorist").join("\n"); - QString letterer = authors.values("letterer").join("\n"); - QString coverArtist = authors.values("cover").join("\n"); - - comic.info.writer = writer; - comic.info.penciller = penciller; - comic.info.inker = inker; - comic.info.colorist = colorist; - comic.info.letterer = letterer; - comic.info.coverArtist = coverArtist; - } - - if (result.contains("cover_date") && !result.value("cover_date").isNull()) { - QString date = result.value("cover_date").toString(); - - QStringList tempList = date.split("-"); - - if (tempList.length() == 3) { - std::reverse(tempList.begin(), tempList.end()); - comic.info.date = tempList.join("/"); - } - } - - if (result.contains("description") && !result.value("description").isNull()) { - comic.info.synopsis = result.value("description"); - } - - if (result.contains("character_credits") && !result.value("character_credits").isNull()) { - comic.info.characters = getCharacters(result.value("character_credits")); - } - - if (result.contains("story_arc_credits") && !result.value("story_arc_credits").isNull()) { - QPair storyArcIdAndName = getFirstStoryArcIdAndName(result.value("story_arc_credits")); - QString storyArcId = storyArcIdAndName.first; - QString storyArcName = storyArcIdAndName.second; - if (!storyArcId.isNull()) { - - QString comicId = result.value("id").toString(); - - QPair arcNumberAndArcCount = getArcNumberAndArcCount(storyArcId, comicId); - if (!arcNumberAndArcCount.first.isNull()) { - QString arcNumber = arcNumberAndArcCount.first; - QString arcCount = arcNumberAndArcCount.second; - - comic.info.storyArc = storyArcName; - comic.info.arcNumber = arcNumber; - comic.info.arcCount = arcCount; - } - } - } - - comic.info.count = count; - - comic.info.publisher = publisher; - - comic.info.edited = true; - } - - return comic; -} - -QString ComicVineDialog::getCharacters(const QVariant &json_characters) -{ - QStringList characters; - - QListIterator it(json_characters.toList()); - QVariantMap resultsValue; - while (it.hasNext()) { - resultsValue = it.next().toMap(); - - characters << resultsValue.value("name").toString(); - } - - return (characters.isEmpty()) ? "" : (characters.join("\n") + "\n"); -} - -QMultiMap ComicVineDialog::getAuthors(const QVariant &json_authors) -{ - QMultiMap authors; - - QListIterator it(json_authors.toList()); - QVariantMap resultsValue; - while (it.hasNext()) { - resultsValue = it.next().toMap(); - - QString authorName = resultsValue.value("name").toString(); - - QStringList roles = resultsValue.value("role").toString().split(","); - foreach (QString role, roles) { - if (role.trimmed() == "writer") - authors.insert("writer", authorName); - else if (role.trimmed() == "inker") - authors.insert("inker", authorName); - else if (role.trimmed() == "penciler" || role.trimmed() == "penciller") - authors.insert("penciller", authorName); - else if (role.trimmed() == "colorist") - authors.insert("colorist", authorName); - else if (role.trimmed() == "letterer") - authors.insert("letterer", authorName); - else if (role.trimmed() == "cover") - authors.insert("cover", authorName); - } - } - - return authors; -} - -QPair ComicVineDialog::getFirstStoryArcIdAndName(const QVariant &json_story_arcs) -{ - QString story_arc_id = QString(); - QString story_arc_name = QString(); - - QListIterator it(json_story_arcs.toList()); - QVariantMap resultsValue; - while (it.hasNext()) { - resultsValue = it.next().toMap(); - story_arc_id = resultsValue.value("id").toString(); - story_arc_name = resultsValue.value("name").toString(); - break; - } - return qMakePair(story_arc_id, story_arc_name); -} - -QPair ComicVineDialog::getArcNumberAndArcCount(const QString &storyArcId, const QString &comicId) -{ - auto comicVineClient = new ComicVineClient; - bool error; - bool timeout; - QByteArray result = comicVineClient->getStoryArcDetail(storyArcId, error, timeout); - if (error || timeout) - return qMakePair(QString(), QString()); - QString json = result; - - QJsonParseError Err; - QVariantMap sc = QJsonDocument::fromJson(json.toUtf8(), &Err).toVariant().toMap(); - - if (Err.error != QJsonParseError::NoError) { - qDebug("Error detected"); - return qMakePair(QString(), QString()); - } - - int numResults = sc.value("number_of_total_results").toInt(); // fix to weird behaviour using hasNext - - if (numResults > 0) { - QVariantMap result = sc.value("results").toMap(); - - if (result.contains("issues")) { - QListIterator it(result.value("issues").toList()); - int arcNumber = 0; - int arcCount = 0; - - QVariantMap resultsValue; - while (it.hasNext()) { - resultsValue = it.next().toMap(); - if (comicId == resultsValue.value("id").toString()) { - arcNumber = arcCount + 1; - } - arcCount++; - } - return qMakePair(QString::number(arcNumber), QString::number(arcCount)); - } - return qMakePair(QString(), QString()); - } - return qMakePair(QString(), QString()); -} - void ComicVineDialog::toggleSkipButton() { if (mode == SingleComicInList) diff --git a/YACReaderLibrary/comic_vine/comic_vine_dialog.h b/YACReaderLibrary/comic_vine/comic_vine_dialog.h index 9ea4b7f1..fa686150 100644 --- a/YACReaderLibrary/comic_vine/comic_vine_dialog.h +++ b/YACReaderLibrary/comic_vine/comic_vine_dialog.h @@ -58,16 +58,11 @@ protected slots: void showSelectComic(const QString &json); void showSortVolumeComics(const QString &json); void queryTimeOut(); - ComicDB parseComicInfo(ComicDB &comic, const QString &json, int count, const QString &publisher); void setLoadingMessage(const QString &message); void goToNextComic(); private: void clearState(); - QString getCharacters(const QVariant &json_characters); - QMultiMap getAuthors(const QVariant &json_authors); - QPair getFirstStoryArcIdAndName(const QVariant &json_story_arcs); - QPair getArcNumberAndArcCount(const QString &storyArcId, const QString &comicId); void toggleSkipButton(); diff --git a/YACReaderLibrary/comic_vine/comic_vine_json_parser.cpp b/YACReaderLibrary/comic_vine/comic_vine_json_parser.cpp new file mode 100644 index 00000000..f6a03813 --- /dev/null +++ b/YACReaderLibrary/comic_vine/comic_vine_json_parser.cpp @@ -0,0 +1,233 @@ + +#include "comic_vine_json_parser.h" + +#include "comic_vine_client.h" + +#include +#include + +QString getCharacters(const QVariant &json_characters); +QMultiMap getAuthors(const QVariant &json_authors); +QPair getFirstStoryArcIdAndName(const QVariant &json_story_arcs); +QPair getArcNumberAndArcCount(const QString &storyArcId, const QString &comicId); +QList getNamesFromList(const QVariant &json_list); + +ComicDB YACReader::parseCVJSONComicInfo(ComicDB &comic, const QString &json, int count, const QString &publisher) +{ + QJsonParseError Err; + + QVariantMap sc = QJsonDocument::fromJson(json.toUtf8(), &Err).toVariant().toMap(); + if (Err.error != QJsonParseError::NoError) { + qDebug("Error detected"); + return comic; + } + + int numResults = sc.value("number_of_total_results").toInt(); // fix to weird behaviour using hasNext + + if (numResults > 0) { + QVariantMap result = sc.value("results").toMap(); + comic.info.title = result.value("name"); + comic.info.number = result.value("issue_number"); + + // changed in 9.13, volume actually means series in ComicVine + comic.info.series = result.value("volume").toMap().value("name"); + + if (result.contains("person_credits") && !result.value("person_credits").isNull()) { + auto authors = getAuthors(result.value("person_credits")); + + QString writer = authors.values("writer").join("\n"); + QString penciller = authors.values("penciller").join("\n"); + QString inker = authors.values("inker").join("\n"); + QString colorist = authors.values("colorist").join("\n"); + QString letterer = authors.values("letterer").join("\n"); + QString coverArtist = authors.values("cover").join("\n"); + + comic.info.writer = writer; + comic.info.penciller = penciller; + comic.info.inker = inker; + comic.info.colorist = colorist; + comic.info.letterer = letterer; + comic.info.coverArtist = coverArtist; + } + + if (result.contains("cover_date") && !result.value("cover_date").isNull()) { + QString date = result.value("cover_date").toString(); + + QStringList tempList = date.split("-"); + + if (tempList.length() == 3) { + std::reverse(tempList.begin(), tempList.end()); + comic.info.date = tempList.join("/"); + } + } + + if (result.contains("description") && !result.value("description").isNull()) { + comic.info.synopsis = result.value("description"); + } + + if (result.contains("character_credits") && !result.value("character_credits").isNull()) { + comic.info.characters = getCharacters(result.value("character_credits")); + } + + if (result.contains("story_arc_credits") && !result.value("story_arc_credits").isNull()) { + QPair storyArcIdAndName = getFirstStoryArcIdAndName(result.value("story_arc_credits")); + QString storyArcId = storyArcIdAndName.first; + QString storyArcName = storyArcIdAndName.second; + if (!storyArcId.isNull()) { + + QString comicId = result.value("id").toString(); + + QPair arcNumberAndArcCount = getArcNumberAndArcCount(storyArcId, comicId); + if (!arcNumberAndArcCount.first.isNull()) { + QString arcNumber = arcNumberAndArcCount.first; + QString arcCount = arcNumberAndArcCount.second; + + comic.info.storyArc = storyArcName; + comic.info.arcNumber = arcNumber; + comic.info.arcCount = arcCount; + } + } + } + + if (result.contains("location_credits") && !result.value("location_credits").isNull()) { + comic.info.locations = getNamesFromList(result.value("location_credits")).join("\n"); + } + + if (result.contains("team_credits") && !result.value("team_credits").isNull()) { + comic.info.teams = getNamesFromList(result.value("team_credits")).join("\n"); + } + + if (result.contains("character_credits") && !result.value("character_credits").isNull()) { + comic.info.characters = getNamesFromList(result.value("character_credits")).join("\n"); + } + + comic.info.count = count; + + comic.info.publisher = publisher; + + comic.info.edited = true; + } + + return comic; +} + +QString getCharacters(const QVariant &json_characters) +{ + QStringList characters; + + QListIterator it(json_characters.toList()); + QVariantMap resultsValue; + while (it.hasNext()) { + resultsValue = it.next().toMap(); + + characters << resultsValue.value("name").toString(); + } + + return (characters.isEmpty()) ? "" : (characters.join("\n") + "\n"); +} + +QMultiMap getAuthors(const QVariant &json_authors) +{ + QMultiMap authors; + + QListIterator it(json_authors.toList()); + QVariantMap resultsValue; + while (it.hasNext()) { + resultsValue = it.next().toMap(); + + QString authorName = resultsValue.value("name").toString(); + + QStringList roles = resultsValue.value("role").toString().split(","); + foreach (QString role, roles) { + if (role.trimmed() == "writer") + authors.insert("writer", authorName); + else if (role.trimmed() == "inker") + authors.insert("inker", authorName); + else if (role.trimmed() == "penciler" || role.trimmed() == "penciller") + authors.insert("penciller", authorName); + else if (role.trimmed() == "colorist") + authors.insert("colorist", authorName); + else if (role.trimmed() == "letterer") + authors.insert("letterer", authorName); + else if (role.trimmed() == "cover") + authors.insert("cover", authorName); + } + } + + return authors; +} + +QPair getFirstStoryArcIdAndName(const QVariant &json_story_arcs) +{ + QString story_arc_id = QString(); + QString story_arc_name = QString(); + + QListIterator it(json_story_arcs.toList()); + QVariantMap resultsValue; + while (it.hasNext()) { + resultsValue = it.next().toMap(); + story_arc_id = resultsValue.value("id").toString(); + story_arc_name = resultsValue.value("name").toString(); + break; + } + return qMakePair(story_arc_id, story_arc_name); +} + +QPair getArcNumberAndArcCount(const QString &storyArcId, const QString &comicId) +{ + auto comicVineClient = new ComicVineClient; + bool error; + bool timeout; + QByteArray result = comicVineClient->getStoryArcDetail(storyArcId, error, timeout); + if (error || timeout) + return qMakePair(QString(), QString()); + QString json = result; + + QJsonParseError Err; + QVariantMap sc = QJsonDocument::fromJson(json.toUtf8(), &Err).toVariant().toMap(); + + if (Err.error != QJsonParseError::NoError) { + qDebug("Error detected"); + return qMakePair(QString(), QString()); + } + + int numResults = sc.value("number_of_total_results").toInt(); // fix to weird behaviour using hasNext + + if (numResults > 0) { + QVariantMap result = sc.value("results").toMap(); + + if (result.contains("issues")) { + QListIterator it(result.value("issues").toList()); + int arcNumber = 0; + int arcCount = 0; + + QVariantMap resultsValue; + while (it.hasNext()) { + resultsValue = it.next().toMap(); + if (comicId == resultsValue.value("id").toString()) { + arcNumber = arcCount + 1; + } + arcCount++; + } + return qMakePair(QString::number(arcNumber), QString::number(arcCount)); + } + return qMakePair(QString(), QString()); + } + return qMakePair(QString(), QString()); +} + +QList getNamesFromList(const QVariant &json_list) +{ + QList names; + + QListIterator it(json_list.toList()); + QVariantMap resultsValue; + while (it.hasNext()) { + resultsValue = it.next().toMap(); + + QString name = resultsValue.value("name").toString(); + names.append(name); + } + + return names; +} diff --git a/YACReaderLibrary/comic_vine/comic_vine_json_parser.h b/YACReaderLibrary/comic_vine/comic_vine_json_parser.h new file mode 100644 index 00000000..6b7bf4c1 --- /dev/null +++ b/YACReaderLibrary/comic_vine/comic_vine_json_parser.h @@ -0,0 +1,13 @@ + +#ifndef COMIC_VINE_JSON_PARSER_H +#define COMIC_VINE_JSON_PARSER_H + +#include "comic_db.h" + +namespace YACReader { + +ComicDB parseCVJSONComicInfo(ComicDB &comic, const QString &json, int count, const QString &publisher); + +} + +#endif // COMIC_VINE_JSON_PARSER_H diff --git a/YACReaderLibrary/db/comic_model.cpp b/YACReaderLibrary/db/comic_model.cpp index e5fcce18..844bcbeb 100644 --- a/YACReaderLibrary/db/comic_model.cpp +++ b/YACReaderLibrary/db/comic_model.cpp @@ -17,12 +17,13 @@ #include "QsLog.h" ComicModel::ComicModel(QObject *parent) - : QAbstractItemModel(parent) + : QAbstractItemModel(parent), showRecent(false), recentDays(1) + { } ComicModel::ComicModel(QSqlQuery &sqlquery, QObject *parent) - : QAbstractItemModel(parent) + : QAbstractItemModel(parent), showRecent(false), recentDays(1) { setupModelData(sqlquery); } @@ -239,6 +240,10 @@ QHash ComicModel::roleNames() const roles[CoverPathRole] = "cover_path"; roles[PublicationDate] = "date"; roles[ReadableTitle] = "readable_title"; + roles[AddedRole] = "added_date"; + roles[TypeRole] = "type"; + roles[ShowRecentRole] = "show_recent"; + roles[RecentRangeRole] = "recent_range"; return roles; } @@ -301,6 +306,14 @@ QVariant ComicModel::data(const QModelIndex &index, int role) const return item->data(Id); else if (role == PublicationDateRole) return QVariant(localizedDate(item->data(PublicationDate).toString())); + else if (role == AddedRole) + return item->data(Added); + else if (role == TypeRole) + return item->data(Type); + else if (role == ShowRecentRole) + return showRecent; + else if (role == ComicModel::RecentRangeRole) + return recentDays * 86400; if (role != Qt::DisplayRole) return QVariant(); @@ -443,6 +456,8 @@ QStringList ComicModel::getPaths(const QString &_source) return paths; } +#define COMIC_MODEL_QUERY_FIELDS "ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date,ci.added,ci.type" + void ComicModel::setupFolderModelData(unsigned long long int folderId, const QString &databasePath) { enableResorting = false; @@ -458,7 +473,7 @@ void ComicModel::setupFolderModelData(unsigned long long int folderId, const QSt { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date " + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "WHERE c.parentId = :parentId"); selectQuery.bindValue(":parentId", folderId); @@ -485,7 +500,7 @@ void ComicModel::setupLabelModelData(unsigned long long parentLabel, const QStri { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date " + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "INNER JOIN comic_label cl ON (c.id == cl.comic_id) " "WHERE cl.label_id = :parentLabelId " @@ -529,7 +544,7 @@ void ComicModel::setupReadingListModelData(unsigned long long parentReadingList, foreach (qulonglong id, ids) { QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date " + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "INNER JOIN comic_reading_list crl ON (c.id == crl.comic_id) " "WHERE crl.reading_list_id = :parentReadingList " @@ -566,7 +581,7 @@ void ComicModel::setupFavoritesModelData(const QString &databasePath) { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date " + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "INNER JOIN comic_default_reading_list cdrl ON (c.id == cdrl.comic_id) " "WHERE cdrl.default_reading_list_id = :parentDefaultListId " @@ -595,7 +610,7 @@ void ComicModel::setupReadingModelData(const QString &databasePath) { QSqlDatabase db = DataBaseManagement::loadDatabase(databasePath); QSqlQuery selectQuery(db); - selectQuery.prepare("SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date " + selectQuery.prepare("SELECT " COMIC_MODEL_QUERY_FIELDS " " "FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) " "WHERE ci.hasBeenOpened = 1 AND ci.read = 0 " "ORDER BY ci.lastTimeOpened DESC"); @@ -652,7 +667,7 @@ void ComicModel::setupModelData(QSqlQuery &sqlquery) return naturalSortLessThanCI(c1->data(ComicModel::FileName).toString(), c2->data(ComicModel::FileName).toString()); } else { if (c1->data(ComicModel::Number).isNull() == false && c2->data(ComicModel::Number).isNull() == false) { - return c1->data(ComicModel::Number).toInt() < c2->data(ComicModel::Number).toInt(); + return naturalSortLessThanCI(c1->data(ComicModel::Number).toString(), c2->data(ComicModel::Number).toString()); } else { return c2->data(ComicModel::Number).isNull(); } @@ -794,7 +809,7 @@ QVector ComicModel::setComicsRead(QList l return getReadList(); } -void ComicModel::setComicsManga(QList list, bool isManga) +void ComicModel::setComicsType(QList list, FileType type) { QString connectionName = ""; { @@ -803,7 +818,7 @@ void ComicModel::setComicsManga(QList list, bool isManga) foreach (QModelIndex mi, list) { bool found; ComicDB c = DBHelper::loadComic(_data.value(mi.row())->data(ComicModel::Id).toULongLong(), db, found); - c.info.manga = isManga; + c.info.type = QVariant::fromValue(type); DBHelper::update(&(c.info), db); } db.commit(); @@ -1122,6 +1137,26 @@ bool ComicModel::isFavorite(const QModelIndex &index) return isFavorite; } +void ComicModel::setShowRecent(bool showRecent) +{ + if (this->showRecent == showRecent) + return; + + this->showRecent = showRecent; + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { ComicModel::ShowRecentRole }); +} + +void ComicModel::setRecentRange(int days) +{ + if (this->recentDays == days) + return; + + this->recentDays = days; + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { ComicModel::RecentRangeRole }); +} + void ComicModel::updateRating(int rating, QModelIndex mi) { ComicDB comic = getComic(mi); diff --git a/YACReaderLibrary/db/comic_model.h b/YACReaderLibrary/db/comic_model.h index af6e05bd..b7b6f854 100644 --- a/YACReaderLibrary/db/comic_model.h +++ b/YACReaderLibrary/db/comic_model.h @@ -31,11 +31,13 @@ public: Path = 6, Hash = 7, ReadColumn = 8, - IsBis = 9, + IsBis = 9, // TODO_METADATA: Remove this column CurrentPage = 10, Rating = 11, HasBeenOpened = 12, PublicationDate = 13, + Added = 14, + Type = 15, }; enum Roles { @@ -55,6 +57,10 @@ public: CoverPathRole, PublicationDateRole, ReadableTitle, + AddedRole, + TypeRole, + ShowRecentRole, + RecentRangeRole, }; enum Mode { @@ -107,7 +113,7 @@ public: // setComicInfoForAllComics(); --> inserta la información común a todos los cómics de una sola vez. // setComicInfoForSelectedComis(QList list); -->inserta la información común para los comics seleccionados QVector setComicsRead(QList list, YACReaderComicReadStatus read); - void setComicsManga(QList list, bool isManga); + void setComicsType(QList list, FileType type); qint64 asignNumbers(QList list, int startingNumber); // void remove(ComicDB * comic, int row); void removeInTransaction(int row); @@ -135,6 +141,9 @@ public: ComicModel::Mode getMode() { return mode; } unsigned long long int getSourceId() { return sourceId; } + void setShowRecent(bool visible); + void setRecentRange(int days); + QHash roleNames() const override; public slots: @@ -164,6 +173,9 @@ private: qulonglong sourceId; QString localizedDate(const QString &dbDate) const; + bool showRecent; + qlonglong recentDays; + signals: void isEmpty(); void searchNumResults(int); diff --git a/YACReaderLibrary/db/data_base_management.cpp b/YACReaderLibrary/db/data_base_management.cpp index b22a7c73..8c086198 100644 --- a/YACReaderLibrary/db/data_base_management.cpp +++ b/YACReaderLibrary/db/data_base_management.cpp @@ -64,23 +64,29 @@ static QString fields = "title," //"coverSizeRatio," cover may have changed since the info was exported... //"originalCoverSize," // h/w // new 9.8 fields - "manga"; + "manga," + // new 9.13 fields + "added," + "type," + "editor," + "imprint," + "teams," + "locations," + "series," + "alternateSeries," + "alternateNumber," + "alternateCount," + "languageISO," + "seriesGroup," + "mainCharacterOrTeam," + "review," + "tags"; DataBaseManagement::DataBaseManagement() : QObject(), dataBasesList() { } -/*TreeModel * DataBaseManagement::newTreeModel(QString path) -{ - //la consulta se ejecuta... - QSqlQuery selectQuery(loadDatabase(path)); - selectQuery.setForwardOnly(true); - selectQuery.exec("select * from folder order by parentId,name"); - //selectQuery.finish(); - return new TreeModel(selectQuery); -}*/ - QSqlDatabase DataBaseManagement::createDatabase(QString name, QString path) { return createDatabase(QDir::cleanPath(path) + "/" + name + ".ydb"); @@ -102,12 +108,12 @@ QSqlDatabase DataBaseManagement::createDatabase(QString dest) // pragma.finish(); DataBaseManagement::createTables(db); - QSqlQuery query("INSERT INTO folder (parentId, name, path) " - "VALUES (1,'root', '/')", - db); + QSqlQuery insertRootQuery(db); + insertRootQuery.prepare("INSERT INTO folder (parentId, name, path, added) " + "VALUES (1, 'root', '/', :added)"); + insertRootQuery.bindValue(":added", QDateTime::currentSecsSinceEpoch()); + insertRootQuery.exec(); } - // query.finish(); - // db.close(); return db; } @@ -156,13 +162,13 @@ bool DataBaseManagement::createTables(QSqlDatabase &database) "coverPage INTEGER DEFAULT 1," "numPages INTEGER," - "number INTEGER," + "number TEXT," // changed to text from INTEGER (9.13) "isBis BOOLEAN," "count INTEGER," "volume TEXT," "storyArc TEXT," - "arcNumber INTEGER," + "arcNumber TEXT," // changed to text from INTEGER (9.13) "arcCount INTEGER," "genere TEXT," @@ -174,11 +180,11 @@ bool DataBaseManagement::createTables(QSqlDatabase &database) "letterer TEXT," "coverArtist TEXT," - "date TEXT," // dd/mm/yyyy --> se mostrará en 3 campos diferentes + "date TEXT," // publication date dd/mm/yyyy --> se mostrará en 3 campos diferentes "publisher TEXT," "format TEXT," "color BOOLEAN," - "ageRating BOOLEAN," // this is actually a string (TEXT), funny thing is that the current implementation works + "ageRating TEXT," "synopsis TEXT," "characters TEXT," @@ -190,7 +196,7 @@ bool DataBaseManagement::createTables(QSqlDatabase &database) // new 7.0 fields "hasBeenOpened BOOLEAN DEFAULT 0," - "rating INTEGER DEFAULT 0," + "rating INTEGER DEFAULT 0," // TODO_METADATA change type to REAL with two decimals "currentPage INTEGER DEFAULT 1, " "bookmark1 INTEGER DEFAULT -1, " "bookmark2 INTEGER DEFAULT -1, " @@ -205,8 +211,23 @@ bool DataBaseManagement::createTables(QSqlDatabase &database) "coverSizeRatio REAL," "originalCoverSize STRING," // h/w // new 9.8 fields - "manga BOOLEAN DEFAULT 0" - + "manga BOOLEAN DEFAULT 0," // deprecated 9.13 + // new 9.13 fields + "added INTEGER," + "type INTEGER DEFAULT 0," // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic, 4 = 4koma + "editor TEXT," + "imprint TEXT," + "teams TEXT," + "locations TEXT," + "series TEXT," + "alternateSeries TEXT," + "alternateNumber TEXT," + "alternateCount INTEGER," + "languageISO TEXT," + "seriesGroup TEXT," + "mainCharacterOrTeam TEXT," + "review TEXT," + "tags TEXT" ")"); success = success && queryComicInfo.exec(); // queryComicInfo.finish(); @@ -226,7 +247,11 @@ bool DataBaseManagement::createTables(QSqlDatabase &database) "firstChildHash TEXT," "customImage TEXT," // new 9.8 fields - "manga BOOLEAN DEFAULT 0," + "manga BOOLEAN DEFAULT 0," // deprecated 9.13 + // new 9.13 fields + "type INTEGER DEFAULT 0," // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic, 4 = 4koma + "added INTEGER," + "updated INTEGER," // updated when the folder gets new content "FOREIGN KEY(parentId) REFERENCES folder(id) ON DELETE CASCADE)"); success = success && queryFolder.exec(); @@ -460,7 +485,25 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) //-- // new 9.8 fields - "manga = :manga" + "manga = :manga," + + // new 9.13 fields + "added = :added," + "type = :type," // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic, + "editor = :editor," + "imprint = :imprint," + "teams = :teams," + "locations = :locations," + "series = :series," + "alternateSeries = :alternateSeries," + "alternateNumber = :alternateNumber," + "alternateCount = :alternateCount," + "languageISO = :languageISO," + "seriesGroup = :seriesGroup," + "mainCharacterOrTeam = :mainCharacterOrTeam," + "review = :review," + "tags = :tags" + //-- " WHERE hash = :hash "); @@ -496,6 +539,21 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) "comicVineID," "lastTimeOpened," "coverSizeRatio," + "manga," + "added," + "type," + "editor," + "imprint," + "teams," + "locations," + "series," + "alternateSeries," + "alternateNumber," + "alternateCount," + "languageISO," + "seriesGroup," + "mainCharacterOrTeam," + "review," "hash)" "VALUES (:title," @@ -539,6 +597,23 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) ":coverSizeRatio," ":originalCoverSize," + ":manga," + ":added," + ":type," + ":editor," + ":imprint," + ":teams," + ":locations," + ":series," + ":alternateSeries," + ":alternateNumber," + ":alternateCount," + ":languageISO," + ":seriesGroup," + ":mainCharacterOrTeam," + ":review," + ":tags," + ":hash )"); QSqlRecord record = newInfo.record(); @@ -596,6 +671,8 @@ bool DataBaseManagement::importComicsInfo(QString source, QString dest) return b; } + +// TODO: update fields // TODO fix these bindings void DataBaseManagement::bindValuesFromRecord(const QSqlRecord &record, QSqlQuery &query) { @@ -654,6 +731,22 @@ void DataBaseManagement::bindValuesFromRecord(const QSqlRecord &record, QSqlQuer bindValue("coverSizeRatio", record, query); bindValue("originalCoverSize", record, query); + bindValue("added", record, query); + bindValue("type", record, query); + bindValue("editor", record, query); + bindValue("imprint", record, query); + bindValue("teams", record, query); + bindValue("locations", record, query); + bindValue("series", record, query); + bindValue("alternateSeries", record, query); + bindValue("alternateNumber", record, query); + bindValue("alternateCount", record, query); + bindValue("languageISO", record, query); + bindValue("seriesGroup", record, query); + bindValue("mainCharacterOrTeam", record, query); + bindValue("review", record, query); + bindValue("tags", record, query); + bindValue("hash", record, query); } @@ -757,6 +850,7 @@ bool DataBaseManagement::updateToCurrentVersion(const QString &path) bool pre8 = false; bool pre9_5 = false; bool pre9_8 = false; + bool pre9_13 = false; QString fullPath = path + "/library.ydb"; @@ -770,6 +864,8 @@ bool DataBaseManagement::updateToCurrentVersion(const QString &path) pre9_5 = true; if (compareVersions(DataBaseManagement::checkValidDB(fullPath), "9.8.0") < 0) pre9_8 = true; + if (compareVersions(DataBaseManagement::checkValidDB(fullPath), "9.13.0") < 0) + pre9_13 = true; QString connectionName = ""; bool returnValue = false; @@ -895,6 +991,53 @@ bool DataBaseManagement::updateToCurrentVersion(const QString &path) returnValue = returnValue && successAddingColumns; } } + + if (pre9_13) { + { // comic_info + QStringList columnDefs; + columnDefs << "added INTEGER"; + columnDefs << "type INTEGER DEFAULT 0"; // 0 = comic, 1 = manga, 2 = manga left to right, 3 = webcomic, + columnDefs << "editor TEXT"; + columnDefs << "imprint TEXT"; + columnDefs << "teams TEXT"; + columnDefs << "locations TEXT"; + columnDefs << "series TEXT"; + columnDefs << "alternateSeries TEXT"; + columnDefs << "alternateNumber TEXT"; + columnDefs << "alternateCount INTEGER"; + columnDefs << "languageISO TEXT"; + columnDefs << "seriesGroup TEXT"; + columnDefs << "mainCharacterOrTeam TEXT"; + columnDefs << "review TEXT"; + columnDefs << "tags TEXT"; + bool successAddingColumns = addColumns("comic_info", columnDefs, db); + returnValue = returnValue && successAddingColumns; + + QSqlQuery updateTypeQueryToManga(db); + updateTypeQueryToManga.prepare("UPDATE comic_info SET type = manga"); + bool successMigratingManga = updateTypeQueryToManga.exec(); + returnValue = returnValue && successMigratingManga; + + QSqlQuery updateNumberQueryToBis(db); + updateNumberQueryToBis.prepare("UPDATE comic_info SET number = number + 0.5 WHERE isBis = 1"); + bool successMigratingBis = updateNumberQueryToBis.exec(); + returnValue = returnValue && successMigratingBis; + } + { // folder + QStringList columnDefs; + columnDefs << "added INTEGER"; + columnDefs << "updated INTEGER"; + columnDefs << "type INTEGER DEFAULT 0"; + + bool successAddingColumns = addColumns("folder", columnDefs, db); + returnValue = returnValue && successAddingColumns; + + QSqlQuery updateTypeQueryToManga(db); + updateTypeQueryToManga.prepare("UPDATE folder SET type = manga"); + bool successMigratingManga = updateTypeQueryToManga.exec(); + returnValue = returnValue && successMigratingManga; + } + } } connectionName = db.connectionName(); } diff --git a/YACReaderLibrary/db/data_base_management.h b/YACReaderLibrary/db/data_base_management.h index 2c269e2e..29302162 100644 --- a/YACReaderLibrary/db/data_base_management.h +++ b/YACReaderLibrary/db/data_base_management.h @@ -42,7 +42,6 @@ private: public: DataBaseManagement(); - // TreeModel * newTreeModel(QString path); // crea una base de datos y todas sus tablas static QSqlDatabase createDatabase(QString name, QString path); static QSqlDatabase createDatabase(QString dest); diff --git a/YACReaderLibrary/db/folder_model.cpp b/YACReaderLibrary/db/folder_model.cpp index 97a61760..28c19634 100644 --- a/YACReaderLibrary/db/folder_model.cpp +++ b/YACReaderLibrary/db/folder_model.cpp @@ -6,8 +6,6 @@ #include "db_helper.h" #include "qnaturalsorting.h" #include "yacreader_global_gui.h" -#include "QsLog.h" -#include "query_parser.h" #include @@ -54,21 +52,19 @@ void drawMacOSXFinishedFolderIcon() #define ROOT 1 FolderModel::FolderModel(QObject *parent) - : QAbstractItemModel(parent), isSubfolder(false), rootItem(nullptr), folderIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder.svg")), folderFinishedIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder_finished.svg")) + : QAbstractItemModel(parent), isSubfolder(false), rootItem(nullptr), folderIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder.svg")), folderFinishedIcon(YACReader::noHighlightedIcon(":/images/sidebar/folder_finished.svg")), showRecent(false), recentDays(1) { } FolderModel::FolderModel(QSqlQuery &sqlquery, QObject *parent) - : QAbstractItemModel(parent), isSubfolder(false), rootItem(nullptr) + : QAbstractItemModel(parent), isSubfolder(false), rootItem(nullptr), showRecent(false), recentDays(1) { - // lo m�s probable es que el nodo ra�z no necesite tener informaci�n QList rootData; - rootData << "root"; // id 0, padre 0, title "root" (el id, y el id del padre van a ir en la clase TreeItem) + rootData << "root"; // id 1, parent 1, title "root" rootItem = new FolderItem(rootData); rootItem->id = ROOT; rootItem->parentItem = nullptr; setupModelData(sqlquery, rootItem); - // sqlquery.finish(); } FolderModel::~FolderModel() @@ -98,7 +94,13 @@ QHash FolderModel::roleNames() const roles[IdRole] = "id"; roles[MangaRole] = "is_manga"; roles[CoverPathRole] = "cover_path"; - roles[FolderName] = "name"; + roles[FolderNameRole] = "name"; + roles[NumChildrenRole] = "num_children"; + roles[TypeRole] = "type"; + roles[AddedRole] = "added"; + roles[UpdatedRole] = "updated"; + roles[ShowRecentRole] = "show_recent"; + roles[RecentRangeRole] = "recent_range"; return roles; } @@ -150,7 +152,7 @@ QVariant FolderModel::data(const QModelIndex &index, int role) const #endif } - if (role == FolderModel::FolderName) { + if (role == FolderModel::FolderNameRole) { return item->data(FolderModel::Name); } @@ -169,6 +171,24 @@ QVariant FolderModel::data(const QModelIndex &index, int role) const if (role == FolderModel::CoverPathRole) return getCoverUrlPathForComicHash(item->data(FirstChildHash).toString()); + if (role == FolderModel::NumChildrenRole) + return item->data(NumChildren); + + if (role == FolderModel::TypeRole) + return item->data(Type); + + if (role == FolderModel::AddedRole) + return item->data(Added); + + if (role == FolderModel::UpdatedRole) + return item->data(Updated); + + if (role == FolderModel::ShowRecentRole) + return showRecent; + + if (role == FolderModel::RecentRangeRole) + return recentDays * 86400; + if (role != Qt::DisplayRole) return QVariant(); @@ -183,7 +203,8 @@ Qt::ItemFlags FolderModel::flags(const QModelIndex &index) const return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDropEnabled | Qt::ItemIsDragEnabled; } -QVariant FolderModel::headerData(int section, Qt::Orientation orientation, +QVariant FolderModel::headerData(int section, + Qt::Orientation orientation, int role) const { if (rootItem == nullptr) { @@ -323,65 +344,39 @@ void FolderModel::setupModelData(QSqlQuery &sqlquery, FolderItem *parent) int manga = record.indexOf("manga"); int id = record.indexOf("id"); int parentId = record.indexOf("parentId"); + int numChildren = record.indexOf("numChildren"); int firstChildHash = record.indexOf("firstChildHash"); + int customImage = record.indexOf("customImage"); + int type = record.indexOf("type"); + int added = record.indexOf("added"); + int updated = record.indexOf("updated"); while (sqlquery.next()) { QList data; - data << sqlquery.value(name).toString(); - data << sqlquery.value(path).toString(); - data << sqlquery.value(finished).toBool(); - data << sqlquery.value(completed).toBool(); - data << sqlquery.value(manga).toBool(); - data << sqlquery.value(firstChildHash).toString(); + data << sqlquery.value(name); + data << sqlquery.value(path); + data << sqlquery.value(finished); + data << sqlquery.value(completed); + data << sqlquery.value(manga); + data << sqlquery.value(numChildren); + data << sqlquery.value(firstChildHash); + data << sqlquery.value(customImage); + data << sqlquery.value(type); + data << sqlquery.value(added); + data << sqlquery.value(updated); + auto item = new FolderItem(data); item->id = sqlquery.value(id).toULongLong(); // la inserci�n de hijos se hace de forma ordenada FolderItem *parent = items.value(sqlquery.value(parentId).toULongLong()); - // if(parent !=0) //TODO if parent==0 the parent of item was removed from the DB and delete on cascade didn't work, ERROR. parent->appendChild(item); // se a�ade el item al map, de forma que se pueda encontrar como padre en siguientes iteraciones items.insert(item->id, item); } } -void FolderModel::updateFolderModelData(QSqlQuery &sqlquery, FolderItem *parent) -{ - Q_UNUSED(parent); - - QSqlRecord record = sqlquery.record(); - - int name = record.indexOf("name"); - int path = record.indexOf("path"); - int finished = record.indexOf("finished"); - int completed = record.indexOf("completed"); - int manga = record.indexOf("manga"); - int id = record.indexOf("id"); - int parentId = record.indexOf("parentId"); - int firstChildHash = record.indexOf("firstChildHash"); - - while (sqlquery.next()) { - QList data; - - data << sqlquery.value(name).toString(); - data << sqlquery.value(path).toString(); - data << sqlquery.value(finished).toBool(); - data << sqlquery.value(completed).toBool(); - data << sqlquery.value(manga).toBool(); - data << sqlquery.value(firstChildHash).toString(); - auto item = new FolderItem(data); - - item->id = sqlquery.value(id).toULongLong(); - // la inserci�n de hijos se hace de forma ordenada - FolderItem *parent = items.value(sqlquery.value(parentId).toULongLong()); - if (parent != 0) // TODO if parent==0 the parent of item was removed from the DB and delete on cascade didn't work, ERROR. - parent->appendChild(item); - // se a�ade el item al map, de forma que se pueda encontrar como padre en siguientes iteraciones - items.insert(item->id, item); - } -} - QString FolderModel::getDatabase() { return _databasePath; @@ -406,7 +401,7 @@ void FolderModel::updateFolderCompletedStatus(const QModelIndexList &list, bool if (!isSubfolder) { Folder f = DBHelper::loadFolder(item->id, db); - f.setCompleted(status); + f.completed = status; DBHelper::update(f, db); } } @@ -415,7 +410,7 @@ void FolderModel::updateFolderCompletedStatus(const QModelIndexList &list, bool } QSqlDatabase::removeDatabase(connectionName); - emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::FirstChildHash)); + emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } void FolderModel::updateFolderFinishedStatus(const QModelIndexList &list, bool status) @@ -430,7 +425,7 @@ void FolderModel::updateFolderFinishedStatus(const QModelIndexList &list, bool s if (!isSubfolder) { Folder f = DBHelper::loadFolder(item->id, db); - f.setFinished(status); + f.finished = status; DBHelper::update(f, db); } } @@ -439,10 +434,10 @@ void FolderModel::updateFolderFinishedStatus(const QModelIndexList &list, bool s } QSqlDatabase::removeDatabase(connectionName); - emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::FirstChildHash)); + emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } -void FolderModel::updateFolderManga(const QModelIndexList &list, bool manga) +void FolderModel::updateFolderType(const QModelIndexList &list, YACReader::FileType type) { QString connectionName = ""; { @@ -451,19 +446,19 @@ void FolderModel::updateFolderManga(const QModelIndexList &list, bool manga) foreach (QModelIndex mi, list) { auto item = static_cast(mi.internalPointer()); - std::function setManga; - setManga = [&setManga](FolderItem *item, bool manga) -> void { - item->setData(FolderModel::Manga, manga); + std::function setType; + setType = [&setType](FolderItem *item, YACReader::FileType type) -> void { + item->setData(FolderModel::Type, QVariant::fromValue(type)); for (auto child : item->children()) { - setManga(child, manga); + setType(child, type); } }; - setManga(item, manga); + setType(item, type); if (!isSubfolder) { - DBHelper::updateFolderTreeManga(item->id, db, manga); + DBHelper::updateFolderTreeType(item->id, db, type); } } db.commit(); @@ -471,7 +466,7 @@ void FolderModel::updateFolderManga(const QModelIndexList &list, bool manga) } QSqlDatabase::removeDatabase(connectionName); - emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::FirstChildHash)); + emit dataChanged(index(list.first().row(), FolderModel::Name), index(list.last().row(), FolderModel::Updated)); } QStringList FolderModel::getSubfoldersNames(const QModelIndex &mi) @@ -552,7 +547,13 @@ Folder FolderModel::getFolder(const QModelIndex &mi) folderItem->parent()->data(Columns::Path).toString() + "/" + name, folderItem->data(Columns::Completed).toBool(), folderItem->data(Columns::Finished).toBool(), - folderItem->data(Columns::Manga).toBool()); + folderItem->data(Columns::Manga).toBool(), + folderItem->data(Columns::NumChildren).toInt(), + folderItem->data(Columns::FirstChildHash).toString(), + folderItem->data(Columns::CustomImage).toString(), + folderItem->data(Columns::Type).value(), + folderItem->data(Columns::Added).toLongLong(), + folderItem->data(Columns::Updated).toLongLong()); return folder; } @@ -589,74 +590,6 @@ QModelIndex FolderModel::getIndexFromFolder(const Folder &folder, const QModelIn return QModelIndex(); } -void FolderModel::fetchMoreFromDB(const QModelIndex &parent) -{ - FolderItem *item; - if (parent.isValid()) - item = static_cast(parent.internalPointer()); - else - item = rootItem; - - // Remove all children - if (item->childCount() > 0) { - beginRemoveRows(parent, 0, item->childCount() - 1); - item->clearChildren(); - endRemoveRows(); - } - - QString connectionName = ""; - { - QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); - - QList items; - QList nextLevelItems; - - QSqlQuery selectQuery(db); - selectQuery.prepare("select * from folder where id <> 1 and parentId = :parentId order by parentId,name"); - - items << item; - bool firstLevelUpdated = false; - while (items.size() > 0) { - nextLevelItems.clear(); - foreach (FolderItem *item, items) { - QLOG_DEBUG() << "ID " << item->id; - selectQuery.bindValue(":parentId", item->id); - - selectQuery.exec(); - - if (!firstLevelUpdated) { - // NO size support - int numResults = 0; - while (selectQuery.next()) - numResults++; - - if (!selectQuery.seek(-1)) - selectQuery.exec(); - // END no size support - - beginInsertRows(parent, 0, numResults - 1); - } - - updateFolderModelData(selectQuery, item); - - if (!firstLevelUpdated) { - endInsertRows(); - firstLevelUpdated = true; - } - - nextLevelItems << item->children(); - } - - items.clear(); - items = nextLevelItems; - } - connectionName = db.connectionName(); - } - QLOG_DEBUG() << "item->childCount()-1" << item->childCount() - 1; - - QSqlDatabase::removeDatabase(connectionName); -} - QModelIndex FolderModel::addFolderAtParent(const QString &folderName, const QModelIndex &parent) { FolderItem *parentItem; @@ -670,7 +603,9 @@ QModelIndex FolderModel::addFolderAtParent(const QString &folderName, const QMod newFolder.name = folderName; newFolder.parentId = parentItem->id; newFolder.path = parentItem->data(Columns::Path).toString() + "/" + folderName; - newFolder.setManga(parentItem->data(Columns::Manga).toBool()); + newFolder.manga = parentItem->data(Columns::Manga).toBool(); + newFolder.type = parentItem->data(Columns::Type).value(); + QString connectionName = ""; { QSqlDatabase db = DataBaseManagement::loadDatabase(_databasePath); @@ -687,7 +622,13 @@ QModelIndex FolderModel::addFolderAtParent(const QString &folderName, const QMod data << newFolder.path; data << false; // finished data << true; // completed - data << newFolder.isManga(); + data << newFolder.manga; + data << 0; // numChildren + data << QVariant(); // first child hash, new folder is empty + data << QVariant(); // custom cover + data << QVariant::fromValue(newFolder.type); + data << newFolder.added; + data << newFolder.updated; auto item = new FolderItem(data); item->id = newFolder.id; @@ -708,6 +649,26 @@ QUrl FolderModel::getCoverUrlPathForComicHash(const QString &hash) const return QUrl("file:" + _databasePath + "/covers/" + hash + ".jpg"); } +void FolderModel::setShowRecent(bool showRecent) +{ + if (this->showRecent == showRecent) + return; + + this->showRecent = showRecent; + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { FolderModel::ShowRecentRole }); +} + +void FolderModel::setRecentRange(int days) +{ + if (this->recentDays == days) + return; + + this->recentDays = days; + + emit dataChanged(index(0, 0), index(rowCount() - 1, 0), { FolderModel::RecentRangeRole }); +} + void FolderModel::deleteFolder(const QModelIndex &mi) { beginRemoveRows(mi.parent(), mi.row(), mi.row()); diff --git a/YACReaderLibrary/db/folder_model.h b/YACReaderLibrary/db/folder_model.h index cae4de9e..c8e286e7 100644 --- a/YACReaderLibrary/db/folder_model.h +++ b/YACReaderLibrary/db/folder_model.h @@ -66,13 +66,10 @@ public: void setupModelData(QString path); QString getDatabase(); QString getFolderPath(const QModelIndex &folder); - // QModelIndex indexFromItem(FolderItem * item, int column); - - // bool isFilterEnabled(){return filterEnabled;}; void updateFolderCompletedStatus(const QModelIndexList &list, bool status); void updateFolderFinishedStatus(const QModelIndexList &list, bool status); - void updateFolderManga(const QModelIndexList &list, bool manga); + void updateFolderType(const QModelIndexList &list, YACReader::FileType type); QStringList getSubfoldersNames(const QModelIndex &mi); FolderModel *getSubfoldersModel(const QModelIndex &mi); @@ -80,28 +77,40 @@ public: Folder getFolder(const QModelIndex &mi); QModelIndex getIndexFromFolder(const Folder &folder, const QModelIndex &parent = QModelIndex()); - void fetchMoreFromDB(const QModelIndex &parent); - QModelIndex addFolderAtParent(const QString &folderName, const QModelIndex &parent); Q_INVOKABLE QUrl getCoverUrlPathForComicHash(const QString &hash) const; + void setShowRecent(bool showRecent); + void setRecentRange(int days); + enum Columns { Name = 0, - Path = 1, - Finished = 2, - Completed = 3, - Manga = 4, - FirstChildHash = 5 - }; // id INTEGER PRIMARY KEY, parentId INTEGER NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL + Path, + Finished, + Completed, + Manga, // deprecated + NumChildren, + FirstChildHash, + CustomImage, + Type, // FileType + Added, + Updated, + }; enum Roles { FinishedRole = Qt::UserRole + 1, CompletedRole, IdRole, - MangaRole, + MangaRole, // deprecated CoverPathRole, - FolderName, + FolderNameRole, + NumChildrenRole, + TypeRole, + AddedRole, + UpdatedRole, + ShowRecentRole, + RecentRangeRole, }; bool isSubfolder; @@ -112,9 +121,7 @@ public slots: private: void fullSetup(QSqlQuery &sqlquery, FolderItem *parent); - void setupModelData(QSqlQuery &sqlquery, FolderItem *parent); - void updateFolderModelData(QSqlQuery &sqlquery, FolderItem *parent); FolderItem *rootItem; // el árbol QMap items; // relación entre folders @@ -123,6 +130,9 @@ private: QIcon folderIcon; QIcon folderFinishedIcon; + + bool showRecent; + qlonglong recentDays; }; #endif diff --git a/YACReaderLibrary/db/query_lexer.cpp b/YACReaderLibrary/db/query_lexer.cpp index 7b317cdc..c8f65ba7 100644 --- a/YACReaderLibrary/db/query_lexer.cpp +++ b/YACReaderLibrary/db/query_lexer.cpp @@ -17,6 +17,14 @@ Token QueryLexer::next() case '(': case ')': return single(Token::Type::opcode); + case ':': + return single(Token::Type::equal); + case '=': + return equal(); + case '<': + return minor(); + case '>': + return major(); case '"': return quotedWord(); default: @@ -44,7 +52,7 @@ Token QueryLexer::word() auto start = index; get(); auto current = peek(); - while (current != '\0' && !isSpace(current) && current != '"' && current != '(' && current != ')') { + while (current != '\0' && !isSpace(current) && current != '"' && current != '(' && current != ')' && current != ':' && current != '=' && current != '<' && current != '>') { get(); current = peek(); } @@ -70,6 +78,45 @@ Token QueryLexer::quotedWord() return Token(Token::Type::eof); } +Token QueryLexer::minor() +{ + auto start = index; + get(); + auto current = peek(); + if (current == '=') { + get(); + return Token(Token::Type::minorOrEqual, input.substr(start, index - start)); + } + + return Token(Token::Type::minor, input.substr(start, index - start)); +} + +Token QueryLexer::major() +{ + auto start = index; + get(); + auto current = peek(); + if (current == '=') { + get(); + return Token(Token::Type::majorOrEqual, input.substr(start, index - start)); + } + + return Token(Token::Type::major, input.substr(start, index - start)); +} + +Token QueryLexer::equal() +{ + auto start = index; + get(); + auto current = peek(); + if (current == '=') { + get(); + return Token(Token::Type::exactEqual, input.substr(start, index - start)); + } + + return Token(Token::Type::equal, input.substr(start, index - start)); +} + bool QueryLexer::isSpace(char c) { switch (c) { diff --git a/YACReaderLibrary/db/query_lexer.h b/YACReaderLibrary/db/query_lexer.h index 4cc2b61f..d3136d0b 100644 --- a/YACReaderLibrary/db/query_lexer.h +++ b/YACReaderLibrary/db/query_lexer.h @@ -11,6 +11,12 @@ public: opcode, word, quotedWord, + equal, // = + exactEqual, // == + minor, + major, + minorOrEqual, + majorOrEqual, undefined }; @@ -50,6 +56,9 @@ private: Token single(Token::Type type); Token word(); Token quotedWord(); + Token minor(); + Token major(); + Token equal(); bool isSpace(char c); }; diff --git a/YACReaderLibrary/db/query_parser.cpp b/YACReaderLibrary/db/query_parser.cpp index 2015fd73..631c1734 100644 --- a/YACReaderLibrary/db/query_parser.cpp +++ b/YACReaderLibrary/db/query_parser.cpp @@ -5,19 +5,25 @@ #include #include +#include + const std::map> QueryParser::fieldNames { - { FieldType::numeric, { "numpages", "number", "count", "arcnumber", "arccount" } }, - { FieldType::text, { "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes" } }, - { FieldType::boolean, { "isbis", "color", "read", "manga" } }, - { FieldType::date, { "date" } }, + // TODO_METADATA support dates + { FieldType::numeric, { "numpages", "count", "arccount", "alternateCount", "rating" } }, + { FieldType::text, { "number", "arcnumber", "title", "volume", "storyarc", "genere", "writer", "penciller", "inker", "colorist", "letterer", "coverartist", "publisher", "format", "agerating", "synopsis", "characters", "notes", "editor", "imprint", "teams", "locations", "series", "alternateSeries", "alternateNumber", "languageISO", "seriesGroup", "mainCharacterOrTeam", "review", "tags" } }, + { FieldType::boolean, { "color", "read", "edited", "hasBeenOpened" } }, + { FieldType::date, { "date", "added", "lastTimeOpened" } }, { FieldType::filename, { "filename" } }, { FieldType::folder, { "folder" } }, { FieldType::booleanFolder, { "completed", "finished" } }, + { FieldType::enumField, { "type" } }, + { FieldType::enumFieldFolder, { "foldertype" } } }; int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPosition) const { - if (t == "token") { + // TODO: add some semantic checks, not all operators apply to all fields + if (t == "expression") { ++bindPosition; if (toLower(children[0].t) == "all") { sqlString += "("; @@ -26,7 +32,15 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi } sqlString += "UPPER(c.filename) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ") OR "; sqlString += "UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; - } else if (isIn(fieldType(children[0].t), { FieldType::numeric, FieldType::boolean })) { + } else if (isIn(fieldType(children[0].t), { FieldType::numeric })) { + std::string sqlOperator; + if (expOperator == ":" || expOperator == "=" || expOperator == "==") { + sqlOperator = "="; + } else { + sqlOperator = expOperator; + } + sqlString += "ci." + children[0].t + " " + sqlOperator + " :bindPosition" + std::to_string(bindPosition) + " "; + } else if (isIn(fieldType(children[0].t), { FieldType::boolean, FieldType::enumField })) { sqlString += "ci." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; } else if (fieldType(children[0].t) == FieldType::filename) { sqlString += "(UPPER(c." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; @@ -34,8 +48,23 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi sqlString += "(UPPER(f.name) LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; } else if (fieldType(children[0].t) == FieldType::booleanFolder) { sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; + } else if (fieldType(children[0].t) == FieldType::enumFieldFolder) { + if (children[0].t == "foldertype") { + sqlString += "f.type = :bindPosition" + std::to_string(bindPosition) + " "; + } else { + sqlString += "f." + children[0].t + " = :bindPosition" + std::to_string(bindPosition) + " "; + } } else { - sqlString += "(UPPER(ci." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + if (expOperator == "=" || expOperator == ":" || expOperator == "") { + sqlString += "(UPPER(ci." + children[0].t + ") LIKE UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } else { + if (expOperator == "==") { + sqlString += "(UPPER(ci." + children[0].t + ") = UPPER(:bindPosition" + std::to_string(bindPosition) + ")) "; + } else { + // support for <,>,<=,>= in text fields makes sense for number, arcNumber, alternateNumber, but (TODO) the syntax won't prevent other fields from using this operators + sqlString += "(CAST(ci." + children[0].t + " as REAL) " + expOperator + " CAST(:bindPosition" + std::to_string(bindPosition) + " as REAL)) "; + } + } } } else if (t == "not") { sqlString += "(NOT "; @@ -54,7 +83,7 @@ int QueryParser::TreeNode::buildSqlString(std::string &sqlString, int bindPositi int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) const { - if (t == "token") { + if (t == "expression") { std::string bind_string(":bindPosition" + std::to_string(++bindPosition)); if (isIn(fieldType(children[0].t), { FieldType::numeric })) { selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t)); @@ -67,8 +96,30 @@ int QueryParser::TreeNode::bindValues(QSqlQuery &selectQuery, int bindPosition) } else { selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(value)); } + } else if ((isIn(fieldType(children[0].t), { FieldType::enumField, FieldType::enumFieldFolder }))) { + auto enumType = children[0].t; + auto value = toLower(children[1].t); + if (enumType == "type" || enumType == "foldertype") { + if (value == "comic") { + selectQuery.bindValue(QString::fromStdString(bind_string), 0); + } else if (value == "manga") { + selectQuery.bindValue(QString::fromStdString(bind_string), 1); + } else if (value == "westernmanga") { + selectQuery.bindValue(QString::fromStdString(bind_string), 2); + } else if (value == "webcomic" || value == "web") { + selectQuery.bindValue(QString::fromStdString(bind_string), 3); + } else if (value == "4koma" || value == "yonkoma") { + selectQuery.bindValue(QString::fromStdString(bind_string), 4); + } + } else { + selectQuery.bindValue(QString::fromStdString(bind_string), std::stoi(children[1].t)); + } } else { - selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString("%%" + children[1].t + "%%")); + if (expOperator == "=" || expOperator == ":" || expOperator == "") { + selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString("%%" + children[1].t + "%%")); + } else { + selectQuery.bindValue(QString::fromStdString(bind_string), QString::fromStdString(children[1].t)); + } } } else if (t == "not") { bindPosition = children[0].bindValues(selectQuery, bindPosition); @@ -151,6 +202,16 @@ void QueryParser::advance() currentToken = lexer.next(); } +bool QueryParser::isOperatorToken(Token::Type type) +{ + return type == Token::Type::equal || + type == Token::Type::exactEqual || + type == Token::Type::minor || + type == Token::Type::minorOrEqual || + type == Token::Type::major || + type == Token::Type::majorOrEqual; +} + QueryParser::FieldType QueryParser::fieldType(const std::string &str) { for (const auto &names : fieldNames) { @@ -223,25 +284,39 @@ QueryParser::TreeNode QueryParser::locationExpression() if (!isIn(tokenType(), { Token::Type::word, Token::Type::quotedWord })) { throw std::invalid_argument("Invalid syntax. Expected a lookup name or a word"); } + + return expression(); +} + +QueryParser::TreeNode QueryParser::expression() +{ + if (tokenType() == Token::Type::word) { + auto left = token(true); + if (isOperatorToken(tokenType())) { + auto expOperator = token(true); + if (tokenType() != Token::Type::word && tokenType() != Token::Type::quotedWord) { + throw std::invalid_argument("missing right operand"); + } + auto right = token(true); + + return TreeNode("expression", { TreeNode(toLower(left), {}), TreeNode(right, {}) }, expOperator); + } else { + return TreeNode("expression", { TreeNode("all", {}), TreeNode(left, {}) }); + } + } + return baseToken(); } QueryParser::TreeNode QueryParser::baseToken() { if (tokenType() == Token::Type::quotedWord) { - return TreeNode("token", { TreeNode("all", {}), TreeNode(token(true), {}) }); + return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) }); } - auto words(split(token(true), ':')); - - if (words.size() > 1 && fieldType(words[0].toStdString()) != FieldType::unknown) { - auto loc(toLower(words[0].toStdString())); - words.erase(words.begin()); - if (words.size() == 1 && tokenType() == Token::Type::quotedWord) { - return TreeNode("token", { TreeNode(loc, {}), TreeNode(token(true), {}) }); - } - return TreeNode("token", { TreeNode(loc, {}), TreeNode(join(words, ":"), {}) }); + if (tokenType() == Token::Type::word) { + return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) }); } - return TreeNode("token", { TreeNode("all", {}), TreeNode(join(words, ":"), {}) }); + return TreeNode("expression", { TreeNode("all", {}), TreeNode(token(true), {}) }); } diff --git a/YACReaderLibrary/db/query_parser.h b/YACReaderLibrary/db/query_parser.h index 69a5a315..db62b127 100644 --- a/YACReaderLibrary/db/query_parser.h +++ b/YACReaderLibrary/db/query_parser.h @@ -9,8 +9,8 @@ #include #include -#define SEARCH_FOLDERS_QUERY "SELECT DISTINCT f.id, f.parentId, f.name, f.path, f.finished, f.completed, f.numChildren, f.firstChildHash FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) WHERE " -#define SEARCH_COMICS_QUERY "SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.coverSizeRatio,ci.lastTimeOpened,ci.manga FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) LEFT JOIN folder f ON (f.id == c.parentId) WHERE " +#define SEARCH_FOLDERS_QUERY "SELECT DISTINCT * FROM folder f LEFT JOIN comic c ON (f.id = c.parentId) INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) WHERE " +#define SEARCH_COMICS_QUERY "SELECT ci.number,ci.title,c.fileName,ci.numPages,c.id,c.parentId,c.path,ci.hash,ci.read,ci.isBis,ci.currentPage,ci.rating,ci.hasBeenOpened,ci.date,ci.added,ci.type FROM comic c INNER JOIN comic_info ci ON (c.comicInfoId = ci.id) LEFT JOIN folder f ON (f.id == c.parentId) WHERE " /** * This class is used to generate an SQL query string from a search expression, @@ -25,7 +25,9 @@ * and_expression ::= not_expression [ [ 'and' ] and_expression ] * not_expression ::= [ 'not' ] location_expression * location_expression ::= base_token | ( '(' or_expression ')' ) - * base_token ::= a sequence of letters and colons, perhaps quoted + * expression :: base_token | base_token 'operator' base_token + * operator :: [':' '=' '<' '>' '<=' '=>'] + * base_token ::= a sequence of letters, perhaps quoted * * Usage Example: * QSqlQuery selectQuery(db); @@ -47,9 +49,10 @@ public: struct TreeNode { std::string t; std::vector children; + std::string expOperator; - explicit TreeNode(std::string t, std::vector children) - : t(t), children(children) + explicit TreeNode(std::string t, std::vector children, std::string expOperator = "") + : t(t), children(children), expOperator(expOperator) { } @@ -78,6 +81,8 @@ private: return std::find(v.begin(), v.end(), e) != v.end(); } + bool isOperatorToken(Token::Type type); + enum class FieldType { unknown, numeric, text, @@ -85,7 +90,9 @@ private: date, folder, booleanFolder, - filename }; + filename, + enumField, + enumFieldFolder }; static FieldType fieldType(const std::string &str); static std::string join(const QStringList &strings, const std::string &delim); @@ -95,6 +102,7 @@ private: TreeNode andExpression(); TreeNode notExpression(); TreeNode locationExpression(); + TreeNode expression(); TreeNode baseToken(); static const std::map> fieldNames; diff --git a/YACReaderLibrary/db_helper.cpp b/YACReaderLibrary/db_helper.cpp index 7d2bb8f1..201a48da 100644 --- a/YACReaderLibrary/db_helper.cpp +++ b/YACReaderLibrary/db_helper.cpp @@ -66,7 +66,7 @@ QList DBHelper::getFolderComicsFromLibraryForReading(qulonglong l return naturalSortLessThanCI(c1->name, c2->name); } else { if (c1->info.number.isNull() == false && c2->info.number.isNull() == false) { - return c1->info.number.toInt() < c2->info.number.toInt(); + return naturalSortLessThanCI(c1->info.number.toString(), c2->info.number.toString()); } else { return c2->info.number.isNull(); } @@ -637,9 +637,28 @@ void DBHelper::update(ComicInfo *comicInfo, QSqlDatabase &db) //-- // new 9.8 fields - "manga = :manga" + "manga = :manga," //-- - " WHERE id = :id "); + + // new 9.13 fields + "added = :added," + "type = :type," + "editor = :editor," + "imprint = :imprint," + "teams = :teams," + "locations = :locations," + "series = :series," + "alternateSeries = :alternateSeries," + "alternateNumber = :alternateNumber," + "alternateCount = :alternateCount," + "languageISO = :languageISO," + "seriesGroup = :seriesGroup," + "mainCharacterOrTeam = :mainCharacterOrTeam," + "review = :review," + "tags = :tags" + + //-- + " WHERE id = :id"); updateComicInfo.bindValue(":title", comicInfo->title); @@ -698,7 +717,28 @@ void DBHelper::update(ComicInfo *comicInfo, QSqlDatabase &db) updateComicInfo.bindValue(":coverSizeRatio", comicInfo->coverSizeRatio); updateComicInfo.bindValue(":originalCoverSize", comicInfo->originalCoverSize); + updateComicInfo.bindValue(":added", comicInfo->added); + auto intType = static_cast(comicInfo->type.value()); + updateComicInfo.bindValue(":type", intType); + updateComicInfo.bindValue(":editor", comicInfo->editor); + updateComicInfo.bindValue(":imprint", comicInfo->imprint); + updateComicInfo.bindValue(":teams", comicInfo->teams); + updateComicInfo.bindValue(":locations", comicInfo->locations); + updateComicInfo.bindValue(":series", comicInfo->series); + updateComicInfo.bindValue(":alternateSeries", comicInfo->alternateSeries); + updateComicInfo.bindValue(":alternateNumber", comicInfo->alternateNumber); + updateComicInfo.bindValue(":alternateCount", comicInfo->alternateCount); + updateComicInfo.bindValue(":languageISO", comicInfo->languageISO); + updateComicInfo.bindValue(":seriesGroup", comicInfo->seriesGroup); + updateComicInfo.bindValue(":mainCharacterOrTeam", comicInfo->mainCharacterOrTeam); + updateComicInfo.bindValue(":review", comicInfo->review); + updateComicInfo.bindValue(":tags", comicInfo->tags); + updateComicInfo.exec(); + + QLOG_INFO() << updateComicInfo.lastError().databaseText(); + QLOG_INFO() << updateComicInfo.lastError().text(); + QLOG_INFO() << updateComicInfo.lastQuery(); } void DBHelper::updateRead(ComicInfo *comicInfo, QSqlDatabase &db) @@ -718,12 +758,10 @@ void DBHelper::update(const Folder &folder, QSqlDatabase &db) QSqlQuery updateFolderInfo(db); updateFolderInfo.prepare("UPDATE folder SET " "finished = :finished, " - "completed = :completed, " - "manga = :manga " + "completed = :completed " "WHERE id = :id "); - updateFolderInfo.bindValue(":finished", folder.isFinished() ? 1 : 0); - updateFolderInfo.bindValue(":completed", folder.isCompleted() ? 1 : 0); - updateFolderInfo.bindValue(":manga", folder.isManga() ? 1 : 0); + updateFolderInfo.bindValue(":finished", folder.finished ? 1 : 0); + updateFolderInfo.bindValue(":completed", folder.completed ? 1 : 0); updateFolderInfo.bindValue(":id", folder.id); updateFolderInfo.exec(); } @@ -762,7 +800,7 @@ Folder DBHelper::updateChildrenInfo(qulonglong folderId, QSqlDatabase &db) } else { for (auto item : updatedSubfolders) { auto f = static_cast(item); - auto firstChildHash = f->getFirstChildHash(); + auto firstChildHash = f->firstChildHash; if (!firstChildHash.isEmpty()) { coverHash = firstChildHash; break; @@ -770,16 +808,16 @@ Folder DBHelper::updateChildrenInfo(qulonglong folderId, QSqlDatabase &db) } } - folder.setNumChildren(subfolders.count() + comics.count()); - folder.setFirstChildHash(coverHash); + folder.numChildren = subfolders.count() + comics.count(); + folder.firstChildHash = coverHash; QSqlQuery updateFolderInfo(db); updateFolderInfo.prepare("UPDATE folder SET " "numChildren = :numChildren, " "firstChildHash = :firstChildHash " "WHERE id = :id "); - updateFolderInfo.bindValue(":numChildren", folder.getNumChildren()); - updateFolderInfo.bindValue(":firstChildHash", folder.getFirstChildHash()); + updateFolderInfo.bindValue(":numChildren", folder.numChildren); + updateFolderInfo.bindValue(":firstChildHash", folder.firstChildHash); updateFolderInfo.bindValue(":id", folderId); updateFolderInfo.exec(); @@ -854,7 +892,7 @@ void DBHelper::updateReadingRemoteProgress(const ComicInfo &comicInfo, QSqlDatab updateComicInfo.bindValue(":read", comicInfo.read ? 1 : 0); updateComicInfo.bindValue(":currentPage", comicInfo.currentPage); updateComicInfo.bindValue(":hasBeenOpened", comicInfo.hasBeenOpened ? 1 : 0); - updateComicInfo.bindValue(":lastTimeOpened", QDateTime::currentMSecsSinceEpoch() / 1000); + updateComicInfo.bindValue(":lastTimeOpened", QDateTime::currentSecsSinceEpoch()); updateComicInfo.bindValue(":id", comicInfo.id); updateComicInfo.bindValue(":rating", comicInfo.rating); updateComicInfo.exec(); @@ -1067,7 +1105,7 @@ void DBHelper::updateFromRemoteClientWithHash(const QList &comics) updateComicInfo.bindValue(":read", info.read ? 1 : 0); updateComicInfo.bindValue(":currentPage", info.currentPage); updateComicInfo.bindValue(":hasBeenOpened", info.hasBeenOpened ? 1 : 0); - updateComicInfo.bindValue(":lastTimeOpened", QDateTime::currentMSecsSinceEpoch() / 1000); + updateComicInfo.bindValue(":lastTimeOpened", QDateTime::currentSecsSinceEpoch()); updateComicInfo.bindValue(":id", info.id); updateComicInfo.bindValue(":rating", info.rating); updateComicInfo.exec(); @@ -1194,12 +1232,16 @@ void DBHelper::updateComicsInfo(QList &comics, const QString &databaseP // inserts qulonglong DBHelper::insert(Folder *folder, QSqlDatabase &db) { + auto added = QDateTime::currentSecsSinceEpoch(); + folder->added = added; + QSqlQuery query(db); - query.prepare("INSERT INTO folder (parentId, name, path) " - "VALUES (:parentId, :name, :path)"); + query.prepare("INSERT INTO folder (parentId, name, path, added) " + "VALUES (:parentId, :name, :path, :added)"); query.bindValue(":parentId", folder->parentId); query.bindValue(":name", folder->name); query.bindValue(":path", folder->path); + query.bindValue(":added", added); query.exec(); return query.lastInsertId().toULongLong(); @@ -1207,16 +1249,21 @@ qulonglong DBHelper::insert(Folder *folder, QSqlDatabase &db) qulonglong DBHelper::insert(ComicDB *comic, QSqlDatabase &db, bool insertAllInfo) { + auto added = QDateTime::currentSecsSinceEpoch(); + if (!comic->info.existOnDb) { QSqlQuery comicInfoInsert(db); - comicInfoInsert.prepare("INSERT INTO comic_info (hash,numPages,coverSizeRatio,originalCoverSize) " - "VALUES (:hash,:numPages,:coverSizeRatio,:originalCoverSize)"); + + comicInfoInsert.prepare("INSERT INTO comic_info (hash,numPages,coverSizeRatio,originalCoverSize,added) " + "VALUES (:hash,:numPages,:coverSizeRatio,:originalCoverSize,:added)"); comicInfoInsert.bindValue(":hash", comic->info.hash); comicInfoInsert.bindValue(":numPages", comic->info.numPages); comicInfoInsert.bindValue(":coverSizeRatio", comic->info.coverSizeRatio); comicInfoInsert.bindValue(":originalCoverSize", comic->info.originalCoverSize); + comicInfoInsert.bindValue(":added", added); comicInfoInsert.exec(); comic->info.id = comicInfoInsert.lastInsertId().toULongLong(); + comic->info.added = added; comic->_hasCover = false; if (insertAllInfo) { @@ -1234,6 +1281,14 @@ qulonglong DBHelper::insert(ComicDB *comic, QSqlDatabase &db, bool insertAllInfo query.bindValue(":path", comic->path); query.exec(); + QSqlQuery updateFolder(db); + updateFolder.prepare("UPDATE folder SET " + "updated = :updated " + "WHERE id = :id "); + updateFolder.bindValue(":updated", added); + updateFolder.bindValue(":id", comic->parentId); + updateFolder.exec(); + return query.lastInsertId().toULongLong(); } @@ -1342,6 +1397,7 @@ void DBHelper::insertComicsInReadingList(const QList &comicsList, qulon db.commit(); } + // queries QList DBHelper::getFoldersFromParent(qulonglong parentId, QSqlDatabase &db, bool sort) { @@ -1356,20 +1412,33 @@ QList DBHelper::getFoldersFromParent(qulonglong parentId, QSqlDat int name = record.indexOf("name"); int path = record.indexOf("path"); + int finished = record.indexOf("finished"); + int completed = record.indexOf("completed"); + int manga = record.indexOf("manga"); int id = record.indexOf("id"); int numChildren = record.indexOf("numChildren"); int firstChildHash = record.indexOf("firstChildHash"); int customImage = record.indexOf("customImage"); + int type = record.indexOf("type"); + int added = record.indexOf("added"); + int updated = record.indexOf("updated"); Folder *currentItem; while (selectQuery.next()) { // TODO sort by sort indicator and name currentItem = new Folder(selectQuery.value(id).toULongLong(), parentId, selectQuery.value(name).toString(), selectQuery.value(path).toString()); - if (!selectQuery.value(numChildren).isNull() && selectQuery.value(numChildren).isValid()) - currentItem->setNumChildren(selectQuery.value(numChildren).toInt()); - currentItem->setFirstChildHash(selectQuery.value(firstChildHash).toString()); - currentItem->setCustomImage(selectQuery.value(customImage).toString()); + currentItem->finished = selectQuery.value(finished).toBool(); + currentItem->completed = selectQuery.value(completed).toBool(); + if (!selectQuery.value(numChildren).isNull() && selectQuery.value(numChildren).isValid()) { + currentItem->numChildren = selectQuery.value(numChildren).toInt(); + } + currentItem->firstChildHash = selectQuery.value(firstChildHash).toString(); + currentItem->customImage = selectQuery.value(customImage).toString(); + currentItem->manga = selectQuery.value(manga).toBool(); + currentItem->type = selectQuery.value(type).value(); + currentItem->added = selectQuery.value(added).toLongLong(); + currentItem->updated = selectQuery.value(updated).toLongLong(); int lessThan = 0; @@ -1524,21 +1593,21 @@ QList