comparison src/gui/pages/search.cc @ 250:c130f47f6f48

*: many many changes e.g. the search page is actually implemented now!
author Paper <paper@paper.us.eu.org>
date Sun, 04 Feb 2024 21:17:17 -0500
parents 4d461ef7d424
children 862d0d8619f6
comparison
equal deleted inserted replaced
249:6b2441c776dd 250:c130f47f6f48
1 #include "gui/pages/search.h" 1 #include "gui/pages/search.h"
2 2 #include "core/anime.h"
3 SearchPage::SearchPage(QWidget* parent) : QWidget(parent) { 3 #include "core/anime_db.h"
4 } 4 #include "core/strings.h"
5 #include "core/http.h"
6 #include "core/session.h"
7 #include "core/filesystem.h"
8 #include "gui/widgets/text.h"
9 #include "gui/dialog/information.h"
10 #include "track/media.h"
11 #include "gui/translate/anime.h"
12 #include "services/services.h"
13
14 #include <QHeaderView>
15 #include <QVBoxLayout>
16 #include <QToolBar>
17 #include <QTreeView>
18 #include <QDate>
19 #include <QMenu>
20
21 #include <iostream>
22 #include <sstream>
23 #include <fstream>
24 #include <algorithm>
25
26 #include "pugixml.hpp"
27 #include "anitomy/anitomy.h"
28
29 SearchPageListSortFilter::SearchPageListSortFilter(QObject* parent) : QSortFilterProxyModel(parent) {
30 }
31
32 bool SearchPageListSortFilter::lessThan(const QModelIndex& l, const QModelIndex& r) const {
33 QVariant left = sourceModel()->data(l, sortRole());
34 QVariant right = sourceModel()->data(r, sortRole());
35
36 switch (left.userType()) {
37 case QMetaType::Int:
38 case QMetaType::UInt:
39 case QMetaType::LongLong:
40 case QMetaType::ULongLong:
41 return left.toInt() < right.toInt();
42 case QMetaType::QDate:
43 return left.toDate() < right.toDate();
44 case QMetaType::QString:
45 default: // meh
46 return QString::compare(left.toString(), right.toString(), Qt::CaseInsensitive) < 0;
47 }
48 }
49
50 /* -------------------------------------------- */
51
52 SearchPageListModel::SearchPageListModel(QObject* parent) : QAbstractListModel(parent) {
53 }
54
55 void SearchPageListModel::ParseSearch(const std::vector<int>& ids) {
56 /* hack!!! */
57 if (!rowCount(index(0))) {
58 beginInsertRows(QModelIndex(), 0, 0);
59 endInsertRows();
60 }
61
62 beginResetModel();
63
64 this->ids = ids;
65
66 endResetModel();
67 }
68
69 int SearchPageListModel::rowCount(const QModelIndex& parent) const {
70 return ids.size();
71 (void)(parent);
72 }
73
74 int SearchPageListModel::columnCount(const QModelIndex& parent) const {
75 return NB_COLUMNS;
76 (void)(parent);
77 }
78
79 QVariant SearchPageListModel::headerData(const int section, const Qt::Orientation orientation, const int role) const {
80 switch (role) {
81 case Qt::DisplayRole: {
82 switch (section) {
83 case SR_TITLE: return tr("Anime title");
84 case SR_EPISODES: return tr("Episode");
85 case SR_TYPE: return tr("Type");
86 case SR_SCORE: return tr("Score");
87 case SR_SEASON: return tr("Season");
88 default: return {};
89 }
90 break;
91 }
92 case Qt::TextAlignmentRole: {
93 switch (section) {
94 case SR_TITLE: return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
95 case SR_TYPE: return QVariant(Qt::AlignHCenter | Qt::AlignVCenter);
96 case SR_EPISODES:
97 case SR_SCORE:
98 case SR_SEASON: return QVariant(Qt::AlignRight | Qt::AlignVCenter);
99 default: return {};
100 }
101 break;
102 }
103 }
104 return QAbstractListModel::headerData(section, orientation, role);
105 }
106
107 QVariant SearchPageListModel::data(const QModelIndex& index, int role) const {
108 if (!index.isValid())
109 return QVariant();
110
111 const Anime::Anime& anime = Anime::db.items[ids[index.row()]];
112
113 switch (role) {
114 case Qt::DisplayRole:
115 switch (index.column()) {
116 case SR_TITLE: return Strings::ToQString(anime.GetUserPreferredTitle());
117 case SR_TYPE: return Strings::ToQString(Translate::ToLocalString(anime.GetFormat()));
118 case SR_EPISODES: return anime.GetEpisodes();
119 case SR_SCORE: return QString::number(anime.GetAudienceScore()) + "%";
120 case SR_SEASON: return Strings::ToQString(Translate::ToLocalString(anime.GetSeason())) + " " + QString::number(anime.GetAirDate().GetYear().value_or(2000));
121 default: return {};
122 }
123 break;
124 case Qt::UserRole:
125 switch (index.column()) {
126 case SR_SCORE: return anime.GetAudienceScore();
127 case SR_EPISODES: return anime.GetEpisodes();
128 case SR_SEASON: return anime.GetAirDate().GetAsQDate();
129 /* We have to use this to work around some stupid
130 * "conversion ambiguous" error on Linux
131 */
132 default: return data(index, Qt::DisplayRole);
133 }
134 break;
135 case Qt::SizeHintRole: {
136 switch (index.column()) {
137 default: {
138 /* max horizontal size of 100, height size = size of current font */
139 const QString d = data(index, Qt::DisplayRole).toString();
140 const QFontMetrics metric = QFontMetrics(QFont());
141
142 return QSize(std::max(metric.horizontalAdvance(d), 100), metric.height());
143 }
144 }
145 break;
146 }
147 case Qt::TextAlignmentRole:
148 return headerData(index.column(), Qt::Horizontal, Qt::TextAlignmentRole);
149 }
150 return QVariant();
151 }
152
153 Qt::ItemFlags SearchPageListModel::flags(const QModelIndex& index) const {
154 if (!index.isValid())
155 return Qt::NoItemFlags;
156
157 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
158 }
159
160 Anime::Anime* SearchPageListModel::GetAnimeFromIndex(const QModelIndex& index) const {
161 return &Anime::db.items[ids[index.row()]];
162 }
163
164 void SearchPage::DisplayListMenu() {
165 QMenu* menu = new QMenu(this);
166 menu->setAttribute(Qt::WA_DeleteOnClose);
167 menu->setToolTipsVisible(true);
168
169 const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());
170
171 bool add_to_list_enable = true;
172
173 std::set<Anime::Anime*> animes;
174 for (const auto& index : selection.indexes()) {
175 if (!index.isValid())
176 continue;
177
178 Anime::Anime* anime = model->GetAnimeFromIndex(index);
179 if (anime) {
180 animes.insert(anime);
181 if (anime->IsInUserList())
182 add_to_list_enable = false;
183 }
184 }
185
186 menu->addAction(tr("Information"), [this, animes] {
187 for (auto& anime : animes) {
188 InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
189 //UpdateAnime(anime->GetId());
190 }, InformationDialog::PAGE_MAIN_INFO, this);
191
192 dialog->show();
193 dialog->raise();
194 dialog->activateWindow();
195 }
196 });
197 menu->addSeparator();
198 {
199 QMenu* submenu = menu->addMenu(tr("Add to list..."));
200 submenu->addAction(tr("Currently watching"), [animes]{
201 for (auto& anime : animes) {
202 if (!anime->IsInUserList())
203 anime->AddToUserList();
204 anime->SetUserStatus(Anime::ListStatus::CURRENT);
205 Services::UpdateAnimeEntry(anime->GetId());
206 }
207 });
208 submenu->addAction(tr("Completed"), [animes]{
209 for (auto& anime : animes) {
210 if (!anime->IsInUserList())
211 anime->AddToUserList();
212 anime->SetUserStatus(Anime::ListStatus::COMPLETED);
213 Services::UpdateAnimeEntry(anime->GetId());
214 }
215 });
216 submenu->addAction(tr("On hold"), [animes]{
217 for (auto& anime : animes) {
218 if (!anime->IsInUserList())
219 anime->AddToUserList();
220 anime->SetUserStatus(Anime::ListStatus::PAUSED);
221 Services::UpdateAnimeEntry(anime->GetId());
222 }
223 });
224 submenu->addAction(tr("Dropped"), [animes]{
225 for (auto& anime : animes) {
226 if (!anime->IsInUserList())
227 anime->AddToUserList();
228 anime->SetUserStatus(Anime::ListStatus::DROPPED);
229 Services::UpdateAnimeEntry(anime->GetId());
230 }
231 });
232 submenu->addAction(tr("Plan to watch"), [animes]{
233 for (auto& anime : animes) {
234 if (!anime->IsInUserList())
235 anime->AddToUserList();
236 anime->SetUserStatus(Anime::ListStatus::PLANNING);
237 Services::UpdateAnimeEntry(anime->GetId());
238 }
239 });
240 submenu->setEnabled(add_to_list_enable);
241 }
242 menu->popup(QCursor::pos());
243 }
244
245 void SearchPage::ItemDoubleClicked() {
246 /* throw out any other garbage */
247 const QItemSelection selection = sort_model->mapSelectionToSource(treeview->selectionModel()->selection());
248 if (!selection.indexes().first().isValid())
249 return;
250
251 const QModelIndex index = model->index(selection.indexes().first().row());
252 Anime::Anime* anime = model->GetAnimeFromIndex(index);
253
254 InformationDialog* dialog = new InformationDialog(*anime, [this, anime] {
255 //UpdateAnime(anime->GetId());
256 }, InformationDialog::PAGE_MAIN_INFO, this);
257
258 dialog->show();
259 dialog->raise();
260 dialog->activateWindow();
261 }
262
263 SearchPage::SearchPage(QWidget* parent) : QFrame(parent) {
264 setFrameShape(QFrame::Box);
265 setFrameShadow(QFrame::Sunken);
266
267 QVBoxLayout* layout = new QVBoxLayout(this);
268 layout->setContentsMargins(0, 0, 0, 0);
269 layout->setSpacing(0);
270
271 {
272 /* Toolbar */
273 QToolBar* toolbar = new QToolBar(this);
274 toolbar->setMovable(false);
275
276 {
277 QLineEdit* line_edit = new QLineEdit("", toolbar);
278 connect(line_edit, &QLineEdit::returnPressed, this, [this, line_edit]{
279 /* static thread here. */
280 static QThread* thread = nullptr;
281
282 if (thread)
283 return;
284
285 thread = QThread::create([this, line_edit]{
286 model->ParseSearch(Services::Search(Strings::ToUtf8String(line_edit->text())));
287 });
288
289 connect(thread, &QThread::finished, this, []{
290 thread->deleteLater();
291 thread = nullptr;
292 });
293
294 thread->start();
295 });
296 toolbar->addWidget(line_edit);
297 }
298
299 layout->addWidget(toolbar);
300 }
301
302 {
303 QFrame* line = new QFrame(this);
304 line->setFrameShape(QFrame::HLine);
305 line->setFrameShadow(QFrame::Sunken);
306 line->setLineWidth(1);
307 layout->addWidget(line);
308 }
309
310 {
311 treeview = new QTreeView(this);
312 treeview->setUniformRowHeights(true);
313 treeview->setAllColumnsShowFocus(false);
314 treeview->setAlternatingRowColors(true);
315 treeview->setSortingEnabled(true);
316 treeview->setSelectionMode(QAbstractItemView::ExtendedSelection);
317 treeview->setItemsExpandable(false);
318 treeview->setRootIsDecorated(false);
319 treeview->setContextMenuPolicy(Qt::CustomContextMenu);
320 treeview->setFrameShape(QFrame::NoFrame);
321
322 {
323 sort_model = new SearchPageListSortFilter(treeview);
324 model = new SearchPageListModel(treeview);
325 sort_model->setSourceModel(model);
326 sort_model->setSortRole(Qt::UserRole);
327 sort_model->setSortCaseSensitivity(Qt::CaseInsensitive);
328 treeview->setModel(sort_model);
329 }
330
331 // set column sizes
332 treeview->setColumnWidth(SearchPageListModel::SR_TITLE, 400);
333 treeview->setColumnWidth(SearchPageListModel::SR_TYPE, 60);
334 treeview->setColumnWidth(SearchPageListModel::SR_EPISODES, 60);
335 treeview->setColumnWidth(SearchPageListModel::SR_SCORE, 60);
336 treeview->setColumnWidth(SearchPageListModel::SR_SEASON, 100);
337
338 treeview->header()->setStretchLastSection(false);
339
340 /* Double click stuff */
341 connect(treeview, &QAbstractItemView::doubleClicked, this, &SearchPage::ItemDoubleClicked);
342 connect(treeview, &QWidget::customContextMenuRequested, this, &SearchPage::DisplayListMenu);
343
344 layout->addWidget(treeview);
345 }
346 }