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) : QWidget(parent)
75 void MediaView::initialize() {
76 QBoxLayout *layout = new QVBoxLayout(this);
79 splitter = new MiniSplitter();
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 &)));
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);
94 connect(playlistView->selectionModel(),
95 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
96 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
98 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
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);
110 videoAreaWidget = new VideoAreaWidget(this);
111 // videoAreaWidget->setMinimumSize(320,240);
114 videoWidget = new Phonon::VideoWidget(this);
115 videoAreaWidget->setVideoWidget(videoWidget);
117 videoAreaWidget->setListModel(playlistModel);
119 loadingWidget = new LoadingWidget(this);
120 videoAreaWidget->setLoadingWidget(loadingWidget);
122 splitter->addWidget(videoAreaWidget);
124 splitter->setStretchFactor(0, 0);
125 splitter->setStretchFactor(1, 8);
127 // restore splitter state
129 splitter->restoreState(settings.value("splitter").toByteArray());
130 splitter->setChildrenCollapsible(false);
131 connect(splitter, SIGNAL(splitterMoved(int,int)), SLOT(maybeAdjustWindowSize()));
133 layout->addWidget(splitter);
135 errorTimer = new QTimer(this);
136 errorTimer->setSingleShot(true);
137 errorTimer->setInterval(3000);
138 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
140 #ifdef APP_ACTIVATION
141 demoTimer = new QTimer(this);
142 demoTimer->setSingleShot(true);
143 connect(demoTimer, SIGNAL(timeout()), SLOT(demoMessage()));
146 connect(videoAreaWidget, SIGNAL(doubleClicked()),
147 The::globalActions()->value("fullscreen"), SLOT(trigger()));
149 QAction* refineSearchAction = The::globalActions()->value("refine-search");
150 connect(refineSearchAction, SIGNAL(toggled(bool)),
151 sidebar, SLOT(toggleRefineSearch(bool)));
154 << The::globalActions()->value("webpage")
155 << The::globalActions()->value("pagelink")
156 << The::globalActions()->value("videolink")
157 << The::globalActions()->value("open-in-browser")
159 << The::globalActions()->value("snapshot")
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");
172 #ifndef APP_PHONON_SEEK
173 QSlider *slider = MainWindow::instance()->getSlider();
174 connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
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()));
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();
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);
211 YTSearch *ytSearch = new YTSearch(searchParams, this);
212 ytSearch->setAsyncDetails(true);
213 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
214 setVideoSource(ytSearch);
217 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
221 #ifdef APP_ACTIVATION
226 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
229 int currentIndex = getHistoryIndex();
230 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
231 while (history.size() > currentIndex + 1) {
232 VideoSource *vs = history.takeLast();
234 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
239 history.append(videoSource);
243 if (history.size() > 1)
244 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
247 playlistModel->setVideoSource(videoSource);
249 sidebar->showPlaylist();
250 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
251 sidebar->hideSuggestions();
252 sidebar->getHeader()->updateInfo();
254 SearchParams *searchParams = getSearchParams();
255 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
256 playlistView->setClickableAuthors(!isChannel);
261 void MediaView::searchAgain() {
262 VideoSource *currentVideoSource = playlistModel->getVideoSource();
263 setVideoSource(currentVideoSource, false);
266 bool MediaView::canGoBack() {
267 return getHistoryIndex() > 0;
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);
280 bool MediaView::canGoForward() {
281 int currentIndex = getHistoryIndex();
282 return currentIndex >= 0 && currentIndex < history.size() - 1;
285 void MediaView::goForward() {
286 if (canGoForward()) {
287 int currentIndex = getHistoryIndex();
288 VideoSource *nextVideoSource = history.at(currentIndex + 1);
289 setVideoSource(nextVideoSource, false);
293 int MediaView::getHistoryIndex() {
294 return history.lastIndexOf(playlistModel->getVideoSource());
297 void MediaView::appear() {
298 Video *currentVideo = playlistModel->activeVideo();
300 MainWindow::instance()->setWindowTitle(
301 currentVideo->title() + " - " + Constants::NAME);
304 // optimize window for 16:9 video
305 QTimer::singleShot(50, this, SLOT(maybeAdjustWindowSize()));
307 playlistView->setFocus();
310 void MediaView::disappear() {
314 void MediaView::handleError(const QString &message) {
315 qWarning() << __PRETTY_FUNCTION__ << message;
316 #ifdef APP_PHONON_SEEK
319 QTimer::singleShot(500, this, SLOT(startPlaying()));
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());
335 void MediaView::pause() {
337 switch( mediaObject->state() ) {
338 case Phonon::PlayingState:
339 mediaObject->pause();
348 QRegExp MediaView::wordRE(const QString &s) {
349 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
352 void MediaView::stop() {
355 while (!history.isEmpty()) {
356 VideoSource *videoSource = history.takeFirst();
357 if (!videoSource->parent()) delete videoSource;
360 playlistModel->abortSearch();
361 videoAreaWidget->clear();
362 videoAreaWidget->update();
364 playlistView->selectionModel()->clearSelection();
366 downloadItem->stop();
369 currentVideoSize = 0;
371 The::globalActions()->value("refine-search")->setChecked(false);
372 updateSubscriptionAction(0, false);
373 #ifdef APP_ACTIVATION
377 foreach (QAction *action, currentVideoActions)
378 action->setEnabled(false);
380 QAction *a = The::globalActions()->value("download");
381 a->setEnabled(false);
382 a->setVisible(false);
387 currentVideoId.clear();
389 #ifndef APP_PHONON_SEEK
390 QSlider *slider = MainWindow::instance()->getSlider();
391 slider->setEnabled(false);
395 if (snapshotSettings) {
396 delete snapshotSettings;
397 snapshotSettings = 0;
401 const QString & MediaView::getCurrentVideoId() {
402 return currentVideoId;
405 void MediaView::activeRowChanged(int row) {
414 downloadItem->stop();
417 currentVideoSize = 0;
420 Video *video = playlistModel->videoAt(row);
423 videoAreaWidget->showLoading(video);
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();
431 // video title in titlebar
432 MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
434 // ensure active item is visible
436 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
437 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
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);
447 bool enableDownload = video->license() == Video::LicenseCC;
448 #ifdef APP_ACTIVATION
449 enableDownload = enableDownload || Activation::instance().isLegacy();
452 enableDownload = true;
454 QAction *a = The::globalActions()->value("download");
455 a->setEnabled(enableDownload);
456 a->setVisible(enableDownload);
458 updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
460 foreach (QAction *action, currentVideoActions)
461 action->setEnabled(true);
463 #ifndef APP_PHONON_SEEK
464 QSlider *slider = MainWindow::instance()->getSlider();
465 slider->setEnabled(false);
469 if (snapshotSettings) {
470 delete snapshotSettings;
471 snapshotSettings = 0;
472 MainWindow::instance()->adjustStatusBarVisibility();
475 // see you in gotStreamUrl...
478 void MediaView::gotStreamUrl(QUrl streamUrl) {
480 if (!streamUrl.isValid()) {
485 Video *video = static_cast<Video *>(sender());
487 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
490 video->disconnect(this);
492 currentVideoId = video->id();
494 #ifdef APP_PHONON_SEEK
495 mediaObject->setCurrentSource(streamUrl);
501 // ensure we always have videos ahead
502 playlistModel->searchNeeded();
504 // ensure active item is visible
505 int row = playlistModel->activeRow();
507 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
508 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
511 #ifdef APP_ACTIVATION
512 if (!Activation::instance().isActivated())
513 demoTimer->start(180000);
517 Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
520 ChannelAggregator::instance()->videoWatched(video);
523 void MediaView::downloadStatusChanged() {
524 // qDebug() << __PRETTY_FUNCTION__;
525 switch(downloadItem->status()) {
527 // qDebug() << "Downloading";
528 if (downloadItem->offset() == 0) startPlaying();
531 // qDebug() << "Seeking to" << downloadItem->offset();
532 mediaObject->seek(offsetToTime(downloadItem->offset()));
538 // qDebug() << "Starting";
541 // qDebug() << "Finished" << mediaObject->state();
542 #ifdef APP_PHONON_SEEK
543 MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
547 // qDebug() << "Failed";
551 // qDebug() << "Idle";
556 void MediaView::startPlaying() {
557 // qDebug() << __PRETTY_FUNCTION__;
564 if (downloadItem->offset() == 0) {
565 currentVideoSize = downloadItem->bytesTotal();
566 // qDebug() << "currentVideoSize" << currentVideoSize;
570 QString source = downloadItem->currentFilename();
571 qDebug() << "Playing" << source << QFile::exists(source);
573 mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
576 #ifdef APP_PHONON_SEEK
577 MainWindow::instance()->getSeekSlider()->setEnabled(false);
579 QSlider *slider = MainWindow::instance()->getSlider();
580 slider->setEnabled(true);
584 void MediaView::itemActivated(const QModelIndex &index) {
585 if (playlistModel->rowExists(index.row())) {
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);
596 } else playlistModel->setActiveRow(index.row());
598 // the user doubleclicked on the "Search More" item
600 playlistModel->searchMore();
601 playlistView->selectionModel()->clearSelection();
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
610 if (playlistModel->activeVideo() != skippedVideo) {
611 qDebug() << "Skip of video canceled";
614 int nextRow = playlistModel->rowForVideo(skippedVideo);
616 if (nextRow == -1) return;
617 playlistModel->setActiveRow(nextRow);
621 void MediaView::skip() {
622 int nextRow = playlistModel->nextRow();
623 if (nextRow == -1) return;
624 playlistModel->setActiveRow(nextRow);
627 void MediaView::skipBackward() {
628 int prevRow = playlistModel->previousRow();
629 if (prevRow == -1) return;
630 playlistModel->setActiveRow(prevRow);
633 void MediaView::aboutToFinish() {
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);
646 void MediaView::playbackFinished() {
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()));
658 QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
659 if (stopAfterThisAction->isChecked()) {
660 stopAfterThisAction->setChecked(false);
666 void MediaView::playbackResume() {
669 const qint64 currentTime = mediaObject->currentTime();
670 qDebug() << __PRETTY_FUNCTION__ << currentTime;
672 mediaObject->seek(currentTime);
677 void MediaView::openWebPage() {
678 Video* video = playlistModel->activeVideo();
681 mediaObject->pause();
683 QDesktopServices::openUrl(video->webpage());
686 void MediaView::copyWebPage() {
687 Video* video = playlistModel->activeVideo();
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);
695 void MediaView::copyVideoLink() {
696 Video* video = playlistModel->activeVideo();
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);
704 void MediaView::openInBrowser() {
705 Video* video = playlistModel->activeVideo();
708 mediaObject->pause();
710 QDesktopServices::openUrl(video->getStreamUrl());
713 void MediaView::removeSelected() {
714 if (!playlistView->selectionModel()->hasSelection()) return;
715 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
716 playlistModel->removeIndexes(indexes);
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);
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);
735 void MediaView::moveUpSelected() {
736 if (!playlistView->selectionModel()->hasSelection()) return;
738 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
739 qStableSort(indexes.begin(), indexes.end());
740 playlistModel->move(indexes, true);
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);
748 void MediaView::moveDownSelected() {
749 if (!playlistView->selectionModel()->hasSelection()) return;
751 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
752 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
753 playlistModel->move(indexes, false);
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);
762 void MediaView::setPlaylistVisible(bool visible) {
763 if (splitter->widget(0)->isVisible() == visible) return;
764 splitter->widget(0)->setVisible(visible);
765 playlistView->setFocus();
768 bool MediaView::isPlaylistVisible() {
769 return splitter->widget(0)->isVisible();
772 void MediaView::saveSplitterState() {
774 settings.setValue("splitter", splitter->saveState());
777 #ifdef APP_ACTIVATION
779 static QPushButton *continueButton;
781 void MediaView::demoMessage() {
783 if (mediaObject->state() != Phonon::PlayingState) return;
784 mediaObject->pause();
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);
795 continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
796 continueButton->setEnabled(false);
797 QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
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)));
807 if (msgBox.clickedButton() == buyButton) {
808 MainWindow::instance()->showActivationView();
813 demoTimer->start(600000);
820 void MediaView::updateContinueButton(int value) {
822 continueButton->setText(tr("Continue"));
823 continueButton->setEnabled(true);
825 continueButton->setText(QString::number(value));
831 void MediaView::downloadVideo() {
832 Video* video = playlistModel->activeVideo();
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);
841 void MediaView::snapshot() {
842 qint64 currentTime = mediaObject->currentTime() / 1000;
844 QImage image = videoWidget->snapshot();
845 if (image.isNull()) {
846 qWarning() << "Null snapshot";
850 // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
851 QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
852 videoAreaWidget->showSnapshotPreview(pixmap);
854 Video* video = playlistModel->activeVideo();
857 QString location = SnapshotSettings::getCurrentLocation();
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");
868 if (snapshotSettings) delete snapshotSettings;
869 snapshotSettings = new SnapshotSettings(videoWidget);
870 snapshotSettings->setSnapshot(pixmap, filename);
871 QStatusBar *statusBar = MainWindow::instance()->statusBar();
873 Extra::fadeInWidget(statusBar, statusBar);
875 statusBar->insertPermanentWidget(0, snapshotSettings);
876 snapshotSettings->show();
877 MainWindow::instance()->setStatusBarVisibility(true);
881 void MediaView::fullscreen() {
882 videoAreaWidget->setParent(0);
883 videoAreaWidget->showFullScreen();
886 void MediaView::startDownloading() {
887 Video *video = playlistModel->activeVideo();
889 Video *videoCopy = video->clone();
891 downloadItem->stop();
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();
908 void MediaView::maybeAdjustWindowSize() {
910 if (settings.value("adjustWindowSize", true).toBool())
914 void MediaView::sliderMoved(int value) {
917 #ifndef APP_PHONON_SEEK
919 if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
922 QSlider *slider = MainWindow::instance()->getSlider();
923 if (slider->isSliderDown()) return;
925 qint64 offset = (currentVideoSize * value) / slider->maximum();
927 bool needsDownload = downloadItem->needsDownload(offset);
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));
935 mediaObject->pause();
936 downloadItem->seekTo(offset);
939 // qDebug() << "simple seek";
940 mediaObject->seek(offsetToTime(offset));
946 qint64 MediaView::offsetToTime(qint64 offset) {
948 const qint64 totalTime = mediaObject->totalTime();
949 return ((offset * totalTime) / currentVideoSize);
953 void MediaView::findVideoParts() {
956 Video* video = playlistModel->activeVideo();
959 QString query = video->title();
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 + ")";
967 // numbers from 1 to 15
968 static QString counterNumber = "([1-9]|1[0-5])";
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("[\\(\\)\\[\\]]"));
984 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
986 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
987 Qt::CaseInsensitive);
988 // bool numberAsWords = englishNumberRE.exactMatch(query);
989 query.remove(englishNumberRE);
991 QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
992 Qt::CaseInsensitive);
993 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
994 query.remove(localizedNumberRE);
996 SearchParams *searchParams = new SearchParams();
997 searchParams->setTransient(true);
998 searchParams->setKeywords(query);
999 searchParams->setChannelId(video->channelId());
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);
1010 search(searchParams);
1014 void MediaView::relatedVideos() {
1015 Video* video = playlistModel->activeVideo();
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);
1024 void MediaView::shareViaTwitter() {
1025 Video* video = playlistModel->activeVideo();
1027 QUrl url("https://twitter.com/intent/tweet");
1029 QUrlQueryHelper urlHelper(url);
1030 urlHelper.addQueryItem("via", "minitubeapp");
1031 urlHelper.addQueryItem("text", video->title());
1032 urlHelper.addQueryItem("url", video->webpage());
1034 QDesktopServices::openUrl(url);
1037 void MediaView::shareViaFacebook() {
1038 Video* video = playlistModel->activeVideo();
1040 QUrl url("https://www.facebook.com/sharer.php");
1042 QUrlQueryHelper urlHelper(url);
1043 urlHelper.addQueryItem("t", video->title());
1044 urlHelper.addQueryItem("u", video->webpage());
1046 QDesktopServices::openUrl(url);
1049 void MediaView::shareViaBuffer() {
1050 Video* video = playlistModel->activeVideo();
1052 QUrl url("http://bufferapp.com/add");
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());
1060 QDesktopServices::openUrl(url);
1063 void MediaView::shareViaEmail() {
1064 Video* video = playlistModel->activeVideo();
1066 QUrl url("mailto:");
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" +
1074 urlHelper.addQueryItem("body", body);
1076 QDesktopServices::openUrl(url);
1079 void MediaView::authorPushed(QModelIndex index) {
1080 Video* video = playlistModel->videoAt(index.row());
1083 QString channelId = video->channelId();
1084 // if (channelId.isEmpty()) channelId = video->channelTitle();
1085 if (channelId.isEmpty()) return;
1087 SearchParams *searchParams = new SearchParams();
1088 searchParams->setChannelId(channelId);
1089 searchParams->setSortBy(SearchParams::SortByNewest);
1092 search(searchParams);
1095 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1096 QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1098 QString subscribeTip;
1099 QString subscribeText;
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);
1108 subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
1109 subscribeTip = subscribeText;
1110 subscribeAction->setEnabled(true);
1112 subscribeAction->setText(subscribeText);
1113 subscribeAction->setStatusTip(subscribeTip);
1117 static QIcon tintedIcon;
1118 if (tintedIcon.isNull()) {
1120 sizes << QSize(16, 16);
1121 tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1123 subscribeAction->setIcon(tintedIcon);
1125 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1128 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1131 IconUtils::setupAction(subscribeAction);
1134 void MediaView::toggleSubscription() {
1135 Video *video = playlistModel->activeVideo();
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);
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());
1157 // horizontal black bars
1158 int newHeight = (MainWindow::instance()->height() - h) + (w / ratio);
1159 MainWindow::instance()->resize(MainWindow::instance()->width(), newHeight);