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