]> git.sur5r.net Git - minitube/blob - src/mediaview.cpp
Fix updating QUrl query parameters with Qt5.
[minitube] / src / mediaview.cpp
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 "mediaview.h"
22 #include "playlistmodel.h"
23 #include "playlistview.h"
24 #include "loadingwidget.h"
25 #include "videoareawidget.h"
26 #include "networkaccess.h"
27 #include "minisplitter.h"
28 #include "constants.h"
29 #include "downloadmanager.h"
30 #include "downloaditem.h"
31 #include "mainwindow.h"
32 #include "temporary.h"
33 #include "refinesearchwidget.h"
34 #include "sidebarwidget.h"
35 #include "sidebarheader.h"
36 #ifdef APP_ACTIVATION
37 #include "activation.h"
38 #endif
39 #ifdef APP_EXTRA
40 #include "extra.h"
41 #endif
42 #include "videosource.h"
43 #include "ytsearch.h"
44 #include "searchparams.h"
45 #include "ytsinglevideosource.h"
46 #include "channelaggregator.h"
47 #include "iconutils.h"
48 #include "ytchannel.h"
49 #ifdef APP_SNAPSHOT
50 #include "snapshotsettings.h"
51 #endif
52 #include "datautils.h"
53 #include "compatibility/qurlqueryhelper.h"
54
55 namespace The {
56 NetworkAccess* http();
57 QHash<QString, QAction*>* globalActions();
58 QHash<QString, QMenu*>* globalMenus();
59 QNetworkAccessManager* networkAccessManager();
60 }
61
62 MediaView* MediaView::instance() {
63     static MediaView *i = new MediaView();
64     return i;
65 }
66
67 MediaView::MediaView(QWidget *parent) : QWidget(parent),
68     stopped(false),
69     #ifdef APP_SNAPSHOT
70     snapshotSettings(0),
71     #endif
72     downloadItem(0) { }
73
74 void MediaView::initialize() {
75     QBoxLayout *layout = new QVBoxLayout(this);
76     layout->setMargin(0);
77
78     splitter = new MiniSplitter();
79
80     playlistView = new PlaylistView(this);
81     // respond to the user doubleclicking a playlist item
82     connect(playlistView, SIGNAL(activated(const QModelIndex &)),
83             SLOT(itemActivated(const QModelIndex &)));
84
85     playlistModel = new PlaylistModel();
86     connect(playlistModel, SIGNAL(activeRowChanged(int)),
87             SLOT(activeRowChanged(int)));
88     // needed to restore the selection after dragndrop
89     connect(playlistModel, SIGNAL(needSelectionFor(QList<Video*>)),
90             SLOT(selectVideos(QList<Video*>)));
91     playlistView->setModel(playlistModel);
92
93     connect(playlistView->selectionModel(),
94             SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
95             SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
96
97     connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
98
99     sidebar = new SidebarWidget(this);
100     sidebar->setPlaylist(playlistView);
101     connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()),
102             SLOT(searchAgain()));
103     connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)),
104             sidebar, SLOT(showSuggestions(const QStringList &)));
105     connect(sidebar, SIGNAL(suggestionAccepted(QString)),
106             MainWindow::instance(), SLOT(search(QString)));
107     splitter->addWidget(sidebar);
108
109     videoAreaWidget = new VideoAreaWidget(this);
110     // videoAreaWidget->setMinimumSize(320,240);
111
112 #ifdef APP_PHONON
113     videoWidget = new Phonon::VideoWidget(this);
114     videoAreaWidget->setVideoWidget(videoWidget);
115 #endif
116     videoAreaWidget->setListModel(playlistModel);
117
118     loadingWidget = new LoadingWidget(this);
119     videoAreaWidget->setLoadingWidget(loadingWidget);
120
121     splitter->addWidget(videoAreaWidget);
122
123     splitter->setStretchFactor(0, 0);
124     splitter->setStretchFactor(1, 8);
125
126     // restore splitter state
127     QSettings settings;
128     splitter->restoreState(settings.value("splitter").toByteArray());
129     splitter->setChildrenCollapsible(false);
130
131     layout->addWidget(splitter);
132
133     errorTimer = new QTimer(this);
134     errorTimer->setSingleShot(true);
135     errorTimer->setInterval(3000);
136     connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
137
138 #ifdef APP_ACTIVATION
139     demoTimer = new QTimer(this);
140     demoTimer->setSingleShot(true);
141     connect(demoTimer, SIGNAL(timeout()), SLOT(demoMessage()));
142 #endif
143
144     connect(videoAreaWidget, SIGNAL(doubleClicked()),
145             The::globalActions()->value("fullscreen"), SLOT(trigger()));
146
147     QAction* refineSearchAction = The::globalActions()->value("refine-search");
148     connect(refineSearchAction, SIGNAL(toggled(bool)),
149             sidebar, SLOT(toggleRefineSearch(bool)));
150
151     currentVideoActions
152             << The::globalActions()->value("webpage")
153             << The::globalActions()->value("pagelink")
154             << The::globalActions()->value("videolink")
155             << The::globalActions()->value("open-in-browser")
156            #ifdef APP_SNAPSHOT
157             << The::globalActions()->value("snapshot")
158            #endif
159             << The::globalActions()->value("findVideoParts")
160             << The::globalActions()->value("skip")
161             << The::globalActions()->value("previous")
162             << The::globalActions()->value("stopafterthis")
163             << The::globalActions()->value("related-videos")
164             << The::globalActions()->value("refine-search")
165             << The::globalActions()->value("twitter")
166             << The::globalActions()->value("facebook")
167             << The::globalActions()->value("buffer")
168             << The::globalActions()->value("email");
169
170 #ifndef APP_PHONON_SEEK
171     QSlider *slider = MainWindow::instance()->getSlider();
172     connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
173 #endif
174 }
175
176 #ifdef APP_PHONON
177 void MediaView::setMediaObject(Phonon::MediaObject *mediaObject) {
178     this->mediaObject = mediaObject;
179     Phonon::createPath(mediaObject, videoWidget);
180     connect(mediaObject, SIGNAL(finished()), SLOT(playbackFinished()));
181     connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
182             SLOT(stateChanged(Phonon::State, Phonon::State)));
183     connect(mediaObject, SIGNAL(aboutToFinish()), SLOT(aboutToFinish()));
184 }
185 #endif
186
187 SearchParams* MediaView::getSearchParams() {
188     VideoSource *videoSource = playlistModel->getVideoSource();
189     if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
190         YTSearch *search = dynamic_cast<YTSearch *>(videoSource);
191         return search->getSearchParams();
192     }
193     return 0;
194 }
195
196 void MediaView::search(SearchParams *searchParams) {
197     if (!searchParams->keywords().isEmpty()) {
198         if (searchParams->keywords().startsWith("http://") ||
199                 searchParams->keywords().startsWith("https://")) {
200             QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
201             if (!videoId.isEmpty()) {
202                 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
203                 singleVideoSource->setVideoId(videoId);
204                 setVideoSource(singleVideoSource);
205                 return;
206             }
207         }
208     }
209     YTSearch *ytSearch = new YTSearch(searchParams, this);
210     ytSearch->setAsyncDetails(true);
211     connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
212     setVideoSource(ytSearch);
213 }
214
215 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
216     stopped = false;
217
218 #ifdef APP_ACTIVATION
219     demoTimer->stop();
220 #endif
221     errorTimer->stop();
222
223     // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
224
225     if (addToHistory) {
226         int currentIndex = getHistoryIndex();
227         if (currentIndex >= 0 && currentIndex < history.size() - 1) {
228             while (history.size() > currentIndex + 1) {
229                 VideoSource *vs = history.takeLast();
230                 if (!vs->parent()) {
231                     qDebug() << "Deleting VideoSource" << vs->getName() << vs;
232                     delete vs;
233                 }
234             }
235         }
236         history.append(videoSource);
237     }
238
239 #ifdef APP_EXTRA
240     if (history.size() > 1)
241         Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
242 #endif
243
244     playlistModel->setVideoSource(videoSource);
245
246     sidebar->showPlaylist();
247     sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
248     sidebar->hideSuggestions();
249     sidebar->getHeader()->updateInfo();
250
251     SearchParams *searchParams = getSearchParams();
252     bool isChannel = searchParams && !searchParams->channelId().isEmpty();
253     playlistView->setClickableAuthors(!isChannel);
254
255
256 }
257
258 void MediaView::searchAgain() {
259     VideoSource *currentVideoSource = playlistModel->getVideoSource();
260     setVideoSource(currentVideoSource, false);
261 }
262
263 bool MediaView::canGoBack() {
264     return getHistoryIndex() > 0;
265 }
266
267 void MediaView::goBack() {
268     if (history.size() > 1) {
269         int currentIndex = getHistoryIndex();
270         if (currentIndex > 0) {
271             VideoSource *previousVideoSource = history.at(currentIndex - 1);
272             setVideoSource(previousVideoSource, false, true);
273         }
274     }
275 }
276
277 bool MediaView::canGoForward() {
278     int currentIndex = getHistoryIndex();
279     return currentIndex >= 0 && currentIndex < history.size() - 1;
280 }
281
282 void MediaView::goForward() {
283     if (canGoForward()) {
284         int currentIndex = getHistoryIndex();
285         VideoSource *nextVideoSource = history.at(currentIndex + 1);
286         setVideoSource(nextVideoSource, false);
287     }
288 }
289
290 int MediaView::getHistoryIndex() {
291     return history.lastIndexOf(playlistModel->getVideoSource());
292 }
293
294 void MediaView::appear() {
295     playlistView->setFocus();
296     Video *currentVideo = playlistModel->activeVideo();
297     if (currentVideo) {
298         MainWindow::instance()->setWindowTitle(
299                     currentVideo->title() + " - " + Constants::NAME);
300         MainWindow::instance()->showMessage(currentVideo->description());
301     }
302 }
303
304 void MediaView::disappear() {
305
306 }
307
308 void MediaView::handleError(QString message) {
309     qWarning() << __PRETTY_FUNCTION__ << message;
310 #ifdef APP_PHONON_SEEK
311     mediaObject->play();
312 #else
313     QTimer::singleShot(500, this, SLOT(startPlaying()));
314 #endif
315 }
316
317 #ifdef APP_PHONON
318 void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/) {
319     if (newState == Phonon::PlayingState)
320         videoAreaWidget->showVideo();
321     else if (newState == Phonon::ErrorState) {
322         qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
323         if (mediaObject->errorType() == Phonon::FatalError)
324             handleError(mediaObject->errorString());
325     }
326 }
327 #endif
328
329 void MediaView::pause() {
330 #ifdef APP_PHONON
331     switch( mediaObject->state() ) {
332     case Phonon::PlayingState:
333         mediaObject->pause();
334         break;
335     default:
336         mediaObject->play();
337         break;
338     }
339 #endif
340 }
341
342 QRegExp MediaView::wordRE(QString s) {
343     return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
344 }
345
346 void MediaView::stop() {
347     stopped = true;
348
349     while (!history.isEmpty()) {
350         VideoSource *videoSource = history.takeFirst();
351         if (!videoSource->parent()) delete videoSource;
352     }
353
354     playlistModel->abortSearch();
355     videoAreaWidget->clear();
356     videoAreaWidget->update();
357     errorTimer->stop();
358     playlistView->selectionModel()->clearSelection();
359     if (downloadItem) {
360         downloadItem->stop();
361         delete downloadItem;
362         downloadItem = 0;
363         currentVideoSize = 0;
364     }
365     The::globalActions()->value("refine-search")->setChecked(false);
366     updateSubscriptionAction(0, false);
367 #ifdef APP_ACTIVATION
368     demoTimer->stop();
369 #endif
370
371     foreach (QAction *action, currentVideoActions)
372         action->setEnabled(false);
373
374     QAction *a = The::globalActions()->value("download");
375     a->setEnabled(false);
376     a->setVisible(false);
377
378 #ifdef APP_PHONON
379     mediaObject->stop();
380 #endif
381     currentVideoId.clear();
382
383 #ifndef APP_PHONON_SEEK
384     QSlider *slider = MainWindow::instance()->getSlider();
385     slider->setEnabled(false);
386     slider->setValue(0);
387 #endif
388
389     if (snapshotSettings) {
390         delete snapshotSettings;
391         snapshotSettings = 0;
392     }
393 }
394
395 const QString & MediaView::getCurrentVideoId() {
396     return currentVideoId;
397 }
398
399 void MediaView::activeRowChanged(int row) {
400     if (stopped) return;
401
402     errorTimer->stop();
403
404 #ifdef APP_PHONON
405     mediaObject->stop();
406 #endif
407     if (downloadItem) {
408         downloadItem->stop();
409         delete downloadItem;
410         downloadItem = 0;
411         currentVideoSize = 0;
412     }
413
414     Video *video = playlistModel->videoAt(row);
415     if (!video) return;
416
417     videoAreaWidget->showLoading(video);
418
419     connect(video, SIGNAL(gotStreamUrl(QUrl)),
420             SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
421     connect(video, SIGNAL(errorStreamUrl(QString)),
422             SLOT(skip()), Qt::UniqueConnection);
423     video->loadStreamUrl();
424
425     // video title in titlebar
426     MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
427     MainWindow::instance()->showMessage(video->description());
428
429     // ensure active item is visible
430     if (row != -1) {
431         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
432         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
433     }
434
435     // enable/disable actions
436     The::globalActions()->value("download")->setEnabled(
437                 DownloadManager::instance()->itemForVideo(video) == 0);
438     The::globalActions()->value("previous")->setEnabled(row > 0);
439     The::globalActions()->value("stopafterthis")->setEnabled(true);
440     The::globalActions()->value("related-videos")->setEnabled(true);
441
442     bool enableDownload = video->license() == Video::LicenseCC;
443 #ifdef APP_ACTIVATION
444     enableDownload = enableDownload || Activation::instance().isLegacy();
445 #endif
446 #ifdef APP_DOWNLOADS
447     enableDownload = true;
448 #endif
449     QAction *a = The::globalActions()->value("download");
450     a->setEnabled(enableDownload);
451     a->setVisible(enableDownload);
452
453     updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
454
455     foreach (QAction *action, currentVideoActions)
456         action->setEnabled(true);
457
458 #ifndef APP_PHONON_SEEK
459     QSlider *slider = MainWindow::instance()->getSlider();
460     slider->setEnabled(false);
461     slider->setValue(0);
462 #endif
463
464     if (snapshotSettings) {
465         delete snapshotSettings;
466         snapshotSettings = 0;
467     }
468
469     // see you in gotStreamUrl...
470 }
471
472 void MediaView::gotStreamUrl(QUrl streamUrl) {
473     if (stopped) return;
474     if (!streamUrl.isValid()) {
475         skip();
476         return;
477     }
478
479     Video *video = static_cast<Video *>(sender());
480     if (!video) {
481         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
482         return;
483     }
484     video->disconnect(this);
485
486     currentVideoId = video->id();
487
488 #ifdef APP_PHONON_SEEK
489     mediaObject->setCurrentSource(streamUrl);
490     mediaObject->play();
491 #else
492     startDownloading();
493 #endif
494
495     // ensure we always have videos ahead
496     playlistModel->searchNeeded();
497
498     // ensure active item is visible
499     int row = playlistModel->activeRow();
500     if (row != -1) {
501         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
502         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
503     }
504
505 #ifdef APP_ACTIVATION
506     if (!Activation::instance().isActivated())
507         demoTimer->start(180000);
508 #endif
509
510 #ifdef APP_EXTRA
511     Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
512 #endif
513
514     ChannelAggregator::instance()->videoWatched(video);
515 }
516
517 void MediaView::downloadStatusChanged() {
518     // qDebug() << __PRETTY_FUNCTION__;
519     switch(downloadItem->status()) {
520     case Downloading:
521         // qDebug() << "Downloading";
522         if (downloadItem->offset() == 0) startPlaying();
523         else {
524 #ifdef APP_PHONON
525             // qDebug() << "Seeking to" << downloadItem->offset();
526             mediaObject->seek(offsetToTime(downloadItem->offset()));
527             mediaObject->play();
528 #endif
529         }
530         break;
531     case Starting:
532         // qDebug() << "Starting";
533         break;
534     case Finished:
535         // qDebug() << "Finished" << mediaObject->state();
536 #ifdef APP_PHONON_SEEK
537         MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
538 #endif
539         break;
540     case Failed:
541         // qDebug() << "Failed";
542         skip();
543         break;
544     case Idle:
545         // qDebug() << "Idle";
546         break;
547     }
548 }
549
550 void MediaView::startPlaying() {
551     // qDebug() << __PRETTY_FUNCTION__;
552     if (stopped) return;
553     if (!downloadItem) {
554         skip();
555         return;
556     }
557
558     if (downloadItem->offset() == 0) {
559         currentVideoSize = downloadItem->bytesTotal();
560         // qDebug() << "currentVideoSize" << currentVideoSize;
561     }
562
563     // go!
564     QString source = downloadItem->currentFilename();
565     qDebug() << "Playing" << source << QFile::exists(source);
566 #ifdef APP_PHONON
567     mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
568     mediaObject->play();
569 #endif
570 #ifdef APP_PHONON_SEEK
571     MainWindow::instance()->getSeekSlider()->setEnabled(false);
572 #else
573     QSlider *slider = MainWindow::instance()->getSlider();
574     slider->setEnabled(true);
575 #endif
576 }
577
578 void MediaView::itemActivated(const QModelIndex &index) {
579     if (playlistModel->rowExists(index.row())) {
580
581         // if it's the current video, just rewind and play
582         Video *activeVideo = playlistModel->activeVideo();
583         Video *video = playlistModel->videoAt(index.row());
584         if (activeVideo && video && activeVideo == video) {
585             // mediaObject->seek(0);
586             sliderMoved(0);
587 #ifdef APP_PHONON
588             mediaObject->play();
589 #endif
590         } else playlistModel->setActiveRow(index.row());
591
592         // the user doubleclicked on the "Search More" item
593     } else {
594         playlistModel->searchMore();
595         playlistView->selectionModel()->clearSelection();
596     }
597 }
598
599 void MediaView::skipVideo() {
600     // skippedVideo is useful for DELAYED skip operations
601     // in order to be sure that we're skipping the video we wanted
602     // and not another one
603     if (skippedVideo) {
604         if (playlistModel->activeVideo() != skippedVideo) {
605             qDebug() << "Skip of video canceled";
606             return;
607         }
608         int nextRow = playlistModel->rowForVideo(skippedVideo);
609         nextRow++;
610         if (nextRow == -1) return;
611         playlistModel->setActiveRow(nextRow);
612     }
613 }
614
615 void MediaView::skip() {
616     int nextRow = playlistModel->nextRow();
617     if (nextRow == -1) return;
618     playlistModel->setActiveRow(nextRow);
619 }
620
621 void MediaView::skipBackward() {
622     int prevRow = playlistModel->previousRow();
623     if (prevRow == -1) return;
624     playlistModel->setActiveRow(prevRow);
625 }
626
627 void MediaView::aboutToFinish() {
628 #ifdef APP_PHONON
629     qint64 currentTime = mediaObject->currentTime();
630     qint64 totalTime = mediaObject->totalTime();
631     qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
632     if (totalTime < 1 || currentTime + 10000 < totalTime) {
633         // QTimer::singleShot(500, this, SLOT(playbackResume()));
634         mediaObject->seek(currentTime);
635         mediaObject->play();
636     }
637 #endif
638 }
639
640 void MediaView::playbackFinished() {
641     if (stopped) return;
642
643 #ifdef APP_PHONON
644     const qint64 totalTime = mediaObject->totalTime();
645     const qint64 currentTime = mediaObject->currentTime();
646     qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
647     // add 10 secs for imprecise Phonon backends (VLC, Xine)
648     if (totalTime < 1 || (currentTime > 0 && currentTime + 10000 < totalTime)) {
649         // mediaObject->seek(currentTime);
650         QTimer::singleShot(500, this, SLOT(playbackResume()));
651     } else {
652         QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
653         if (stopAfterThisAction->isChecked()) {
654             stopAfterThisAction->setChecked(false);
655         } else skip();
656     }
657 #endif
658 }
659
660 void MediaView::playbackResume() {
661     if (stopped) return;
662 #ifdef APP_PHONON
663     const qint64 currentTime = mediaObject->currentTime();
664     qDebug() << __PRETTY_FUNCTION__ << currentTime;
665     if (currentTime > 0)
666         mediaObject->seek(currentTime);
667     mediaObject->play();
668 #endif
669 }
670
671 void MediaView::openWebPage() {
672     Video* video = playlistModel->activeVideo();
673     if (!video) return;
674 #ifdef APP_PHONON
675     mediaObject->pause();
676 #endif
677     QDesktopServices::openUrl(video->webpage());
678 }
679
680 void MediaView::copyWebPage() {
681     Video* video = playlistModel->activeVideo();
682     if (!video) return;
683     QString address = video->webpage();
684     QApplication::clipboard()->setText(address);
685     QString message = tr("You can now paste the YouTube link into another application");
686     MainWindow::instance()->showMessage(message);
687 }
688
689 void MediaView::copyVideoLink() {
690     Video* video = playlistModel->activeVideo();
691     if (!video) return;
692     QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
693     QString message = tr("You can now paste the video stream URL into another application")
694             + ". " + tr("The link will be valid only for a limited time.");
695     MainWindow::instance()->showMessage(message);
696 }
697
698 void MediaView::openInBrowser() {
699     Video* video = playlistModel->activeVideo();
700     if (!video) return;
701 #ifdef APP_PHONON
702     mediaObject->pause();
703 #endif
704     QDesktopServices::openUrl(video->getStreamUrl());
705 }
706
707 void MediaView::removeSelected() {
708     if (!playlistView->selectionModel()->hasSelection()) return;
709     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
710     playlistModel->removeIndexes(indexes);
711 }
712
713 void MediaView::selectVideos(QList<Video*> videos) {
714     foreach (Video *video, videos) {
715         QModelIndex index = playlistModel->indexForVideo(video);
716         playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
717         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
718     }
719 }
720
721 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
722                                  const QItemSelection & /*deselected*/) {
723     const bool gotSelection = playlistView->selectionModel()->hasSelection();
724     The::globalActions()->value("remove")->setEnabled(gotSelection);
725     The::globalActions()->value("moveUp")->setEnabled(gotSelection);
726     The::globalActions()->value("moveDown")->setEnabled(gotSelection);
727 }
728
729 void MediaView::moveUpSelected() {
730     if (!playlistView->selectionModel()->hasSelection()) return;
731
732     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
733     qStableSort(indexes.begin(), indexes.end());
734     playlistModel->move(indexes, true);
735
736     // set current index after row moves to something more intuitive
737     int row = indexes.first().row();
738     playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>1?row:1),
739                                                     QItemSelectionModel::NoUpdate);
740 }
741
742 void MediaView::moveDownSelected() {
743     if (!playlistView->selectionModel()->hasSelection()) return;
744
745     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
746     qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
747     playlistModel->move(indexes, false);
748
749     // set current index after row moves to something more intuitive
750     // (respect 1 static item on bottom)
751     int row = indexes.first().row()+1, max = playlistModel->rowCount() - 2;
752     playlistView->selectionModel()->setCurrentIndex(
753                 playlistModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
754 }
755
756 void MediaView::setPlaylistVisible(bool visible) {
757     if (splitter->widget(0)->isVisible() == visible) return;
758     splitter->widget(0)->setVisible(visible);
759     playlistView->setFocus();
760 }
761
762 bool MediaView::isPlaylistVisible() {
763     return splitter->widget(0)->isVisible();
764 }
765
766 void MediaView::saveSplitterState() {
767     QSettings settings;
768     settings.setValue("splitter", splitter->saveState());
769 }
770
771 #ifdef APP_ACTIVATION
772
773 static QPushButton *continueButton;
774
775 void MediaView::demoMessage() {
776 #ifdef APP_PHONON
777     if (mediaObject->state() != Phonon::PlayingState) return;
778     mediaObject->pause();
779 #endif
780
781     QMessageBox msgBox(this);
782     msgBox.setIconPixmap(QPixmap(":/images/app.png").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
783     msgBox.setText(tr("This is just the demo version of %1.").arg(Constants::NAME));
784     msgBox.setInformativeText(tr("It allows you to test the application and see if it works for you."));
785     msgBox.setModal(true);
786     // make it a "sheet" on the Mac
787     msgBox.setWindowModality(Qt::WindowModal);
788
789     continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
790     continueButton->setEnabled(false);
791     QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
792
793     QTimeLine *timeLine = new QTimeLine(6000, this);
794     timeLine->setCurveShape(QTimeLine::LinearCurve);
795     timeLine->setFrameRange(5, 0);
796     connect(timeLine, SIGNAL(frameChanged(int)), SLOT(updateContinueButton(int)));
797     timeLine->start();
798
799     msgBox.exec();
800
801     if (msgBox.clickedButton() == buyButton) {
802         MainWindow::instance()->showActivationView();
803     } else {
804 #ifdef APP_PHONON
805         mediaObject->play();
806 #endif
807         demoTimer->start(600000);
808     }
809
810     delete timeLine;
811
812 }
813
814 void MediaView::updateContinueButton(int value) {
815     if (value == 0) {
816         continueButton->setText(tr("Continue"));
817         continueButton->setEnabled(true);
818     } else {
819         continueButton->setText(QString::number(value));
820     }
821 }
822
823 #endif
824
825 void MediaView::downloadVideo() {
826     Video* video = playlistModel->activeVideo();
827     if (!video) return;
828     DownloadManager::instance()->addItem(video);
829     The::globalActions()->value("downloads")->setVisible(true);
830     QString message = tr("Downloading %1").arg(video->title());
831     MainWindow::instance()->showMessage(message);
832 }
833
834 #ifdef APP_SNAPSHOT
835 void MediaView::snapshot() {
836     qint64 currentTime = mediaObject->currentTime() / 1000;
837
838     QImage image = videoWidget->snapshot();
839     if (image.isNull()) {
840         qWarning() << "Null snapshot";
841         return;
842     }
843
844     // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
845     QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
846     videoAreaWidget->showSnapshotPreview(pixmap);
847
848     Video* video = playlistModel->activeVideo();
849     if (!video) return;
850
851     QString location = SnapshotSettings::getCurrentLocation();
852     QDir dir(location);
853     if (!dir.exists()) dir.mkpath(location);
854     QString basename = video->title();
855     QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
856     basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
857     basename = DataUtils::stringToFilename(basename);
858     QString filename = location + "/" + basename + ".png";
859     qDebug() << filename;
860     image.save(filename, "PNG");
861
862     if (snapshotSettings) delete snapshotSettings;
863     snapshotSettings = new SnapshotSettings(videoWidget);
864     snapshotSettings->setSnapshot(pixmap, filename);
865     QStatusBar *statusBar = MainWindow::instance()->statusBar();
866 #ifdef APP_EXTRA
867     Extra::fadeInWidget(statusBar, statusBar);
868 #endif
869     statusBar->clearMessage();
870     statusBar->insertPermanentWidget(0, snapshotSettings);
871     snapshotSettings->show();
872 }
873 #endif
874
875 void MediaView::fullscreen() {
876     videoAreaWidget->setParent(0);
877     videoAreaWidget->showFullScreen();
878 }
879
880 void MediaView::startDownloading() {
881     Video *video = playlistModel->activeVideo();
882     if (!video) return;
883     Video *videoCopy = video->clone();
884     if (downloadItem) {
885         downloadItem->stop();
886         delete downloadItem;
887     }
888     QString tempFile = Temporary::filename();
889     downloadItem = new DownloadItem(videoCopy, video->getStreamUrl(), tempFile, this);
890     connect(downloadItem, SIGNAL(statusChanged()),
891             SLOT(downloadStatusChanged()), Qt::UniqueConnection);
892     connect(downloadItem, SIGNAL(bufferProgress(int)),
893             loadingWidget, SLOT(bufferStatus(int)), Qt::UniqueConnection);
894     // connect(downloadItem, SIGNAL(finished()), SLOT(itemFinished()));
895     connect(video, SIGNAL(errorStreamUrl(QString)),
896             SLOT(handleError(QString)), Qt::UniqueConnection);
897     connect(downloadItem, SIGNAL(error(QString)),
898             SLOT(handleError(QString)), Qt::UniqueConnection);
899     downloadItem->start();
900 }
901
902 void MediaView::sliderMoved(int value) {
903 #ifdef APP_PHONON
904 #ifndef APP_PHONON_SEEK
905
906     if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
907         return;
908
909     QSlider *slider = MainWindow::instance()->getSlider();
910     if (slider->isSliderDown()) return;
911
912     qint64 offset = (currentVideoSize * value) / slider->maximum();
913
914     bool needsDownload = downloadItem->needsDownload(offset);
915     if (needsDownload) {
916         if (downloadItem->isBuffered(offset)) {
917             qint64 realOffset = downloadItem->blankAtOffset(offset);
918             if (offset < currentVideoSize)
919                 downloadItem->seekTo(realOffset, false);
920             mediaObject->seek(offsetToTime(offset));
921         } else {
922             mediaObject->pause();
923             downloadItem->seekTo(offset);
924         }
925     } else {
926         // qDebug() << "simple seek";
927         mediaObject->seek(offsetToTime(offset));
928     }
929 #endif
930 #endif
931 }
932
933 qint64 MediaView::offsetToTime(qint64 offset) {
934 #ifdef APP_PHONON
935     const qint64 totalTime = mediaObject->totalTime();
936     return ((offset * totalTime) / currentVideoSize);
937 #endif
938 }
939
940 void MediaView::findVideoParts() {
941
942     // parts
943     Video* video = playlistModel->activeVideo();
944     if (!video) return;
945
946     QString query = video->title();
947
948     static QString optionalSpace = "\\s*";
949     static QString staticCounterSeparators = "[\\/\\-]";
950     QString counterSeparators = "( of | " +
951             tr("of", "Used in video parts, as in '2 of 3'") +
952             " |" + staticCounterSeparators + ")";
953
954     // numbers from 1 to 15
955     static QString counterNumber = "([1-9]|1[0-5])";
956
957     // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
958     query.remove(QRegExp(counterNumber + optionalSpace +
959                          counterSeparators + optionalSpace + counterNumber));
960     query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
961     query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
962     query.remove(wordRE("part" + optionalSpace + counterNumber));
963     query.remove(wordRE("episode" + optionalSpace + counterNumber));
964     query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
965                         optionalSpace + counterNumber));
966     query.remove(wordRE(tr("episode",
967                            "This is for video parts, as in 'Cool series - episode 1'") +
968                         optionalSpace + counterNumber));
969     query.remove(QRegExp("[\\(\\)\\[\\]]"));
970
971 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
972
973     QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
974                                       Qt::CaseInsensitive);
975     // bool numberAsWords = englishNumberRE.exactMatch(query);
976     query.remove(englishNumberRE);
977
978     QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
979                                         Qt::CaseInsensitive);
980     // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
981     query.remove(localizedNumberRE);
982
983     SearchParams *searchParams = new SearchParams();
984     searchParams->setTransient(true);
985     searchParams->setKeywords(query);
986     searchParams->setChannelId(video->channelId());
987
988     /*
989     if (!numberAsWords) {
990         qDebug() << "We don't have number as words";
991         // searchParams->setSortBy(SearchParams::SortByNewest);
992         // TODO searchParams->setReverseOrder(true);
993         // TODO searchParams->setMax(50);
994     }
995     */
996
997     search(searchParams);
998
999 }
1000
1001 void MediaView::relatedVideos() {
1002     Video* video = playlistModel->activeVideo();
1003     if (!video) return;
1004     YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
1005     singleVideoSource->setVideo(video->clone());
1006     singleVideoSource->setAsyncDetails(true);
1007     setVideoSource(singleVideoSource);
1008     The::globalActions()->value("related-videos")->setEnabled(false);
1009 }
1010
1011 void MediaView::shareViaTwitter() {
1012     Video* video = playlistModel->activeVideo();
1013     if (!video) return;
1014     QUrl url("https://twitter.com/intent/tweet");
1015     {
1016         QUrlQueryHelper urlHelper(url);
1017         urlHelper.addQueryItem("via", "minitubeapp");
1018         urlHelper.addQueryItem("text", video->title());
1019         urlHelper.addQueryItem("url", video->webpage());
1020     }
1021     QDesktopServices::openUrl(url);
1022 }
1023
1024 void MediaView::shareViaFacebook() {
1025     Video* video = playlistModel->activeVideo();
1026     if (!video) return;
1027     QUrl url("https://www.facebook.com/sharer.php");
1028     {
1029         QUrlQueryHelper urlHelper(url);
1030         urlHelper.addQueryItem("t", video->title());
1031         urlHelper.addQueryItem("u", video->webpage());
1032     }
1033     QDesktopServices::openUrl(url);
1034 }
1035
1036 void MediaView::shareViaBuffer() {
1037     Video* video = playlistModel->activeVideo();
1038     if (!video) return;
1039     QUrl url("http://bufferapp.com/add");
1040     {
1041         QUrlQueryHelper urlHelper(url);
1042         urlHelper.addQueryItem("via", "minitubeapp");
1043         urlHelper.addQueryItem("text", video->title());
1044         urlHelper.addQueryItem("url", video->webpage());
1045         urlHelper.addQueryItem("picture", video->thumbnailUrl());
1046     }
1047     QDesktopServices::openUrl(url);
1048 }
1049
1050 void MediaView::shareViaEmail() {
1051     Video* video = playlistModel->activeVideo();
1052     if (!video) return;
1053     QUrl url("mailto:");
1054     {
1055         QUrlQueryHelper urlHelper(url);
1056         urlHelper.addQueryItem("subject", video->title());
1057         const QString body = video->title() + "\n" +
1058                 video->webpage() + "\n\n" +
1059                 tr("Sent from %1").arg(Constants::NAME) + "\n" +
1060                 Constants::WEBSITE;
1061         urlHelper.addQueryItem("body", body);
1062     }
1063     QDesktopServices::openUrl(url);
1064 }
1065
1066 void MediaView::authorPushed(QModelIndex index) {
1067     Video* video = playlistModel->videoAt(index.row());
1068     if (!video) return;
1069
1070     QString channelId = video->channelId();
1071     // if (channelId.isEmpty()) channelId = video->channelTitle();
1072     if (channelId.isEmpty()) return;
1073
1074     SearchParams *searchParams = new SearchParams();
1075     searchParams->setChannelId(channelId);
1076     searchParams->setSortBy(SearchParams::SortByNewest);
1077
1078     // go!
1079     search(searchParams);
1080 }
1081
1082 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1083     QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1084
1085     QString subscribeTip;
1086     QString subscribeText;
1087     if (!video) {
1088         subscribeText = subscribeAction->property("originalText").toString();
1089         subscribeAction->setEnabled(false);
1090     } else if (subscribed) {
1091         subscribeText = tr("Unsubscribe from %1").arg(video->channelTitle());
1092         subscribeTip = subscribeText;
1093         subscribeAction->setEnabled(true);
1094     } else {
1095         subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
1096         subscribeTip = subscribeText;
1097         subscribeAction->setEnabled(true);
1098     }
1099     subscribeAction->setText(subscribeText);
1100     subscribeAction->setStatusTip(subscribeTip);
1101
1102     if (subscribed) {
1103 #ifdef Q_OS_LINUX
1104         static QIcon tintedIcon;
1105         if (tintedIcon.isNull()) {
1106             QList<QSize> sizes;
1107             sizes << QSize(16, 16);
1108             tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1109         }
1110         subscribeAction->setIcon(tintedIcon);
1111 #else
1112         subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1113 #endif
1114     } else {
1115         subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1116     }
1117
1118     IconUtils::setupAction(subscribeAction);
1119 }
1120
1121 void MediaView::toggleSubscription() {
1122     Video *video = playlistModel->activeVideo();
1123     if (!video) return;
1124     QString userId = video->channelId();
1125     if (userId.isEmpty()) return;
1126     bool subscribed = YTChannel::isSubscribed(userId);
1127     if (subscribed) YTChannel::unsubscribe(userId);
1128     else YTChannel::subscribe(userId);
1129     updateSubscriptionAction(video, !subscribed);
1130 }