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