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