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