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