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