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"
28 #include "ivchannelsource.h"
32 #include "ytjschannelsource.h"
33 #include "ytjssearch.h"
36 const QString recentKeywordsKey = "recentKeywords";
37 const QString recentChannelsKey = "recentChannels";
40 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
41 videoSource = nullptr;
45 m_activeVideo = nullptr;
50 authorHovered = false;
51 authorPressed = false;
54 int PlaylistModel::rowCount(const QModelIndex & /*parent*/) const {
55 int count = videos.size();
57 // add the message item
58 if (videos.isEmpty() || !searching) count++;
63 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
64 int row = index.row();
66 if (row == videos.size()) {
71 return ItemTypeShowMore;
73 if (!errorMessage.isEmpty()) return errorMessage;
74 if (searching) return QString(); // tr("Searching...");
75 if (canSearchMore) return tr("Show %1 More").arg("").simplified();
77 return tr("No videos");
79 return tr("No more videos");
80 case Qt::TextAlignmentRole:
81 return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
82 case Qt::ForegroundRole:
83 return palette.color(QPalette::Dark);
84 case Qt::BackgroundRole:
85 if (!errorMessage.isEmpty())
86 return palette.color(QPalette::ToolTipBase);
93 } else if (row < 0 || row >= videos.size())
96 Video *video = videos.at(row);
100 return ItemTypeVideo;
102 return QVariant::fromValue(QPointer<Video>(video));
103 case ActiveTrackRole:
104 return video == m_activeVideo;
105 case Qt::DisplayRole:
106 return video->getTitle();
107 case HoveredItemRole:
108 return hoveredRow == index.row();
109 case AuthorHoveredRole:
110 return authorHovered;
111 case AuthorPressedRole:
112 return authorPressed;
114 case Qt::StatusTipRole:
115 return video->description();
122 void PlaylistModel::setActiveRow(int row, bool notify) {
123 if (rowExists(row)) {
125 Video *previousVideo = m_activeVideo;
126 m_activeVideo = videoAt(row);
128 int oldactiverow = m_activeRow;
130 if (rowExists(oldactiverow))
131 emit dataChanged(createIndex(oldactiverow, 0),
132 createIndex(oldactiverow, columnCount() - 1));
134 emit dataChanged(createIndex(m_activeRow, 0), createIndex(m_activeRow, columnCount() - 1));
135 if (notify) emit activeVideoChanged(m_activeVideo, previousVideo);
139 m_activeVideo = nullptr;
143 int PlaylistModel::nextRow() const {
144 int nextRow = m_activeRow + 1;
145 if (rowExists(nextRow)) return nextRow;
149 int PlaylistModel::previousRow() const {
150 int prevRow = m_activeRow - 1;
151 if (rowExists(prevRow)) return prevRow;
155 Video *PlaylistModel::videoAt(int row) const {
156 if (rowExists(row)) return videos.at(row);
160 Video *PlaylistModel::activeVideo() const {
161 return m_activeVideo;
164 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
170 qDeleteAll(deletedVideos);
171 deletedVideos.clear();
173 m_activeVideo = nullptr;
178 this->videoSource = videoSource;
179 connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)), SLOT(addVideos(QVector<Video *>)),
180 Qt::UniqueConnection);
181 connect(videoSource, SIGNAL(finished(int)), SLOT(searchFinished(int)), Qt::UniqueConnection);
182 connect(videoSource, SIGNAL(error(QString)), SLOT(searchError(QString)), Qt::UniqueConnection);
183 connect(videoSource, &QObject::destroyed, this,
184 [this, videoSource] {
185 if (this->videoSource == videoSource) {
186 this->videoSource = nullptr;
189 Qt::UniqueConnection);
191 canSearchMore = true;
195 void PlaylistModel::searchMore() {
196 if (!canSearchMore || videoSource == nullptr || searching) return;
198 firstSearch = startIndex == 1;
199 max = videoSource->maxResults();
200 if (max == 0) max = 20;
201 errorMessage.clear();
202 videoSource->loadVideos(max, startIndex);
206 void PlaylistModel::searchNeeded() {
207 const int desiredRowsAhead = 10;
208 int remainingRows = videos.size() - m_activeRow;
209 if (remainingRows < desiredRowsAhead) searchMore();
212 void PlaylistModel::abortSearch() {
213 QMutexLocker locker(&mutex);
215 if (videoSource) videoSource->abort();
221 m_activeVideo = nullptr;
226 void PlaylistModel::searchFinished(int total) {
229 canSearchMore = videoSource->hasMoreVideos();
231 // update the message item
232 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
234 if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
237 void PlaylistModel::searchError(const QString &message) {
238 errorMessage = message;
239 // update the message item
240 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
243 void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
244 if (newVideos.isEmpty()) return;
245 videos.reserve(videos.size() + newVideos.size());
246 beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
247 videos.append(newVideos);
249 for (Video *video : newVideos) {
250 connect(video, SIGNAL(gotThumbnail()), SLOT(updateVideoSender()), Qt::UniqueConnection);
251 video->loadThumbnail();
255 void PlaylistModel::handleFirstVideo(Video *video) {
257 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
258 if (currentVideoRow != -1)
259 setActiveRow(currentVideoRow, false);
261 if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
264 auto clazz = videoSource->metaObject()->className();
265 if (clazz == QLatin1String("YTSearch") || clazz == QLatin1String("IVSearch") ||
266 clazz == QLatin1String("IVChannelSource") || clazz == QLatin1String("YTJSSearch") ||
267 clazz == QLatin1String("YTJSChannelSource")) {
268 static const int maxRecentElements = 10;
270 SearchParams *searchParams;
271 if (clazz == QLatin1String("YTSearch")) {
272 auto search = qobject_cast<YTSearch *>(videoSource);
273 searchParams = search->getSearchParams();
274 } else if (clazz == QLatin1String("IVSearch")) {
275 auto search = qobject_cast<IVSearch *>(videoSource);
276 searchParams = search->getSearchParams();
277 } else if (clazz == QLatin1String("IVChannelSource")) {
278 auto search = qobject_cast<IVChannelSource *>(videoSource);
279 searchParams = search->getSearchParams();
280 } else if (clazz == QLatin1String("YTJSSearch")) {
281 auto search = qobject_cast<YTJSSearch *>(videoSource);
282 searchParams = search->getSearchParams();
283 } else if (clazz == QLatin1String("YTJSChannelSource")) {
284 auto search = qobject_cast<YTJSChannelSource *>(videoSource);
285 searchParams = search->getSearchParams();
289 QString query = searchParams->keywords();
290 if (!query.isEmpty() && !searchParams->isTransient()) {
291 if (query.startsWith("http://")) {
292 // Save the video title
293 query += "|" + videos.at(0)->getTitle();
295 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
296 keywords.removeAll(query);
297 keywords.prepend(query);
298 while (keywords.size() > maxRecentElements)
299 keywords.removeLast();
300 settings.setValue(recentKeywordsKey, keywords);
304 QString channelId = searchParams->channelId();
305 if (!channelId.isEmpty() && !searchParams->isTransient()) {
307 if (!video->getChannelId().isEmpty() &&
308 video->getChannelId() != video->getChannelTitle())
309 value = video->getChannelId() + "|" + video->getChannelTitle();
311 value = video->getChannelTitle();
312 QStringList channels = settings.value(recentChannelsKey).toStringList();
313 channels.removeAll(value);
314 channels.removeAll(channelId);
315 channels.prepend(value);
316 while (channels.size() > maxRecentElements)
317 channels.removeLast();
318 settings.setValue(recentChannelsKey, channels);
323 void PlaylistModel::updateVideoSender() {
324 Video *video = static_cast<Video *>(sender());
326 qDebug() << "Cannot get sender";
329 int row = rowForVideo(video);
330 emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
333 void PlaylistModel::emitDataChanged() {
334 QModelIndex index = createIndex(rowCount() - 1, 0);
335 emit dataChanged(index, index);
340 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
341 beginRemoveRows(QModelIndex(), position, position + rows - 1);
342 for (int row = 0; row < rows; ++row) {
343 Video *video = videos.takeAt(position);
349 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
350 QVector<Video *> originalList(videos);
351 for (const QModelIndex &index : indexes) {
352 if (index.row() >= originalList.size()) continue;
353 Video *video = originalList.at(index.row());
354 int idx = videos.indexOf(video);
356 beginRemoveRows(QModelIndex(), idx, idx);
357 deletedVideos.append(video);
358 if (m_activeVideo == video) {
359 m_activeVideo = nullptr;
362 videos.removeAll(video);
369 // --- Sturm und drang ---
371 Qt::DropActions PlaylistModel::supportedDropActions() const {
372 return Qt::CopyAction;
375 Qt::DropActions PlaylistModel::supportedDragActions() const {
376 return Qt::CopyAction;
379 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
380 if (index.isValid()) {
381 if (index.row() == videos.size()) {
382 // don't drag the "show more" item
383 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
385 return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
387 return Qt::ItemIsDropEnabled;
390 QStringList PlaylistModel::mimeTypes() const {
392 types << "application/x-minitube-video";
396 QMimeData *PlaylistModel::mimeData(const QModelIndexList &indexes) const {
397 VideoMimeData *mime = new VideoMimeData();
399 for (const QModelIndex &it : indexes) {
401 if (row >= 0 && row < videos.size()) mime->addVideo(videos.at(it.row()));
407 bool PlaylistModel::dropMimeData(const QMimeData *data,
408 Qt::DropAction action,
411 const QModelIndex &parent) {
412 if (action == Qt::IgnoreAction) return true;
414 if (!data->hasFormat("application/x-minitube-video")) return false;
416 if (column > 0) return false;
421 else if (parent.isValid())
422 beginRow = parent.row();
424 beginRow = rowCount(QModelIndex());
426 const VideoMimeData *videoMimeData = qobject_cast<const VideoMimeData *>(data);
427 if (!videoMimeData) return false;
429 const QVector<Video *> &droppedVideos = videoMimeData->getVideos();
430 for (Video *video : droppedVideos) {
432 int videoRow = videos.indexOf(video);
433 removeRows(videoRow, 1, QModelIndex());
435 // and then add them again at the new position
436 beginInsertRows(QModelIndex(), beginRow, beginRow);
437 videos.insert(beginRow, video);
441 // fix m_activeRow after all this
442 m_activeRow = videos.indexOf(m_activeVideo);
444 // let the MediaView restore the selection
445 emit needSelectionFor(droppedVideos);
450 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
451 if (videoId.isEmpty()) return -1;
452 for (int i = 0; i < videos.size(); ++i) {
453 Video *v = videos.at(i);
454 // qDebug() << "Comparing" << v->id() << videoId;
455 if (v->getId() == videoId) return i;
460 int PlaylistModel::rowForVideo(Video *video) {
461 return videos.indexOf(video);
464 QModelIndex PlaylistModel::indexForVideo(Video *video) {
465 return createIndex(videos.indexOf(video), 0);
468 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
469 QVector<Video *> movedVideos;
471 for (const QModelIndex &index : indexes) {
472 int row = index.row();
473 if (row >= videos.size()) continue;
474 // qDebug() << "index row" << row;
475 Video *video = videoAt(row);
476 movedVideos << video;
479 int end = up ? -1 : rowCount() - 1, mod = up ? -1 : 1;
480 for (Video *video : movedVideos) {
481 int row = rowForVideo(video);
482 if (row + mod == end) {
486 // qDebug() << "video row" << row;
487 removeRows(row, 1, QModelIndex());
494 beginInsertRows(QModelIndex(), row, row);
495 videos.insert(row, video);
499 emit needSelectionFor(movedVideos);
504 void PlaylistModel::setHoveredRow(int row) {
505 int oldRow = hoveredRow;
507 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
508 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
511 void PlaylistModel::clearHover() {
512 int oldRow = hoveredRow;
514 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
517 void PlaylistModel::updateHoveredRow() {
518 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
521 /* clickable author */
523 void PlaylistModel::enterAuthorHover() {
524 if (authorHovered) return;
525 authorHovered = true;
529 void PlaylistModel::exitAuthorHover() {
530 if (!authorHovered) return;
531 authorHovered = false;
533 setHoveredRow(hoveredRow);
536 void PlaylistModel::enterAuthorPressed() {
537 if (authorPressed) return;
538 authorPressed = true;
542 void PlaylistModel::exitAuthorPressed() {
543 if (!authorPressed) return;
544 authorPressed = false;