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 "searchvideosource.h"
31 const QString recentKeywordsKey = "recentKeywords";
32 const QString recentChannelsKey = "recentChannels";
35 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
36 videoSource = nullptr;
40 m_activeVideo = nullptr;
45 authorHovered = false;
46 authorPressed = false;
49 int PlaylistModel::rowCount(const QModelIndex & /*parent*/) const {
50 int count = videos.size();
52 // add the message item
53 if (videos.isEmpty() || !searching) count++;
58 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
59 int row = index.row();
61 if (row == videos.size()) {
66 return ItemTypeShowMore;
68 if (!errorMessage.isEmpty()) return errorMessage;
69 if (searching) return QString(); // tr("Searching...");
70 if (canSearchMore) return tr("Show %1 More").arg("").simplified();
72 return tr("No videos");
74 return tr("No more videos");
75 case Qt::TextAlignmentRole:
76 return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
77 case Qt::ForegroundRole:
78 return palette.color(QPalette::Dark);
79 case Qt::BackgroundRole:
80 if (!errorMessage.isEmpty())
81 return palette.color(QPalette::ToolTipBase);
88 } else if (row < 0 || row >= videos.size())
91 Video *video = videos.at(row);
97 return QVariant::fromValue(QPointer<Video>(video));
99 return video == m_activeVideo;
100 case Qt::DisplayRole:
101 return video->getTitle();
102 case HoveredItemRole:
103 return hoveredRow == index.row();
104 case AuthorHoveredRole:
105 return authorHovered;
106 case AuthorPressedRole:
107 return authorPressed;
109 case Qt::StatusTipRole:
110 return video->description();
117 void PlaylistModel::setActiveRow(int row, bool notify) {
118 if (rowExists(row)) {
120 Video *previousVideo = m_activeVideo;
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 activeVideoChanged(m_activeVideo, previousVideo);
134 m_activeVideo = nullptr;
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) {
165 qDeleteAll(deletedVideos);
166 deletedVideos.clear();
168 m_activeVideo = nullptr;
173 this->videoSource = videoSource;
174 connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)), SLOT(addVideos(QVector<Video *>)),
175 Qt::UniqueConnection);
176 connect(videoSource, SIGNAL(finished(int)), SLOT(searchFinished(int)), Qt::UniqueConnection);
177 connect(videoSource, SIGNAL(error(QString)), SLOT(searchError(QString)), Qt::UniqueConnection);
178 connect(videoSource, &QObject::destroyed, this,
179 [this, videoSource] {
180 if (this->videoSource == videoSource) {
181 this->videoSource = nullptr;
184 Qt::UniqueConnection);
186 canSearchMore = true;
190 void PlaylistModel::searchMore() {
191 if (!canSearchMore || videoSource == nullptr || searching) return;
193 firstSearch = startIndex == 1;
194 max = videoSource->maxResults();
195 if (max == 0) max = 20;
196 errorMessage.clear();
197 videoSource->loadVideos(max, startIndex);
201 void PlaylistModel::searchNeeded() {
202 const int desiredRowsAhead = 10;
203 int remainingRows = videos.size() - m_activeRow;
204 if (remainingRows < desiredRowsAhead) searchMore();
207 void PlaylistModel::abortSearch() {
208 QMutexLocker locker(&mutex);
210 if (videoSource) videoSource->abort();
216 m_activeVideo = nullptr;
221 void PlaylistModel::searchFinished(int total) {
224 canSearchMore = videoSource->hasMoreVideos();
226 // update the message item
227 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
229 if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
232 void PlaylistModel::searchError(const QString &message) {
233 errorMessage = message;
234 // update the message item
235 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
238 void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
239 if (newVideos.isEmpty()) return;
240 videos.reserve(videos.size() + newVideos.size());
241 beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
242 videos.append(newVideos);
244 for (Video *video : newVideos) {
245 connect(video, SIGNAL(gotThumbnail()), SLOT(updateVideoSender()), Qt::UniqueConnection);
246 video->loadThumbnail();
250 void PlaylistModel::handleFirstVideo(Video *video) {
252 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
253 if (currentVideoRow != -1)
254 setActiveRow(currentVideoRow, false);
256 if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
259 auto clazz = videoSource->metaObject()->className();
260 if (clazz == QLatin1String("SearchVideoSource")) {
261 auto search = qobject_cast<SearchVideoSource *>(videoSource);
262 SearchParams *searchParams = search->getSearchParams();
265 static const int maxRecentElements = 10;
266 QString query = searchParams->keywords();
267 if (!query.isEmpty() && !searchParams->isTransient()) {
268 if (query.startsWith("http://")) {
269 // Save the video title
270 query += "|" + videos.at(0)->getTitle();
272 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
273 keywords.removeAll(query);
274 keywords.prepend(query);
275 while (keywords.size() > maxRecentElements)
276 keywords.removeLast();
277 settings.setValue(recentKeywordsKey, keywords);
281 QString channelId = searchParams->channelId();
282 if (!channelId.isEmpty() && !searchParams->isTransient()) {
284 if (!video->getChannelId().isEmpty() &&
285 video->getChannelId() != video->getChannelTitle())
286 value = video->getChannelId() + "|" + video->getChannelTitle();
288 value = video->getChannelTitle();
289 QStringList channels = settings.value(recentChannelsKey).toStringList();
290 channels.removeAll(value);
291 channels.removeAll(channelId);
292 channels.prepend(value);
293 while (channels.size() > maxRecentElements)
294 channels.removeLast();
295 settings.setValue(recentChannelsKey, channels);
300 void PlaylistModel::updateVideoSender() {
301 Video *video = static_cast<Video *>(sender());
303 qDebug() << "Cannot get sender";
306 int row = rowForVideo(video);
307 emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
310 void PlaylistModel::emitDataChanged() {
311 QModelIndex index = createIndex(rowCount() - 1, 0);
312 emit dataChanged(index, index);
317 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
318 beginRemoveRows(QModelIndex(), position, position + rows - 1);
319 for (int row = 0; row < rows; ++row) {
320 Video *video = videos.takeAt(position);
326 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
327 QVector<Video *> originalList(videos);
328 for (const QModelIndex &index : indexes) {
329 if (index.row() >= originalList.size()) continue;
330 Video *video = originalList.at(index.row());
331 int idx = videos.indexOf(video);
333 beginRemoveRows(QModelIndex(), idx, idx);
334 deletedVideos.append(video);
335 if (m_activeVideo == video) {
336 m_activeVideo = nullptr;
339 videos.removeAll(video);
346 // --- Sturm und drang ---
348 Qt::DropActions PlaylistModel::supportedDropActions() const {
349 return Qt::CopyAction;
352 Qt::DropActions PlaylistModel::supportedDragActions() const {
353 return Qt::CopyAction;
356 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
357 if (index.isValid()) {
358 if (index.row() == videos.size()) {
359 // don't drag the "show more" item
360 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
362 return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
364 return Qt::ItemIsDropEnabled;
367 QStringList PlaylistModel::mimeTypes() const {
369 types << "application/x-minitube-video";
373 QMimeData *PlaylistModel::mimeData(const QModelIndexList &indexes) const {
374 VideoMimeData *mime = new VideoMimeData();
376 for (const QModelIndex &it : indexes) {
378 if (row >= 0 && row < videos.size()) mime->addVideo(videos.at(it.row()));
384 bool PlaylistModel::dropMimeData(const QMimeData *data,
385 Qt::DropAction action,
388 const QModelIndex &parent) {
389 if (action == Qt::IgnoreAction) return true;
391 if (!data->hasFormat("application/x-minitube-video")) return false;
393 if (column > 0) return false;
398 else if (parent.isValid())
399 beginRow = parent.row();
401 beginRow = rowCount(QModelIndex());
403 const VideoMimeData *videoMimeData = qobject_cast<const VideoMimeData *>(data);
404 if (!videoMimeData) return false;
406 const QVector<Video *> &droppedVideos = videoMimeData->getVideos();
407 for (Video *video : droppedVideos) {
409 int videoRow = videos.indexOf(video);
410 removeRows(videoRow, 1, QModelIndex());
412 // and then add them again at the new position
413 beginInsertRows(QModelIndex(), beginRow, beginRow);
414 if (beginRow >= videos.size()) {
415 videos.push_back(video);
417 videos.insert(beginRow, video);
422 // fix m_activeRow after all this
423 m_activeRow = videos.indexOf(m_activeVideo);
425 // let the MediaView restore the selection
426 emit needSelectionFor(droppedVideos);
431 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
432 if (videoId.isEmpty()) return -1;
433 for (int i = 0; i < videos.size(); ++i) {
434 Video *v = videos.at(i);
435 // qDebug() << "Comparing" << v->id() << videoId;
436 if (v->getId() == videoId) return i;
441 int PlaylistModel::rowForVideo(Video *video) {
442 return videos.indexOf(video);
445 QModelIndex PlaylistModel::indexForVideo(Video *video) {
446 return createIndex(videos.indexOf(video), 0);
449 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
450 QVector<Video *> movedVideos;
452 for (const QModelIndex &index : indexes) {
453 int row = index.row();
454 if (row >= videos.size()) continue;
455 // qDebug() << "index row" << row;
456 Video *video = videoAt(row);
457 movedVideos << video;
460 int end = up ? -1 : rowCount() - 1, mod = up ? -1 : 1;
461 for (Video *video : movedVideos) {
462 int row = rowForVideo(video);
463 if (row + mod == end) {
467 // qDebug() << "video row" << row;
468 removeRows(row, 1, QModelIndex());
475 beginInsertRows(QModelIndex(), row, row);
476 videos.insert(row, video);
480 emit needSelectionFor(movedVideos);
485 void PlaylistModel::setHoveredRow(int row) {
486 int oldRow = hoveredRow;
488 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
489 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
492 void PlaylistModel::clearHover() {
493 int oldRow = hoveredRow;
495 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
498 void PlaylistModel::updateHoveredRow() {
499 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;