]> git.sur5r.net Git - minitube/blob - src/playlistmodel.cpp
New upstream version 3.6.1
[minitube] / src / playlistmodel.cpp
1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
6 Minitube is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 Minitube is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with Minitube.  If not, see <http://www.gnu.org/licenses/>.
18
19 $END_LICENSE */
20
21 #include "playlistmodel.h"
22 #include "mediaview.h"
23 #include "searchparams.h"
24 #include "video.h"
25 #include "videomimedata.h"
26 #include "videosource.h"
27
28 #include "ivchannelsource.h"
29 #include "ivsearch.h"
30 #include "ytsearch.h"
31
32 #include "ytjschannelsource.h"
33 #include "ytjssearch.h"
34
35 namespace {
36 const QString recentKeywordsKey = "recentKeywords";
37 const QString recentChannelsKey = "recentChannels";
38 } // namespace
39
40 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
41     videoSource = nullptr;
42     searching = false;
43     canSearchMore = true;
44     firstSearch = false;
45     m_activeVideo = nullptr;
46     m_activeRow = -1;
47     startIndex = 1;
48     max = 0;
49     hoveredRow = -1;
50     authorHovered = false;
51     authorPressed = false;
52 }
53
54 int PlaylistModel::rowCount(const QModelIndex & /*parent*/) const {
55     int count = videos.size();
56
57     // add the message item
58     if (videos.isEmpty() || !searching) count++;
59
60     return count;
61 }
62
63 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
64     int row = index.row();
65
66     if (row == videos.size()) {
67         QPalette palette;
68
69         switch (role) {
70         case ItemTypeRole:
71             return ItemTypeShowMore;
72         case Qt::DisplayRole:
73             if (!errorMessage.isEmpty()) return errorMessage;
74             if (searching) return QString(); // tr("Searching...");
75             if (canSearchMore) return tr("Show %1 More").arg("").simplified();
76             if (videos.isEmpty())
77                 return tr("No videos");
78             else
79                 return tr("No more videos");
80         case Qt::TextAlignmentRole:
81             return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
82         case Qt::ForegroundRole:
83             return palette.color(QPalette::Dark);
84         case Qt::BackgroundRole:
85             if (!errorMessage.isEmpty())
86                 return palette.color(QPalette::ToolTipBase);
87             else
88                 return QVariant();
89         default:
90             return QVariant();
91         }
92
93     } else if (row < 0 || row >= videos.size())
94         return QVariant();
95
96     Video *video = videos.at(row);
97
98     switch (role) {
99     case ItemTypeRole:
100         return ItemTypeVideo;
101     case VideoRole:
102         return QVariant::fromValue(QPointer<Video>(video));
103     case ActiveTrackRole:
104         return video == m_activeVideo;
105     case Qt::DisplayRole:
106         return video->getTitle();
107     case HoveredItemRole:
108         return hoveredRow == index.row();
109     case AuthorHoveredRole:
110         return authorHovered;
111     case AuthorPressedRole:
112         return authorPressed;
113         /*
114     case Qt::StatusTipRole:
115         return video->description();
116         */
117     }
118
119     return QVariant();
120 }
121
122 void PlaylistModel::setActiveRow(int row, bool notify) {
123     if (rowExists(row)) {
124         m_activeRow = row;
125         Video *previousVideo = m_activeVideo;
126         m_activeVideo = videoAt(row);
127
128         int oldactiverow = m_activeRow;
129
130         if (rowExists(oldactiverow))
131             emit dataChanged(createIndex(oldactiverow, 0),
132                              createIndex(oldactiverow, columnCount() - 1));
133
134         emit dataChanged(createIndex(m_activeRow, 0), createIndex(m_activeRow, columnCount() - 1));
135         if (notify) emit activeVideoChanged(m_activeVideo, previousVideo);
136
137     } else {
138         m_activeRow = -1;
139         m_activeVideo = nullptr;
140     }
141 }
142
143 int PlaylistModel::nextRow() const {
144     int nextRow = m_activeRow + 1;
145     if (rowExists(nextRow)) return nextRow;
146     return -1;
147 }
148
149 int PlaylistModel::previousRow() const {
150     int prevRow = m_activeRow - 1;
151     if (rowExists(prevRow)) return prevRow;
152     return -1;
153 }
154
155 Video *PlaylistModel::videoAt(int row) const {
156     if (rowExists(row)) return videos.at(row);
157     return nullptr;
158 }
159
160 Video *PlaylistModel::activeVideo() const {
161     return m_activeVideo;
162 }
163
164 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
165     beginResetModel();
166
167     qDeleteAll(videos);
168     videos.clear();
169
170     qDeleteAll(deletedVideos);
171     deletedVideos.clear();
172
173     m_activeVideo = nullptr;
174     m_activeRow = -1;
175     startIndex = 1;
176     endResetModel();
177
178     this->videoSource = videoSource;
179     connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)), SLOT(addVideos(QVector<Video *>)),
180             Qt::UniqueConnection);
181     connect(videoSource, SIGNAL(finished(int)), SLOT(searchFinished(int)), Qt::UniqueConnection);
182     connect(videoSource, SIGNAL(error(QString)), SLOT(searchError(QString)), Qt::UniqueConnection);
183     connect(videoSource, &QObject::destroyed, this,
184             [this, videoSource] {
185                 if (this->videoSource == videoSource) {
186                     this->videoSource = nullptr;
187                 }
188             },
189             Qt::UniqueConnection);
190
191     canSearchMore = true;
192     searchMore();
193 }
194
195 void PlaylistModel::searchMore() {
196     if (!canSearchMore || videoSource == nullptr || searching) return;
197     searching = true;
198     firstSearch = startIndex == 1;
199     max = videoSource->maxResults();
200     if (max == 0) max = 20;
201     errorMessage.clear();
202     videoSource->loadVideos(max, startIndex);
203     startIndex += max;
204 }
205
206 void PlaylistModel::searchNeeded() {
207     const int desiredRowsAhead = 10;
208     int remainingRows = videos.size() - m_activeRow;
209     if (remainingRows < desiredRowsAhead) searchMore();
210 }
211
212 void PlaylistModel::abortSearch() {
213     QMutexLocker locker(&mutex);
214     beginResetModel();
215     if (videoSource) videoSource->abort();
216     qDeleteAll(videos);
217     videos.clear();
218     videos.squeeze();
219     searching = false;
220     m_activeRow = -1;
221     m_activeVideo = nullptr;
222     startIndex = 1;
223     endResetModel();
224 }
225
226 void PlaylistModel::searchFinished(int total) {
227     Q_UNUSED(total);
228     searching = false;
229     canSearchMore = videoSource->hasMoreVideos();
230
231     // update the message item
232     emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
233
234     if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
235 }
236
237 void PlaylistModel::searchError(const QString &message) {
238     errorMessage = message;
239     // update the message item
240     emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
241 }
242
243 void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
244     if (newVideos.isEmpty()) return;
245     videos.reserve(videos.size() + newVideos.size());
246     beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
247     videos.append(newVideos);
248     endInsertRows();
249     for (Video *video : newVideos) {
250         connect(video, SIGNAL(gotThumbnail()), SLOT(updateVideoSender()), Qt::UniqueConnection);
251         video->loadThumbnail();
252     }
253 }
254
255 void PlaylistModel::handleFirstVideo(Video *video) {
256     QSettings settings;
257     int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
258     if (currentVideoRow != -1)
259         setActiveRow(currentVideoRow, false);
260     else {
261         if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
262     }
263
264     auto clazz = videoSource->metaObject()->className();
265     if (clazz == QLatin1String("YTSearch") || clazz == QLatin1String("IVSearch") ||
266         clazz == QLatin1String("IVChannelSource") || clazz == QLatin1String("YTJSSearch") ||
267         clazz == QLatin1String("YTJSChannelSource")) {
268         static const int maxRecentElements = 10;
269
270         SearchParams *searchParams;
271         if (clazz == QLatin1String("YTSearch")) {
272             auto search = qobject_cast<YTSearch *>(videoSource);
273             searchParams = search->getSearchParams();
274         } else if (clazz == QLatin1String("IVSearch")) {
275             auto search = qobject_cast<IVSearch *>(videoSource);
276             searchParams = search->getSearchParams();
277         } else if (clazz == QLatin1String("IVChannelSource")) {
278             auto search = qobject_cast<IVChannelSource *>(videoSource);
279             searchParams = search->getSearchParams();
280         } else if (clazz == QLatin1String("YTJSSearch")) {
281             auto search = qobject_cast<YTJSSearch *>(videoSource);
282             searchParams = search->getSearchParams();
283         } else if (clazz == QLatin1String("YTJSChannelSource")) {
284             auto search = qobject_cast<YTJSChannelSource *>(videoSource);
285             searchParams = search->getSearchParams();
286         }
287
288         // save keyword
289         QString query = searchParams->keywords();
290         if (!query.isEmpty() && !searchParams->isTransient()) {
291             if (query.startsWith("http://")) {
292                 // Save the video title
293                 query += "|" + videos.at(0)->getTitle();
294             }
295             QStringList keywords = settings.value(recentKeywordsKey).toStringList();
296             keywords.removeAll(query);
297             keywords.prepend(query);
298             while (keywords.size() > maxRecentElements)
299                 keywords.removeLast();
300             settings.setValue(recentKeywordsKey, keywords);
301         }
302
303         // save channel
304         QString channelId = searchParams->channelId();
305         if (!channelId.isEmpty() && !searchParams->isTransient()) {
306             QString value;
307             if (!video->getChannelId().isEmpty() &&
308                 video->getChannelId() != video->getChannelTitle())
309                 value = video->getChannelId() + "|" + video->getChannelTitle();
310             else
311                 value = video->getChannelTitle();
312             QStringList channels = settings.value(recentChannelsKey).toStringList();
313             channels.removeAll(value);
314             channels.removeAll(channelId);
315             channels.prepend(value);
316             while (channels.size() > maxRecentElements)
317                 channels.removeLast();
318             settings.setValue(recentChannelsKey, channels);
319         }
320     }
321 }
322
323 void PlaylistModel::updateVideoSender() {
324     Video *video = static_cast<Video *>(sender());
325     if (!video) {
326         qDebug() << "Cannot get sender";
327         return;
328     }
329     int row = rowForVideo(video);
330     emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
331 }
332
333 void PlaylistModel::emitDataChanged() {
334     QModelIndex index = createIndex(rowCount() - 1, 0);
335     emit dataChanged(index, index);
336 }
337
338 // --- item removal
339
340 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
341     beginRemoveRows(QModelIndex(), position, position + rows - 1);
342     for (int row = 0; row < rows; ++row) {
343         Video *video = videos.takeAt(position);
344     }
345     endRemoveRows();
346     return true;
347 }
348
349 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
350     QVector<Video *> originalList(videos);
351     for (const QModelIndex &index : indexes) {
352         if (index.row() >= originalList.size()) continue;
353         Video *video = originalList.at(index.row());
354         int idx = videos.indexOf(video);
355         if (idx != -1) {
356             beginRemoveRows(QModelIndex(), idx, idx);
357             deletedVideos.append(video);
358             if (m_activeVideo == video) {
359                 m_activeVideo = nullptr;
360                 m_activeRow = -1;
361             }
362             videos.removeAll(video);
363             endRemoveRows();
364         }
365     }
366     videos.squeeze();
367 }
368
369 // --- Sturm und drang ---
370
371 Qt::DropActions PlaylistModel::supportedDropActions() const {
372     return Qt::CopyAction;
373 }
374
375 Qt::DropActions PlaylistModel::supportedDragActions() const {
376     return Qt::CopyAction;
377 }
378
379 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
380     if (index.isValid()) {
381         if (index.row() == videos.size()) {
382             // don't drag the "show more" item
383             return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
384         } else
385             return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
386     }
387     return Qt::ItemIsDropEnabled;
388 }
389
390 QStringList PlaylistModel::mimeTypes() const {
391     QStringList types;
392     types << "application/x-minitube-video";
393     return types;
394 }
395
396 QMimeData *PlaylistModel::mimeData(const QModelIndexList &indexes) const {
397     VideoMimeData *mime = new VideoMimeData();
398
399     for (const QModelIndex &it : indexes) {
400         int row = it.row();
401         if (row >= 0 && row < videos.size()) mime->addVideo(videos.at(it.row()));
402     }
403
404     return mime;
405 }
406
407 bool PlaylistModel::dropMimeData(const QMimeData *data,
408                                  Qt::DropAction action,
409                                  int row,
410                                  int column,
411                                  const QModelIndex &parent) {
412     if (action == Qt::IgnoreAction) return true;
413
414     if (!data->hasFormat("application/x-minitube-video")) return false;
415
416     if (column > 0) return false;
417
418     int beginRow;
419     if (row != -1)
420         beginRow = row;
421     else if (parent.isValid())
422         beginRow = parent.row();
423     else
424         beginRow = rowCount(QModelIndex());
425
426     const VideoMimeData *videoMimeData = qobject_cast<const VideoMimeData *>(data);
427     if (!videoMimeData) return false;
428
429     const QVector<Video *> &droppedVideos = videoMimeData->getVideos();
430     for (Video *video : droppedVideos) {
431         // remove videos
432         int videoRow = videos.indexOf(video);
433         removeRows(videoRow, 1, QModelIndex());
434
435         // and then add them again at the new position
436         beginInsertRows(QModelIndex(), beginRow, beginRow);
437         videos.insert(beginRow, video);
438         endInsertRows();
439     }
440
441     // fix m_activeRow after all this
442     m_activeRow = videos.indexOf(m_activeVideo);
443
444     // let the MediaView restore the selection
445     emit needSelectionFor(droppedVideos);
446
447     return true;
448 }
449
450 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
451     if (videoId.isEmpty()) return -1;
452     for (int i = 0; i < videos.size(); ++i) {
453         Video *v = videos.at(i);
454         // qDebug() << "Comparing" << v->id() << videoId;
455         if (v->getId() == videoId) return i;
456     }
457     return -1;
458 }
459
460 int PlaylistModel::rowForVideo(Video *video) {
461     return videos.indexOf(video);
462 }
463
464 QModelIndex PlaylistModel::indexForVideo(Video *video) {
465     return createIndex(videos.indexOf(video), 0);
466 }
467
468 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
469     QVector<Video *> movedVideos;
470
471     for (const QModelIndex &index : indexes) {
472         int row = index.row();
473         if (row >= videos.size()) continue;
474         // qDebug() << "index row" << row;
475         Video *video = videoAt(row);
476         movedVideos << video;
477     }
478
479     int end = up ? -1 : rowCount() - 1, mod = up ? -1 : 1;
480     for (Video *video : movedVideos) {
481         int row = rowForVideo(video);
482         if (row + mod == end) {
483             end = row;
484             continue;
485         }
486         // qDebug() << "video row" << row;
487         removeRows(row, 1, QModelIndex());
488
489         if (up)
490             row--;
491         else
492             row++;
493
494         beginInsertRows(QModelIndex(), row, row);
495         videos.insert(row, video);
496         endInsertRows();
497     }
498
499     emit needSelectionFor(movedVideos);
500 }
501
502 /* row hovering */
503
504 void PlaylistModel::setHoveredRow(int row) {
505     int oldRow = hoveredRow;
506     hoveredRow = row;
507     emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
508     emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
509 }
510
511 void PlaylistModel::clearHover() {
512     int oldRow = hoveredRow;
513     hoveredRow = -1;
514     emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
515 }
516
517 void PlaylistModel::updateHoveredRow() {
518     emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
519 }
520
521 /* clickable author */
522
523 void PlaylistModel::enterAuthorHover() {
524     if (authorHovered) return;
525     authorHovered = true;
526     updateHoveredRow();
527 }
528
529 void PlaylistModel::exitAuthorHover() {
530     if (!authorHovered) return;
531     authorHovered = false;
532     updateHoveredRow();
533     setHoveredRow(hoveredRow);
534 }
535
536 void PlaylistModel::enterAuthorPressed() {
537     if (authorPressed) return;
538     authorPressed = true;
539     updateHoveredRow();
540 }
541
542 void PlaylistModel::exitAuthorPressed() {
543     if (!authorPressed) return;
544     authorPressed = false;
545     updateHoveredRow();
546 }