]> Some of my projects - localmylist.git/commitdiff
Initial version of dynamic model.
authorAPTX <marek321@gmail.com>
Sat, 31 Aug 2013 10:19:32 +0000 (12:19 +0200)
committerAPTX <marek321@gmail.com>
Sat, 31 Aug 2013 10:19:32 +0000 (12:19 +0200)
26 files changed:
localmylist-management/localmylist-management.pro
localmylist-management/registertabs.cpp
localmylist-management/tabs/dynamicmodeltab.cpp [new file with mode: 0644]
localmylist-management/tabs/dynamicmodeltab.h [new file with mode: 0644]
localmylist-management/tabs/dynamicmodeltab.ui [new file with mode: 0644]
localmylist/database.cpp
localmylist/database.h
localmylist/databaseclasses.cpp
localmylist/databaseclasses.h
localmylist/dynamicmodel/data.cpp [new file with mode: 0644]
localmylist/dynamicmodel/data.h [new file with mode: 0644]
localmylist/dynamicmodel/datamodel.cpp [new file with mode: 0644]
localmylist/dynamicmodel/datamodel.h [new file with mode: 0644]
localmylist/dynamicmodel/datatype.cpp [new file with mode: 0644]
localmylist/dynamicmodel/datatype.h [new file with mode: 0644]
localmylist/dynamicmodel/dynamicmodel_global.h [new file with mode: 0644]
localmylist/dynamicmodel/model.cpp [new file with mode: 0644]
localmylist/dynamicmodel/model.h [new file with mode: 0644]
localmylist/dynamicmodel/node.cpp [new file with mode: 0644]
localmylist/dynamicmodel/node.h [new file with mode: 0644]
localmylist/dynamicmodel/typerelation.cpp [new file with mode: 0644]
localmylist/dynamicmodel/typerelation.h [new file with mode: 0644]
localmylist/dynamicmodel/types.cpp [new file with mode: 0644]
localmylist/dynamicmodel/types.h [new file with mode: 0644]
localmylist/localmylist.pro
localmylist/share/schema/schema.sql

index 074c58771e749322482c7a64ba4ac00cdd051fe4..f3a1948ebf9dd88be3696a02b917ab7d732db94b 100644 (file)
@@ -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)
index 8e0ea67fddb7440da06e5d80e2e3b667833eebcd..bf00af856945274dc42a57f6d58c2c15cea3a88a 100644 (file)
@@ -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<PendingRequestTab>();
        TabWidget::registerTab<DatabaseLogTab>();
        TabWidget::registerTab<ClientLogTab>();
+       TabWidget::registerTab<DynamicModelTab>();
 }
diff --git a/localmylist-management/tabs/dynamicmodeltab.cpp b/localmylist-management/tabs/dynamicmodeltab.cpp
new file mode 100644 (file)
index 0000000..ed881bd
--- /dev/null
@@ -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 &current, 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 (file)
index 0000000..7023d1f
--- /dev/null
@@ -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<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 &current, 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 (file)
index 0000000..710f419
--- /dev/null
@@ -0,0 +1,62 @@
+<?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>
index 3396933f1f5aed27e69b85fee901ffb99670b338..7614c7ff647469de4dd1cdadb76a233c6c2b4314 100644 (file)
@@ -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
index fb3f0c4e908e3f75d118f48b0b16a36171cf0ac9..996450b7c2a1d1f818870f080f161b73261d807a 100644 (file)
@@ -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();
index 703ffe41546a644460adb1203e29c465ae51b12f..ff4005c4640d0b4d1e8374fcbd490acfa04d36bc 100644 (file)
@@ -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;
index 912a6dc2ae324c39d62ba7a66149cecaaf2cf30d..0366bdab09051a377b439e1e0e978e7bfaba4ea7 100644 (file)
@@ -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 (file)
index 0000000..57f85df
--- /dev/null
@@ -0,0 +1,361 @@
+#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
diff --git a/localmylist/dynamicmodel/data.h b/localmylist/dynamicmodel/data.h
new file mode 100644 (file)
index 0000000..ede3d4c
--- /dev/null
@@ -0,0 +1,110 @@
+#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
diff --git a/localmylist/dynamicmodel/datamodel.cpp b/localmylist/dynamicmodel/datamodel.cpp
new file mode 100644 (file)
index 0000000..7e7232d
--- /dev/null
@@ -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<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
diff --git a/localmylist/dynamicmodel/datamodel.h b/localmylist/dynamicmodel/datamodel.h
new file mode 100644 (file)
index 0000000..8f589ac
--- /dev/null
@@ -0,0 +1,56 @@
+#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
diff --git a/localmylist/dynamicmodel/datatype.cpp b/localmylist/dynamicmodel/datatype.cpp
new file mode 100644 (file)
index 0000000..2457f0f
--- /dev/null
@@ -0,0 +1,90 @@
+#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
diff --git a/localmylist/dynamicmodel/datatype.h b/localmylist/dynamicmodel/datatype.h
new file mode 100644 (file)
index 0000000..76b338d
--- /dev/null
@@ -0,0 +1,103 @@
+#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
diff --git a/localmylist/dynamicmodel/dynamicmodel_global.h b/localmylist/dynamicmodel/dynamicmodel_global.h
new file mode 100644 (file)
index 0000000..97c3693
--- /dev/null
@@ -0,0 +1,29 @@
+#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
diff --git a/localmylist/dynamicmodel/model.cpp b/localmylist/dynamicmodel/model.cpp
new file mode 100644 (file)
index 0000000..d55180f
--- /dev/null
@@ -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<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
diff --git a/localmylist/dynamicmodel/model.h b/localmylist/dynamicmodel/model.h
new file mode 100644 (file)
index 0000000..037a329
--- /dev/null
@@ -0,0 +1,78 @@
+#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
diff --git a/localmylist/dynamicmodel/node.cpp b/localmylist/dynamicmodel/node.cpp
new file mode 100644 (file)
index 0000000..552bc1a
--- /dev/null
@@ -0,0 +1,319 @@
+#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
diff --git a/localmylist/dynamicmodel/node.h b/localmylist/dynamicmodel/node.h
new file mode 100644 (file)
index 0000000..1790d2b
--- /dev/null
@@ -0,0 +1,71 @@
+#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
diff --git a/localmylist/dynamicmodel/typerelation.cpp b/localmylist/dynamicmodel/typerelation.cpp
new file mode 100644 (file)
index 0000000..5889df6
--- /dev/null
@@ -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 <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
diff --git a/localmylist/dynamicmodel/typerelation.h b/localmylist/dynamicmodel/typerelation.h
new file mode 100644 (file)
index 0000000..58fd766
--- /dev/null
@@ -0,0 +1,195 @@
+#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
diff --git a/localmylist/dynamicmodel/types.cpp b/localmylist/dynamicmodel/types.cpp
new file mode 100644 (file)
index 0000000..df77a71
--- /dev/null
@@ -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 <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
diff --git a/localmylist/dynamicmodel/types.h b/localmylist/dynamicmodel/types.h
new file mode 100644 (file)
index 0000000..1698970
--- /dev/null
@@ -0,0 +1,158 @@
+#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
index 030b99a55bdb2443bc436a738adf1aa7d5e87183..98e4063e141cef784c36c50fb83b3d6f5cb8b6d7 100644 (file)
@@ -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
index 8891d19adbdce8c1494ab9535323621368861945..258f66a6965bf466c6ee59e313b55744dd924782 100644 (file)
@@ -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);