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