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