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 "mediaview.h"
23 #include "searchparams.h"
25 #include "videomimedata.h"
26 #include "videosource.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) count++;
56 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
57 int row = index.row();
59 if (row == videos.size()) {
64 return ItemTypeShowMore;
66 if (!errorMessage.isEmpty()) return errorMessage;
67 if (searching) return tr("Searching...");
68 if (canSearchMore) return tr("Show %1 More").arg("").simplified();
70 return tr("No videos");
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);
79 return palette.color(QPalette::Dark);
80 case Qt::BackgroundColorRole:
81 if (!errorMessage.isEmpty())
82 return palette.color(QPalette::ToolTipBase);
89 } else if (row < 0 || row >= videos.size())
92 Video *video = videos.at(row);
98 return QVariant::fromValue(QPointer<Video>(video));
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;
110 case Qt::StatusTipRole:
111 return video->description();
118 void PlaylistModel::setActiveRow(int row, bool notify) {
119 if (rowExists(row)) {
121 m_activeVideo = videoAt(row);
123 int oldactiverow = m_activeRow;
125 if (rowExists(oldactiverow))
126 emit dataChanged(createIndex(oldactiverow, 0),
127 createIndex(oldactiverow, columnCount() - 1));
129 emit dataChanged(createIndex(m_activeRow, 0), createIndex(m_activeRow, columnCount() - 1));
130 if (notify) emit activeRowChanged(row);
138 int PlaylistModel::nextRow() const {
139 int nextRow = m_activeRow + 1;
140 if (rowExists(nextRow)) return nextRow;
144 int PlaylistModel::previousRow() const {
145 int prevRow = m_activeRow - 1;
146 if (rowExists(prevRow)) return prevRow;
150 Video *PlaylistModel::videoAt(int row) const {
151 if (rowExists(row)) return videos.at(row);
155 Video *PlaylistModel::activeVideo() const {
156 return m_activeVideo;
159 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
161 while (!videos.isEmpty())
162 delete videos.takeFirst();
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);
177 void PlaylistModel::searchMore(int max) {
178 if (searching) return;
180 firstSearch = startIndex == 1;
182 errorMessage.clear();
183 videoSource->loadVideos(max, startIndex);
187 void PlaylistModel::searchMore() {
188 searchMore(maxItems);
191 void PlaylistModel::searchNeeded() {
192 const int desiredRowsAhead = 10;
193 int remainingRows = videos.size() - m_activeRow;
194 if (remainingRows < desiredRowsAhead) searchMore(maxItems);
197 void PlaylistModel::abortSearch() {
198 QMutexLocker locker(&mutex);
200 if (videoSource) videoSource->abort();
211 void PlaylistModel::searchFinished(int total) {
214 canSearchMore = videoSource->hasMoreVideos();
216 // update the message item
217 emit dataChanged(createIndex(maxItems, 0), createIndex(maxItems, columnCount() - 1));
219 if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
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));
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);
234 for (Video *video : newVideos) {
235 connect(video, SIGNAL(gotThumbnail()), SLOT(updateVideoSender()), Qt::UniqueConnection);
236 video->loadThumbnail();
240 void PlaylistModel::handleFirstVideo(Video *video) {
242 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
243 if (currentVideoRow != -1)
244 setActiveRow(currentVideoRow, false);
246 if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
249 if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
250 static const int maxRecentElements = 10;
252 YTSearch *search = qobject_cast<YTSearch *>(videoSource);
253 SearchParams *searchParams = search->getSearchParams();
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();
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);
271 QString channelId = searchParams->channelId();
272 if (!channelId.isEmpty() && !searchParams->isTransient()) {
274 if (!video->getChannelId().isEmpty() &&
275 video->getChannelId() != video->getChannelTitle())
276 value = video->getChannelId() + "|" + video->getChannelTitle();
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);
290 void PlaylistModel::updateVideoSender() {
291 Video *video = static_cast<Video *>(sender());
293 qDebug() << "Cannot get sender";
296 int row = rowForVideo(video);
297 emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
300 void PlaylistModel::emitDataChanged() {
301 QModelIndex index = createIndex(rowCount() - 1, 0);
302 emit dataChanged(index, index);
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);
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);
325 beginRemoveRows(QModelIndex(), idx, idx);
326 delitems.append(video);
327 videos.removeAll(video);
328 video->deleteLater();
332 qDeleteAll(delitems);
336 // --- Sturm und drang ---
338 Qt::DropActions PlaylistModel::supportedDropActions() const {
339 return Qt::CopyAction;
342 Qt::DropActions PlaylistModel::supportedDragActions() const {
343 return Qt::CopyAction;
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;
352 return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
354 return Qt::ItemIsDropEnabled;
357 QStringList PlaylistModel::mimeTypes() const {
359 types << "application/x-minitube-video";
363 QMimeData *PlaylistModel::mimeData(const QModelIndexList &indexes) const {
364 VideoMimeData *mime = new VideoMimeData();
366 for (const QModelIndex &it : indexes) {
368 if (row >= 0 && row < videos.size()) mime->addVideo(videos.at(it.row()));
374 bool PlaylistModel::dropMimeData(const QMimeData *data,
375 Qt::DropAction action,
378 const QModelIndex &parent) {
379 if (action == Qt::IgnoreAction) return true;
381 if (!data->hasFormat("application/x-minitube-video")) return false;
383 if (column > 0) return false;
388 else if (parent.isValid())
389 beginRow = parent.row();
391 beginRow = rowCount(QModelIndex());
393 const VideoMimeData *videoMimeData = qobject_cast<const VideoMimeData *>(data);
394 if (!videoMimeData) return false;
396 const QVector<Video *> &droppedVideos = videoMimeData->getVideos();
397 for (Video *video : droppedVideos) {
399 int videoRow = videos.indexOf(video);
400 removeRows(videoRow, 1, QModelIndex());
402 // and then add them again at the new position
403 beginInsertRows(QModelIndex(), beginRow, beginRow);
404 videos.insert(beginRow, video);
408 // fix m_activeRow after all this
409 m_activeRow = videos.indexOf(m_activeVideo);
411 // let the MediaView restore the selection
412 emit needSelectionFor(droppedVideos);
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;
427 int PlaylistModel::rowForVideo(Video *video) {
428 return videos.indexOf(video);
431 QModelIndex PlaylistModel::indexForVideo(Video *video) {
432 return createIndex(videos.indexOf(video), 0);
435 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
436 QVector<Video *> movedVideos;
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;
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) {
453 // qDebug() << "video row" << row;
454 removeRows(row, 1, QModelIndex());
461 beginInsertRows(QModelIndex(), row, row);
462 videos.insert(row, video);
466 emit needSelectionFor(movedVideos);
471 void PlaylistModel::setHoveredRow(int row) {
472 int oldRow = hoveredRow;
474 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
475 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
478 void PlaylistModel::clearHover() {
479 int oldRow = hoveredRow;
481 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
484 void PlaylistModel::updateHoveredRow() {
485 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
488 /* clickable author */
490 void PlaylistModel::enterAuthorHover() {
491 if (authorHovered) return;
492 authorHovered = true;
496 void PlaylistModel::exitAuthorHover() {
497 if (!authorHovered) return;
498 authorHovered = false;
500 setHoveredRow(hoveredRow);
503 void PlaylistModel::enterAuthorPressed() {
504 if (authorPressed) return;
505 authorPressed = true;
509 void PlaylistModel::exitAuthorPressed() {
510 if (!authorPressed) return;
511 authorPressed = false;