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