]> git.sur5r.net Git - minitube/blob - src/mediaview.cpp
Removed unused preprocessor directive
[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 "utils.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(startToolbarSearch(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 #ifdef APP_PHONON_SEEK
306     mediaObject->play();
307 #else
308     QTimer::singleShot(500, this, SLOT(startPlaying()));
309 #endif
310 }
311
312 #ifdef APP_PHONON
313 void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/) {
314     if (newState == Phonon::PlayingState)
315         videoAreaWidget->showVideo();
316     else if (newState == Phonon::ErrorState) {
317         qDebug() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
318         if (mediaObject->errorType() == Phonon::FatalError)
319             handleError(mediaObject->errorString());
320     }
321 }
322 #endif
323
324 void MediaView::pause() {
325 #ifdef APP_PHONON
326     switch( mediaObject->state() ) {
327     case Phonon::PlayingState:
328         mediaObject->pause();
329         break;
330     default:
331         mediaObject->play();
332         break;
333     }
334 #endif
335 }
336
337 QRegExp MediaView::wordRE(QString s) {
338     return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
339 }
340
341 void MediaView::stop() {
342     stopped = true;
343
344     while (!history.isEmpty()) {
345         VideoSource *videoSource = history.takeFirst();
346         if (!videoSource->parent()) delete videoSource;
347     }
348
349     playlistModel->abortSearch();
350     videoAreaWidget->clear();
351     videoAreaWidget->update();
352     errorTimer->stop();
353     playlistView->selectionModel()->clearSelection();
354     if (downloadItem) {
355         downloadItem->stop();
356         delete downloadItem;
357         downloadItem = 0;
358         currentVideoSize = 0;
359     }
360     The::globalActions()->value("refine-search")->setChecked(false);
361     updateSubscriptionAction(0, false);
362 #ifdef APP_ACTIVATION
363     demoTimer->stop();
364 #endif
365
366     foreach (QAction *action, currentVideoActions)
367         action->setEnabled(false);
368
369     QAction *a = The::globalActions()->value("download");
370     a->setEnabled(false);
371     a->setVisible(false);
372
373 #ifdef APP_PHONON
374     mediaObject->stop();
375 #endif
376     currentVideoId.clear();
377
378 #ifndef APP_PHONON_SEEK
379     QSlider *slider = MainWindow::instance()->getSlider();
380     slider->setEnabled(false);
381     slider->setValue(0);
382 #endif
383
384     if (snapshotSettings) {
385         delete snapshotSettings;
386         snapshotSettings = 0;
387     }
388 }
389
390 const QString & MediaView::getCurrentVideoId() {
391     return currentVideoId;
392 }
393
394 void MediaView::activeRowChanged(int row) {
395     if (stopped) return;
396
397     errorTimer->stop();
398
399 #ifdef APP_PHONON
400     mediaObject->stop();
401 #endif
402     if (downloadItem) {
403         downloadItem->stop();
404         delete downloadItem;
405         downloadItem = 0;
406         currentVideoSize = 0;
407     }
408
409     Video *video = playlistModel->videoAt(row);
410     if (!video) return;
411
412     videoAreaWidget->showLoading(video);
413
414     connect(video, SIGNAL(gotStreamUrl(QUrl)),
415             SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
416     connect(video, SIGNAL(errorStreamUrl(QString)),
417             SLOT(skip()), Qt::UniqueConnection);
418     video->loadStreamUrl();
419
420     // video title in titlebar
421     MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
422     MainWindow::instance()->showMessage(video->description());
423
424     // ensure active item is visible
425     if (row != -1) {
426         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
427         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
428     }
429
430     // enable/disable actions
431     The::globalActions()->value("download")->setEnabled(
432                 DownloadManager::instance()->itemForVideo(video) == 0);
433     The::globalActions()->value("previous")->setEnabled(row > 0);
434     The::globalActions()->value("stopafterthis")->setEnabled(true);
435     The::globalActions()->value("related-videos")->setEnabled(true);
436
437     bool enableDownload = video->license() == Video::LicenseCC;
438 #ifdef APP_ACTIVATION
439     enableDownload = enableDownload || Activation::instance().isLegacy();
440 #endif
441 #ifdef APP_DOWNLOADS
442     enableDownload = true;
443 #endif
444     QAction *a = The::globalActions()->value("download");
445     a->setEnabled(enableDownload);
446     a->setVisible(enableDownload);
447
448     updateSubscriptionAction(video, YTUser::isSubscribed(video->userId()));
449
450     foreach (QAction *action, currentVideoActions)
451         action->setEnabled(true);
452
453 #ifndef APP_PHONON_SEEK
454     QSlider *slider = MainWindow::instance()->getSlider();
455     slider->setEnabled(false);
456     slider->setValue(0);
457 #endif
458
459     if (snapshotSettings) {
460         delete snapshotSettings;
461         snapshotSettings = 0;
462     }
463
464     // see you in gotStreamUrl...
465 }
466
467 void MediaView::gotStreamUrl(QUrl streamUrl) {
468     if (stopped) return;
469     if (!streamUrl.isValid()) {
470         skip();
471         return;
472     }
473
474     Video *video = static_cast<Video *>(sender());
475     if (!video) {
476         qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
477         return;
478     }
479     video->disconnect(this);
480
481     currentVideoId = video->id();
482
483 #ifdef APP_PHONON_SEEK
484     mediaObject->setCurrentSource(streamUrl);
485     mediaObject->play();
486 #else
487     startDownloading();
488 #endif
489
490     // ensure we always have 10 videos ahead
491     playlistModel->searchNeeded();
492
493     // ensure active item is visible
494     int row = playlistModel->activeRow();
495     if (row != -1) {
496         QModelIndex index = playlistModel->index(row, 0, QModelIndex());
497         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
498     }
499
500 #ifdef APP_ACTIVATION
501     if (!Activation::instance().isActivated())
502         demoTimer->start(180000);
503 #endif
504
505 #ifdef APP_EXTRA
506     Extra::notify(video->title(), video->author(), video->formattedDuration());
507 #endif
508
509     ChannelAggregator::instance()->videoWatched(video);
510 }
511
512 void MediaView::downloadStatusChanged() {
513     // qDebug() << __PRETTY_FUNCTION__;
514     switch(downloadItem->status()) {
515     case Downloading:
516         // qDebug() << "Downloading";
517         if (downloadItem->offset() == 0) startPlaying();
518         else {
519 #ifdef APP_PHONON
520             // qDebug() << "Seeking to" << downloadItem->offset();
521             mediaObject->seek(offsetToTime(downloadItem->offset()));
522             mediaObject->play();
523 #endif
524         }
525         break;
526     case Starting:
527         // qDebug() << "Starting";
528         break;
529     case Finished:
530         // qDebug() << "Finished" << mediaObject->state();
531 #ifdef APP_PHONON_SEEK
532         MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
533 #endif
534         break;
535     case Failed:
536         // qDebug() << "Failed";
537         skip();
538         break;
539     case Idle:
540         // qDebug() << "Idle";
541         break;
542     }
543 }
544
545 void MediaView::startPlaying() {
546     // qDebug() << __PRETTY_FUNCTION__;
547     if (stopped) return;
548     if (!downloadItem) {
549         skip();
550         return;
551     }
552
553     if (downloadItem->offset() == 0) {
554         currentVideoSize = downloadItem->bytesTotal();
555         // qDebug() << "currentVideoSize" << currentVideoSize;
556     }
557
558     // go!
559     QString source = downloadItem->currentFilename();
560     qDebug() << "Playing" << source << QFile::exists(source);
561 #ifdef APP_PHONON
562     mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
563     mediaObject->play();
564 #endif
565 #ifdef APP_PHONON_SEEK
566     MainWindow::instance()->getSeekSlider()->setEnabled(false);
567 #else
568     QSlider *slider = MainWindow::instance()->getSlider();
569     slider->setEnabled(true);
570 #endif
571 }
572
573 void MediaView::itemActivated(const QModelIndex &index) {
574     if (playlistModel->rowExists(index.row())) {
575
576         // if it's the current video, just rewind and play
577         Video *activeVideo = playlistModel->activeVideo();
578         Video *video = playlistModel->videoAt(index.row());
579         if (activeVideo && video && activeVideo == video) {
580             // mediaObject->seek(0);
581             sliderMoved(0);
582 #ifdef APP_PHONON
583             mediaObject->play();
584 #endif
585         } else playlistModel->setActiveRow(index.row());
586
587         // the user doubleclicked on the "Search More" item
588     } else {
589         playlistModel->searchMore();
590         playlistView->selectionModel()->clearSelection();
591     }
592 }
593
594 void MediaView::skipVideo() {
595     // skippedVideo is useful for DELAYED skip operations
596     // in order to be sure that we're skipping the video we wanted
597     // and not another one
598     if (skippedVideo) {
599         if (playlistModel->activeVideo() != skippedVideo) {
600             qDebug() << "Skip of video canceled";
601             return;
602         }
603         int nextRow = playlistModel->rowForVideo(skippedVideo);
604         nextRow++;
605         if (nextRow == -1) return;
606         playlistModel->setActiveRow(nextRow);
607     }
608 }
609
610 void MediaView::skip() {
611     int nextRow = playlistModel->nextRow();
612     if (nextRow == -1) return;
613     playlistModel->setActiveRow(nextRow);
614 }
615
616 void MediaView::skipBackward() {
617     int prevRow = playlistModel->previousRow();
618     if (prevRow == -1) return;
619     playlistModel->setActiveRow(prevRow);
620 }
621
622 void MediaView::aboutToFinish() {
623 #ifdef APP_PHONON
624     qint64 currentTime = mediaObject->currentTime();
625     qint64 totalTime = mediaObject->totalTime();
626     qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
627     if (totalTime < 1 || currentTime + 10000 < totalTime) {
628         // QTimer::singleShot(500, this, SLOT(playbackResume()));
629         mediaObject->seek(currentTime);
630         mediaObject->play();
631     }
632 #endif
633 }
634
635 void MediaView::playbackFinished() {
636     if (stopped) return;
637
638 #ifdef APP_PHONON
639     const qint64 totalTime = mediaObject->totalTime();
640     const qint64 currentTime = mediaObject->currentTime();
641     qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
642     // add 10 secs for imprecise Phonon backends (VLC, Xine)
643     if (totalTime < 1 || (currentTime > 0 && currentTime + 10000 < totalTime)) {
644         // mediaObject->seek(currentTime);
645         QTimer::singleShot(500, this, SLOT(playbackResume()));
646     } else {
647         QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
648         if (stopAfterThisAction->isChecked()) {
649             stopAfterThisAction->setChecked(false);
650         } else skip();
651     }
652 #endif
653 }
654
655 void MediaView::playbackResume() {
656     if (stopped) return;
657 #ifdef APP_PHONON
658     const qint64 currentTime = mediaObject->currentTime();
659     qDebug() << __PRETTY_FUNCTION__ << currentTime;
660     if (currentTime > 0)
661         mediaObject->seek(currentTime);
662     mediaObject->play();
663 #endif
664 }
665
666 void MediaView::openWebPage() {
667     Video* video = playlistModel->activeVideo();
668     if (!video) return;
669 #ifdef APP_PHONON
670     mediaObject->pause();
671 #endif
672     QDesktopServices::openUrl(video->webpage());
673 }
674
675 void MediaView::copyWebPage() {
676     Video* video = playlistModel->activeVideo();
677     if (!video) return;
678     QString address = video->webpage().toString();
679     QApplication::clipboard()->setText(address);
680     QString message = tr("You can now paste the YouTube link into another application");
681     MainWindow::instance()->showMessage(message);
682 }
683
684 void MediaView::copyVideoLink() {
685     Video* video = playlistModel->activeVideo();
686     if (!video) return;
687     QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
688     QString message = tr("You can now paste the video stream URL into another application")
689             + ". " + tr("The link will be valid only for a limited time.");
690     MainWindow::instance()->showMessage(message);
691 }
692
693 void MediaView::openInBrowser() {
694     Video* video = playlistModel->activeVideo();
695     if (!video) return;
696 #ifdef APP_PHONON
697     mediaObject->pause();
698 #endif
699     QDesktopServices::openUrl(video->getStreamUrl());
700 }
701
702 void MediaView::removeSelected() {
703     if (!playlistView->selectionModel()->hasSelection()) return;
704     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
705     playlistModel->removeIndexes(indexes);
706 }
707
708 void MediaView::selectVideos(QList<Video*> videos) {
709     foreach (Video *video, videos) {
710         QModelIndex index = playlistModel->indexForVideo(video);
711         playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
712         playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
713     }
714 }
715
716 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
717                                  const QItemSelection & /*deselected*/) {
718     const bool gotSelection = playlistView->selectionModel()->hasSelection();
719     The::globalActions()->value("remove")->setEnabled(gotSelection);
720     The::globalActions()->value("moveUp")->setEnabled(gotSelection);
721     The::globalActions()->value("moveDown")->setEnabled(gotSelection);
722 }
723
724 void MediaView::moveUpSelected() {
725     if (!playlistView->selectionModel()->hasSelection()) return;
726
727     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
728     qStableSort(indexes.begin(), indexes.end());
729     playlistModel->move(indexes, true);
730
731     // set current index after row moves to something more intuitive
732     int row = indexes.first().row();
733     playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>1?row:1),
734                                                     QItemSelectionModel::NoUpdate);
735 }
736
737 void MediaView::moveDownSelected() {
738     if (!playlistView->selectionModel()->hasSelection()) return;
739
740     QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
741     qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
742     playlistModel->move(indexes, false);
743
744     // set current index after row moves to something more intuitive
745     // (respect 1 static item on bottom)
746     int row = indexes.first().row()+1, max = playlistModel->rowCount() - 2;
747     playlistView->selectionModel()->setCurrentIndex(
748                 playlistModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
749 }
750
751 void MediaView::setPlaylistVisible(bool visible) {
752     if (splitter->widget(0)->isVisible() == visible) return;
753     splitter->widget(0)->setVisible(visible);
754     playlistView->setFocus();
755 }
756
757 bool MediaView::isPlaylistVisible() {
758     return splitter->widget(0)->isVisible();
759 }
760
761 void MediaView::saveSplitterState() {
762     QSettings settings;
763     settings.setValue("splitter", splitter->saveState());
764 }
765
766 #ifdef APP_ACTIVATION
767
768 static QPushButton *continueButton;
769
770 void MediaView::demoMessage() {
771 #ifdef APP_PHONON
772     if (mediaObject->state() != Phonon::PlayingState) return;
773     mediaObject->pause();
774 #endif
775
776     QMessageBox msgBox(this);
777     msgBox.setIconPixmap(QPixmap(":/images/app.png").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
778     msgBox.setText(tr("This is just the demo version of %1.").arg(Constants::NAME));
779     msgBox.setInformativeText(tr("It allows you to test the application and see if it works for you."));
780     msgBox.setModal(true);
781     // make it a "sheet" on the Mac
782     msgBox.setWindowModality(Qt::WindowModal);
783
784     continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
785     continueButton->setEnabled(false);
786     QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
787
788     QTimeLine *timeLine = new QTimeLine(6000, this);
789     timeLine->setCurveShape(QTimeLine::LinearCurve);
790     timeLine->setFrameRange(5, 0);
791     connect(timeLine, SIGNAL(frameChanged(int)), SLOT(updateContinueButton(int)));
792     timeLine->start();
793
794     msgBox.exec();
795
796     if (msgBox.clickedButton() == buyButton) {
797         MainWindow::instance()->showActivationView();
798     } else {
799 #ifdef APP_PHONON
800         mediaObject->play();
801 #endif
802         demoTimer->start(600000);
803     }
804
805     delete timeLine;
806
807 }
808
809 void MediaView::updateContinueButton(int value) {
810     if (value == 0) {
811         continueButton->setText(tr("Continue"));
812         continueButton->setEnabled(true);
813     } else {
814         continueButton->setText(QString::number(value));
815     }
816 }
817
818 #endif
819
820 void MediaView::downloadVideo() {
821     Video* video = playlistModel->activeVideo();
822     if (!video) return;
823     DownloadManager::instance()->addItem(video);
824     The::globalActions()->value("downloads")->setVisible(true);
825     QString message = tr("Downloading %1").arg(video->title());
826     MainWindow::instance()->showMessage(message);
827 }
828
829 #ifdef APP_SNAPSHOT
830 void MediaView::snapshot() {
831     qint64 currentTime = mediaObject->currentTime() / 1000;
832
833     QImage image = videoWidget->snapshot();
834     if (image.isNull()) {
835         qWarning() << "Null snapshot";
836         return;
837     }
838
839     // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
840     QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
841     videoAreaWidget->showSnapshotPreview(pixmap);
842
843     Video* video = playlistModel->activeVideo();
844     if (!video) return;
845
846     QString location = SnapshotSettings::getCurrentLocation();
847     QString basename = video->title();
848     QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
849     basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
850     basename = DataUtils::stringToFilename(basename);
851     QString filename = location + "/" + basename + ".png";
852     qDebug() << filename;
853     image.save(filename, "PNG");
854
855     if (snapshotSettings) delete snapshotSettings;
856     snapshotSettings = new SnapshotSettings(videoWidget);
857     snapshotSettings->setSnapshot(pixmap, filename);
858     QStatusBar *statusBar = MainWindow::instance()->statusBar();
859 #ifdef APP_EXTRA
860     Extra::fadeInWidget(statusBar, statusBar);
861 #endif
862     statusBar->clearMessage();
863     statusBar->insertPermanentWidget(0, snapshotSettings);
864     snapshotSettings->show();
865 }
866 #endif
867
868 void MediaView::fullscreen() {
869     videoAreaWidget->setParent(0);
870     videoAreaWidget->showFullScreen();
871 }
872
873 void MediaView::startDownloading() {
874     Video *video = playlistModel->activeVideo();
875     if (!video) return;
876     Video *videoCopy = video->clone();
877     if (downloadItem) {
878         downloadItem->stop();
879         delete downloadItem;
880     }
881     QString tempFile = Temporary::filename();
882     downloadItem = new DownloadItem(videoCopy, video->getStreamUrl(), tempFile, this);
883     connect(downloadItem, SIGNAL(statusChanged()),
884             SLOT(downloadStatusChanged()), Qt::UniqueConnection);
885     connect(downloadItem, SIGNAL(bufferProgress(int)),
886             loadingWidget, SLOT(bufferStatus(int)), Qt::UniqueConnection);
887     // connect(downloadItem, SIGNAL(finished()), SLOT(itemFinished()));
888     connect(video, SIGNAL(errorStreamUrl(QString)),
889             SLOT(handleError(QString)), Qt::UniqueConnection);
890     connect(downloadItem, SIGNAL(error(QString)),
891             SLOT(handleError(QString)), Qt::UniqueConnection);
892     downloadItem->start();
893 }
894
895 void MediaView::sliderMoved(int value) {
896 #ifdef APP_PHONON
897 #ifndef APP_PHONON_SEEK
898
899     if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
900         return;
901
902     QSlider *slider = MainWindow::instance()->getSlider();
903     if (slider->isSliderDown()) return;
904
905     qint64 offset = (currentVideoSize * value) / slider->maximum();
906
907     bool needsDownload = downloadItem->needsDownload(offset);
908     if (needsDownload) {
909         if (downloadItem->isBuffered(offset)) {
910             qint64 realOffset = downloadItem->blankAtOffset(offset);
911             if (offset < currentVideoSize)
912                 downloadItem->seekTo(realOffset, false);
913             mediaObject->seek(offsetToTime(offset));
914         } else {
915             mediaObject->pause();
916             downloadItem->seekTo(offset);
917         }
918     } else {
919         // qDebug() << "simple seek";
920         mediaObject->seek(offsetToTime(offset));
921     }
922 #endif
923 #endif
924 }
925
926 qint64 MediaView::offsetToTime(qint64 offset) {
927 #ifdef APP_PHONON
928     const qint64 totalTime = mediaObject->totalTime();
929     return ((offset * totalTime) / currentVideoSize);
930 #endif
931 }
932
933 void MediaView::findVideoParts() {
934
935     // parts
936     Video* video = playlistModel->activeVideo();
937     if (!video) return;
938
939     QString query = video->title();
940
941     static QString optionalSpace = "\\s*";
942     static QString staticCounterSeparators = "[\\/\\-]";
943     QString counterSeparators = "( of | " +
944             tr("of", "Used in video parts, as in '2 of 3'") +
945             " |" + staticCounterSeparators + ")";
946
947     // numbers from 1 to 15
948     static QString counterNumber = "([1-9]|1[0-5])";
949
950     // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
951     query.remove(QRegExp(counterNumber + optionalSpace +
952                          counterSeparators + optionalSpace + counterNumber));
953     query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
954     query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
955     query.remove(wordRE("part" + optionalSpace + counterNumber));
956     query.remove(wordRE("episode" + optionalSpace + counterNumber));
957     query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
958                         optionalSpace + counterNumber));
959     query.remove(wordRE(tr("episode",
960                            "This is for video parts, as in 'Cool series - episode 1'") +
961                         optionalSpace + counterNumber));
962     query.remove(QRegExp("[\\(\\)\\[\\]]"));
963
964 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
965
966     QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
967                                       Qt::CaseInsensitive);
968     // bool numberAsWords = englishNumberRE.exactMatch(query);
969     query.remove(englishNumberRE);
970
971     QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
972                                         Qt::CaseInsensitive);
973     // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
974     query.remove(localizedNumberRE);
975
976     SearchParams *searchParams = new SearchParams();
977     searchParams->setTransient(true);
978     searchParams->setKeywords(query);
979     searchParams->setAuthor(video->author());
980
981     /*
982     if (!numberAsWords) {
983         qDebug() << "We don't have number as words";
984         // searchParams->setSortBy(SearchParams::SortByNewest);
985         // TODO searchParams->setReverseOrder(true);
986         // TODO searchParams->setMax(50);
987     }
988     */
989
990     search(searchParams);
991
992 }
993
994 void MediaView::relatedVideos() {
995     Video* video = playlistModel->activeVideo();
996     if (!video) return;
997     YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
998     singleVideoSource->setVideoId(video->id());
999     setVideoSource(singleVideoSource);
1000     The::globalActions()->value("related-videos")->setEnabled(false);
1001 }
1002
1003 void MediaView::shareViaTwitter() {
1004     Video* video = playlistModel->activeVideo();
1005     if (!video) return;
1006     QUrl url("https://twitter.com/intent/tweet");
1007 #if QT_VERSION >= 0x050000
1008     {
1009         QUrl &u = url;
1010         QUrlQuery url;
1011 #endif
1012         url.addQueryItem("via", "minitubeapp");
1013         url.addQueryItem("text", video->title());
1014         url.addQueryItem("url", video->webpage().toString());
1015 #if QT_VERSION >= 0x050000
1016         u.setQuery(url);
1017     }
1018 #endif
1019     QDesktopServices::openUrl(url);
1020 }
1021
1022 void MediaView::shareViaFacebook() {
1023     Video* video = playlistModel->activeVideo();
1024     if (!video) return;
1025     QUrl url("https://www.facebook.com/sharer.php");
1026 #if QT_VERSION >= 0x050000
1027     {
1028         QUrl &u = url;
1029         QUrlQuery url;
1030 #endif
1031         url.addQueryItem("t", video->title());
1032         url.addQueryItem("u", video->webpage().toString());
1033 #if QT_VERSION >= 0x050000
1034         u.setQuery(url);
1035     }
1036 #endif
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 #if QT_VERSION >= 0x050000
1045     {
1046         QUrl &u = url;
1047         QUrlQuery url;
1048 #endif
1049         url.addQueryItem("via", "minitubeapp");
1050         url.addQueryItem("text", video->title());
1051         url.addQueryItem("url", video->webpage().toString());
1052         url.addQueryItem("picture", video->thumbnailUrl());
1053 #if QT_VERSION >= 0x050000
1054         u.setQuery(url);
1055     }
1056 #endif
1057     QDesktopServices::openUrl(url);
1058 }
1059
1060 void MediaView::shareViaEmail() {
1061     Video* video = playlistModel->activeVideo();
1062     if (!video) return;
1063     QUrl url("mailto:");
1064 #if QT_VERSION >= 0x050000
1065     {
1066         QUrl &u = url;
1067         QUrlQuery url;
1068 #endif
1069         url.addQueryItem("subject", video->title());
1070         QString body = video->title() + "\n" +
1071                 video->webpage().toString() + "\n\n" +
1072                 tr("Sent from %1").arg(Constants::NAME) + "\n" +
1073                 Constants::WEBSITE;
1074         url.addQueryItem("body", body);
1075 #if QT_VERSION >= 0x050000
1076         u.setQuery(url);
1077     }
1078 #endif
1079     QDesktopServices::openUrl(url);
1080 }
1081
1082 void MediaView::authorPushed(QModelIndex index) {
1083     Video* video = playlistModel->videoAt(index.row());
1084     if (!video) return;
1085
1086     QString channel = video->userId();
1087     if (channel.isEmpty()) channel = video->author();
1088     if (channel.isEmpty()) return;
1089
1090     SearchParams *searchParams = new SearchParams();
1091     searchParams->setAuthor(channel);
1092     searchParams->setSortBy(SearchParams::SortByNewest);
1093
1094     // go!
1095     search(searchParams);
1096 }
1097
1098 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1099     QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1100
1101     QString subscribeTip;
1102     QString subscribeText;
1103     if (!video) {
1104         subscribeText = subscribeAction->property("originalText").toString();
1105         subscribeAction->setEnabled(false);
1106     } else if (subscribed) {
1107         subscribeText = tr("Unsubscribe from %1").arg(video->author());
1108         subscribeTip = subscribeText;
1109         subscribeAction->setEnabled(true);
1110     } else {
1111         subscribeText = tr("Subscribe to %1").arg(video->author());
1112         subscribeTip = subscribeText;
1113         subscribeAction->setEnabled(true);
1114     }
1115     subscribeAction->setText(subscribeText);
1116     subscribeAction->setStatusTip(subscribeTip);
1117
1118     if (subscribed) {
1119 #ifdef Q_OS_LINUX
1120         static QIcon tintedIcon;
1121         if (tintedIcon.isNull()) {
1122             QList<QSize> sizes;
1123             sizes << QSize(16, 16);
1124             tintedIcon = Utils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1125         }
1126         subscribeAction->setIcon(tintedIcon);
1127 #else
1128         subscribeAction->setIcon(Utils::icon("bookmark-remove"));
1129 #endif
1130     } else {
1131         subscribeAction->setIcon(Utils::icon("bookmark-new"));
1132     }
1133
1134     Utils::setupAction(subscribeAction);
1135 }
1136
1137 void MediaView::toggleSubscription() {
1138     Video *video = playlistModel->activeVideo();
1139     if (!video) return;
1140     QString userId = video->userId();
1141     if (userId.isEmpty()) return;
1142     bool subscribed = YTUser::isSubscribed(userId);
1143     if (subscribed) YTUser::unsubscribe(userId);
1144     else YTUser::subscribe(userId);
1145     updateSubscriptionAction(video, !subscribed);
1146 }