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