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