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"
55 NetworkAccess* http();
56 QHash<QString, QAction*>* globalActions();
57 QHash<QString, QMenu*>* globalMenus();
58 QNetworkAccessManager* networkAccessManager();
61 MediaView* MediaView::instance() {
62 static MediaView *i = new MediaView();
66 MediaView::MediaView(QWidget *parent) : QWidget(parent),
73 void MediaView::initialize() {
74 QBoxLayout *layout = new QVBoxLayout(this);
77 splitter = new MiniSplitter();
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 &)));
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);
92 connect(playlistView->selectionModel(),
93 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
94 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
96 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
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);
108 videoAreaWidget = new VideoAreaWidget(this);
109 // videoAreaWidget->setMinimumSize(320,240);
112 videoWidget = new Phonon::VideoWidget(this);
113 videoAreaWidget->setVideoWidget(videoWidget);
115 videoAreaWidget->setListModel(playlistModel);
117 loadingWidget = new LoadingWidget(this);
118 videoAreaWidget->setLoadingWidget(loadingWidget);
120 splitter->addWidget(videoAreaWidget);
122 splitter->setStretchFactor(0, 0);
123 splitter->setStretchFactor(1, 8);
125 // restore splitter state
127 splitter->restoreState(settings.value("splitter").toByteArray());
128 splitter->setChildrenCollapsible(false);
130 layout->addWidget(splitter);
132 errorTimer = new QTimer(this);
133 errorTimer->setSingleShot(true);
134 errorTimer->setInterval(3000);
135 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
137 #ifdef APP_ACTIVATION
138 demoTimer = new QTimer(this);
139 demoTimer->setSingleShot(true);
140 connect(demoTimer, SIGNAL(timeout()), SLOT(demoMessage()));
143 connect(videoAreaWidget, SIGNAL(doubleClicked()),
144 The::globalActions()->value("fullscreen"), SLOT(trigger()));
146 QAction* refineSearchAction = The::globalActions()->value("refine-search");
147 connect(refineSearchAction, SIGNAL(toggled(bool)),
148 sidebar, SLOT(toggleRefineSearch(bool)));
151 << The::globalActions()->value("webpage")
152 << The::globalActions()->value("pagelink")
153 << The::globalActions()->value("videolink")
154 << The::globalActions()->value("open-in-browser")
156 << The::globalActions()->value("snapshot")
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");
169 #ifndef APP_PHONON_SEEK
170 QSlider *slider = MainWindow::instance()->getSlider();
171 connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
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()));
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();
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);
208 YTSearch *ytSearch = new YTSearch(searchParams, this);
209 ytSearch->setAsyncDetails(true);
210 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
211 setVideoSource(ytSearch);
214 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
217 #ifdef APP_ACTIVATION
222 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
225 int currentIndex = getHistoryIndex();
226 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
227 while (history.size() > currentIndex + 1) {
228 VideoSource *vs = history.takeLast();
230 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
235 history.append(videoSource);
239 if (history.size() > 1)
240 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
243 playlistModel->setVideoSource(videoSource);
245 sidebar->showPlaylist();
246 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
247 sidebar->hideSuggestions();
248 sidebar->getHeader()->updateInfo();
250 SearchParams *searchParams = getSearchParams();
251 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
252 playlistView->setClickableAuthors(!isChannel);
257 void MediaView::searchAgain() {
258 VideoSource *currentVideoSource = playlistModel->getVideoSource();
259 setVideoSource(currentVideoSource, false);
262 bool MediaView::canGoBack() {
263 return getHistoryIndex() > 0;
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);
276 bool MediaView::canGoForward() {
277 int currentIndex = getHistoryIndex();
278 return currentIndex >= 0 && currentIndex < history.size() - 1;
281 void MediaView::goForward() {
282 if (canGoForward()) {
283 int currentIndex = getHistoryIndex();
284 VideoSource *nextVideoSource = history.at(currentIndex + 1);
285 setVideoSource(nextVideoSource, false);
289 int MediaView::getHistoryIndex() {
290 return history.lastIndexOf(playlistModel->getVideoSource());
293 void MediaView::appear() {
294 playlistView->setFocus();
295 Video *currentVideo = playlistModel->activeVideo();
297 MainWindow::instance()->setWindowTitle(
298 currentVideo->title() + " - " + Constants::NAME);
299 MainWindow::instance()->showMessage(currentVideo->description());
303 void MediaView::disappear() {
307 void MediaView::handleError(QString message) {
308 qWarning() << __PRETTY_FUNCTION__ << message;
309 #ifdef APP_PHONON_SEEK
312 QTimer::singleShot(500, this, SLOT(startPlaying()));
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());
328 void MediaView::pause() {
330 switch( mediaObject->state() ) {
331 case Phonon::PlayingState:
332 mediaObject->pause();
341 QRegExp MediaView::wordRE(QString s) {
342 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
345 void MediaView::stop() {
348 while (!history.isEmpty()) {
349 VideoSource *videoSource = history.takeFirst();
350 if (!videoSource->parent()) delete videoSource;
353 playlistModel->abortSearch();
354 videoAreaWidget->clear();
355 videoAreaWidget->update();
357 playlistView->selectionModel()->clearSelection();
359 downloadItem->stop();
362 currentVideoSize = 0;
364 The::globalActions()->value("refine-search")->setChecked(false);
365 updateSubscriptionAction(0, false);
366 #ifdef APP_ACTIVATION
370 foreach (QAction *action, currentVideoActions)
371 action->setEnabled(false);
373 QAction *a = The::globalActions()->value("download");
374 a->setEnabled(false);
375 a->setVisible(false);
380 currentVideoId.clear();
382 #ifndef APP_PHONON_SEEK
383 QSlider *slider = MainWindow::instance()->getSlider();
384 slider->setEnabled(false);
388 if (snapshotSettings) {
389 delete snapshotSettings;
390 snapshotSettings = 0;
394 const QString & MediaView::getCurrentVideoId() {
395 return currentVideoId;
398 void MediaView::activeRowChanged(int row) {
407 downloadItem->stop();
410 currentVideoSize = 0;
413 Video *video = playlistModel->videoAt(row);
416 videoAreaWidget->showLoading(video);
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();
424 // video title in titlebar
425 MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
426 MainWindow::instance()->showMessage(video->description());
428 // ensure active item is visible
430 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
431 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
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);
441 bool enableDownload = video->license() == Video::LicenseCC;
442 #ifdef APP_ACTIVATION
443 enableDownload = enableDownload || Activation::instance().isLegacy();
446 enableDownload = true;
448 QAction *a = The::globalActions()->value("download");
449 a->setEnabled(enableDownload);
450 a->setVisible(enableDownload);
452 updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
454 foreach (QAction *action, currentVideoActions)
455 action->setEnabled(true);
457 #ifndef APP_PHONON_SEEK
458 QSlider *slider = MainWindow::instance()->getSlider();
459 slider->setEnabled(false);
463 if (snapshotSettings) {
464 delete snapshotSettings;
465 snapshotSettings = 0;
468 // see you in gotStreamUrl...
471 void MediaView::gotStreamUrl(QUrl streamUrl) {
473 if (!streamUrl.isValid()) {
478 Video *video = static_cast<Video *>(sender());
480 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
483 video->disconnect(this);
485 currentVideoId = video->id();
487 #ifdef APP_PHONON_SEEK
488 mediaObject->setCurrentSource(streamUrl);
494 // ensure we always have videos ahead
495 playlistModel->searchNeeded();
497 // ensure active item is visible
498 int row = playlistModel->activeRow();
500 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
501 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
504 #ifdef APP_ACTIVATION
505 if (!Activation::instance().isActivated())
506 demoTimer->start(180000);
510 Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
513 ChannelAggregator::instance()->videoWatched(video);
516 void MediaView::downloadStatusChanged() {
517 // qDebug() << __PRETTY_FUNCTION__;
518 switch(downloadItem->status()) {
520 // qDebug() << "Downloading";
521 if (downloadItem->offset() == 0) startPlaying();
524 // qDebug() << "Seeking to" << downloadItem->offset();
525 mediaObject->seek(offsetToTime(downloadItem->offset()));
531 // qDebug() << "Starting";
534 // qDebug() << "Finished" << mediaObject->state();
535 #ifdef APP_PHONON_SEEK
536 MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
540 // qDebug() << "Failed";
544 // qDebug() << "Idle";
549 void MediaView::startPlaying() {
550 // qDebug() << __PRETTY_FUNCTION__;
557 if (downloadItem->offset() == 0) {
558 currentVideoSize = downloadItem->bytesTotal();
559 // qDebug() << "currentVideoSize" << currentVideoSize;
563 QString source = downloadItem->currentFilename();
564 qDebug() << "Playing" << source << QFile::exists(source);
566 mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
569 #ifdef APP_PHONON_SEEK
570 MainWindow::instance()->getSeekSlider()->setEnabled(false);
572 QSlider *slider = MainWindow::instance()->getSlider();
573 slider->setEnabled(true);
577 void MediaView::itemActivated(const QModelIndex &index) {
578 if (playlistModel->rowExists(index.row())) {
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);
589 } else playlistModel->setActiveRow(index.row());
591 // the user doubleclicked on the "Search More" item
593 playlistModel->searchMore();
594 playlistView->selectionModel()->clearSelection();
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
603 if (playlistModel->activeVideo() != skippedVideo) {
604 qDebug() << "Skip of video canceled";
607 int nextRow = playlistModel->rowForVideo(skippedVideo);
609 if (nextRow == -1) return;
610 playlistModel->setActiveRow(nextRow);
614 void MediaView::skip() {
615 int nextRow = playlistModel->nextRow();
616 if (nextRow == -1) return;
617 playlistModel->setActiveRow(nextRow);
620 void MediaView::skipBackward() {
621 int prevRow = playlistModel->previousRow();
622 if (prevRow == -1) return;
623 playlistModel->setActiveRow(prevRow);
626 void MediaView::aboutToFinish() {
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);
639 void MediaView::playbackFinished() {
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()));
651 QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
652 if (stopAfterThisAction->isChecked()) {
653 stopAfterThisAction->setChecked(false);
659 void MediaView::playbackResume() {
662 const qint64 currentTime = mediaObject->currentTime();
663 qDebug() << __PRETTY_FUNCTION__ << currentTime;
665 mediaObject->seek(currentTime);
670 void MediaView::openWebPage() {
671 Video* video = playlistModel->activeVideo();
674 mediaObject->pause();
676 QDesktopServices::openUrl(video->webpage());
679 void MediaView::copyWebPage() {
680 Video* video = playlistModel->activeVideo();
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);
688 void MediaView::copyVideoLink() {
689 Video* video = playlistModel->activeVideo();
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);
697 void MediaView::openInBrowser() {
698 Video* video = playlistModel->activeVideo();
701 mediaObject->pause();
703 QDesktopServices::openUrl(video->getStreamUrl());
706 void MediaView::removeSelected() {
707 if (!playlistView->selectionModel()->hasSelection()) return;
708 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
709 playlistModel->removeIndexes(indexes);
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);
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);
728 void MediaView::moveUpSelected() {
729 if (!playlistView->selectionModel()->hasSelection()) return;
731 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
732 qStableSort(indexes.begin(), indexes.end());
733 playlistModel->move(indexes, true);
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);
741 void MediaView::moveDownSelected() {
742 if (!playlistView->selectionModel()->hasSelection()) return;
744 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
745 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
746 playlistModel->move(indexes, false);
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);
755 void MediaView::setPlaylistVisible(bool visible) {
756 if (splitter->widget(0)->isVisible() == visible) return;
757 splitter->widget(0)->setVisible(visible);
758 playlistView->setFocus();
761 bool MediaView::isPlaylistVisible() {
762 return splitter->widget(0)->isVisible();
765 void MediaView::saveSplitterState() {
767 settings.setValue("splitter", splitter->saveState());
770 #ifdef APP_ACTIVATION
772 static QPushButton *continueButton;
774 void MediaView::demoMessage() {
776 if (mediaObject->state() != Phonon::PlayingState) return;
777 mediaObject->pause();
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);
788 continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
789 continueButton->setEnabled(false);
790 QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
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)));
800 if (msgBox.clickedButton() == buyButton) {
801 MainWindow::instance()->showActivationView();
806 demoTimer->start(600000);
813 void MediaView::updateContinueButton(int value) {
815 continueButton->setText(tr("Continue"));
816 continueButton->setEnabled(true);
818 continueButton->setText(QString::number(value));
824 void MediaView::downloadVideo() {
825 Video* video = playlistModel->activeVideo();
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);
834 void MediaView::snapshot() {
835 qint64 currentTime = mediaObject->currentTime() / 1000;
837 QImage image = videoWidget->snapshot();
838 if (image.isNull()) {
839 qWarning() << "Null snapshot";
843 // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
844 QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
845 videoAreaWidget->showSnapshotPreview(pixmap);
847 Video* video = playlistModel->activeVideo();
850 QString location = SnapshotSettings::getCurrentLocation();
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");
861 if (snapshotSettings) delete snapshotSettings;
862 snapshotSettings = new SnapshotSettings(videoWidget);
863 snapshotSettings->setSnapshot(pixmap, filename);
864 QStatusBar *statusBar = MainWindow::instance()->statusBar();
866 Extra::fadeInWidget(statusBar, statusBar);
868 statusBar->clearMessage();
869 statusBar->insertPermanentWidget(0, snapshotSettings);
870 snapshotSettings->show();
874 void MediaView::fullscreen() {
875 videoAreaWidget->setParent(0);
876 videoAreaWidget->showFullScreen();
879 void MediaView::startDownloading() {
880 Video *video = playlistModel->activeVideo();
882 Video *videoCopy = video->clone();
884 downloadItem->stop();
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();
901 void MediaView::sliderMoved(int value) {
903 #ifndef APP_PHONON_SEEK
905 if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
908 QSlider *slider = MainWindow::instance()->getSlider();
909 if (slider->isSliderDown()) return;
911 qint64 offset = (currentVideoSize * value) / slider->maximum();
913 bool needsDownload = downloadItem->needsDownload(offset);
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));
921 mediaObject->pause();
922 downloadItem->seekTo(offset);
925 // qDebug() << "simple seek";
926 mediaObject->seek(offsetToTime(offset));
932 qint64 MediaView::offsetToTime(qint64 offset) {
934 const qint64 totalTime = mediaObject->totalTime();
935 return ((offset * totalTime) / currentVideoSize);
939 void MediaView::findVideoParts() {
942 Video* video = playlistModel->activeVideo();
945 QString query = video->title();
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 + ")";
953 // numbers from 1 to 15
954 static QString counterNumber = "([1-9]|1[0-5])";
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("[\\(\\)\\[\\]]"));
970 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
972 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
973 Qt::CaseInsensitive);
974 // bool numberAsWords = englishNumberRE.exactMatch(query);
975 query.remove(englishNumberRE);
977 QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
978 Qt::CaseInsensitive);
979 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
980 query.remove(localizedNumberRE);
982 SearchParams *searchParams = new SearchParams();
983 searchParams->setTransient(true);
984 searchParams->setKeywords(query);
985 searchParams->setChannelId(video->channelId());
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);
996 search(searchParams);
1000 void MediaView::relatedVideos() {
1001 Video* video = playlistModel->activeVideo();
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);
1010 void MediaView::shareViaTwitter() {
1011 Video* video = playlistModel->activeVideo();
1013 QUrl url("https://twitter.com/intent/tweet");
1014 #if QT_VERSION >= 0x050000
1019 url.addQueryItem("via", "minitubeapp");
1020 url.addQueryItem("text", video->title());
1021 url.addQueryItem("url", video->webpage());
1022 #if QT_VERSION >= 0x050000
1026 QDesktopServices::openUrl(url);
1029 void MediaView::shareViaFacebook() {
1030 Video* video = playlistModel->activeVideo();
1032 QUrl url("https://www.facebook.com/sharer.php");
1033 #if QT_VERSION >= 0x050000
1038 url.addQueryItem("t", video->title());
1039 url.addQueryItem("u", video->webpage());
1040 #if QT_VERSION >= 0x050000
1044 QDesktopServices::openUrl(url);
1047 void MediaView::shareViaBuffer() {
1048 Video* video = playlistModel->activeVideo();
1050 QUrl url("http://bufferapp.com/add");
1051 #if QT_VERSION >= 0x050000
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
1064 QDesktopServices::openUrl(url);
1067 void MediaView::shareViaEmail() {
1068 Video* video = playlistModel->activeVideo();
1070 QUrl url("mailto:");
1071 #if QT_VERSION >= 0x050000
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" +
1081 url.addQueryItem("body", body);
1082 #if QT_VERSION >= 0x050000
1086 QDesktopServices::openUrl(url);
1089 void MediaView::authorPushed(QModelIndex index) {
1090 Video* video = playlistModel->videoAt(index.row());
1093 QString channelId = video->channelId();
1094 // if (channelId.isEmpty()) channelId = video->channelTitle();
1095 if (channelId.isEmpty()) return;
1097 SearchParams *searchParams = new SearchParams();
1098 searchParams->setChannelId(channelId);
1099 searchParams->setSortBy(SearchParams::SortByNewest);
1102 search(searchParams);
1105 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1106 QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1108 QString subscribeTip;
1109 QString subscribeText;
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);
1118 subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
1119 subscribeTip = subscribeText;
1120 subscribeAction->setEnabled(true);
1122 subscribeAction->setText(subscribeText);
1123 subscribeAction->setStatusTip(subscribeTip);
1127 static QIcon tintedIcon;
1128 if (tintedIcon.isNull()) {
1130 sizes << QSize(16, 16);
1131 tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1133 subscribeAction->setIcon(tintedIcon);
1135 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1138 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1141 IconUtils::setupAction(subscribeAction);
1144 void MediaView::toggleSubscription() {
1145 Video *video = playlistModel->activeVideo();
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);