fonts.cpp \
aniaddsyntaxhighlighter.cpp \
settingsdialog.cpp \
- codeeditor.cpp
+ codeeditor.cpp \
+ tabs/dynamicmodeltab.cpp
HEADERS += mainwindow.h \
databaseconnectiondialog.h \
fonts.h \
aniaddsyntaxhighlighter.h \
settingsdialog.h \
- codeeditor.h
+ codeeditor.h \
+ tabs/dynamicmodeltab.h
FORMS += mainwindow.ui \
databaseconnectiondialog.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)
#include "tabs/pendingrequesttab.h"
#include "tabs/databaselogtab.h"
#include "tabs/clientlogtab.h"
+#include "tabs/dynamicmodeltab.h"
void registerTabs()
{
TabWidget::registerTab<PendingRequestTab>();
TabWidget::registerTab<DatabaseLogTab>();
TabWidget::registerTab<ClientLogTab>();
+ TabWidget::registerTab<DynamicModelTab>();
}
--- /dev/null
+#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());
+}
--- /dev/null
+#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<DynamicModelTab>
+{
+ 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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DynamicModelTab</class>
+ <widget class="QWidget" name="DynamicModelTab">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>637</width>
+ <height>458</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <layout class="QGridLayout" name="gridLayout">
+ <item row="0" column="0">
+ <widget class="FilterLineEdit" name="filterInput"/>
+ </item>
+ <item row="0" column="1">
+ <widget class="QComboBox" name="filterType"/>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLineEdit" name="modelQuery"/>
+ </item>
+ <item row="1" column="1">
+ <widget class="QPushButton" name="modelQueryButton">
+ <property name="text">
+ <string>Set</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="MyListView" name="myListView"/>
+ </item>
+ </layout>
+ </widget>
+ <customwidgets>
+ <customwidget>
+ <class>FilterLineEdit</class>
+ <extends>QLineEdit</extends>
+ <header>filterlineedit.h</header>
+ </customwidget>
+ <customwidget>
+ <class>MyListView</class>
+ <extends>QTreeView</extends>
+ <header>mylistview.h</header>
+ </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>
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));
{
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())
{
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))
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();
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();
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();
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();
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();
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();
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();
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
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();
AnimeTitle::AnimeTitle(int aid, TitleType type, const QString &language, const QString &title)
{
+ this->titleId = 0;
this->aid = aid;
this->type = type;
this->language = language;
OfficialTitle = 4
};
+ int titleId;
int aid;
TitleType type;
QString language;
--- /dev/null
+#include "data.h"
+
+#include "node.h"
+#include "datatype.h"
+
+#include <QDebug>
+
+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
--- /dev/null
+#ifndef DATA_H
+#define DATA_H
+
+#include "../localmylist_global.h"
+
+#include <QList>
+#include <QVariant>
+
+#include "../databaseclasses.h"
+
+namespace LocalMyList {
+namespace DynamicModel {
+
+class DataType;
+class Node;
+typedef QList<Node *> 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
--- /dev/null
+#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<QString, TypeRelation *>());
+
+ 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
--- /dev/null
+#ifndef DATAMODEL_H
+#define DATAMODEL_H
+
+#include <functional>
+#include "../localmylist_global.h"
+#include "dynamicmodel_global.h"
+#include "node.h"
+
+#include <QObject>
+#include <QSet>
+#include <QHash>
+
+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<QString, DataType *> dataTypeNames;
+ QSet<DataType *> dataTypes;
+
+ QHash<QString, QHash<QString, TypeRelation *> > typeRelations;
+};
+
+} // namespace DynamicModel
+} // namespace LocalMyList
+
+#endif // DATAMODEL_H
--- /dev/null
+#include "datatype.h"
+
+#include <QtGlobal>
+#include <QSqlQuery>
+#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
--- /dev/null
+#ifndef ABSTRACTDATATYPE_H
+#define ABSTRACTDATATYPE_H
+
+#include "../localmylist_global.h"
+#include "datamodel.h"
+#include "../sqlasyncquery.h"
+
+#include <QObject>
+#include <QString>
+#include <QStringList>
+
+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<typename T, typename F> 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<typename T, typename F> void genericUpdate(Data *data, SqlResultIteratorInterface &it, F func)
+ {
+ if (!it.next())
+ return;
+
+ T *typedData = static_cast<T *>(data);
+ T oldTypedData(this);
+
+ oldTypedData = *typedData;
+ func(*typedData, it);
+
+ data->updated(&oldTypedData);
+ }
+
+ SqlAsyncQuery *query;
+ mutable int m_size;
+ QHash<int, Data *> m_dataStore;
+
+private:
+ DataModel *m_model;
+};
+
+} // namespace DynamicModel
+} // namespace LocalMyList
+
+#endif // ABSTRACTDATATYPE_H
--- /dev/null
+#ifndef DYNAMICMODEL_GLOBAL_H
+#define DYNAMICMODEL_GLOBAL_H
+
+#include <functional>
+
+namespace LocalMyList {
+namespace DynamicModel {
+
+class Node;
+class Data;
+
+typedef ::std::function<Node *(Data *, int)> NodeFactory;
+
+enum Operation {
+ UpdateOperation,
+ InsertOperation,
+ DeleteOperation
+};
+
+enum MoveType {
+ NoMove,
+ SuccessfulMove,
+ OutOfBoundsMove
+};
+
+} // namespace DynamicModel
+} // namespace LocalMyList
+
+#endif // DYNAMICMODEL_GLOBAL_H
--- /dev/null
+#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<Node *>(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<Node*>(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<Node *>(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<Node *>(parent.internalPointer());
+
+ return parentItem->childCount();
+}
+
+int Model::columnCount(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return static_cast<Node *>(parent.internalPointer())->columnCount();
+ else
+ return rootItem->columnCount();
+}
+
+bool Model::canFetchMore(const QModelIndex &parent) const
+{
+ Node *parentItem;
+ if (parent.isValid())
+ parentItem = static_cast<Node *>(parent.internalPointer());
+ else
+ parentItem = rootItem;
+
+ return parentItem->canFetchMore();
+}
+
+void Model::fetchMore(const QModelIndex &parent)
+{
+ Node *node;
+
+ if (parent.isValid())
+ node = static_cast<Node *>(parent.internalPointer());
+ else
+ node = rootItem;
+
+ node->fetchMore();
+}
+
+bool Model::hasChildren(const QModelIndex &parent) const
+{
+ if (parent.isValid())
+ return static_cast<Node *>(parent.internalPointer())->hasChildren();
+ else
+ return rootItem->hasChildren();
+}
+
+Node *Model::node(const QModelIndex &idx) const
+{
+ if (!idx.isValid())
+ return 0;
+ return static_cast<Node *>(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
--- /dev/null
+#ifndef MODEL_H
+#define MODEL_H
+
+#include "../localmylist_global.h"
+#include <QAbstractItemModel>
+#include <QStringList>
+
+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
--- /dev/null
+#include "node.h"
+#include "datatype.h"
+
+#include "dynamicmodel_global.h"
+#include "data.h"
+#include "model.h"
+#include "typerelation.h"
+#include <QModelIndex>
+
+#include <QDebug>
+
+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<Node *>(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() : "<root>");
+ 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
--- /dev/null
+#ifndef NODE_H
+#define NODE_H
+
+#include "../localmylist_global.h"
+#include "dynamicmodel_global.h"
+
+#include <QVariant>
+#include <QList>
+
+namespace LocalMyList {
+namespace DynamicModel {
+
+class Model;
+class Data;
+class DataType;
+
+class Node;
+typedef QList<Node *> 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
--- /dev/null
+#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 <QDebug>
+
+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<AnimeTitleData *>(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<AnimeTitleData *>(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
--- /dev/null
+#ifndef TYPERELATION_H
+#define TYPERELATION_H
+
+#include "../localmylist_global.h"
+#include <QObject>
+
+#include <QString>
+#include "node.h"
+#include "dynamicmodel_global.h"
+
+namespace LocalMyList {
+namespace DynamicModel {
+
+typedef QList<int> 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
--- /dev/null
+#include "types.h"
+
+#include "../database.h"
+#include "../mylist.h"
+#include "datamodel.h"
+#include "typerelation.h"
+#include "data.h"
+#include "node.h"
+
+#include <QDebug>
+
+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<AnimeData>(data, it, fillAnimeData);
+}
+
+NodeCompare AnimeType::nodeCompareFunction() const
+{
+ return [](Node *a, Node *b) -> bool
+ {
+ const AnimeData *aa = static_cast<AnimeData *>(a->data());
+ const AnimeData *ab = static_cast<AnimeData *>(b->data());
+ return aa->animeData.titleRomaji < ab->animeData.titleRomaji;
+ };
+}
+
+Data *AnimeType::readEntry(const SqlResultIteratorInterface &it)
+{
+ return genericReadEntry<AnimeData>(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<EpisodeData>(data, it, fillEpisodeData);
+}
+
+NodeCompare EpisodeType::nodeCompareFunction() const
+{
+ return [](Node *a, Node *b) -> bool
+ {
+ const EpisodeData *aa = static_cast<EpisodeData *>(a->data());
+ const EpisodeData *ab = static_cast<EpisodeData *>(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<EpisodeData>(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<FileData>(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<FileLocationData>(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<AnimeTitleData>(it, fillAnimeTitleData);
+}
+
+void AnimeTitleType::fillAnimeTitleData(AnimeTitleData &data, const SqlResultIteratorInterface &query)
+{
+ Database::readAnimeTitleData(query, data.animeTitleData, 1);
+}
+
+} // namespace DynamicModel
+} // namespace LocalMyList
--- /dev/null
+#ifndef TYPES_H
+#define TYPES_H
+
+#include "../localmylist_global.h"
+#include "datatype.h"
+#include "data.h"
+
+#include <functional>
+
+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
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 \
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 \
include/LocalMyList/MyListNode \
include/LocalMyList/Settings \
include/LocalMyList/UnknownFileLookupTask \
+ include/LocalMyList/UnknownFileLookupTask \
include/LocalMyList/FileLocationCheckTask \
include/LocalMyList/RequestHandler \
include/LocalMyList/DirectoryWatcher
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);