3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
21 #include "playlistmodel.h"
22 #include "videomimedata.h"
23 #include "videosource.h"
26 #include "searchparams.h"
27 #include "mediaview.h"
29 static const int maxItems = 50;
30 static const QString recentKeywordsKey = "recentKeywords";
31 static const QString recentChannelsKey = "recentChannels";
33 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
43 authorHovered = false;
44 authorPressed = false;
47 int PlaylistModel::rowCount(const QModelIndex &/*parent*/) const {
48 int count = videos.size();
50 // add the message item
51 if (videos.isEmpty() || !searching)
57 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
59 int row = index.row();
61 if (row == videos.size()) {
67 return ItemTypeShowMore;
69 if (!errorMessage.isEmpty()) return errorMessage;
70 if (searching) return tr("Searching...");
71 if (canSearchMore) return tr("Show %1 More").arg("").simplified();
72 if (videos.isEmpty()) return tr("No videos");
73 else return tr("No more videos");
74 case Qt::TextAlignmentRole:
75 return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
76 case Qt::ForegroundRole:
77 if (!errorMessage.isEmpty())
78 return palette.color(QPalette::ToolTipText);
80 return palette.color(QPalette::Dark);
81 case Qt::BackgroundColorRole:
82 if (!errorMessage.isEmpty())
83 return palette.color(QPalette::ToolTipBase);
90 } else if (row < 0 || row >= videos.size())
93 Video *video = videos.at(row);
99 return QVariant::fromValue(QPointer<Video>(video));
100 case ActiveTrackRole:
101 return video == m_activeVideo;
102 case Qt::DisplayRole:
103 return video->title();
104 case HoveredItemRole:
105 return hoveredRow == index.row();
106 case AuthorHoveredRole:
107 return authorHovered;
108 case AuthorPressedRole:
109 return authorPressed;
111 case Qt::StatusTipRole:
112 return video->description();
119 void PlaylistModel::setActiveRow(int row, bool notify) {
120 if ( rowExists( row ) ) {
123 m_activeVideo = videoAt(row);
125 int oldactiverow = m_activeRow;
127 if ( rowExists( oldactiverow ) )
128 emit dataChanged( createIndex( oldactiverow, 0 ), createIndex( oldactiverow, columnCount() - 1 ) );
130 emit dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() - 1 ) );
131 if (notify) emit activeRowChanged(row);
140 int PlaylistModel::nextRow() const {
141 int nextRow = m_activeRow + 1;
142 if (rowExists(nextRow))
147 int PlaylistModel::previousRow() const {
148 int prevRow = m_activeRow - 1;
149 if (rowExists(prevRow))
154 Video* PlaylistModel::videoAt( int row ) const {
155 if ( rowExists( row ) )
156 return videos.at( row );
160 Video* PlaylistModel::activeVideo() const {
161 return m_activeVideo;
164 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
166 while (!videos.isEmpty()) delete videos.takeFirst();
173 this->videoSource = videoSource;
174 connect(videoSource, SIGNAL(gotVideos(QList<Video*>)),
175 SLOT(addVideos(QList<Video*>)), Qt::UniqueConnection);
176 connect(videoSource, SIGNAL(finished(int)),
177 SLOT(searchFinished(int)), Qt::UniqueConnection);
178 connect(videoSource, SIGNAL(error(QString)),
179 SLOT(searchError(QString)), Qt::UniqueConnection);
184 void PlaylistModel::searchMore(int max) {
185 if (searching) return;
187 firstSearch = startIndex == 1;
189 errorMessage.clear();
190 videoSource->loadVideos(max, startIndex);
194 void PlaylistModel::searchMore() {
195 searchMore(maxItems);
198 void PlaylistModel::searchNeeded() {
199 const int desiredRowsAhead = 10;
200 int remainingRows = videos.size() - m_activeRow;
201 if (remainingRows < desiredRowsAhead)
202 searchMore(maxItems);
205 void PlaylistModel::abortSearch() {
206 QMutexLocker locker(&mutex);
208 // while (!videos.isEmpty()) delete videos.takeFirst();
209 // if (videoSource) videoSource->abort();
218 void PlaylistModel::searchFinished(int total) {
219 qDebug() << __PRETTY_FUNCTION__ << total;
221 canSearchMore = videoSource->hasMoreVideos();
223 // update the message item
224 emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
226 if (!videoSource->getSuggestions().isEmpty())
227 emit haveSuggestions(videoSource->getSuggestions());
229 if (firstSearch && !videos.isEmpty())
230 handleFirstVideo(videos.first());
233 void PlaylistModel::searchError(const QString &message) {
234 errorMessage = message;
235 // update the message item
236 emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
239 void PlaylistModel::addVideos(QList<Video*> newVideos) {
240 if (newVideos.isEmpty()) return;
241 beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
242 videos.append(newVideos);
244 foreach (Video* video, newVideos) {
245 connect(video, SIGNAL(gotThumbnail()),
246 SLOT(updateVideoSender()), Qt::UniqueConnection);
247 video->loadThumbnail();
248 qApp->processEvents();
252 void PlaylistModel::handleFirstVideo(Video *video) {
254 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
255 if (currentVideoRow != -1) setActiveRow(currentVideoRow, false);
258 if (!settings.value("manualplay", false).toBool())
263 if (!settings.value("manualplay", false).toBool()) {
264 int newActiveRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
265 if (newActiveRow != -1) setActiveRow(newActiveRow, false);
266 else setActiveRow(0);
269 if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
271 static const int maxRecentElements = 10;
273 YTSearch *search = qobject_cast<YTSearch *>(videoSource);
274 SearchParams *searchParams = search->getSearchParams();
277 QString query = searchParams->keywords();
278 if (!query.isEmpty() && !searchParams->isTransient()) {
279 if (query.startsWith("http://")) {
280 // Save the video title
281 query += "|" + videos.first()->title();
283 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
284 keywords.removeAll(query);
285 keywords.prepend(query);
286 while (keywords.size() > maxRecentElements)
287 keywords.removeLast();
288 settings.setValue(recentKeywordsKey, keywords);
292 QString channelId = searchParams->channelId();
293 if (!channelId.isEmpty() && !searchParams->isTransient()) {
295 if (!video->channelId().isEmpty() && video->channelId() != video->channelTitle())
296 value = video->channelId() + "|" + video->channelTitle();
297 else value = video->channelTitle();
298 QStringList channels = settings.value(recentChannelsKey).toStringList();
299 channels.removeAll(value);
300 channels.removeAll(channelId);
301 channels.prepend(value);
302 while (channels.size() > maxRecentElements)
303 channels.removeLast();
304 settings.setValue(recentChannelsKey, channels);
309 void PlaylistModel::updateVideoSender() {
310 Video *video = static_cast<Video *>(sender());
312 qDebug() << "Cannot get sender";
315 int row = rowForVideo(video);
316 emit dataChanged( createIndex( row, 0 ), createIndex( row, columnCount() - 1 ) );
319 void PlaylistModel::emitDataChanged() {
320 QModelIndex index = createIndex(rowCount()-1, 0);
321 emit dataChanged(index, index);
327 * This function does not free memory
329 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
330 beginRemoveRows(QModelIndex(), position, position+rows-1);
331 for (int row = 0; row < rows; ++row) {
332 videos.removeAt(position);
338 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
339 QList<Video*> originalList(videos);
340 QList<Video*> delitems;
341 foreach (const QModelIndex &index, indexes) {
342 if (index.row() >= originalList.size()) continue;
343 Video* video = originalList.at(index.row());
344 int idx = videos.indexOf(video);
346 beginRemoveRows(QModelIndex(), idx, idx);
347 delitems.append(video);
348 videos.removeAll(video);
353 qDeleteAll(delitems);
357 // --- Sturm und drang ---
361 Qt::DropActions PlaylistModel::supportedDropActions() const {
362 return Qt::CopyAction;
365 Qt::DropActions PlaylistModel::supportedDragActions() const {
366 return Qt::CopyAction;
369 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
370 if (index.isValid()) {
371 if (index.row() == videos.size()) {
372 // don't drag the "show more" item
373 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
374 } else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
376 return Qt::ItemIsDropEnabled;
379 QStringList PlaylistModel::mimeTypes() const {
381 types << "application/x-minitube-video";
385 QMimeData* PlaylistModel::mimeData( const QModelIndexList &indexes ) const {
386 VideoMimeData* mime = new VideoMimeData();
388 foreach( const QModelIndex &it, indexes ) {
390 if (row >= 0 && row < videos.size())
391 mime->addVideo( videos.at( it.row() ) );
397 bool PlaylistModel::dropMimeData(const QMimeData *data,
398 Qt::DropAction action, int row, int column,
399 const QModelIndex &parent) {
400 if (action == Qt::IgnoreAction)
403 if (!data->hasFormat("application/x-minitube-video"))
412 else if (parent.isValid())
413 beginRow = parent.row();
415 beginRow = rowCount(QModelIndex());
417 const VideoMimeData* videoMimeData = qobject_cast<const VideoMimeData*>( data );
418 if(!videoMimeData ) return false;
420 QList<Video*> droppedVideos = videoMimeData->videos();
421 foreach( Video *video, droppedVideos) {
424 int videoRow = videos.indexOf(video);
425 removeRows(videoRow, 1, QModelIndex());
427 // and then add them again at the new position
428 beginInsertRows(QModelIndex(), beginRow, beginRow);
429 videos.insert(beginRow, video);
434 // fix m_activeRow after all this
435 m_activeRow = videos.indexOf(m_activeVideo);
437 // let the MediaView restore the selection
438 emit needSelectionFor(droppedVideos);
444 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
445 if (videoId.isEmpty()) return -1;
446 for (int i = 0; i < videos.size(); ++i) {
447 Video *v = videos.at(i);
448 // qDebug() << "Comparing" << v->id() << videoId;
449 if (v->id() == videoId) return i;
454 int PlaylistModel::rowForVideo(Video* video) {
455 return videos.indexOf(video);
458 QModelIndex PlaylistModel::indexForVideo(Video* video) {
459 return createIndex(videos.indexOf(video), 0);
462 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
463 QList<Video*> movedVideos;
465 foreach (const QModelIndex &index, indexes) {
466 int row = index.row();
467 if (row >= videos.size()) continue;
468 // qDebug() << "index row" << row;
469 Video *video = videoAt(row);
470 movedVideos << video;
473 int end=up ? -1 : rowCount()-1, mod=up ? -1 : 1;
474 foreach (Video *video, movedVideos) {
476 int row = rowForVideo(video);
477 if (row+mod==end) { end=row; continue; }
478 // qDebug() << "video row" << row;
479 removeRows(row, 1, QModelIndex());
484 beginInsertRows(QModelIndex(), row, row);
485 videos.insert(row, video);
490 emit needSelectionFor(movedVideos);
496 void PlaylistModel::setHoveredRow(int row) {
497 int oldRow = hoveredRow;
499 emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
500 emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
503 void PlaylistModel::clearHover() {
504 int oldRow = hoveredRow;
506 emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1) );
509 void PlaylistModel::updateHoveredRow() {
510 emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
513 /* clickable author */
515 void PlaylistModel::enterAuthorHover() {
516 if (authorHovered) return;
517 authorHovered = true;
521 void PlaylistModel::exitAuthorHover() {
522 if (!authorHovered) return;
523 authorHovered = false;
525 setHoveredRow(hoveredRow);
528 void PlaylistModel::enterAuthorPressed() {
529 if (authorPressed) return;
530 authorPressed = true;
534 void PlaylistModel::exitAuthorPressed() {
535 if (!authorPressed) return;
536 authorPressed = false;