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 = 10;
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()) {
65 boldFont.setBold(true);
69 return ItemTypeShowMore;
71 if (!errorMessage.isEmpty()) return errorMessage;
72 if (searching) return tr("Searching...");
73 if (canSearchMore) return tr("Show %1 More").arg(maxItems);
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);
82 return palette.color(QPalette::Dark);
83 case Qt::BackgroundColorRole:
84 if (!errorMessage.isEmpty())
85 return palette.color(QPalette::ToolTipBase);
94 } else if (row < 0 || row >= videos.size())
97 Video *video = videos.at(row);
101 return ItemTypeVideo;
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();
121 void PlaylistModel::setActiveRow(int row, bool notify) {
122 if ( rowExists( row ) ) {
125 m_activeVideo = videoAt(row);
127 int oldactiverow = m_activeRow;
129 if ( rowExists( oldactiverow ) )
130 emit dataChanged( createIndex( oldactiverow, 0 ), createIndex( oldactiverow, columnCount() - 1 ) );
132 emit dataChanged( createIndex( m_activeRow, 0 ), createIndex( m_activeRow, columnCount() - 1 ) );
133 if (notify) emit activeRowChanged(row);
142 int PlaylistModel::nextRow() const {
143 int nextRow = m_activeRow + 1;
144 if (rowExists(nextRow))
149 int PlaylistModel::previousRow() const {
150 int prevRow = m_activeRow - 1;
151 if (rowExists(prevRow))
156 Video* PlaylistModel::videoAt( int row ) const {
157 if ( rowExists( row ) )
158 return videos.at( row );
162 Video* PlaylistModel::activeVideo() const {
163 return m_activeVideo;
166 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
167 while (!videos.isEmpty())
168 delete videos.takeFirst();
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);
186 void PlaylistModel::searchMore(int max) {
187 if (searching) return;
189 firstSearch = skip == 1;
191 errorMessage.clear();
192 videoSource->loadVideos(max, skip);
196 void PlaylistModel::searchMore() {
197 searchMore(maxItems);
200 void PlaylistModel::searchNeeded() {
201 int remainingRows = videos.size() - m_activeRow;
202 int rowsNeeded = maxItems - remainingRows;
204 searchMore(rowsNeeded);
207 void PlaylistModel::abortSearch() {
208 while (!videos.isEmpty())
209 delete videos.takeFirst();
211 // if (videoSource) videoSource->abort();
218 void PlaylistModel::searchFinished(int total) {
220 canSearchMore = total >= max;
222 // update the message item
223 emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
225 if (!videoSource->getSuggestions().isEmpty())
226 emit haveSuggestions(videoSource->getSuggestions());
228 if (firstSearch && !videos.isEmpty())
229 handleFirstVideo(videos.first());
232 void PlaylistModel::searchError(QString message) {
233 errorMessage = message;
234 // update the message item
235 emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
238 void PlaylistModel::addVideos(QList<Video*> newVideos) {
239 if (newVideos.isEmpty()) return;
240 beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
241 videos.append(newVideos);
243 foreach (Video* video, newVideos) {
244 connect(video, SIGNAL(gotThumbnail()),
245 SLOT(updateThumbnail()), Qt::UniqueConnection);
246 video->loadThumbnail();
250 void PlaylistModel::handleFirstVideo(Video *video) {
252 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
253 if (currentVideoRow != -1) setActiveRow(currentVideoRow, false);
256 if (!settings.value("manualplay", false).toBool())
261 if (!settings.value("manualplay", false).toBool()) {
262 int newActiveRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
263 if (newActiveRow != -1) setActiveRow(newActiveRow, false);
264 else setActiveRow(0);
267 if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
269 static const int maxRecentElements = 10;
271 YTSearch *search = dynamic_cast<YTSearch *>(videoSource);
272 SearchParams *searchParams = search->getSearchParams();
275 QString query = searchParams->keywords();
276 if (!query.isEmpty() && !searchParams->isTransient()) {
277 if (query.startsWith("http://")) {
278 // Save the video title
279 query += "|" + videos.first()->title();
281 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
282 keywords.removeAll(query);
283 keywords.prepend(query);
284 while (keywords.size() > maxRecentElements)
285 keywords.removeLast();
286 settings.setValue(recentKeywordsKey, keywords);
290 QString channel = searchParams->author();
291 if (!channel.isEmpty() && !searchParams->isTransient()) {
293 if (!video->userId().isEmpty() && video->userId() != video->author())
294 value = video->userId() + "|" + video->author();
295 else value = video->author();
296 QStringList channels = settings.value(recentChannelsKey).toStringList();
297 channels.removeAll(value);
298 channels.removeAll(channel);
299 channels.prepend(value);
300 while (channels.size() > maxRecentElements)
301 channels.removeLast();
302 settings.setValue(recentChannelsKey, channels);
307 void PlaylistModel::updateThumbnail() {
309 Video *video = static_cast<Video *>(sender());
311 qDebug() << "Cannot get sender";
315 int row = rowForVideo(video);
316 emit dataChanged( createIndex( row, 0 ), createIndex( row, columnCount() - 1 ) );
323 * This function does not free memory
325 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
326 beginRemoveRows(QModelIndex(), position, position+rows-1);
327 for (int row = 0; row < rows; ++row) {
328 videos.removeAt(position);
334 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
335 QList<Video*> originalList(videos);
336 QList<Video*> delitems;
337 foreach (QModelIndex index, indexes) {
338 if (index.row() >= originalList.size()) continue;
339 Video* video = originalList.at(index.row());
340 int idx = videos.indexOf(video);
342 beginRemoveRows(QModelIndex(), idx, idx);
343 delitems.append(video);
344 videos.removeAll(video);
349 qDeleteAll(delitems);
353 // --- Sturm und drang ---
357 Qt::DropActions PlaylistModel::supportedDropActions() const {
358 return Qt::CopyAction;
361 Qt::DropActions PlaylistModel::supportedDragActions() const {
362 return Qt::CopyAction;
365 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
366 if (index.isValid()) {
367 if (index.row() == videos.size()) {
368 // don't drag the "show 10 more" item
369 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
370 } else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
372 return Qt::ItemIsDropEnabled;
375 QStringList PlaylistModel::mimeTypes() const {
377 types << "application/x-minitube-video";
381 QMimeData* PlaylistModel::mimeData( const QModelIndexList &indexes ) const {
382 VideoMimeData* mime = new VideoMimeData();
384 foreach( const QModelIndex &it, indexes ) {
386 if (row >= 0 && row < videos.size())
387 mime->addVideo( videos.at( it.row() ) );
393 bool PlaylistModel::dropMimeData(const QMimeData *data,
394 Qt::DropAction action, int row, int column,
395 const QModelIndex &parent) {
396 if (action == Qt::IgnoreAction)
399 if (!data->hasFormat("application/x-minitube-video"))
408 else if (parent.isValid())
409 beginRow = parent.row();
411 beginRow = rowCount(QModelIndex());
413 const VideoMimeData* videoMimeData = dynamic_cast<const VideoMimeData*>( data );
414 if(!videoMimeData ) return false;
416 QList<Video*> droppedVideos = videoMimeData->videos();
417 foreach( Video *video, droppedVideos) {
420 int videoRow = videos.indexOf(video);
421 removeRows(videoRow, 1, QModelIndex());
423 // and then add them again at the new position
424 beginInsertRows(QModelIndex(), beginRow, beginRow);
425 videos.insert(beginRow, video);
430 // fix m_activeRow after all this
431 m_activeRow = videos.indexOf(m_activeVideo);
433 // let the MediaView restore the selection
434 emit needSelectionFor(droppedVideos);
440 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
441 for (int i = 0; i < videos.size(); ++i) {
442 Video *v = videos.at(i);
443 if (v->id() == videoId) return i;
448 int PlaylistModel::rowForVideo(Video* video) {
449 return videos.indexOf(video);
452 QModelIndex PlaylistModel::indexForVideo(Video* video) {
453 return createIndex(videos.indexOf(video), 0);
456 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
457 QList<Video*> movedVideos;
459 foreach (QModelIndex index, indexes) {
460 int row = index.row();
461 if (row >= videos.size()) continue;
462 // qDebug() << "index row" << row;
463 Video *video = videoAt(row);
464 movedVideos << video;
467 int end=up ? -1 : rowCount()-1, mod=up ? -1 : 1;
468 foreach (Video *video, movedVideos) {
470 int row = rowForVideo(video);
471 if (row+mod==end) { end=row; continue; }
472 // qDebug() << "video row" << row;
473 removeRows(row, 1, QModelIndex());
478 beginInsertRows(QModelIndex(), row, row);
479 videos.insert(row, video);
484 emit needSelectionFor(movedVideos);
490 void PlaylistModel::setHoveredRow(int row) {
491 int oldRow = hoveredRow;
493 emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
494 emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
497 void PlaylistModel::clearHover() {
498 emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
502 /* clickable author */
504 void PlaylistModel::enterAuthorHover() {
505 if (authorHovered) return;
506 authorHovered = true;
510 void PlaylistModel::exitAuthorHover() {
511 if (!authorHovered) return;
512 authorHovered = false;
514 setHoveredRow(hoveredRow);
517 void PlaylistModel::enterAuthorPressed() {
518 if (authorPressed) return;
519 authorPressed = true;
523 void PlaylistModel::exitAuthorPressed() {
524 if (!authorPressed) return;
525 authorPressed = false;
529 void PlaylistModel::updateAuthor() {
530 emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );