From: APTX Date: Sat, 31 Aug 2013 10:19:32 +0000 (+0200) Subject: Initial version of dynamic model. X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=26e57a3de9d2b8b319247a77720da0fd64494062;p=localmylist.git Initial version of dynamic model. --- diff --git a/localmylist-management/localmylist-management.pro b/localmylist-management/localmylist-management.pro index 074c587..f3a1948 100644 --- a/localmylist-management/localmylist-management.pro +++ b/localmylist-management/localmylist-management.pro @@ -31,7 +31,8 @@ SOURCES += main.cpp\ fonts.cpp \ aniaddsyntaxhighlighter.cpp \ settingsdialog.cpp \ - codeeditor.cpp + codeeditor.cpp \ + tabs/dynamicmodeltab.cpp HEADERS += mainwindow.h \ databaseconnectiondialog.h \ @@ -53,7 +54,8 @@ HEADERS += mainwindow.h \ fonts.h \ aniaddsyntaxhighlighter.h \ settingsdialog.h \ - codeeditor.h + codeeditor.h \ + tabs/dynamicmodeltab.h FORMS += mainwindow.ui \ databaseconnectiondialog.ui \ @@ -64,7 +66,8 @@ FORMS += mainwindow.ui \ tabs/unknownfilestab.ui \ tabs/pendingrequesttab.ui \ tabs/databaselogtab.ui \ - tabs/clientlogtab.ui + tabs/clientlogtab.ui \ + tabs/dynamicmodeltab.ui include(../localmylist.pri) include(qtsingleapplication/qtsingleapplication.pri) diff --git a/localmylist-management/registertabs.cpp b/localmylist-management/registertabs.cpp index 8e0ea67..bf00af8 100644 --- a/localmylist-management/registertabs.cpp +++ b/localmylist-management/registertabs.cpp @@ -6,6 +6,7 @@ #include "tabs/pendingrequesttab.h" #include "tabs/databaselogtab.h" #include "tabs/clientlogtab.h" +#include "tabs/dynamicmodeltab.h" void registerTabs() { @@ -16,4 +17,5 @@ void registerTabs() TabWidget::registerTab(); TabWidget::registerTab(); TabWidget::registerTab(); + TabWidget::registerTab(); } diff --git a/localmylist-management/tabs/dynamicmodeltab.cpp b/localmylist-management/tabs/dynamicmodeltab.cpp new file mode 100644 index 0000000..ed881bd --- /dev/null +++ b/localmylist-management/tabs/dynamicmodeltab.cpp @@ -0,0 +1,191 @@ +#include "dynamicmodeltab.h" +#include "ui_dynamicmodeltab.h" + +#include "mainwindow.h" +#include "database.h" +#include "mylist.h" +#include "mylistfiltermodel.h" +#include "mylistitemdelegate.h" + +#include "dynamicmodel/model.h" +#include "dynamicmodel/datamodel.h" +#include "dynamicmodel/types.h" +#include "dynamicmodel/typerelation.h" + +using namespace LocalMyList::DynamicModel; + +DynamicModelTab::DynamicModelTab(QWidget *parent) : + AbstractTabBase(parent), + ui(new Ui::DynamicModelTab) +{ + ui->setupUi(this); + setLabel(name()); +} + +DynamicModelTab::~DynamicModelTab() +{ + delete ui; +} + +QString DynamicModelTab::staticId() +{ + return "dynamic_model"; +} + +QString DynamicModelTab::name() +{ + return tr("Dynamic Model"); +} + +void DynamicModelTab::init() +{ + dataModel = new DataModel(this); + dataModel->registerDataType(new AnimeType); + dataModel->registerDataType(new EpisodeType); + dataModel->registerDataType(new FileType); + dataModel->registerDataType(new FileLocationType); + dataModel->registerDataType(new AnimeTitleType); + dataModel->registerTypeRelation(new RootAnimeRelation(this)); + dataModel->registerTypeRelation(new RootEpisodeRelation(this)); + dataModel->registerTypeRelation(new AnimeEpisodeRelation(this)); + dataModel->registerTypeRelation(new EpisodeFileRelation(this)); + dataModel->registerTypeRelation(new FileFileLocationRelation(this)); + dataModel->registerTypeRelation(new RootAnimeTitleRelation(this)); + dataModel->registerTypeRelation(new AnimeTitleAnimeRelation(this)); + dataModel->registerTypeRelation(new AnimeTitleEpisodeRelation(this)); + dataModel->registerTypeRelation(new AnimeAnimeTitleRelation(this)); + + model = new Model(this); + model->setDataModel(dataModel); + + myListFilterModel = new MyListFilterModel(this); + myListFilterModel->setSourceModel(model); + ui->myListView->setModel(myListFilterModel); + ui->myListView->setItemDelegate(new MyListItemDelegate(ui->myListView)); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + ui->myListView->header()->setSectionResizeMode(0, QHeaderView::Stretch); +#else + ui->myListView->header()->setResizeMode(0, QHeaderView::Stretch); +#endif + ui->myListView->header()->setStretchLastSection(false); + ui->myListView->header()->resizeSection(4, 200); + + ui->filterType->addItems(QStringList() + << tr("Fixed String") + << tr("Wildcard") + << tr("Regexp")); + + connect(ui->myListView, SIGNAL(renameTest(int)), mainWindow(), SLOT(openRenameScriptEditor(int))); + connect(ui->myListView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), this, SLOT(currentSelectionChanged(QModelIndex,QModelIndex))); + connect(ui->filterInput, SIGNAL(textChanged(QString)), this, SLOT(currentSelectionChanged())); + + connect(model, SIGNAL(queryChanged(QString)), ui->modelQuery, SLOT(setText(QString))); + //model->setQuery("anime|episode|file|file_location"); + model->setQuery("anime|episode"); +} + +void DynamicModelTab::activate() +{ + ui->filterInput->setFocus(); +} + +void DynamicModelTab::reload() +{ + model->reload(); +} + +void DynamicModelTab::changeEvent(QEvent *e) +{ + QWidget::changeEvent(e); + switch (e->type()) { + case QEvent::LanguageChange: + ui->retranslateUi(this); + break; + default: + break; + } +} + +void DynamicModelTab::on_filterInput_textChanged(const QString &filter) +{ + switch (ui->filterType->currentIndex()) + { + case 1: + myListFilterModel->setFilterWildcard(filter); + break; + case 2: + myListFilterModel->setFilterRegExp(filter); + break; + case 0: + default: + myListFilterModel->setFilterFixedString(filter); + break; + } +} + +void DynamicModelTab::on_filterType_currentIndexChanged(int) +{ + on_filterInput_textChanged(ui->filterInput->text()); +} + +void DynamicModelTab::on_filterInput_keyUpPressed() +{ + selectedRow = qMax(-1, selectedRow - 1); + updateSelection(); + +} + +void DynamicModelTab::on_filterInput_keyDownPressed() +{ + int newSelectedRow = qMin(model->rowCount() - 1, selectedRow + 1); + + if (selectedRow == newSelectedRow) + return; + + selectedRow = newSelectedRow; + updateSelection(); +} + +void DynamicModelTab::on_filterInput_returnPressed() +{ + if (selectedRow < 0) + return; + + const QModelIndex idx = myListFilterModel->index(selectedRow, 0); +// on_myListView_openFileRequested(idx); +} + +void DynamicModelTab::currentSelectionChanged(const QModelIndex ¤t, const QModelIndex &) +{ + selectedRow = current.row(); +} + +void DynamicModelTab::currentSelectionChanged() +{ + selectedRow = -1; +} + +void DynamicModelTab::updateSelection() +{ + if (selectedRow < 0) + { + ui->myListView->selectionModel()->clear(); + return; + } + + const QModelIndex idx = myListFilterModel->index(selectedRow, 0); + ui->myListView->selectionModel()-> + setCurrentIndex(idx, QItemSelectionModel::ClearAndSelect + | QItemSelectionModel::Rows); +} + +void DynamicModelTab::on_modelQuery_returnPressed() +{ + model->setQuery(ui->modelQuery->text()); +} + +void DynamicModelTab::on_modelQueryButton_clicked() +{ + model->setQuery(ui->modelQuery->text()); +} diff --git a/localmylist-management/tabs/dynamicmodeltab.h b/localmylist-management/tabs/dynamicmodeltab.h new file mode 100644 index 0000000..7023d1f --- /dev/null +++ b/localmylist-management/tabs/dynamicmodeltab.h @@ -0,0 +1,65 @@ +#ifndef DYNAMICMODELTAB_H +#define DYNAMICMODELTAB_H + +#include "abstracttab.h" + +class MyListFilterModel; + +namespace LocalMyList { +namespace DynamicModel { +class DataModel; +class Model; +} +} + +namespace Ui { +class DynamicModelTab; +} + +class DynamicModelTab : public AbstractTabBase +{ + Q_OBJECT + +public: + explicit DynamicModelTab(QWidget *parent = 0); + ~DynamicModelTab(); + + static QString staticId(); + static QString name(); + + void init(); + void activate(); + + void reload(); + +protected: + void changeEvent(QEvent *e); + +private slots: + void on_filterInput_textChanged(const QString &filter); + void on_filterType_currentIndexChanged(int); + + void on_filterInput_keyUpPressed(); + void on_filterInput_keyDownPressed(); + void on_filterInput_returnPressed(); + + void currentSelectionChanged(const QModelIndex ¤t, const QModelIndex &previous); + void currentSelectionChanged(); + + void on_modelQuery_returnPressed(); + + void on_modelQueryButton_clicked(); + +private: + void updateSelection(); + + Ui::DynamicModelTab *ui; + + MyListFilterModel *myListFilterModel; + LocalMyList::DynamicModel::DataModel *dataModel; + LocalMyList::DynamicModel::Model *model; + + int selectedRow; +}; + +#endif // DYNAMICMODELTAB_H diff --git a/localmylist-management/tabs/dynamicmodeltab.ui b/localmylist-management/tabs/dynamicmodeltab.ui new file mode 100644 index 0000000..710f419 --- /dev/null +++ b/localmylist-management/tabs/dynamicmodeltab.ui @@ -0,0 +1,62 @@ + + + DynamicModelTab + + + + 0 + 0 + 637 + 458 + + + + Form + + + + 0 + + + 0 + + + + + + + + + + + + + + + + Set + + + + + + + + + + + + + FilterLineEdit + QLineEdit +
filterlineedit.h
+
+ + MyListView + QTreeView +
mylistview.h
+
+
+ + +
diff --git a/localmylist/database.cpp b/localmylist/database.cpp index 3396933..7614c7f 100644 --- a/localmylist/database.cpp +++ b/localmylist/database.cpp @@ -855,7 +855,7 @@ bool Database::updateFilesFromPendingMyListUpdate(const PendingMyListUpdate &req bool Database::addTitle(const AnimeTitle &title) { - QSqlQuery &q = prepare("INSERT INTO anime_title VALUES(:aid, :type, :language, :title)"); + QSqlQuery &q = prepare("INSERT INTO anime_title VALUES(DEFAULT, :aid, :type, :language, :title)"); q.bindValue(":aid", title.aid); q.bindValue(":type", int(title.type)); @@ -1587,6 +1587,14 @@ bool Database::connect() { d = new DatabaseInternal(); d->db = QSqlDatabase::addDatabase("QPSQL", connectionName); + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + QObject::connect(d->db.driver(), SIGNAL(notification(QString,QSqlDriver::NotificationSource,QVariant)), + this, SLOT(handleNotification(QString,QSqlDriver::NotificationSource,QVariant))); +#else + QObject::connect(d->db.driver(), SIGNAL(notification(QString)), + this, SLOT(handleNotification(QString))); +#endif } else if (d->db.isOpen()) { @@ -1608,13 +1616,7 @@ bool Database::connect() qWarning() << "Failed opening database connection." << d->db.lastError(); return success; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) - QObject::connect(d->db.driver(), SIGNAL(notification(QString,QSqlDriver::NotificationSource,QVariant)), - this, SLOT(handleNotification(QString,QSqlDriver::NotificationSource,QVariant))); -#else - QObject::connect(d->db.driver(), SIGNAL(notification(QString)), - this, SLOT(handleNotification(QString))); -#endif + // Workaround for https://bugreports.qt-project.org/browse/QTBUG-30076 #if QT_VERSION <= QT_VERSION_CHECK(4, 8, 4) || (QT_VERSION > QT_VERSION_CHECK(5, 0, 0) && QT_VERSION <= QT_VERSION_CHECK(5, 0, 2)) @@ -1650,7 +1652,16 @@ void Database::disconnect() emit disconnected(); } -void Database::readAnimeData(SqlResultIteratorInterface &result, Anime &data, int offset) +void Database::readAnimeTitleData(const SqlResultIteratorInterface &result, AnimeTitle &data, int offset) +{ + data.titleId = result.value(offset++).toInt(); + data.aid = result.value(offset++).toInt(); + data.type = AnimeTitle::TitleType(result.value(offset++).toInt()); + data.language = result.value(offset++).toString(); + data.title = result.value(offset++).toString(); +} + +void Database::readAnimeData(const SqlResultIteratorInterface &result, Anime &data, int offset) { data.aid = result.value(offset++).toInt(); data.entryAdded = result.value(offset++).toDateTime(); @@ -1677,7 +1688,7 @@ void Database::readAnimeData(SqlResultIteratorInterface &result, Anime &data, in data.myTempVoteDate = result.value(offset++).toDateTime(); } -void Database::readEpisodeData(SqlResultIteratorInterface &result, Episode &data, int offset) +void Database::readEpisodeData(const SqlResultIteratorInterface &result, Episode &data, int offset) { data.eid = result.value(offset++).toInt(); data.aid = result.value(offset++).toInt(); @@ -1700,7 +1711,7 @@ void Database::readEpisodeData(SqlResultIteratorInterface &result, Episode &data data.myVoteDate = result.value(offset++).toDateTime(); } -void Database::readFileData(SqlResultIteratorInterface &result, File &data, int offset) +void Database::readFileData(const SqlResultIteratorInterface &result, File &data, int offset) { data.fid = result.value(offset++).toInt(); data.eid = result.value(offset++).toInt(); @@ -1738,7 +1749,7 @@ void Database::readFileData(SqlResultIteratorInterface &result, File &data, int data.myOther = result.value(offset++).toString(); } -void Database::readFileLocationData(SqlResultIteratorInterface &result, FileLocation &data, int offset) +void Database::readFileLocationData(const SqlResultIteratorInterface &result, FileLocation &data, int offset) { data.locationId = result.value(offset++).toInt(); data.fid = result.value(offset++).toInt(); @@ -1748,7 +1759,7 @@ void Database::readFileLocationData(SqlResultIteratorInterface &result, FileLoca data.failedRename = result.value(offset++).toBool(); } -void Database::readOpenFileData(SqlResultIteratorInterface &result, OpenFileData &data, int offset) +void Database::readOpenFileData(const SqlResultIteratorInterface &result, OpenFileData &data, int offset) { data.fid = result.value(offset++).toInt(); data.animeTitle = result.value(offset++).toString(); @@ -1757,7 +1768,7 @@ void Database::readOpenFileData(SqlResultIteratorInterface &result, OpenFileData data.path = result.value(offset++).toString(); } -void Database::readUnknownFileData(SqlResultIteratorInterface &result, UnknownFile &data, int offset) +void Database::readUnknownFileData(const SqlResultIteratorInterface &result, UnknownFile &data, int offset) { data.ed2k = result.value(offset++).toByteArray(); data.size = result.value(offset++).toLongLong(); @@ -1765,7 +1776,7 @@ void Database::readUnknownFileData(SqlResultIteratorInterface &result, UnknownFi data.path = result.value(offset++).toString(); } -void Database::readPendingMyListUpdateData(SqlResultIteratorInterface &query, PendingMyListUpdate &data, int offset) +void Database::readPendingMyListUpdateData(const SqlResultIteratorInterface &query, PendingMyListUpdate &data, int offset) { data.updateId = query.value(offset++).toLongLong(); data.fid = query.value(offset++).toInt(); @@ -1793,6 +1804,11 @@ void Database::readPendingMyListUpdateData(SqlResultIteratorInterface &query, Pe data.lid = query.value(offset++).toInt(); } +QString Database::animeTitleFields() +{ + return "at.title_id, at.aid, at.type, at.language, at.title "; +} + QString Database::animeFields() { return diff --git a/localmylist/database.h b/localmylist/database.h index fb3f0c4..996450b 100644 --- a/localmylist/database.h +++ b/localmylist/database.h @@ -150,14 +150,16 @@ public slots: bool connect(); void disconnect(); - static void readAnimeData(SqlResultIteratorInterface &result, Anime &data, int offset = 0); - static void readEpisodeData(SqlResultIteratorInterface &result, Episode &data, int offset = 0); - static void readFileData(SqlResultIteratorInterface &result, File &data, int offset = 0); - static void readFileLocationData(SqlResultIteratorInterface &result, FileLocation &data, int offset = 0); - static void readOpenFileData(SqlResultIteratorInterface &result, OpenFileData &data, int offset = 0); - static void readUnknownFileData(SqlResultIteratorInterface &result, UnknownFile &data, int offset = 0); - static void readPendingMyListUpdateData(SqlResultIteratorInterface &result, PendingMyListUpdate &data, int offset = 0); - + static void readAnimeTitleData(const SqlResultIteratorInterface &result, AnimeTitle &data, int offset = 0); + static void readAnimeData(const SqlResultIteratorInterface &result, Anime &data, int offset = 0); + static void readEpisodeData(const SqlResultIteratorInterface &result, Episode &data, int offset = 0); + static void readFileData(const SqlResultIteratorInterface &result, File &data, int offset = 0); + static void readFileLocationData(const SqlResultIteratorInterface &result, FileLocation &data, int offset = 0); + static void readOpenFileData(const SqlResultIteratorInterface &result, OpenFileData &data, int offset = 0); + static void readUnknownFileData(const SqlResultIteratorInterface &result, UnknownFile &data, int offset = 0); + static void readPendingMyListUpdateData(const SqlResultIteratorInterface &result, PendingMyListUpdate &data, int offset = 0); + + static QString animeTitleFields(); static QString animeFields(); static QString episodeFields(); static QString fileFields(); diff --git a/localmylist/databaseclasses.cpp b/localmylist/databaseclasses.cpp index 703ffe4..ff4005c 100644 --- a/localmylist/databaseclasses.cpp +++ b/localmylist/databaseclasses.cpp @@ -4,6 +4,7 @@ namespace LocalMyList { AnimeTitle::AnimeTitle(int aid, TitleType type, const QString &language, const QString &title) { + this->titleId = 0; this->aid = aid; this->type = type; this->language = language; diff --git a/localmylist/databaseclasses.h b/localmylist/databaseclasses.h index 912a6dc..0366bda 100644 --- a/localmylist/databaseclasses.h +++ b/localmylist/databaseclasses.h @@ -18,6 +18,7 @@ struct LOCALMYLISTSHARED_EXPORT AnimeTitle OfficialTitle = 4 }; + int titleId; int aid; TitleType type; QString language; diff --git a/localmylist/dynamicmodel/data.cpp b/localmylist/dynamicmodel/data.cpp new file mode 100644 index 0000000..57f85df --- /dev/null +++ b/localmylist/dynamicmodel/data.cpp @@ -0,0 +1,361 @@ +#include "data.h" + +#include "node.h" +#include "datatype.h" + +#include + +namespace LocalMyList { +namespace DynamicModel { + +QString stateIdToState(int id) +{ + switch (id) + { + case -1: + return QObject::tr("Mixed"); + case 0: + return QObject::tr("Unknown"); + case 1: + return QObject::tr("On HDD"); + case 2: + return QObject::tr("On Cd"); + case 3: + return QObject::tr("Deleted"); + } + return QString(); +} + +Data::Data(DataType *dataType) : m_type(dataType) +{ +} + +Data::~Data() +{ + Q_ASSERT(references.isEmpty()); +} + +QVariant Data::data(int row, int role) const +{ + Q_UNUSED(row); + Q_UNUSED(role); + return QVariant(); +} + +void Data::ref(Node *node) +{ + Q_ASSERT(!references.contains(node)); + + references.append(node); +} + +void Data::deref(Node *node) +{ + Q_ASSERT(references.isEmpty()); + + bool removed = references.removeOne(node); + + Q_ASSERT_X(removed, "deref", "Removing node that was not referenced"); + + if (references.isEmpty()) + type()->released(this); +} + +void Data::updated(Data *oldData) +{ + foreach (Node *node, references) + { + Q_ASSERT_X(node->parent(), "dynamicmodel", "Updating node without parent"); + node->updated(UpdateOperation); +// node->parent()->childUpdate(node, oldData, UpdateOperation); + } +} + +void Data::added(Data *newData) +{ + foreach (Node *node, references) + { + if (node->childDataType() == newData->type()) + node->childAdded(newData); + } +} + +AnimeData::AnimeData(DataType *dataType) : Data(dataType) +{ +} + +AnimeData &AnimeData::operator=(AnimeData &other) +{ + animeData = other.animeData; + episodesInMyList = other.episodesInMyList; + watchedEpisodes = other.episodesInMyList; + myState = other.myState; + + return *this; +} + +int AnimeData::id() const +{ + return animeData.aid; +} + +QVariant AnimeData::data(int column, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case 0: + return animeData.titleRomaji; + case 1: + if (animeData.totalEpisodeCount) + return QString("%1 of %2") + .arg(episodesInMyList).arg(animeData.totalEpisodeCount); + return QString("%1 of (%2)") + .arg(episodesInMyList) + .arg(qMax(animeData.highestEpno, + episodesInMyList)); + case 2: + if (animeData.rating < 1) + return "n/a"; + return QString::number(animeData.rating, 'f', 2); + case 3: + if (animeData.myVote < 1) + return "n/a"; + return QString::number(animeData.myVote, 'f', 2); + case 4: + return QString("%1 of %2").arg(watchedEpisodes) + .arg(episodesInMyList); + case 5: + return stateIdToState(myState); + } + break; + case Qt::ToolTipRole: + switch (column) + { + case 0: + if (!animeData.titleEnglish.isEmpty() && !animeData.titleKanji.isEmpty()) + return QString("%1 -- %2").arg(animeData.titleEnglish) + .arg(animeData.titleKanji); + if (!animeData.titleEnglish.isEmpty()) + return animeData.titleEnglish; + if (!animeData.titleKanji.isEmpty()) + return animeData.titleKanji; + } + break; + case Qt::EditRole: + switch (column) + { + case 3: + return animeData.myVote; + } + break; + } + + return QVariant(); +} + +// ========================================================== + +EpisodeData::EpisodeData(DataType *dataType) : Data(dataType) +{ +} + +EpisodeData &EpisodeData::operator=(EpisodeData &other) +{ + episodeData = other.episodeData; + myStates = other.myStates; + watchedDate = other.watchedDate; + episodeTypeOrdering = other.episodeTypeOrdering; + return *this; +} + +int EpisodeData::id() const +{ + return episodeData.eid; +} + +QVariant EpisodeData::data(int column, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case 0: + return episodeData.titleEnglish; + case 1: + return episodeData.type + QString::number(episodeData.epno); + case 2: + if (episodeData.rating < 1) + return "n/a"; + return QString::number(episodeData.rating, 'f', 2); + case 3: + if (episodeData.myVote < 1) + return "n/a"; + return QString::number(episodeData.myVote, 'f', 2); + case 4: + if (!watchedDate.isValid()) + return QObject::tr("No"); + return QObject::tr("Yes, on %1").arg(watchedDate.toString()); + case 5: + return stateIdToState(myStates); + } + break; + case Qt::ToolTipRole: + switch (column) + { + case 0: + if (!episodeData.titleEnglish.isEmpty() && !episodeData.titleKanji.isEmpty()) + return QString("%1 -- %2").arg(episodeData.titleKanji) + .arg(episodeData.titleRomaji); + if (!episodeData.titleEnglish.isEmpty()) + return episodeData.titleEnglish; + if (!episodeData.titleKanji.isEmpty()) + return episodeData.titleKanji; + } + break; + case Qt::EditRole: + switch (column) + { + case 3: + return episodeData.myVote; + } + break; + } + + return QVariant(); +} + +FileData::FileData(DataType *dataType) : Data(dataType) +{ +} + +FileData &FileData::operator=(FileData &other) +{ + fileData = other.fileData; + return *this; +} + +int FileData::id() const +{ + return fileData.fid; +} + +QVariant FileData::data(int column, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (column) + { + case 0: + if (!fileData.gid) + return QObject::tr("Unknown"); + return fileData.groupName; + case 1: + return "v" + QString::number(fileData.version); + case 2: + return fileData.quality; + case 3: + return ""; + case 4: + if (!fileData.myWatched.isValid()) + return QObject::tr("No"); + return QObject::tr("Yes, on %1").arg(fileData.myWatched.toString()); + case 5: + return stateIdToState(fileData.myState); + } + return QVariant(); +} + +FileLocationData::FileLocationData(DataType *dataType) : Data(dataType) +{ +} + +FileLocationData &FileLocationData::operator=(FileLocationData &other) +{ + fileLocationData = other.fileLocationData; + hostName = other.hostName; + return *this; +} + +int FileLocationData::id() const +{ + return fileLocationData.locationId; +} + +QVariant FileLocationData::data(int column, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (column) + { + case 0: + return fileLocationData.path; + case 1: + return QString("%1 (%2)").arg(hostName) + .arg(fileLocationData.hostId); + case 2: + return ""; + case 3: + return ""; + case 4: + if (!fileLocationData.renamed.isValid()) + return QObject::tr("No"); + if (fileLocationData.failedRename) + return QObject::tr("Rename failed"); + return QObject::tr("Yes, on %1").arg(fileLocationData.renamed.toString()); + } + return QVariant(); +} + +AnimeTitleData::AnimeTitleData(DataType *dataType) : Data(dataType) +{ + +} + +AnimeTitleData &AnimeTitleData::operator=(AnimeTitleData &other) +{ + animeTitleData = other.animeTitleData; + return *this; +} + +int AnimeTitleData::id() const +{ + return animeTitleData.titleId; +} + +QVariant AnimeTitleData::data(int column, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + switch (column) + { + case 0: + return animeTitleData.title; + case 1: + return animeTitleData.language; + case 2: + switch(animeTitleData.type) + { + case AnimeTitle::PrimaryTitle: + return QObject::tr("Official"); + case AnimeTitle::Synonym: + return QObject::tr("Synonym"); + case AnimeTitle::ShortTitle: + return QObject::tr("Short"); + case AnimeTitle::OfficialTitle: + return QObject::tr("Official"); + default: + return QObject::tr("Unknown"); + } + } + return QVariant(); +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/data.h b/localmylist/dynamicmodel/data.h new file mode 100644 index 0000000..ede3d4c --- /dev/null +++ b/localmylist/dynamicmodel/data.h @@ -0,0 +1,110 @@ +#ifndef DATA_H +#define DATA_H + +#include "../localmylist_global.h" + +#include +#include + +#include "../databaseclasses.h" + +namespace LocalMyList { +namespace DynamicModel { + +class DataType; +class Node; +typedef QList NodeList; + +class LOCALMYLISTSHARED_EXPORT Data +{ +public: + Data(DataType *dataType); + ~Data(); + + virtual int id() const = 0; + virtual QVariant data(int row, int role) const; + + DataType *type() const { return m_type; } + // Referencing + void ref(Node *node); + void deref(Node *node); + + void updated(Data *oldData); + void added(Data *newData); + +private: + NodeList references; + DataType * const m_type; +}; + +class LOCALMYLISTSHARED_EXPORT AnimeData : public Data +{ +public: + AnimeData(DataType *dataType); + AnimeData &operator=(AnimeData &other); + + int id() const; + QVariant data(int column, int role) const; + + Anime animeData; + int episodesInMyList; + int watchedEpisodes; + int myState; +}; + +class LOCALMYLISTSHARED_EXPORT EpisodeData : public Data +{ +public: + EpisodeData(DataType *dataType); + EpisodeData &operator=(EpisodeData &other); + + int id() const; + QVariant data(int column, int role) const; + + Episode episodeData; + QDateTime watchedDate; + int episodeTypeOrdering; + int myStates; +}; + +class LOCALMYLISTSHARED_EXPORT FileData : public Data +{ +public: + FileData(DataType *dataType); + FileData &operator=(FileData &other); + + int id() const; + QVariant data(int column, int role) const; + + File fileData; +}; + +class LOCALMYLISTSHARED_EXPORT FileLocationData : public Data +{ +public: + FileLocationData(DataType *dataType); + FileLocationData &operator=(FileLocationData &other); + + int id() const; + QVariant data(int column, int role) const; + + FileLocation fileLocationData; + QString hostName; +}; + +class LOCALMYLISTSHARED_EXPORT AnimeTitleData : public Data +{ +public: + AnimeTitleData(DataType *dataType); + AnimeTitleData &operator=(AnimeTitleData &other); + + int id() const; + QVariant data(int column, int role) const; + + AnimeTitle animeTitleData; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // DATA_H diff --git a/localmylist/dynamicmodel/datamodel.cpp b/localmylist/dynamicmodel/datamodel.cpp new file mode 100644 index 0000000..7e7232d --- /dev/null +++ b/localmylist/dynamicmodel/datamodel.cpp @@ -0,0 +1,81 @@ +#include "datamodel.h" + +#include "datatype.h" +#include "typerelation.h" + +namespace LocalMyList { +namespace DynamicModel { + +DataModel::DataModel(QObject *parent) : QObject(parent) +{ +} + +DataModel::~DataModel() +{ + qDeleteAll(dataTypes); +} + +bool DataModel::registerDataType(DataType *dataType) +{ + if (dataTypes.contains(dataType)) + return true; + + if (dataTypeNames.contains(dataType->name())) + return false; + + dataTypes.insert(dataType); + dataTypeNames.insert(dataType->name(), dataType); + dataType->m_model = this; + dataType->registerd(); + return true; +} + +bool DataModel::registerTypeRelation(TypeRelation *typeRelation) +{ + Q_ASSERT(typeRelation); + + if (!typeRelation->sourceType().isEmpty() && !dataTypeNames.contains(typeRelation->sourceType())) + return false; + + const auto destinationTypeIt = dataTypeNames.find(typeRelation->destinationType()); + if (destinationTypeIt == dataTypeNames.constEnd()) + return false; + + auto it = typeRelations.find(typeRelation->sourceType()); + + if (it == typeRelations.end()) + it = typeRelations.insert(typeRelation->sourceType(), QHash()); + + auto inner = it.value().find(typeRelation->destinationType()); + if (inner != it.value().end()) + return false; + + it.value().insert(typeRelation->destinationType(), typeRelation); + typeRelation->m_dataType = destinationTypeIt.value(); + return true; +} + +DataType *DataModel::dataType(const QString &name) const +{ + DataType *t = dataTypeNames.value(name, 0); + Q_ASSERT(t); + return t; +} + +TypeRelation *DataModel::typeRelation(const QString &source, const QString &destiantion) +{ + const auto it = typeRelations.find(source); + + if (it == typeRelations.constEnd()) + return 0; + + const auto inner = it.value().find(destiantion); + + if (inner == it.value().constEnd()) + return 0; + + return inner.value(); +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/datamodel.h b/localmylist/dynamicmodel/datamodel.h new file mode 100644 index 0000000..8f589ac --- /dev/null +++ b/localmylist/dynamicmodel/datamodel.h @@ -0,0 +1,56 @@ +#ifndef DATAMODEL_H +#define DATAMODEL_H + +#include +#include "../localmylist_global.h" +#include "dynamicmodel_global.h" +#include "node.h" + +#include +#include +#include + +namespace LocalMyList { +namespace DynamicModel { + +class DataType; +class TypeRelation; + +class LOCALMYLISTSHARED_EXPORT DataModel : public QObject +{ + Q_OBJECT + +public: + DataModel(QObject *parent = 0); + ~DataModel(); + + bool registerDataType(DataType *dataType); + bool registerTypeRelation(TypeRelation *typeRelation); + + DataType *dataType(const QString &name) const; + TypeRelation *typeRelation(const QString &source, const QString &destiantion); + + + +private slots: +/* void animeUpdate(int aid); + void episodeUpdate(int eid, int aid); + void fileUpdate(int fid, int eid, int aid); + void fileLocationUpdate(int id); + + void animeInsert(int aid); + void episodeInsert(int eid, int aid); + void fileInsert(int fid, int eid, int aid); + void fileLocationInsert(int locationId, int fid); +*/ +private: + QHash dataTypeNames; + QSet dataTypes; + + QHash > typeRelations; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // DATAMODEL_H diff --git a/localmylist/dynamicmodel/datatype.cpp b/localmylist/dynamicmodel/datatype.cpp new file mode 100644 index 0000000..2457f0f --- /dev/null +++ b/localmylist/dynamicmodel/datatype.cpp @@ -0,0 +1,90 @@ +#include "datatype.h" + +#include +#include +#include "../database.h" +#include "../mylist.h" +#include "data.h" + +namespace LocalMyList { +namespace DynamicModel { + +DataType::DataType(QObject *parent) : QObject(parent), m_size(0), m_model(0) +{ + query = new SqlAsyncQuery(this); +} + +DataType::~DataType() +{ + +} + +DataModel *DataType::model() const +{ + return m_model; +} + +QStringList DataType::availableChildRelations() const +{ + return QStringList(); +} + +Data *DataType::data(int key) const +{ + return m_dataStore.value(key, 0); +} + +void DataType::registerd() +{ +} + +void DataType::unregistered() +{ +} + +void DataType::update(Data *data) +{ + Q_UNUSED(data); +} + +void DataType::childUpdate(Node *parent, const Data *oldData, Operation operation) +{ + Q_UNUSED(parent); + Q_UNUSED(oldData); + Q_UNUSED(operation); +} + +void DataType::released(Data *data) +{ + Q_ASSERT(data != 0); + + bool removed = m_dataStore.remove(data->id()); + + Q_ASSERT_X(removed, "released", "releasing node not in data store"); + + delete data; +} + +int DataType::sizeHelper(const QString &tableName, const QString &keyName) const +{ + if (m_size) + return m_size; + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT count(%1) FROM %2") + .arg(keyName, tableName)); + + if (!MyList::instance()->database()->exec(q)) + return 0; + + if (!q.next()) + return 0; + + m_size = q.value(0).toInt(); + q.finish(); + + return m_size; +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/datatype.h b/localmylist/dynamicmodel/datatype.h new file mode 100644 index 0000000..76b338d --- /dev/null +++ b/localmylist/dynamicmodel/datatype.h @@ -0,0 +1,103 @@ +#ifndef ABSTRACTDATATYPE_H +#define ABSTRACTDATATYPE_H + +#include "../localmylist_global.h" +#include "datamodel.h" +#include "../sqlasyncquery.h" + +#include +#include +#include + +namespace LocalMyList { +namespace DynamicModel { + +class Data; +class Node; +class DataModel; + +typedef bool (*NodeCompare)(Node *a, Node *b); + +class LOCALMYLISTSHARED_EXPORT DataType : public QObject +{ + friend class DataModel; + Q_OBJECT +public: + DataType(QObject *parent = 0); + virtual ~DataType(); + + DataModel *model() const; + + virtual QString name() const = 0; + QStringList availableChildRelations() const; + + virtual QString baseQuery() const = 0; + + Data *data(int key) const; + virtual int size() const = 0; + + // Register + virtual void registerd(); + virtual void unregistered(); + + // Update + virtual void update(Data *data); + virtual void childUpdate(Node *child, const Data *oldData, Operation operation); + + // Release + void released(Data *data); + + virtual NodeCompare nodeCompareFunction() const = 0; + + // Type relation interface + virtual Data *readEntry(const SqlResultIteratorInterface &it) = 0; + +protected: + int sizeHelper(const QString &tableName, const QString &keyName) const; + + template Data *genericReadEntry(const SqlResultIteratorInterface &it, F func) + { + auto typedData = new T(this); + + func(*typedData, it); + + Data *newData = typedData; + Data *currentData = data(typedData->id()); + if (currentData) + { + delete typedData; + newData = currentData; + } + else + { + m_dataStore.insert(typedData->id(), newData); + } + return newData; + } + + template void genericUpdate(Data *data, SqlResultIteratorInterface &it, F func) + { + if (!it.next()) + return; + + T *typedData = static_cast(data); + T oldTypedData(this); + + oldTypedData = *typedData; + func(*typedData, it); + + data->updated(&oldTypedData); + } + + SqlAsyncQuery *query; + mutable int m_size; + QHash m_dataStore; + +private: + DataModel *m_model; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // ABSTRACTDATATYPE_H diff --git a/localmylist/dynamicmodel/dynamicmodel_global.h b/localmylist/dynamicmodel/dynamicmodel_global.h new file mode 100644 index 0000000..97c3693 --- /dev/null +++ b/localmylist/dynamicmodel/dynamicmodel_global.h @@ -0,0 +1,29 @@ +#ifndef DYNAMICMODEL_GLOBAL_H +#define DYNAMICMODEL_GLOBAL_H + +#include + +namespace LocalMyList { +namespace DynamicModel { + +class Node; +class Data; + +typedef ::std::function NodeFactory; + +enum Operation { + UpdateOperation, + InsertOperation, + DeleteOperation +}; + +enum MoveType { + NoMove, + SuccessfulMove, + OutOfBoundsMove +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // DYNAMICMODEL_GLOBAL_H diff --git a/localmylist/dynamicmodel/model.cpp b/localmylist/dynamicmodel/model.cpp new file mode 100644 index 0000000..d55180f --- /dev/null +++ b/localmylist/dynamicmodel/model.cpp @@ -0,0 +1,258 @@ +#include "model.h" + +#include "node.h" +#include "datamodel.h" +#include "datatype.h" +#include "typerelation.h" + +namespace LocalMyList { +namespace DynamicModel { + +Model::Model(QObject *parent) : + QAbstractItemModel(parent), m_dataModel(0) +{ + rootItem = createRootNode(); +} + +Model::~Model() +{ + delete rootItem; +} + +QString Model::query() const +{ + return m_query; +} + +void Model::setQuery(const QString &query) +{ + if (query == m_query) + return; + + dataTypeNames = query.split(QChar('|')); + reload(); + + m_query = query; + emit queryChanged(query); +} + +QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + return rootItem->data(section, role); + + return QVariant(); +} + +Qt::ItemFlags Model::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return 0; + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +QVariant Model::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + Node *item = static_cast(index.internalPointer()); + + return item->data(index.column(), role); +} + +QModelIndex Model::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + return QModelIndex(); + + Node *parentItem; + + if (!parent.isValid()) + parentItem = rootItem; + else + parentItem = static_cast(parent.internalPointer()); + + Node *childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + else + return QModelIndex(); +} + +QModelIndex Model::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + Node *childItem = static_cast(index.internalPointer()); + Node *parentItem = childItem->parent(); + + if (parentItem == rootItem) + return QModelIndex(); + + return createIndex(parentItem->row(), 0, parentItem); +} + +int Model::rowCount(const QModelIndex &parent) const +{ + Node *parentItem; + if (parent.column() > 0) + return 0; + + if (!parent.isValid()) + parentItem = rootItem; + else + parentItem = static_cast(parent.internalPointer()); + + return parentItem->childCount(); +} + +int Model::columnCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return static_cast(parent.internalPointer())->columnCount(); + else + return rootItem->columnCount(); +} + +bool Model::canFetchMore(const QModelIndex &parent) const +{ + Node *parentItem; + if (parent.isValid()) + parentItem = static_cast(parent.internalPointer()); + else + parentItem = rootItem; + + return parentItem->canFetchMore(); +} + +void Model::fetchMore(const QModelIndex &parent) +{ + Node *node; + + if (parent.isValid()) + node = static_cast(parent.internalPointer()); + else + node = rootItem; + + node->fetchMore(); +} + +bool Model::hasChildren(const QModelIndex &parent) const +{ + if (parent.isValid()) + return static_cast(parent.internalPointer())->hasChildren(); + else + return rootItem->hasChildren(); +} + +Node *Model::node(const QModelIndex &idx) const +{ + if (!idx.isValid()) + return 0; + return static_cast(idx.internalPointer()); +} + +QModelIndex Model::index(Node *node) const +{ + if (!node || node == rootItem) + return QModelIndex(); + return createIndex(node->row(), 0, node); +} + +DataModel *Model::dataModel() const +{ + return m_dataModel; +} + +DataType *Model::rootDataType() const +{ + return m_dataModel->dataType("anime"); +} + +DataType *Model::grandChildDataType(Node *node) const +{ + int d = node->depth() + 1; + + return childDataType(d); +} + +DataType *Model::childDataType(int i) const +{ + if (dataTypeNames.count() <= i) + return 0; + return dataModel()->dataType(dataTypeNames.at(i)); +} + +void Model::reload() +{ + beginResetModel(); + delete rootItem; + rootItem = createRootNode(); + endResetModel(); +} + +void Model::setDataModel(DataModel *dataModel) +{ + if (m_dataModel == dataModel) + return; + + m_dataModel = dataModel; + emit dataModelChanged(dataModel); + + reload(); +} + +void Model::episodeInsert(int aid, int eid) +{ + DataType *episodeDataType = m_dataModel->dataType("episode"); + + if (!episodeDataType) + return; + + if (!m_dataModel->dataType("anime")) + return; + + QString previousDataTypeName = QString(); + DataType *previousDataType = 0; + + for (const QString &dataTypeName : dataTypeNames) + { + DataType *currentDataType = m_dataModel->dataType(dataTypeName); + + if (currentDataType == episodeDataType) + { + TypeRelation *rel = m_dataModel->typeRelation(previousDataTypeName, dataTypeName); + + if (previousDataTypeName.isNull()) + { + // The root is the parent, just see if it needs to be added. + } + else + { + IdList ids = rel->getParents(eid); + + + } + } + + previousDataTypeName = dataTypeName; + } +} + +Node *Model::createRootNode() +{ + int size = (m_dataModel && !dataTypeNames.isEmpty()) + ? dataModel()->dataType(dataTypeNames.at(0))->size() + : 0; + Node *n = new Node(this, 0, size, 0); + if (m_dataModel && !dataTypeNames.isEmpty()) + n->setChildDataType(dataModel()->dataType(dataTypeNames.at(0))); + return n; +} + + +} // namespace DynamicModel +} // namespace Local diff --git a/localmylist/dynamicmodel/model.h b/localmylist/dynamicmodel/model.h new file mode 100644 index 0000000..037a329 --- /dev/null +++ b/localmylist/dynamicmodel/model.h @@ -0,0 +1,78 @@ +#ifndef MODEL_H +#define MODEL_H + +#include "../localmylist_global.h" +#include +#include + +namespace LocalMyList { +namespace DynamicModel { + +class Node; +class DataModel; +class DataType; + +class LOCALMYLISTSHARED_EXPORT Model : public QAbstractItemModel +{ + Q_OBJECT + Q_PROPERTY(DataModel* dataModel READ dataModel WRITE setDataModel NOTIFY dataModelChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + friend class Node; + +public: + explicit Model(QObject *parent = 0); + ~Model(); + + QString query() const; + void setQuery(const QString &query); + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + Qt::ItemFlags flags(const QModelIndex &index) const; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + QModelIndex parent(const QModelIndex &index) const; + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + int columnCount(const QModelIndex &parent = QModelIndex()) const; + + // Lazy loading + bool canFetchMore(const QModelIndex &parent) const; + void fetchMore(const QModelIndex &parent); + bool hasChildren(const QModelIndex &parent = QModelIndex()) const; + + Node *node(const QModelIndex &idx) const; + QModelIndex index(Node *node) const; + + DataModel *dataModel() const; + + DataType *rootDataType() const; + + DataType *grandChildDataType(Node *node) const; + DataType *childDataType(int i) const; + +public slots: + void reload(); + + void setDataModel(DataModel *dataModel); + +private slots: + void episodeInsert(int aid, int eid); + +signals: + void dataModelChanged(DataModel *dataModel); + void queryChanged(QString query); + +private: + Node *createRootNode(); + + Node *rootItem; + DataModel* m_dataModel; + + QStringList dataTypeNames; + QString m_query; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // MODEL_H diff --git a/localmylist/dynamicmodel/node.cpp b/localmylist/dynamicmodel/node.cpp new file mode 100644 index 0000000..552bc1a --- /dev/null +++ b/localmylist/dynamicmodel/node.cpp @@ -0,0 +1,319 @@ +#include "node.h" +#include "datatype.h" + +#include "dynamicmodel_global.h" +#include "data.h" +#include "model.h" +#include "typerelation.h" +#include + +#include + +namespace LocalMyList { +namespace DynamicModel { + +Node::Node(Model *model, Node *parent, int totalRowCount, Data *data) + : m_model(model), m_parent(parent), m_totalRowCount(totalRowCount), + m_data(data), m_childType(0) +{ + Q_ASSERT_X((parent && data) || (!parent && !data), "dynamic model", "Root node has no data and no parent. Other nodes must have both"); + + if (!data) + return; + m_data->ref(this); +} + +Node::~Node() +{ + if (!m_data) + return; + + m_data->deref(this); + qDeleteAll(m_children); +} + +DataType *Node::childDataType() const +{ + return m_childType; +} + +void Node::setChildDataType(DataType *dataType) +{ +// Q_ASSERT_X(dataType, "dynamicmodel", "NULL data type"); + m_childType = dataType; +} + +Node *Node::parent() const +{ + return m_parent; +} + +Node *Node::child(int row) const +{ + return m_children.value(row); +} + +int Node::childCount() const +{ + return m_children.count(); +} + +int Node::columnCount() const +{ + return 5; +} + +int Node::row() const +{ + if (m_parent) + return m_parent->m_children.indexOf(const_cast(this)); + + return 0; +} + +bool Node::hasChildren() const +{ + if (this == m_model->rootItem) + return true; + return totalRowCount() > 0; +} + +QVariant Node::data(int column, int role) const +{ +// qDebug() << parent() << column; + if (parent()) + return m_data->data(column, role); + + if (role != Qt::DisplayRole) + return QVariant(); + + switch (column) + { + case 0: + return QObject::tr("Title"); + case 1: + return QObject::tr("Episode / Version"); + case 2: + return QObject::tr("Rating / Quality"); + case 3: + return QObject::tr("Vote"); + case 4: + return QObject::tr("Watched / Renamed"); + } + + return QVariant(); +} + +Data *Node::data() const +{ + return m_data; +} + +int Node::totalRowCount() const +{ + return m_totalRowCount;// ? m_totalRowCount : childDataType() ? childDataType()->size() : 0; +} + +bool Node::canFetchMore() const +{ + if (isRoot() && !totalRowCount()) + return true; + + if (childCount() < totalRowCount()) + return true; + return false; +} + +void Node::fetchMore() +{ + if (!m_childType) + return; + qDebug() << "fetchMore" << this; + NodeList newItems; + + TypeRelation *rel = 0; + if (isRoot()) + rel = m_model->dataModel()->typeRelation(QString(), childDataType()->name()); + else + rel = m_model->dataModel()->typeRelation(m_data->type()->name(), childDataType()->name()); + + if (!rel) + return; + + DataType *grandChildDataType = m_model->grandChildDataType(this); +/* qDebug() << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; + qDebug() << "currentType\t" << (m_data ? m_data->type()->name() : ""); + qDebug() << "grandChildDataType\t" << (grandChildDataType ? grandChildDataType->name() : QString("0")); +// qDebug() << "rowCountType\t" << (rowCountType ? rowCountType->name() : QString("0")); + qDebug() << "getting from rel" << rel->sourceType() << rel->destinationType(); +*/ + auto factory = childNodeFactory(); + + + newItems = rel->getChildren(m_data, childCount(), grandChildDataType, factory); + + const QModelIndex parent = m_model->index(this); + const int newrows = newItems.count(); + + if (!newrows) + return; + + qDebug() << parent << m_model->rowCount(parent) << m_model->rowCount(parent) + newrows - 1; + m_model->beginInsertRows(parent, m_model->rowCount(parent), m_model->rowCount(parent) + newrows - 1); + while (newItems.count()) + m_children << newItems.takeFirst(); + m_model->endInsertRows(); + + qDebug() << m_children.count(); qDebug() << m_model->rowCount(); +} + +void Node::fetchComplete() +{ + +} + +MoveType Node::moveChild(Node *child, Operation type) +{ +qDebug() << "a"; + const QModelIndex idx = m_model->index(this); + + if (type == InsertOperation) + { + auto it = std::upper_bound(m_children.begin(), m_children.end(), child, m_data->type()->nodeCompareFunction()); + + if (it == m_children.end() && canFetchMore()) + { + delete child; + return OutOfBoundsMove; + } + + const int newRow = qMax(0, (it - m_children.begin()) - 1); + + m_model->beginInsertRows(idx, newRow, newRow); + it = m_children.insert(it, child); + m_model->endInsertRows(); + return SuccessfulMove; + } +qDebug() << "b"; + + const auto oldPos = std::find(m_children.begin(), m_children.end(), child); + const int oldRow = oldPos == m_children.end() ? -1 : oldPos - m_children.begin(); + + if (type == DeleteOperation) + { + if (oldRow != -1) + { + m_model->beginRemoveRows(idx, oldRow, oldRow); + m_children.removeAt(oldRow); + m_model->endRemoveRows(); + } + delete child; + return SuccessfulMove; + } +qDebug() << "c"; + + NodeCompare func = child->data()->type()->nodeCompareFunction(); + const auto lower = std::upper_bound(m_children.begin(), oldPos, child, func); +qDebug() << "d"; + const auto upper = std::lower_bound(oldPos, m_children.end(), child, func); + // No move needed + if (lower == upper) + { + qDebug()<< "NoMove"; + return NoMove; + } +qDebug() << "e"; + + const auto it = (lower == oldPos) ? upper : lower; + + // Added item is not in the currently loaded data + if (it == m_children.end() && canFetchMore()) + { + m_model->beginRemoveRows(idx, oldRow, oldRow); + m_children.removeAt(oldRow); + m_model->endRemoveRows(); + delete child; + + return OutOfBoundsMove; + } +qDebug() << "f"; + + const int nextRow = it - m_children.begin(); + const int newRow = qMax(0, nextRow - 1); + + if (oldRow < newRow) + { + m_model->beginMoveRows(idx, oldRow, oldRow, idx, nextRow); + m_children.move(oldRow, newRow); + m_model->endMoveRows(); + } + else + { + m_model->beginMoveRows(idx, oldRow, oldRow, idx, newRow); + m_children.move(oldRow, nextRow); + m_model->endMoveRows(); + } +qDebug() << "g"; + + return SuccessfulMove; +} + +int Node::depth() const +{ + Node *node = parent(); + int depth = 0; + while (node) + { + ++depth; + node = node->parent(); + } + return depth; +} + +NodeFactory Node::childNodeFactory() +{ + return [=](Data *d, int c) -> Node * + { + Node *n = new Node(m_model, this, c, d); + n->setChildDataType(m_model->grandChildDataType(this)); + return n; + }; +} + +int Node::id() const +{ + return m_data->id(); +} + +void Node::childAdded(Data *newData) +{ + qDebug() << "childAdded" << newData; + +/* Node *childNode = childNodeFactory()(newData); + + MoveType moveType = moveChild(childNode, InsertOperation); + + if (moveType == OutOfBoundsMove) + delete childNode; +*/ +} +/* +void Node::childUpdate(Node *child, const Data *newData, Operation operation) +{ + +} +*/ +bool Node::updated(Operation type) +{ + Q_UNUSED(type); + + const int r = row(); + const QModelIndex parentIndex(m_model->index(parent())); + emit m_model->dataChanged(m_model->index(r, 0, parentIndex), + m_model->index(r, columnCount() - 1, parentIndex)); + + return false; +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/node.h b/localmylist/dynamicmodel/node.h new file mode 100644 index 0000000..1790d2b --- /dev/null +++ b/localmylist/dynamicmodel/node.h @@ -0,0 +1,71 @@ +#ifndef NODE_H +#define NODE_H + +#include "../localmylist_global.h" +#include "dynamicmodel_global.h" + +#include +#include + +namespace LocalMyList { +namespace DynamicModel { + +class Model; +class Data; +class DataType; + +class Node; +typedef QList NodeList; + +class LOCALMYLISTSHARED_EXPORT Node { +public: + Node(Model *model, Node *parent, int totalRowCount, Data *data); + ~Node(); + + DataType *childDataType() const; + void setChildDataType(DataType *dataType); + + bool isRoot() const { return !m_parent; } + + // Structure + Node *parent() const; + Node *child(int row) const; + int childCount() const; + int columnCount() const; + int row() const; + bool hasChildren() const; + + // Data + int id() const; + QVariant data(int column, int role) const; + Data *data() const; + int totalRowCount() const; + + bool canFetchMore() const; + void fetchMore(); + void fetchComplete(); + + // Changes + void childAdded(Data *newData); + bool updated(Operation type); + MoveType moveChild(Node *child, Operation type); + + // Misc + int depth() const; + +private: + NodeFactory childNodeFactory(); + + Node *m_parent; + NodeList m_children; + int m_totalRowCount; + Model *m_model; + + Data *m_data; + DataType *m_childType; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // NODE_H diff --git a/localmylist/dynamicmodel/typerelation.cpp b/localmylist/dynamicmodel/typerelation.cpp new file mode 100644 index 0000000..5889df6 --- /dev/null +++ b/localmylist/dynamicmodel/typerelation.cpp @@ -0,0 +1,548 @@ +#include "typerelation.h" + +#include "../mylist.h" +#include "../database.h" +#include "../databaseclasses.h" +#include "node.h" +#include "datatype.h" +#include "data.h" +#include "types.h" + +#include + +namespace LocalMyList { +namespace DynamicModel { + +TypeRelation::TypeRelation(QObject *parent) : QObject(parent), m_dataType(0) +{ +} + +IdList TypeRelation::getParents(int id) +{ + Q_UNUSED(id); + return IdList(); +} + +DataType *TypeRelation::dataType() const +{ + return m_dataType; +} + +QString TypeRelation::childRowCountQuery(DataType *type) const +{ + static const QString zeroQuery("0"); + + if (!type) + return zeroQuery; + + TypeRelation *rel = dataType()->model()->typeRelation(destinationType(), type->name()); + + qDebug() << "relation" << rel->sourceType() << rel->destinationType(); + if (!rel) + return zeroQuery; + + return rel->rowCountQuery(); +} + +// =========================================================================== + +RootAnimeRelation::RootAnimeRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString RootAnimeRelation::sourceType() const +{ + return QString(); +} + +QString RootAnimeRelation::destinationType() const +{ + return "anime"; +} + +NodeList RootAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + Q_UNUSED(parent); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, " + "%1 " + "ORDER BY title_romaji ASC " + "LIMIT :limit " + "OFFSET :offset ") + .arg(dataType()->baseQuery()) + .arg(childRowCountQuery(rowCountType))); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + + return newItems; +} + +QString RootAnimeRelation::rowCountQuery() const +{ + return "(SELECT COUNT(aid) FROM anime)"; +} + +// ================================================= + +RootAnimeTitleRelation::RootAnimeTitleRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString RootAnimeTitleRelation::sourceType() const +{ + return QString(); +} + +QString RootAnimeTitleRelation::destinationType() const +{ + return "anime_title"; +} + +NodeList RootAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + Q_UNUSED(parent); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, %1 " + " ORDER BY title ASC " + "LIMIT :limit " + "OFFSET :offset ") + .arg(dataType()->baseQuery()) + .arg(childRowCountQuery(rowCountType))); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + + return newItems; +} + +QString RootAnimeTitleRelation::rowCountQuery() const +{ + return "(SELECT COUNT(title_id) FROM anime_title)"; +} + +// ================================================= + + +RootEpisodeRelation::RootEpisodeRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString RootEpisodeRelation::sourceType() const +{ + return QString(); +} + +QString RootEpisodeRelation::destinationType() const +{ + return "episode"; +} + +NodeList RootEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + Q_UNUSED(parent) + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT " + " %2, " + " %1 " + " ORDER BY et.ordering ASC, e.epno ASC " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + + return newItems; +} + +QString RootEpisodeRelation::rowCountQuery() const +{ + return "(SELECT COUNT(eid) FROM episode)"; +} + +// ================================================= + +AnimeEpisodeRelation::AnimeEpisodeRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString AnimeEpisodeRelation::sourceType() const +{ + return "anime"; +} + +QString AnimeEpisodeRelation::destinationType() const +{ + return "episode"; +} + +NodeList AnimeEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT " + " %2, " + " %1 " + " WHERE e.aid = :aid " + " ORDER BY et.ordering ASC, e.epno ASC " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":aid", parent->id()); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + + return newItems; +} + +QString AnimeEpisodeRelation::rowCountQuery() const +{ + return "(SELECT COUNT(eid) FROM episode WHERE aid = a.aid)"; +} + +IdList AnimeEpisodeRelation::getParents(int id) +{ + QSqlQuery &q = MyList::instance()->database()->prepare( + "SELECT e.aid FROM episode e WHERE e.eid = :eid"); + q.bindValue(":eid", id); + + IdList ret; + + if (!q.exec()) + return ret; + + while (q.next()) + ret << q.value(0).toInt(); + + q.finish(); + + return ret; +} + +// ================================================= + +AnimeAnimeTitleRelation::AnimeAnimeTitleRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString AnimeAnimeTitleRelation::sourceType() const +{ + return "anime"; +} + +QString AnimeAnimeTitleRelation::destinationType() const +{ + return "anime_title"; +} + +NodeList AnimeAnimeTitleRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT " + " %2, " + " %1 " + " WHERE at.aid = :aid " + " ORDER BY at.type ASC, at.title ASC " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":aid", parent->id()); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + + return newItems; +} + +QString AnimeAnimeTitleRelation::rowCountQuery() const +{ + return "(SELECT count(title_id) FROM anime_title WHERE aid = a.aid)"; +} + +// ================================================= + +EpisodeFileRelation::EpisodeFileRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString EpisodeFileRelation::sourceType() const +{ + return "episode"; +} + +QString EpisodeFileRelation::destinationType() const +{ + return "file"; +} + +NodeList EpisodeFileRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, %1 " + " FROM file f " + " WHERE f.eid = :eida " + "UNION " + "SELECT %2, %1 FROM file f " + " JOIN file_episode_rel fer ON (fer.fid = f.fid) " + " WHERE fer.eid = :eidb ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":eida", parent->id()); + q.bindValue(":eidb", parent->id()); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + return newItems; +} + +QString EpisodeFileRelation::rowCountQuery() const +{ + return + " (SELECT COUNT(fid) " + " FROM ( " + " SELECT fid " + " FROM file " + " WHERE eid = e.eid " + " UNION " + " SELECT f.fid FROM file f " + " JOIN file_episode_rel fer ON (fer.fid = f.fid) " + " WHERE fer.eid = e.eid) sq) "; +} + +// ================================================= + +FileFileLocationRelation::FileFileLocationRelation(QObject *parent) : TypeRelation(parent) +{ + +} + +QString FileFileLocationRelation::sourceType() const +{ + return "file"; +} + +QString FileFileLocationRelation::destinationType() const +{ + return "file_location"; +} + +NodeList FileFileLocationRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, %1 " + " WHERE fl.fid = :fid " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":fid", parent->id()); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + return newItems; +} + +QString FileFileLocationRelation::rowCountQuery() const +{ + return "(SELECT COUNT(location_id) FROM file_location WHERE fid = f.fid)"; +} + +// ================================================= + +AnimeTitleAnimeRelation::AnimeTitleAnimeRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString AnimeTitleAnimeRelation::sourceType() const +{ + return "anime_title"; +} + +QString AnimeTitleAnimeRelation::destinationType() const +{ + return "anime"; +} + +NodeList AnimeTitleAnimeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + int aid = static_cast(parent)->animeTitleData.aid; + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, %1 " + " WHERE a.aid = :aid " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":aid", aid); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + return newItems; +} + +QString AnimeTitleAnimeRelation::rowCountQuery() const +{ + return "(SELECT COUNT(aid) FROM anime WHERE aid = at.aid)"; +} + +// ================================================= + +AnimeTitleEpisodeRelation::AnimeTitleEpisodeRelation(QObject *parent) : TypeRelation(parent) +{ +} + +QString AnimeTitleEpisodeRelation::sourceType() const +{ + return "anime_title"; +} + +QString AnimeTitleEpisodeRelation::destinationType() const +{ + return "episode"; +} + +NodeList AnimeTitleEpisodeRelation::getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) +{ + if (!parent) + return NodeList(); + + int aid = static_cast(parent)->animeTitleData.aid; + + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT %2, %1 " + " WHERE e.aid = :aid " + " LIMIT :limit " + " OFFSET :offset ").arg(dataType()->baseQuery(), childRowCountQuery(rowCountType))); + q.bindValue(":aid", aid); + q.bindValue(":limit", LIMIT); + q.bindValue(":offset", offset); + + if (!q.exec()) + return NodeList(); + + NodeList newItems; + QSqlResultIterator it(q); + while (it.next()) + { + int totalRowCount = it.value(0).toInt(); + Data *data = dataType()->readEntry(it); + auto node = nodeFactory(data, totalRowCount); + newItems << node; + } + return newItems; +} + +QString AnimeTitleEpisodeRelation::rowCountQuery() const +{ + return "(SELECT COUNT(eid) FROM episode WHERE aid = at.aid)"; +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/typerelation.h b/localmylist/dynamicmodel/typerelation.h new file mode 100644 index 0000000..58fd766 --- /dev/null +++ b/localmylist/dynamicmodel/typerelation.h @@ -0,0 +1,195 @@ +#ifndef TYPERELATION_H +#define TYPERELATION_H + +#include "../localmylist_global.h" +#include + +#include +#include "node.h" +#include "dynamicmodel_global.h" + +namespace LocalMyList { +namespace DynamicModel { + +typedef QList IdList; + +class LOCALMYLISTSHARED_EXPORT TypeRelation : public QObject +{ + Q_OBJECT + friend class DataModel; +public: + TypeRelation(QObject *parent = 0); + + virtual QString sourceType() const = 0; + virtual QString destinationType() const = 0; + + virtual NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory) = 0; + virtual QString rowCountQuery() const = 0; + virtual IdList getParents(int id); + + DataType *dataType() const; + +protected: + QString childRowCountQuery(DataType *type) const; + + static const int LIMIT = 400; + +private: + DataType *m_dataType; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT RootAnimeRelation : public TypeRelation +{ + Q_OBJECT +public: + RootAnimeRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT RootAnimeTitleRelation : public TypeRelation +{ + Q_OBJECT +public: + RootAnimeTitleRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT RootEpisodeRelation : public TypeRelation +{ + Q_OBJECT +public: + RootEpisodeRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeEpisodeRelation : public TypeRelation +{ + Q_OBJECT +public: + AnimeEpisodeRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; + IdList getParents(int id); +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeAnimeTitleRelation : public TypeRelation +{ + Q_OBJECT +public: + AnimeAnimeTitleRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT EpisodeFileRelation : public TypeRelation +{ + Q_OBJECT +public: + EpisodeFileRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT EpisodeFileLocationRelation : public TypeRelation +{ + Q_OBJECT +public: + EpisodeFileLocationRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT FileFileLocationRelation : public TypeRelation +{ + Q_OBJECT +public: + FileFileLocationRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeTitleAnimeRelation : public TypeRelation +{ + Q_OBJECT +public: + AnimeTitleAnimeRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +// ========================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeTitleEpisodeRelation : public TypeRelation +{ + Q_OBJECT +public: + AnimeTitleEpisodeRelation(QObject *parent); + + QString sourceType() const; + QString destinationType() const; + + NodeList getChildren(Data *parent, int offset, DataType *rowCountType, NodeFactory nodeFactory); + QString rowCountQuery() const; +}; + +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // TYPERELATION_H diff --git a/localmylist/dynamicmodel/types.cpp b/localmylist/dynamicmodel/types.cpp new file mode 100644 index 0000000..df77a71 --- /dev/null +++ b/localmylist/dynamicmodel/types.cpp @@ -0,0 +1,378 @@ +#include "types.h" + +#include "../database.h" +#include "../mylist.h" +#include "datamodel.h" +#include "typerelation.h" +#include "data.h" +#include "node.h" + +#include + +namespace LocalMyList { +namespace DynamicModel { + +QString AnimeType::name() const +{ + return "anime"; +} + +QStringList AnimeType::availableChildRelations() const +{ + return QStringList(); +} + +QString AnimeType::baseQuery() const +{ + return QString( + " (SELECT COUNT(e.eid) " + " FROM episode e " + " WHERE e.aid = a.aid), " + " (SELECT COUNT(DISTINCT eid) " + " FROM " + " (SELECT e.eid FROM episode e " + " JOIN file f ON (f.eid = e.eid) " + " WHERE e.aid = a.aid " + " AND f.my_watched IS NOT NULL " + " UNION " + " SELECT e.eid FROM episode e " + " JOIN file_episode_rel fer ON fer.eid = e.eid " + " JOIN file f ON f.fid = fer.fid " + " WHERE e.aid = a.aid " + " AND f.my_watched IS NOT NULL) sq), " + " (SELECT CASE WHEN array_length(my_state_array, 1) > 1 THEN -1 ELSE my_state_array[1] END " + " FROM " + " (SELECT array_agg(my_state) my_state_array " + " FROM " + " (SELECT my_state " + " FROM file " + " WHERE aid = a.aid " + " UNION " + " SELECT f.my_state " + " FROM file f " + " JOIN file_episode_rel fer ON (fer.fid = f.eid) " + " JOIN episode e ON (e.eid = fer.eid AND e.aid = a.aid) " + " ) AS sq) AS sq) AS my_state, " + " %1 " + " FROM anime a ") + .arg(Database::animeFields()); +} + +int AnimeType::size() const +{ + return sizeHelper("anime", "aid"); +} + +void AnimeType::registerd() +{ + connect(MyList::instance()->database(), SIGNAL(animeUpdate(int)), this, SLOT(animeUpdated(int))); +} + +void AnimeType::update(Data *data) +{ + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT 0, %1 " + "WHERE aid = :aid ") + .arg(baseQuery())); + q.bindValue(":aid", data->id()); + + if (!q.exec()) + return; + + QSqlResultIterator it(q); + + genericUpdate(data, it, fillAnimeData); +} + +NodeCompare AnimeType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + const AnimeData *aa = static_cast(a->data()); + const AnimeData *ab = static_cast(b->data()); + return aa->animeData.titleRomaji < ab->animeData.titleRomaji; + }; +} + +Data *AnimeType::readEntry(const SqlResultIteratorInterface &it) +{ + return genericReadEntry(it, fillAnimeData); +} + +void AnimeType::animeUpdated(int aid) +{ + const auto it = m_dataStore.find(aid); + + if (it == m_dataStore.constEnd()) + return; + + update(*it); +} + +void AnimeType::fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query) +{ + data.episodesInMyList = query.value(1).toInt(); + data.watchedEpisodes = query.value(2).toInt(); + data.myState = query.value(3).toInt(); + Database::readAnimeData(query, data.animeData, 4); +} + +// ============================================================================================================= + +QString EpisodeType::name() const +{ + return "episode"; +} + +QString EpisodeType::baseQuery() const +{ + return QString( + " (SELECT MIN(my_watched) " + " FROM " + " (SELECT my_watched " + " FROM file " + " WHERE eid = e.eid " + " AND my_watched IS NOT NULL " + " UNION " + " SELECT f.my_watched " + " FROM file f " + " JOIN file_episode_rel fer ON (fer.fid = f.fid) " + " WHERE fer.eid = e.eid " + " AND my_watched IS NOT NULL) AS sq) AS my_watched, " + " (SELECT CASE WHEN array_length(my_state_array, 1) > 1 THEN -1 ELSE my_state_array[1] END " + " FROM " + " (SELECT array_agg(my_state) my_state_array " + " FROM " + " (SELECT my_state " + " FROM file " + " WHERE eid = e.eid " + " UNION " + " SELECT f.my_state " + " FROM file f " + " JOIN file_episode_rel fer ON (fer.fid = f.fid) " + " WHERE fer.eid = e.eid) AS sq) AS sq) AS my_state, " + " et.ordering, %1 " + " FROM episode e " + " JOIN episode_type et ON (et.type = e.type)") + .arg(Database::episodeFields()); +} + +int EpisodeType::size() const +{ + return sizeHelper("episode", "eid"); +} + +void EpisodeType::registerd() +{ + connect(MyList::instance()->database(), SIGNAL(episodeUpdate(int,int)), this, SLOT(episodeUpdated(int,int))); +} + +void EpisodeType::update(Data *data) +{ + QSqlQuery q = MyList::instance()->database()->prepareOneShot(QString( + "SELECT 0, %1 " + "WHERE eid = :eid ") + .arg(baseQuery())); + q.bindValue(":eid", data->id()); + + if (!q.exec()) + return; + + QSqlResultIterator it(q); + + genericUpdate(data, it, fillEpisodeData); +} + +NodeCompare EpisodeType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + const EpisodeData *aa = static_cast(a->data()); + const EpisodeData *ab = static_cast(b->data()); + if (aa->episodeTypeOrdering == ab->episodeTypeOrdering) + return aa->episodeData.epno < ab->episodeData.epno; + return aa->episodeTypeOrdering < ab->episodeTypeOrdering; + + }; +} + +Data *EpisodeType::readEntry(const SqlResultIteratorInterface &it) +{ + return genericReadEntry(it, fillEpisodeData); +} + +void EpisodeType::episodeUpdated(int eid, int aid) +{ + Q_UNUSED(aid); + const auto it = m_dataStore.find(eid); + + if (it == m_dataStore.constEnd()) + return; + + update(*it); +} + +void EpisodeType::fillEpisodeData(EpisodeData &data, const SqlResultIteratorInterface &query) +{ + data.watchedDate = query.value(1).toDateTime(); + data.myStates = query.value(2).toInt(); + data.episodeTypeOrdering = query.value(3).toInt(); + Database::readEpisodeData(query, data.episodeData, 4); +} + +// ============================================================================================================= + +QString FileType::name() const +{ + return "file"; +} + +QString FileType::baseQuery() const +{ + return QString( + "%1") + .arg(Database::fileFields()); +} + +int FileType::size() const +{ + return sizeHelper("file", "fid"); +} + +void FileType::update(Data *data) +{ + Q_UNUSED(data); +} + +void FileType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) +{ + Q_UNUSED(parentData); + Q_UNUSED(oldData); + Q_UNUSED(newData); + Q_UNUSED(operation); +} + +NodeCompare FileType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + return a < b; + }; +} + +Data *FileType::readEntry(const SqlResultIteratorInterface &it) +{ + return genericReadEntry(it, fillFileData); +} + +void FileType::fillFileData(FileData &data, const SqlResultIteratorInterface &query) +{ + Database::readFileData(query, data.fileData, 1); +} + +// ============================================================================================================= + +QString FileLocationType::name() const +{ + return "file_location"; +} + +QString FileLocationType::baseQuery() const +{ + return QString( + "h.name, %1 " + " FROM file_location fl " + " JOIN host h ON (fl.host_id = h.host_id) ") + .arg(Database::fileLocationFields()); +} + +int FileLocationType::size() const +{ + return sizeHelper("file_location", "location_id"); +} + +void FileLocationType::update(Data *data) +{ + Q_UNUSED(data); +} + +void FileLocationType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) +{ + Q_UNUSED(parentData); + Q_UNUSED(oldData); + Q_UNUSED(newData); + Q_UNUSED(operation); +} + +NodeCompare FileLocationType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + return a < b; + }; +} + +Data *FileLocationType::readEntry(const SqlResultIteratorInterface &it) +{ + return genericReadEntry(it, fillFileLocationData); +} + +void FileLocationType::fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query) +{ + data.hostName = query.value(1).toString(); + Database::readFileLocationData(query, data.fileLocationData, 2); +} + +// ============================================================================================================= + +QString AnimeTitleType::name() const +{ + return "anime_title"; +} + +QString AnimeTitleType::baseQuery() const +{ + return QString( + "%1 " + " FROM anime_title at ") + .arg(Database::animeTitleFields()); +} + +int AnimeTitleType::size() const +{ + return sizeHelper("anime_title", "title"); +} + +void AnimeTitleType::update(Data *data) +{ + Q_UNUSED(data); +} + +void AnimeTitleType::childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation) +{ + Q_UNUSED(parentData); + Q_UNUSED(oldData); + Q_UNUSED(newData); + Q_UNUSED(operation); +} + +NodeCompare AnimeTitleType::nodeCompareFunction() const +{ + return [](Node *a, Node *b) -> bool + { + return a < b; + }; +} + +Data *AnimeTitleType::readEntry(const SqlResultIteratorInterface &it) +{ + return genericReadEntry(it, fillAnimeTitleData); +} + +void AnimeTitleType::fillAnimeTitleData(AnimeTitleData &data, const SqlResultIteratorInterface &query) +{ + Database::readAnimeTitleData(query, data.animeTitleData, 1); +} + +} // namespace DynamicModel +} // namespace LocalMyList diff --git a/localmylist/dynamicmodel/types.h b/localmylist/dynamicmodel/types.h new file mode 100644 index 0000000..1698970 --- /dev/null +++ b/localmylist/dynamicmodel/types.h @@ -0,0 +1,158 @@ +#ifndef TYPES_H +#define TYPES_H + +#include "../localmylist_global.h" +#include "datatype.h" +#include "data.h" + +#include + +namespace LocalMyList { +namespace DynamicModel { +/* +class LOCALMYLISTSHARED_EXPORT RootType : public DataType +{ + QString name() const; + QStringList availableChildRelations() const; + + QString baseQuery() const; + + int size() const; + + void update(Data *data); + void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + + NodeCompare nodeCompareFunction() const; + + Data *readEntry(const SqlResultIteratorInterface &it); + +private: + void fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query); +}; +*/ +// ============================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeType : public DataType +{ + Q_OBJECT + + QString name() const; + QStringList availableChildRelations() const; + + QString baseQuery() const; + + int size() const; + + void registerd(); + + void update(Data *data); + void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + + NodeCompare nodeCompareFunction() const; + +protected: + Data *readEntry(const SqlResultIteratorInterface &it); + +private slots: + void animeUpdated(int aid); + +private: + static void fillAnimeData(AnimeData &data, const SqlResultIteratorInterface &query); +}; + +// ============================================================================================================= + +class LOCALMYLISTSHARED_EXPORT EpisodeType : public DataType +{ + Q_OBJECT + + QString name() const; + + QString baseQuery() const; + + int size() const; + + void registerd(); + + void update(Data *data); + void added(int id); + + NodeCompare nodeCompareFunction() const; + +protected: + Data *readEntry(const SqlResultIteratorInterface &it); + +private slots: + void episodeUpdated(int eid, int aid); + +private: + static void fillEpisodeData(EpisodeData &data, const SqlResultIteratorInterface &query); +}; + +// ============================================================================================================= + +class LOCALMYLISTSHARED_EXPORT FileType : public DataType +{ + Q_OBJECT + + QString name() const; + QString baseQuery() const; + int size() const; + + void update(Data *data); + void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + + NodeCompare nodeCompareFunction() const; + + Data *readEntry(const SqlResultIteratorInterface &it); + +private: + static void fillFileData(FileData &data, const SqlResultIteratorInterface &query); +}; + +// ============================================================================================================= + +class LOCALMYLISTSHARED_EXPORT FileLocationType : public DataType +{ + Q_OBJECT + + QString name() const; + QString baseQuery() const; + int size() const; + + void update(Data *data); + void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + + NodeCompare nodeCompareFunction() const; + + Data *readEntry(const SqlResultIteratorInterface &it); + +private: + static void fillFileLocationData(FileLocationData &data, const SqlResultIteratorInterface &query); +}; + +// ============================================================================================================= + +class LOCALMYLISTSHARED_EXPORT AnimeTitleType : public DataType +{ + Q_OBJECT + + QString name() const; + QString baseQuery() const; + + int size() const; + + void update(Data *data); + void childUpdate(Data *parentData, const Data *oldData, const Data *newData, Operation operation); + + NodeCompare nodeCompareFunction() const; + + Data *readEntry(const SqlResultIteratorInterface &it); + +private: + static void fillAnimeTitleData(AnimeTitleData &data, const SqlResultIteratorInterface &query); +}; +} // namespace DynamicModel +} // namespace LocalMyList + +#endif // TYPES_H diff --git a/localmylist/localmylist.pro b/localmylist/localmylist.pro index 030b99a..98e4063 100644 --- a/localmylist/localmylist.pro +++ b/localmylist/localmylist.pro @@ -31,9 +31,16 @@ SOURCES += \ sqlquery.cpp \ sqlasyncquery.cpp \ sqlasyncqueryinternal.cpp \ - asyncquerytask.cpp \ filelocationchecktask.cpp \ - messagehandler.cpp + messagehandler.cpp \ + asyncquerytask.cpp \ + dynamicmodel/data.cpp \ + dynamicmodel/node.cpp \ + dynamicmodel/model.cpp \ + dynamicmodel/datatype.cpp \ + dynamicmodel/types.cpp \ + dynamicmodel/datamodel.cpp \ + dynamicmodel/typerelation.cpp HEADERS += \ localmylist_global.h \ @@ -59,9 +66,16 @@ HEADERS += \ sqlasyncquery.h \ sqlasyncqueryinternal.h \ asyncquerytask.h \ - sqlresultiteratorinterface.h \ filelocationchecktask.h \ - messagehandler.h + sqlresultiteratorinterface.h \ + dynamicmodel/data.h \ + dynamicmodel/node.h \ + dynamicmodel/model.h \ + dynamicmodel/datatype.h \ + dynamicmodel/dynamicmodel_global.h \ + dynamicmodel/types.h \ + dynamicmodel/datamodel.h \ + dynamicmodel/typerelation.h CONV_HEADERS += \ include/LocalMyList/AbstractTask \ @@ -77,6 +91,7 @@ CONV_HEADERS += \ include/LocalMyList/MyListNode \ include/LocalMyList/Settings \ include/LocalMyList/UnknownFileLookupTask \ + include/LocalMyList/UnknownFileLookupTask \ include/LocalMyList/FileLocationCheckTask \ include/LocalMyList/RequestHandler \ include/LocalMyList/DirectoryWatcher diff --git a/localmylist/share/schema/schema.sql b/localmylist/share/schema/schema.sql index 8891d19..258f66a 100644 --- a/localmylist/share/schema/schema.sql +++ b/localmylist/share/schema/schema.sql @@ -36,10 +36,12 @@ CREATE INDEX my_vote_idx ON anime USING btree (my_vote); CREATE INDEX my_temp_vote_idx ON anime USING btree (my_temp_vote); CREATE TABLE anime_title ( + title_id serial NOT NULL, aid integer NOT NULL, type integer DEFAULT 1, language character(8) DEFAULT ''::bpchar, title character varying(500) NOT NULL, + CONSTRAINT anime_title_pk PRIMARY KEY (title_id), CONSTRAINT unique_title UNIQUE (aid, type, language, title) ); CREATE INDEX aid_idx ON anime_title USING btree (aid);