1 /* $BEGIN_LICENSE
2
3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
5
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.
10
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.
15
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/>.
18
19 $END_LICENSE */
20
21 #include "playlistmodel.h"
22 #include "mediaview.h"
23 #include "playlistitemdelegate.h"
24 #include "searchparams.h"
25 #include "video.h"
26 #include "videomimedata.h"
27 #include "videosource.h"
28
29 #include "searchvideosource.h"
30
31 namespace {
32 const QString recentKeywordsKey = "recentKeywords";
33 const QString recentChannelsKey = "recentChannels";
34 } // namespace
35
PlaylistModel(QWidget * parent)36 PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
37 videoSource = nullptr;
38 searching = false;
39 canSearchMore = true;
40 firstSearch = false;
41 m_activeVideo = nullptr;
42 m_activeRow = -1;
43 startIndex = 1;
44 max = 0;
45 hoveredRow = -1;
46 authorHovered = false;
47 authorPressed = false;
48 }
49
rowCount(const QModelIndex &) const50 int PlaylistModel::rowCount(const QModelIndex & /*parent*/) const {
51 int count = videos.size();
52
53 // add the message item
54 if (videos.isEmpty() || !searching) count++;
55
56 return count;
57 }
58
data(const QModelIndex & index,int role) const59 QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
60 int row = index.row();
61
62 if (row == videos.size()) {
63 QPalette palette;
64
65 switch (role) {
66 case ItemTypeRole:
67 return ItemTypeShowMore;
68 case Qt::DisplayRole:
69 if (!errorMessage.isEmpty()) return errorMessage;
70 if (searching) return QString(); // tr("Searching...");
71 if (canSearchMore) return tr("Show %1 More").arg("").simplified();
72 if (videos.isEmpty())
73 return tr("No videos");
74 else
75 return tr("No more videos");
76 case Qt::TextAlignmentRole:
77 return QVariant(int(Qt::AlignHCenter | Qt::AlignVCenter));
78 case Qt::ForegroundRole:
79 return palette.color(QPalette::Dark);
80 case Qt::BackgroundRole:
81 if (!errorMessage.isEmpty())
82 return palette.color(QPalette::ToolTipBase);
83 else
84 return QVariant();
85 default:
86 return QVariant();
87 }
88
89 } else if (row < 0 || row >= videos.size())
90 return QVariant();
91
92 Video *video = videos.at(row);
93
94 switch (role) {
95 case ItemTypeRole:
96 return ItemTypeVideo;
97 case VideoRole:
98 return QVariant::fromValue(QPointer<Video>(video));
99 case ActiveTrackRole:
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;
109 /*
110 case Qt::StatusTipRole:
111 return video->description();
112 */
113 }
114
115 return QVariant();
116 }
117
setActiveRow(int row,bool notify)118 void PlaylistModel::setActiveRow(int row, bool notify) {
119 if (rowExists(row)) {
120 m_activeRow = row;
121 Video *previousVideo = m_activeVideo;
122 m_activeVideo = videoAt(row);
123
124 int oldactiverow = m_activeRow;
125
126 if (rowExists(oldactiverow))
127 emit dataChanged(createIndex(oldactiverow, 0),
128 createIndex(oldactiverow, columnCount() - 1));
129
130 emit dataChanged(createIndex(m_activeRow, 0), createIndex(m_activeRow, columnCount() - 1));
131 if (notify) emit activeVideoChanged(m_activeVideo, previousVideo);
132
133 } else {
134 m_activeRow = -1;
135 m_activeVideo = nullptr;
136 }
137 }
138
nextRow() const139 int PlaylistModel::nextRow() const {
140 int nextRow = m_activeRow + 1;
141 if (rowExists(nextRow)) return nextRow;
142 return -1;
143 }
144
previousRow() const145 int PlaylistModel::previousRow() const {
146 int prevRow = m_activeRow - 1;
147 if (rowExists(prevRow)) return prevRow;
148 return -1;
149 }
150
videoAt(int row) const151 Video *PlaylistModel::videoAt(int row) const {
152 if (rowExists(row)) return videos.at(row);
153 return nullptr;
154 }
155
activeVideo() const156 Video *PlaylistModel::activeVideo() const {
157 return m_activeVideo;
158 }
159
setVideoSource(VideoSource * videoSource)160 void PlaylistModel::setVideoSource(VideoSource *videoSource) {
161 beginResetModel();
162
163 qDeleteAll(videos);
164 videos.clear();
165
166 qDeleteAll(deletedVideos);
167 deletedVideos.clear();
168
169 m_activeVideo = nullptr;
170 m_activeRow = -1;
171 startIndex = 1;
172 endResetModel();
173
174 this->videoSource = videoSource;
175 connect(videoSource, SIGNAL(gotVideos(QVector<Video *>)), SLOT(addVideos(QVector<Video *>)),
176 Qt::UniqueConnection);
177 connect(videoSource, SIGNAL(finished(int)), SLOT(searchFinished(int)), Qt::UniqueConnection);
178 connect(videoSource, SIGNAL(error(QString)), SLOT(searchError(QString)), Qt::UniqueConnection);
179 connect(videoSource, &QObject::destroyed, this,
180 [this, videoSource] {
181 if (this->videoSource == videoSource) {
182 this->videoSource = nullptr;
183 }
184 },
185 Qt::UniqueConnection);
186
187 canSearchMore = true;
188 searchMore();
189 }
190
searchMore()191 void PlaylistModel::searchMore() {
192 if (!canSearchMore || videoSource == nullptr || searching) return;
193 searching = true;
194 firstSearch = startIndex == 1;
195 max = videoSource->maxResults();
196 if (max == 0) max = 20;
197 errorMessage.clear();
198 videoSource->loadVideos(max, startIndex);
199 startIndex += max;
200 }
201
searchNeeded()202 void PlaylistModel::searchNeeded() {
203 const int desiredRowsAhead = 10;
204 int remainingRows = videos.size() - m_activeRow;
205 if (remainingRows < desiredRowsAhead) searchMore();
206 }
207
abortSearch()208 void PlaylistModel::abortSearch() {
209 QMutexLocker locker(&mutex);
210 beginResetModel();
211 if (videoSource) videoSource->abort();
212 qDeleteAll(videos);
213 videos.clear();
214 videos.squeeze();
215 searching = false;
216 m_activeRow = -1;
217 m_activeVideo = nullptr;
218 startIndex = 1;
219 endResetModel();
220 }
221
searchFinished(int total)222 void PlaylistModel::searchFinished(int total) {
223 Q_UNUSED(total);
224 searching = false;
225 canSearchMore = videoSource->hasMoreVideos();
226
227 // update the message item
228 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
229
230 if (firstSearch && !videos.isEmpty()) handleFirstVideo(videos.at(0));
231 }
232
searchError(const QString & message)233 void PlaylistModel::searchError(const QString &message) {
234 errorMessage = message;
235 // update the message item
236 emit dataChanged(createIndex(videos.size(), 0), createIndex(videos.size(), columnCount() - 1));
237 }
238
addVideos(const QVector<Video * > & newVideos)239 void PlaylistModel::addVideos(const QVector<Video *> &newVideos) {
240 if (newVideos.isEmpty()) return;
241 videos.reserve(videos.size() + newVideos.size());
242 beginInsertRows(QModelIndex(), videos.size(), videos.size() + newVideos.size() - 2);
243 videos.append(newVideos);
244 endInsertRows();
245 for (Video *video : newVideos) {
246 connect(video, &Video::changed, this, [video, this] {
247 int row = rowForVideo(video);
248 emit dataChanged(createIndex(row, 0), createIndex(row, columnCount() - 1));
249 });
250 }
251 }
252
handleFirstVideo(Video * video)253 void PlaylistModel::handleFirstVideo(Video *video) {
254 QSettings settings;
255 int currentVideoRow = rowForCloneVideo(MediaView::instance()->getCurrentVideoId());
256 if (currentVideoRow != -1)
257 setActiveRow(currentVideoRow, false);
258 else {
259 if (!settings.value("manualplay", false).toBool()) setActiveRow(0);
260 }
261
262 auto clazz = videoSource->metaObject()->className();
263 if (clazz == QLatin1String("SearchVideoSource")) {
264 auto search = qobject_cast<SearchVideoSource *>(videoSource);
265 SearchParams *searchParams = search->getSearchParams();
266
267 // save keyword
268 static const int maxRecentElements = 10;
269 QString query = searchParams->keywords();
270 if (!query.isEmpty() && !searchParams->isTransient()) {
271 if (query.startsWith("http://")) {
272 // Save the video title
273 query += "|" + videos.at(0)->getTitle();
274 }
275 QStringList keywords = settings.value(recentKeywordsKey).toStringList();
276 keywords.removeAll(query);
277 keywords.prepend(query);
278 while (keywords.size() > maxRecentElements)
279 keywords.removeLast();
280 settings.setValue(recentKeywordsKey, keywords);
281 }
282
283 // save channel
284 QString channelId = searchParams->channelId();
285 if (!channelId.isEmpty() && !searchParams->isTransient()) {
286 QString value;
287 if (!video->getChannelId().isEmpty() &&
288 video->getChannelId() != video->getChannelTitle())
289 value = video->getChannelId() + "|" + video->getChannelTitle();
290 else
291 value = video->getChannelTitle();
292 QStringList channels = settings.value(recentChannelsKey).toStringList();
293 channels.removeAll(value);
294 channels.removeAll(channelId);
295 channels.prepend(value);
296 while (channels.size() > maxRecentElements)
297 channels.removeLast();
298 settings.setValue(recentChannelsKey, channels);
299 }
300 }
301 }
302
emitDataChanged()303 void PlaylistModel::emitDataChanged() {
304 QModelIndex index = createIndex(rowCount() - 1, 0);
305 emit dataChanged(index, index);
306 }
307
308 // --- item removal
309
removeRows(int position,int rows,const QModelIndex &)310 bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
311 beginRemoveRows(QModelIndex(), position, position + rows - 1);
312 for (int row = 0; row < rows; ++row) {
313 Video *video = videos.takeAt(position);
314 }
315 endRemoveRows();
316 return true;
317 }
318
removeIndexes(QModelIndexList & indexes)319 void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
320 QVector<Video *> originalList(videos);
321 for (const QModelIndex &index : indexes) {
322 if (index.row() >= originalList.size()) continue;
323 Video *video = originalList.at(index.row());
324 int idx = videos.indexOf(video);
325 if (idx != -1) {
326 beginRemoveRows(QModelIndex(), idx, idx);
327 deletedVideos.append(video);
328 if (m_activeVideo == video) {
329 m_activeVideo = nullptr;
330 m_activeRow = -1;
331 }
332 videos.removeAll(video);
333 endRemoveRows();
334 }
335 }
336 videos.squeeze();
337 }
338
339 // --- Sturm und drang ---
340
supportedDropActions() const341 Qt::DropActions PlaylistModel::supportedDropActions() const {
342 return Qt::CopyAction;
343 }
344
supportedDragActions() const345 Qt::DropActions PlaylistModel::supportedDragActions() const {
346 return Qt::CopyAction;
347 }
348
flags(const QModelIndex & index) const349 Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
350 if (index.isValid()) {
351 if (index.row() == videos.size()) {
352 // don't drag the "show more" item
353 return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
354 } else
355 return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
356 }
357 return Qt::ItemIsDropEnabled;
358 }
359
mimeTypes() const360 QStringList PlaylistModel::mimeTypes() const {
361 QStringList types;
362 types << "application/x-minitube-video";
363 return types;
364 }
365
mimeData(const QModelIndexList & indexes) const366 QMimeData *PlaylistModel::mimeData(const QModelIndexList &indexes) const {
367 VideoMimeData *mime = new VideoMimeData();
368
369 for (const QModelIndex &it : indexes) {
370 int row = it.row();
371 if (row >= 0 && row < videos.size()) mime->addVideo(videos.at(it.row()));
372 }
373
374 return mime;
375 }
376
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)377 bool PlaylistModel::dropMimeData(const QMimeData *data,
378 Qt::DropAction action,
379 int row,
380 int column,
381 const QModelIndex &parent) {
382 if (action == Qt::IgnoreAction) return true;
383
384 if (!data->hasFormat("application/x-minitube-video")) return false;
385
386 if (column > 0) return false;
387
388 int beginRow;
389 if (row != -1)
390 beginRow = row;
391 else if (parent.isValid())
392 beginRow = parent.row();
393 else
394 beginRow = rowCount(QModelIndex());
395
396 const VideoMimeData *videoMimeData = qobject_cast<const VideoMimeData *>(data);
397 if (!videoMimeData) return false;
398
399 const QVector<Video *> &droppedVideos = videoMimeData->getVideos();
400 for (Video *video : droppedVideos) {
401 // remove videos
402 int videoRow = videos.indexOf(video);
403 removeRows(videoRow, 1, QModelIndex());
404
405 // and then add them again at the new position
406 beginInsertRows(QModelIndex(), beginRow, beginRow);
407 if (beginRow >= videos.size()) {
408 videos.push_back(video);
409 } else {
410 videos.insert(beginRow, video);
411 }
412 endInsertRows();
413 }
414
415 // fix m_activeRow after all this
416 m_activeRow = videos.indexOf(m_activeVideo);
417
418 // let the MediaView restore the selection
419 emit needSelectionFor(droppedVideos);
420
421 return true;
422 }
423
rowForCloneVideo(const QString & videoId) const424 int PlaylistModel::rowForCloneVideo(const QString &videoId) const {
425 if (videoId.isEmpty()) return -1;
426 for (int i = 0; i < videos.size(); ++i) {
427 Video *v = videos.at(i);
428 // qDebug() << "Comparing" << v->id() << videoId;
429 if (v->getId() == videoId) return i;
430 }
431 return -1;
432 }
433
rowForVideo(Video * video)434 int PlaylistModel::rowForVideo(Video *video) {
435 return videos.indexOf(video);
436 }
437
indexForVideo(Video * video)438 QModelIndex PlaylistModel::indexForVideo(Video *video) {
439 return createIndex(videos.indexOf(video), 0);
440 }
441
move(QModelIndexList & indexes,bool up)442 void PlaylistModel::move(QModelIndexList &indexes, bool up) {
443 QVector<Video *> movedVideos;
444
445 for (const QModelIndex &index : indexes) {
446 int row = index.row();
447 if (row >= videos.size()) continue;
448 // qDebug() << "index row" << row;
449 Video *video = videoAt(row);
450 movedVideos << video;
451 }
452
453 int end = up ? -1 : rowCount() - 1, mod = up ? -1 : 1;
454 for (Video *video : movedVideos) {
455 int row = rowForVideo(video);
456 if (row + mod == end) {
457 end = row;
458 continue;
459 }
460 // qDebug() << "video row" << row;
461 removeRows(row, 1, QModelIndex());
462
463 if (up)
464 row--;
465 else
466 row++;
467
468 beginInsertRows(QModelIndex(), row, row);
469 videos.insert(row, video);
470 endInsertRows();
471 }
472
473 emit needSelectionFor(movedVideos);
474 }
475
476 /* row hovering */
477
setHoveredRow(int row)478 void PlaylistModel::setHoveredRow(int row) {
479 int oldRow = hoveredRow;
480 hoveredRow = row;
481 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
482 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
483 }
484
clearHover()485 void PlaylistModel::clearHover() {
486 int oldRow = hoveredRow;
487 hoveredRow = -1;
488 emit dataChanged(createIndex(oldRow, 0), createIndex(oldRow, columnCount() - 1));
489 }
490
updateHoveredRow()491 void PlaylistModel::updateHoveredRow() {
492 emit dataChanged(createIndex(hoveredRow, 0), createIndex(hoveredRow, columnCount() - 1));
493 }
494
495 /* clickable author */
496
enterAuthorHover()497 void PlaylistModel::enterAuthorHover() {
498 if (authorHovered) return;
499 authorHovered = true;
500 updateHoveredRow();
501 }
502
exitAuthorHover()503 void PlaylistModel::exitAuthorHover() {
504 if (!authorHovered) return;
505 authorHovered = false;
506 updateHoveredRow();
507 setHoveredRow(hoveredRow);
508 }
509
enterAuthorPressed()510 void PlaylistModel::enterAuthorPressed() {
511 if (authorPressed) return;
512 authorPressed = true;
513 updateHoveredRow();
514 }
515
exitAuthorPressed()516 void PlaylistModel::exitAuthorPressed() {
517 if (!authorPressed) return;
518 authorPressed = false;
519 updateHoveredRow();
520 }
521