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