3 This file is part of Minitube.
4 Copyright 2009, Flavio Tordini <flavio.tordini@gmail.com>
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.
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.
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/>.
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"
37 #include "activation.h"
42 #include "videosource.h"
44 #include "searchparams.h"
45 #include "ytsinglevideosource.h"
46 #include "channelaggregator.h"
47 #include "iconutils.h"
48 #include "ytchannel.h"
50 #include "snapshotsettings.h"
52 #include "datautils.h"
53 #include "compatibility/qurlqueryhelper.h"
56 NetworkAccess* http();
57 QHash<QString, QAction*>* globalActions();
58 QHash<QString, QMenu*>* globalMenus();
59 QNetworkAccessManager* networkAccessManager();
62 MediaView* MediaView::instance() {
63 static MediaView *i = new MediaView();
67 MediaView::MediaView(QWidget *parent) : View(parent)
76 void MediaView::initialize() {
77 QBoxLayout *layout = new QVBoxLayout(this);
80 splitter = new MiniSplitter();
82 playlistView = new PlaylistView(this);
83 // respond to the user doubleclicking a playlist item
84 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
85 SLOT(itemActivated(const QModelIndex &)));
87 playlistModel = new PlaylistModel();
88 connect(playlistModel, SIGNAL(activeRowChanged(int)),
89 SLOT(activeRowChanged(int)));
90 // needed to restore the selection after dragndrop
91 connect(playlistModel, SIGNAL(needSelectionFor(QList<Video*>)),
92 SLOT(selectVideos(QList<Video*>)));
93 playlistView->setModel(playlistModel);
95 connect(playlistView->selectionModel(),
96 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
97 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
99 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
101 sidebar = new SidebarWidget(this);
102 sidebar->setPlaylist(playlistView);
103 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()),
104 SLOT(searchAgain()));
105 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)),
106 sidebar, SLOT(showSuggestions(const QStringList &)));
107 connect(sidebar, SIGNAL(suggestionAccepted(QString)),
108 MainWindow::instance(), SLOT(search(QString)));
109 splitter->addWidget(sidebar);
111 videoAreaWidget = new VideoAreaWidget(this);
112 // videoAreaWidget->setMinimumSize(320,240);
115 videoWidget = new Phonon::VideoWidget(this);
116 videoAreaWidget->setVideoWidget(videoWidget);
118 videoAreaWidget->setListModel(playlistModel);
120 loadingWidget = new LoadingWidget(this);
121 videoAreaWidget->setLoadingWidget(loadingWidget);
123 splitter->addWidget(videoAreaWidget);
125 splitter->setStretchFactor(0, 0);
126 splitter->setStretchFactor(1, 8);
128 // restore splitter state
130 splitter->restoreState(settings.value("splitter").toByteArray());
131 splitter->setChildrenCollapsible(false);
132 connect(splitter, SIGNAL(splitterMoved(int,int)), SLOT(maybeAdjustWindowSize()));
134 layout->addWidget(splitter);
136 errorTimer = new QTimer(this);
137 errorTimer->setSingleShot(true);
138 errorTimer->setInterval(3000);
139 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
141 #ifdef APP_ACTIVATION
142 demoTimer = new QTimer(this);
143 demoTimer->setSingleShot(true);
144 connect(demoTimer, SIGNAL(timeout()), SLOT(demoMessage()));
147 connect(videoAreaWidget, SIGNAL(doubleClicked()),
148 The::globalActions()->value("fullscreen"), SLOT(trigger()));
150 QAction* refineSearchAction = The::globalActions()->value("refine-search");
151 connect(refineSearchAction, SIGNAL(toggled(bool)),
152 sidebar, SLOT(toggleRefineSearch(bool)));
155 << The::globalActions()->value("webpage")
156 << The::globalActions()->value("pagelink")
157 << The::globalActions()->value("videolink")
158 << The::globalActions()->value("open-in-browser")
160 << The::globalActions()->value("snapshot")
162 << The::globalActions()->value("findVideoParts")
163 << The::globalActions()->value("skip")
164 << The::globalActions()->value("previous")
165 << The::globalActions()->value("stopafterthis")
166 << The::globalActions()->value("related-videos")
167 << The::globalActions()->value("refine-search")
168 << The::globalActions()->value("twitter")
169 << The::globalActions()->value("facebook")
170 << The::globalActions()->value("buffer")
171 << The::globalActions()->value("email");
173 #ifndef APP_PHONON_SEEK
174 QSlider *slider = MainWindow::instance()->getSlider();
175 connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
180 void MediaView::setMediaObject(Phonon::MediaObject *mediaObject) {
181 this->mediaObject = mediaObject;
182 Phonon::createPath(mediaObject, videoWidget);
183 connect(mediaObject, SIGNAL(finished()), SLOT(playbackFinished()));
184 connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
185 SLOT(stateChanged(Phonon::State, Phonon::State)));
186 connect(mediaObject, SIGNAL(aboutToFinish()), SLOT(aboutToFinish()));
190 SearchParams* MediaView::getSearchParams() {
191 VideoSource *videoSource = playlistModel->getVideoSource();
192 if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
193 YTSearch *search = qobject_cast<YTSearch *>(videoSource);
194 return search->getSearchParams();
199 void MediaView::search(SearchParams *searchParams) {
200 if (!searchParams->keywords().isEmpty()) {
201 if (searchParams->keywords().startsWith("http://") ||
202 searchParams->keywords().startsWith("https://")) {
203 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
204 if (!videoId.isEmpty()) {
205 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
206 singleVideoSource->setVideoId(videoId);
207 setVideoSource(singleVideoSource);
212 YTSearch *ytSearch = new YTSearch(searchParams, this);
213 ytSearch->setAsyncDetails(true);
214 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
215 setVideoSource(ytSearch);
218 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
222 #ifdef APP_ACTIVATION
227 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
230 int currentIndex = getHistoryIndex();
231 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
232 while (history.size() > currentIndex + 1) {
233 VideoSource *vs = history.takeLast();
235 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
240 history.append(videoSource);
244 if (history.size() > 1)
245 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
248 playlistModel->setVideoSource(videoSource);
250 sidebar->showPlaylist();
251 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
252 sidebar->hideSuggestions();
253 sidebar->getHeader()->updateInfo();
255 SearchParams *searchParams = getSearchParams();
256 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
257 playlistView->setClickableAuthors(!isChannel);
262 void MediaView::searchAgain() {
263 VideoSource *currentVideoSource = playlistModel->getVideoSource();
264 setVideoSource(currentVideoSource, false);
267 bool MediaView::canGoBack() {
268 return getHistoryIndex() > 0;
271 void MediaView::goBack() {
272 if (history.size() > 1) {
273 int currentIndex = getHistoryIndex();
274 if (currentIndex > 0) {
275 VideoSource *previousVideoSource = history.at(currentIndex - 1);
276 setVideoSource(previousVideoSource, false, true);
281 bool MediaView::canGoForward() {
282 int currentIndex = getHistoryIndex();
283 return currentIndex >= 0 && currentIndex < history.size() - 1;
286 void MediaView::goForward() {
287 if (canGoForward()) {
288 int currentIndex = getHistoryIndex();
289 VideoSource *nextVideoSource = history.at(currentIndex + 1);
290 setVideoSource(nextVideoSource, false);
294 int MediaView::getHistoryIndex() {
295 return history.lastIndexOf(playlistModel->getVideoSource());
298 void MediaView::appear() {
299 Video *currentVideo = playlistModel->activeVideo();
301 MainWindow::instance()->setWindowTitle(
302 currentVideo->title() + " - " + Constants::NAME);
305 // optimize window for 16:9 video
306 QTimer::singleShot(50, this, SLOT(maybeAdjustWindowSize()));
308 playlistView->setFocus();
311 void MediaView::disappear() {
315 void MediaView::handleError(const QString &message) {
316 qWarning() << __PRETTY_FUNCTION__ << message;
317 #ifdef APP_PHONON_SEEK
320 QTimer::singleShot(500, this, SLOT(startPlaying()));
325 void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/) {
326 if (pauseTime > 0 && (newState == Phonon::PlayingState || newState == Phonon::BufferingState)) {
327 mediaObject->seek(pauseTime);
330 if (newState == Phonon::PlayingState) {
331 videoAreaWidget->showVideo();
332 } else if (newState == Phonon::ErrorState) {
333 qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
334 if (mediaObject->errorType() == Phonon::FatalError)
335 handleError(mediaObject->errorString());
340 void MediaView::pause() {
342 switch( mediaObject->state() ) {
343 case Phonon::PlayingState:
344 mediaObject->pause();
348 if (pauseTimer.hasExpired(60000)) {
349 pauseTimer.invalidate();
350 connect(playlistModel->activeVideo(), SIGNAL(gotStreamUrl(QUrl)), SLOT(resumeWithNewStreamUrl(QUrl)));
351 playlistModel->activeVideo()->loadStreamUrl();
352 } else mediaObject->play();
358 QRegExp MediaView::wordRE(const QString &s) {
359 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
362 void MediaView::stop() {
365 while (!history.isEmpty()) {
366 VideoSource *videoSource = history.takeFirst();
367 if (!videoSource->parent()) delete videoSource;
370 playlistModel->abortSearch();
371 videoAreaWidget->clear();
372 videoAreaWidget->update();
374 playlistView->selectionModel()->clearSelection();
376 downloadItem->stop();
379 currentVideoSize = 0;
381 The::globalActions()->value("refine-search")->setChecked(false);
382 updateSubscriptionAction(0, false);
383 #ifdef APP_ACTIVATION
387 foreach (QAction *action, currentVideoActions)
388 action->setEnabled(false);
390 QAction *a = The::globalActions()->value("download");
391 a->setEnabled(false);
392 a->setVisible(false);
397 currentVideoId.clear();
399 #ifndef APP_PHONON_SEEK
400 QSlider *slider = MainWindow::instance()->getSlider();
401 slider->setEnabled(false);
404 Phonon::SeekSlider *slider = MainWindow::instance()->getSeekSlider();
408 if (snapshotSettings) {
409 delete snapshotSettings;
410 snapshotSettings = 0;
415 const QString & MediaView::getCurrentVideoId() {
416 return currentVideoId;
419 void MediaView::activeRowChanged(int row) {
428 downloadItem->stop();
431 currentVideoSize = 0;
434 Video *video = playlistModel->videoAt(row);
437 videoAreaWidget->showLoading(video);
439 connect(video, SIGNAL(gotStreamUrl(QUrl)),
440 SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
441 connect(video, SIGNAL(errorStreamUrl(QString)),
442 SLOT(skip()), Qt::UniqueConnection);
443 video->loadStreamUrl();
445 // video title in titlebar
446 MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
448 // ensure active item is visible
450 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
451 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
454 // enable/disable actions
455 The::globalActions()->value("download")->setEnabled(
456 DownloadManager::instance()->itemForVideo(video) == 0);
457 The::globalActions()->value("previous")->setEnabled(row > 0);
458 The::globalActions()->value("stopafterthis")->setEnabled(true);
459 The::globalActions()->value("related-videos")->setEnabled(true);
461 bool enableDownload = video->license() == Video::LicenseCC;
462 #ifdef APP_ACTIVATION
463 enableDownload = enableDownload || Activation::instance().isLegacy();
466 enableDownload = true;
468 QAction *a = The::globalActions()->value("download");
469 a->setEnabled(enableDownload);
470 a->setVisible(enableDownload);
472 updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
474 foreach (QAction *action, currentVideoActions)
475 action->setEnabled(true);
477 #ifndef APP_PHONON_SEEK
478 QSlider *slider = MainWindow::instance()->getSlider();
479 slider->setEnabled(false);
484 if (snapshotSettings) {
485 delete snapshotSettings;
486 snapshotSettings = 0;
487 MainWindow::instance()->adjustStatusBarVisibility();
491 // see you in gotStreamUrl...
494 void MediaView::gotStreamUrl(QUrl streamUrl) {
496 if (!streamUrl.isValid()) {
501 Video *video = static_cast<Video *>(sender());
503 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
506 video->disconnect(this);
508 currentVideoId = video->id();
510 #ifdef APP_PHONON_SEEK
511 mediaObject->setCurrentSource(streamUrl);
517 // ensure we always have videos ahead
518 playlistModel->searchNeeded();
520 // ensure active item is visible
521 int row = playlistModel->activeRow();
523 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
524 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
527 #ifdef APP_ACTIVATION
528 if (!Activation::instance().isActivated())
529 demoTimer->start(180000);
533 Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
536 ChannelAggregator::instance()->videoWatched(video);
539 void MediaView::downloadStatusChanged() {
540 // qDebug() << __PRETTY_FUNCTION__;
541 switch(downloadItem->status()) {
543 // qDebug() << "Downloading";
544 if (downloadItem->offset() == 0) startPlaying();
547 // qDebug() << "Seeking to" << downloadItem->offset();
548 mediaObject->seek(offsetToTime(downloadItem->offset()));
554 // qDebug() << "Starting";
557 // qDebug() << "Finished" << mediaObject->state();
558 #ifdef APP_PHONON_SEEK
559 MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
563 // qDebug() << "Failed";
567 // qDebug() << "Idle";
572 void MediaView::startPlaying() {
573 // qDebug() << __PRETTY_FUNCTION__;
580 if (downloadItem->offset() == 0) {
581 currentVideoSize = downloadItem->bytesTotal();
582 // qDebug() << "currentVideoSize" << currentVideoSize;
586 QString source = downloadItem->currentFilename();
587 qDebug() << "Playing" << source << QFile::exists(source);
589 mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
592 #ifdef APP_PHONON_SEEK
593 MainWindow::instance()->getSeekSlider()->setEnabled(false);
595 QSlider *slider = MainWindow::instance()->getSlider();
596 slider->setEnabled(true);
600 void MediaView::itemActivated(const QModelIndex &index) {
601 if (playlistModel->rowExists(index.row())) {
603 // if it's the current video, just rewind and play
604 Video *activeVideo = playlistModel->activeVideo();
605 Video *video = playlistModel->videoAt(index.row());
606 if (activeVideo && video && activeVideo == video) {
607 // mediaObject->seek(0);
612 } else playlistModel->setActiveRow(index.row());
614 // the user doubleclicked on the "Search More" item
616 playlistModel->searchMore();
617 playlistView->selectionModel()->clearSelection();
621 void MediaView::skipVideo() {
622 // skippedVideo is useful for DELAYED skip operations
623 // in order to be sure that we're skipping the video we wanted
624 // and not another one
626 if (playlistModel->activeVideo() != skippedVideo) {
627 qDebug() << "Skip of video canceled";
630 int nextRow = playlistModel->rowForVideo(skippedVideo);
632 if (nextRow == -1) return;
633 playlistModel->setActiveRow(nextRow);
637 void MediaView::skip() {
638 int nextRow = playlistModel->nextRow();
639 if (nextRow == -1) return;
640 playlistModel->setActiveRow(nextRow);
643 void MediaView::skipBackward() {
644 int prevRow = playlistModel->previousRow();
645 if (prevRow == -1) return;
646 playlistModel->setActiveRow(prevRow);
649 void MediaView::aboutToFinish() {
651 qint64 currentTime = mediaObject->currentTime();
652 qint64 totalTime = mediaObject->totalTime();
653 // qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
654 if (totalTime < 1 || currentTime + 10000 < totalTime) {
655 // QTimer::singleShot(500, this, SLOT(playbackResume()));
656 mediaObject->seek(currentTime);
662 void MediaView::playbackFinished() {
666 const qint64 totalTime = mediaObject->totalTime();
667 const qint64 currentTime = mediaObject->currentTime();
668 // qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
669 // add 10 secs for imprecise Phonon backends (VLC, Xine)
670 if (currentTime > 0 && currentTime + 10000 < totalTime) {
671 // mediaObject->seek(currentTime);
672 QTimer::singleShot(500, this, SLOT(playbackResume()));
674 QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
675 if (stopAfterThisAction->isChecked()) {
676 stopAfterThisAction->setChecked(false);
682 void MediaView::playbackResume() {
685 const qint64 currentTime = mediaObject->currentTime();
686 // qDebug() << __PRETTY_FUNCTION__ << currentTime;
688 mediaObject->seek(currentTime);
693 void MediaView::openWebPage() {
694 Video* video = playlistModel->activeVideo();
697 mediaObject->pause();
699 QString url = video->webpage() + QLatin1String("&t=") + QString::number(mediaObject->currentTime() / 1000);
700 QDesktopServices::openUrl(url);
703 void MediaView::copyWebPage() {
704 Video* video = playlistModel->activeVideo();
706 QString address = video->webpage();
707 QApplication::clipboard()->setText(address);
708 QString message = tr("You can now paste the YouTube link into another application");
709 MainWindow::instance()->showMessage(message);
712 void MediaView::copyVideoLink() {
713 Video* video = playlistModel->activeVideo();
715 QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
716 QString message = tr("You can now paste the video stream URL into another application")
717 + ". " + tr("The link will be valid only for a limited time.");
718 MainWindow::instance()->showMessage(message);
721 void MediaView::openInBrowser() {
722 Video* video = playlistModel->activeVideo();
725 mediaObject->pause();
727 QDesktopServices::openUrl(video->getStreamUrl());
730 void MediaView::removeSelected() {
731 if (!playlistView->selectionModel()->hasSelection()) return;
732 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
733 playlistModel->removeIndexes(indexes);
736 void MediaView::selectVideos(QList<Video*> videos) {
737 foreach (Video *video, videos) {
738 QModelIndex index = playlistModel->indexForVideo(video);
739 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
740 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
744 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
745 const QItemSelection & /*deselected*/) {
746 const bool gotSelection = playlistView->selectionModel()->hasSelection();
747 The::globalActions()->value("remove")->setEnabled(gotSelection);
748 The::globalActions()->value("moveUp")->setEnabled(gotSelection);
749 The::globalActions()->value("moveDown")->setEnabled(gotSelection);
752 void MediaView::moveUpSelected() {
753 if (!playlistView->selectionModel()->hasSelection()) return;
755 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
756 qStableSort(indexes.begin(), indexes.end());
757 playlistModel->move(indexes, true);
759 // set current index after row moves to something more intuitive
760 int row = indexes.first().row();
761 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>1?row:1),
762 QItemSelectionModel::NoUpdate);
765 void MediaView::moveDownSelected() {
766 if (!playlistView->selectionModel()->hasSelection()) return;
768 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
769 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
770 playlistModel->move(indexes, false);
772 // set current index after row moves to something more intuitive
773 // (respect 1 static item on bottom)
774 int row = indexes.first().row()+1, max = playlistModel->rowCount() - 2;
775 playlistView->selectionModel()->setCurrentIndex(
776 playlistModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
779 void MediaView::setPlaylistVisible(bool visible) {
780 if (splitter->widget(0)->isVisible() == visible) return;
781 splitter->widget(0)->setVisible(visible);
782 playlistView->setFocus();
785 bool MediaView::isPlaylistVisible() {
786 return splitter->widget(0)->isVisible();
789 void MediaView::saveSplitterState() {
791 settings.setValue("splitter", splitter->saveState());
794 #ifdef APP_ACTIVATION
796 static QPushButton *continueButton;
798 void MediaView::demoMessage() {
800 if (mediaObject->state() != Phonon::PlayingState) return;
801 mediaObject->pause();
804 QMessageBox msgBox(this);
805 msgBox.setIconPixmap(IconUtils::pixmap(":/images/app.png").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
806 msgBox.setText(tr("This is just the demo version of %1.").arg(Constants::NAME));
807 msgBox.setInformativeText(tr("It allows you to test the application and see if it works for you."));
808 msgBox.setModal(true);
809 // make it a "sheet" on the Mac
810 msgBox.setWindowModality(Qt::WindowModal);
812 continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
813 continueButton->setEnabled(false);
814 QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
816 QTimeLine *timeLine = new QTimeLine(6000, this);
817 timeLine->setCurveShape(QTimeLine::LinearCurve);
818 timeLine->setFrameRange(5, 0);
819 connect(timeLine, SIGNAL(frameChanged(int)), SLOT(updateContinueButton(int)));
824 if (msgBox.clickedButton() == buyButton) {
825 MainWindow::instance()->showActivationView();
830 demoTimer->start(600000);
837 void MediaView::updateContinueButton(int value) {
839 continueButton->setText(tr("Continue"));
840 continueButton->setEnabled(true);
842 continueButton->setText(QString::number(value));
848 void MediaView::downloadVideo() {
849 Video* video = playlistModel->activeVideo();
851 DownloadManager::instance()->addItem(video);
852 MainWindow::instance()->showActionInStatusBar(The::globalActions()->value("downloads"), true);
853 QString message = tr("Downloading %1").arg(video->title());
854 MainWindow::instance()->showMessage(message);
858 void MediaView::snapshot() {
859 qint64 currentTime = mediaObject->currentTime() / 1000;
861 QImage image = videoWidget->snapshot();
862 if (image.isNull()) {
863 qWarning() << "Null snapshot";
867 // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
868 QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
869 videoAreaWidget->showSnapshotPreview(pixmap);
871 Video* video = playlistModel->activeVideo();
874 QString location = SnapshotSettings::getCurrentLocation();
876 if (!dir.exists()) dir.mkpath(location);
877 QString basename = video->title();
878 QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
879 basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
880 basename = DataUtils::stringToFilename(basename);
881 QString filename = location + "/" + basename + ".png";
882 qDebug() << filename;
883 image.save(filename, "PNG");
885 if (snapshotSettings) delete snapshotSettings;
886 snapshotSettings = new SnapshotSettings(videoWidget);
887 snapshotSettings->setSnapshot(pixmap, filename);
888 QStatusBar *statusBar = MainWindow::instance()->statusBar();
890 Extra::fadeInWidget(statusBar, statusBar);
892 statusBar->insertPermanentWidget(0, snapshotSettings);
893 snapshotSettings->show();
894 MainWindow::instance()->setStatusBarVisibility(true);
898 void MediaView::fullscreen() {
899 videoAreaWidget->setParent(0);
900 videoAreaWidget->showFullScreen();
903 void MediaView::startDownloading() {
904 Video *video = playlistModel->activeVideo();
906 Video *videoCopy = video->clone();
908 downloadItem->stop();
911 QString tempFile = Temporary::filename();
912 downloadItem = new DownloadItem(videoCopy, video->getStreamUrl(), tempFile, this);
913 connect(downloadItem, SIGNAL(statusChanged()),
914 SLOT(downloadStatusChanged()), Qt::UniqueConnection);
915 connect(downloadItem, SIGNAL(bufferProgress(int)),
916 loadingWidget, SLOT(bufferStatus(int)), Qt::UniqueConnection);
917 // connect(downloadItem, SIGNAL(finished()), SLOT(itemFinished()));
918 connect(video, SIGNAL(errorStreamUrl(QString)),
919 SLOT(handleError(QString)), Qt::UniqueConnection);
920 connect(downloadItem, SIGNAL(error(QString)),
921 SLOT(handleError(QString)), Qt::UniqueConnection);
922 downloadItem->start();
925 void MediaView::resumeWithNewStreamUrl(const QUrl &streamUrl) {
926 pauseTime = mediaObject->currentTime();
927 mediaObject->setCurrentSource(streamUrl);
930 Video *video = static_cast<Video *>(sender());
932 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
935 video->disconnect(this);
938 void MediaView::maybeAdjustWindowSize() {
940 if (settings.value("adjustWindowSize", true).toBool())
944 void MediaView::sliderMoved(int value) {
947 #ifndef APP_PHONON_SEEK
949 if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
952 QSlider *slider = MainWindow::instance()->getSlider();
953 if (slider->isSliderDown()) return;
955 qint64 offset = (currentVideoSize * value) / slider->maximum();
957 bool needsDownload = downloadItem->needsDownload(offset);
959 if (downloadItem->isBuffered(offset)) {
960 qint64 realOffset = downloadItem->blankAtOffset(offset);
961 if (offset < currentVideoSize)
962 downloadItem->seekTo(realOffset, false);
963 mediaObject->seek(offsetToTime(offset));
965 mediaObject->pause();
966 downloadItem->seekTo(offset);
969 // qDebug() << "simple seek";
970 mediaObject->seek(offsetToTime(offset));
976 qint64 MediaView::offsetToTime(qint64 offset) {
978 const qint64 totalTime = mediaObject->totalTime();
979 return ((offset * totalTime) / currentVideoSize);
983 void MediaView::findVideoParts() {
986 Video* video = playlistModel->activeVideo();
989 QString query = video->title();
991 static QString optionalSpace = "\\s*";
992 static QString staticCounterSeparators = "[\\/\\-]";
993 QString counterSeparators = "( of | " +
994 tr("of", "Used in video parts, as in '2 of 3'") +
995 " |" + staticCounterSeparators + ")";
997 // numbers from 1 to 15
998 static QString counterNumber = "([1-9]|1[0-5])";
1000 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
1001 query.remove(QRegExp(counterNumber + optionalSpace +
1002 counterSeparators + optionalSpace + counterNumber));
1003 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
1004 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
1005 query.remove(wordRE("part" + optionalSpace + counterNumber));
1006 query.remove(wordRE("episode" + optionalSpace + counterNumber));
1007 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
1008 optionalSpace + counterNumber));
1009 query.remove(wordRE(tr("episode",
1010 "This is for video parts, as in 'Cool series - episode 1'") +
1011 optionalSpace + counterNumber));
1012 query.remove(QRegExp("[\\(\\)\\[\\]]"));
1014 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
1016 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
1017 Qt::CaseInsensitive);
1018 // bool numberAsWords = englishNumberRE.exactMatch(query);
1019 query.remove(englishNumberRE);
1021 QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
1022 Qt::CaseInsensitive);
1023 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
1024 query.remove(localizedNumberRE);
1026 SearchParams *searchParams = new SearchParams();
1027 searchParams->setTransient(true);
1028 searchParams->setKeywords(query);
1029 searchParams->setChannelId(video->channelId());
1032 if (!numberAsWords) {
1033 qDebug() << "We don't have number as words";
1034 // searchParams->setSortBy(SearchParams::SortByNewest);
1035 // TODO searchParams->setReverseOrder(true);
1036 // TODO searchParams->setMax(50);
1040 search(searchParams);
1044 void MediaView::relatedVideos() {
1045 Video* video = playlistModel->activeVideo();
1047 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
1048 singleVideoSource->setVideo(video->clone());
1049 singleVideoSource->setAsyncDetails(true);
1050 setVideoSource(singleVideoSource);
1051 The::globalActions()->value("related-videos")->setEnabled(false);
1054 void MediaView::shareViaTwitter() {
1055 Video* video = playlistModel->activeVideo();
1057 QUrl url("https://twitter.com/intent/tweet");
1059 QUrlQueryHelper urlHelper(url);
1060 urlHelper.addQueryItem("via", "minitubeapp");
1061 urlHelper.addQueryItem("text", video->title());
1062 urlHelper.addQueryItem("url", video->webpage());
1064 QDesktopServices::openUrl(url);
1067 void MediaView::shareViaFacebook() {
1068 Video* video = playlistModel->activeVideo();
1070 QUrl url("https://www.facebook.com/sharer.php");
1072 QUrlQueryHelper urlHelper(url);
1073 urlHelper.addQueryItem("t", video->title());
1074 urlHelper.addQueryItem("u", video->webpage());
1076 QDesktopServices::openUrl(url);
1079 void MediaView::shareViaBuffer() {
1080 Video* video = playlistModel->activeVideo();
1082 QUrl url("http://bufferapp.com/add");
1084 QUrlQueryHelper urlHelper(url);
1085 urlHelper.addQueryItem("via", "minitubeapp");
1086 urlHelper.addQueryItem("text", video->title());
1087 urlHelper.addQueryItem("url", video->webpage());
1088 urlHelper.addQueryItem("picture", video->thumbnailUrl());
1090 QDesktopServices::openUrl(url);
1093 void MediaView::shareViaEmail() {
1094 Video* video = playlistModel->activeVideo();
1096 QUrl url("mailto:");
1098 QUrlQueryHelper urlHelper(url);
1099 urlHelper.addQueryItem("subject", video->title());
1100 const QString body = video->title() + "\n" +
1101 video->webpage() + "\n\n" +
1102 tr("Sent from %1").arg(Constants::NAME) + "\n" +
1104 urlHelper.addQueryItem("body", body);
1106 QDesktopServices::openUrl(url);
1109 void MediaView::authorPushed(QModelIndex index) {
1110 Video* video = playlistModel->videoAt(index.row());
1113 QString channelId = video->channelId();
1114 // if (channelId.isEmpty()) channelId = video->channelTitle();
1115 if (channelId.isEmpty()) return;
1117 SearchParams *searchParams = new SearchParams();
1118 searchParams->setChannelId(channelId);
1119 searchParams->setSortBy(SearchParams::SortByNewest);
1122 search(searchParams);
1125 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1126 QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1128 QString subscribeTip;
1129 QString subscribeText;
1131 subscribeText = subscribeAction->property("originalText").toString();
1132 subscribeAction->setEnabled(false);
1133 } else if (subscribed) {
1134 subscribeText = tr("Unsubscribe from %1").arg(video->channelTitle());
1135 subscribeTip = subscribeText;
1136 subscribeAction->setEnabled(true);
1138 subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
1139 subscribeTip = subscribeText;
1140 subscribeAction->setEnabled(true);
1142 subscribeAction->setText(subscribeText);
1143 subscribeAction->setStatusTip(subscribeTip);
1147 static QIcon tintedIcon;
1148 if (tintedIcon.isNull()) {
1150 sizes << QSize(16, 16);
1151 tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1153 subscribeAction->setIcon(tintedIcon);
1155 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1158 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1161 IconUtils::setupAction(subscribeAction);
1164 void MediaView::toggleSubscription() {
1165 Video *video = playlistModel->activeVideo();
1167 QString userId = video->channelId();
1168 if (userId.isEmpty()) return;
1169 bool subscribed = YTChannel::isSubscribed(userId);
1171 YTChannel::unsubscribe(userId);
1172 MainWindow::instance()->showMessage(tr("Unsubscribed from %1").arg(video->channelTitle()));
1174 YTChannel::subscribe(userId);
1175 MainWindow::instance()->showMessage(tr("Subscribed to %1").arg(video->channelTitle()));
1177 updateSubscriptionAction(video, !subscribed);
1180 void MediaView::adjustWindowSize() {
1181 if (!MainWindow::instance()->isMaximized() && !MainWindow::instance()->isFullScreen()) {
1182 const double ratio = 16. / 9.;
1183 const int w = videoAreaWidget->width();
1184 const int h = videoAreaWidget->height();
1185 const double currentVideoRatio = (double)w / (double)h;
1186 if (currentVideoRatio != ratio) {
1187 if (false && currentVideoRatio > ratio) {
1188 // we have vertical black bars
1189 int newWidth = (MainWindow::instance()->width() - w) + (h * ratio);
1190 MainWindow::instance()->resize(newWidth, MainWindow::instance()->height());
1192 // horizontal black bars
1193 int newHeight = (MainWindow::instance()->height() - h) + (w / ratio);
1194 MainWindow::instance()->resize(MainWindow::instance()->width(), newHeight);