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),
74 void MediaView::initialize() {
75 QBoxLayout *layout = new QVBoxLayout(this);
78 splitter = new MiniSplitter();
80 playlistView = new PlaylistView(this);
81 // respond to the user doubleclicking a playlist item
82 connect(playlistView, SIGNAL(activated(const QModelIndex &)),
83 SLOT(itemActivated(const QModelIndex &)));
85 playlistModel = new PlaylistModel();
86 connect(playlistModel, SIGNAL(activeRowChanged(int)),
87 SLOT(activeRowChanged(int)));
88 // needed to restore the selection after dragndrop
89 connect(playlistModel, SIGNAL(needSelectionFor(QList<Video*>)),
90 SLOT(selectVideos(QList<Video*>)));
91 playlistView->setModel(playlistModel);
93 connect(playlistView->selectionModel(),
94 SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
95 SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
97 connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
99 sidebar = new SidebarWidget(this);
100 sidebar->setPlaylist(playlistView);
101 connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()),
102 SLOT(searchAgain()));
103 connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)),
104 sidebar, SLOT(showSuggestions(const QStringList &)));
105 connect(sidebar, SIGNAL(suggestionAccepted(QString)),
106 MainWindow::instance(), SLOT(search(QString)));
107 splitter->addWidget(sidebar);
109 videoAreaWidget = new VideoAreaWidget(this);
110 // videoAreaWidget->setMinimumSize(320,240);
113 videoWidget = new Phonon::VideoWidget(this);
114 videoAreaWidget->setVideoWidget(videoWidget);
116 videoAreaWidget->setListModel(playlistModel);
118 loadingWidget = new LoadingWidget(this);
119 videoAreaWidget->setLoadingWidget(loadingWidget);
121 splitter->addWidget(videoAreaWidget);
123 splitter->setStretchFactor(0, 0);
124 splitter->setStretchFactor(1, 8);
126 // restore splitter state
128 splitter->restoreState(settings.value("splitter").toByteArray());
129 splitter->setChildrenCollapsible(false);
131 layout->addWidget(splitter);
133 errorTimer = new QTimer(this);
134 errorTimer->setSingleShot(true);
135 errorTimer->setInterval(3000);
136 connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
138 #ifdef APP_ACTIVATION
139 demoTimer = new QTimer(this);
140 demoTimer->setSingleShot(true);
141 connect(demoTimer, SIGNAL(timeout()), SLOT(demoMessage()));
144 connect(videoAreaWidget, SIGNAL(doubleClicked()),
145 The::globalActions()->value("fullscreen"), SLOT(trigger()));
147 QAction* refineSearchAction = The::globalActions()->value("refine-search");
148 connect(refineSearchAction, SIGNAL(toggled(bool)),
149 sidebar, SLOT(toggleRefineSearch(bool)));
152 << The::globalActions()->value("webpage")
153 << The::globalActions()->value("pagelink")
154 << The::globalActions()->value("videolink")
155 << The::globalActions()->value("open-in-browser")
157 << The::globalActions()->value("snapshot")
159 << The::globalActions()->value("findVideoParts")
160 << The::globalActions()->value("skip")
161 << The::globalActions()->value("previous")
162 << The::globalActions()->value("stopafterthis")
163 << The::globalActions()->value("related-videos")
164 << The::globalActions()->value("refine-search")
165 << The::globalActions()->value("twitter")
166 << The::globalActions()->value("facebook")
167 << The::globalActions()->value("buffer")
168 << The::globalActions()->value("email");
170 #ifndef APP_PHONON_SEEK
171 QSlider *slider = MainWindow::instance()->getSlider();
172 connect(slider, SIGNAL(valueChanged(int)), SLOT(sliderMoved(int)));
177 void MediaView::setMediaObject(Phonon::MediaObject *mediaObject) {
178 this->mediaObject = mediaObject;
179 Phonon::createPath(mediaObject, videoWidget);
180 connect(mediaObject, SIGNAL(finished()), SLOT(playbackFinished()));
181 connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
182 SLOT(stateChanged(Phonon::State, Phonon::State)));
183 connect(mediaObject, SIGNAL(aboutToFinish()), SLOT(aboutToFinish()));
187 SearchParams* MediaView::getSearchParams() {
188 VideoSource *videoSource = playlistModel->getVideoSource();
189 if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
190 YTSearch *search = dynamic_cast<YTSearch *>(videoSource);
191 return search->getSearchParams();
196 void MediaView::search(SearchParams *searchParams) {
197 if (!searchParams->keywords().isEmpty()) {
198 if (searchParams->keywords().startsWith("http://") ||
199 searchParams->keywords().startsWith("https://")) {
200 QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
201 if (!videoId.isEmpty()) {
202 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
203 singleVideoSource->setVideoId(videoId);
204 setVideoSource(singleVideoSource);
209 YTSearch *ytSearch = new YTSearch(searchParams, this);
210 ytSearch->setAsyncDetails(true);
211 connect(ytSearch, SIGNAL(gotDetails()), playlistModel, SLOT(emitDataChanged()));
212 setVideoSource(ytSearch);
215 void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory, bool back) {
218 #ifdef APP_ACTIVATION
223 // qDebug() << "Adding VideoSource" << videoSource->getName() << videoSource;
226 int currentIndex = getHistoryIndex();
227 if (currentIndex >= 0 && currentIndex < history.size() - 1) {
228 while (history.size() > currentIndex + 1) {
229 VideoSource *vs = history.takeLast();
231 qDebug() << "Deleting VideoSource" << vs->getName() << vs;
236 history.append(videoSource);
240 if (history.size() > 1)
241 Extra::slideTransition(playlistView->viewport(), playlistView->viewport(), back);
244 playlistModel->setVideoSource(videoSource);
246 sidebar->showPlaylist();
247 sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
248 sidebar->hideSuggestions();
249 sidebar->getHeader()->updateInfo();
251 SearchParams *searchParams = getSearchParams();
252 bool isChannel = searchParams && !searchParams->channelId().isEmpty();
253 playlistView->setClickableAuthors(!isChannel);
258 void MediaView::searchAgain() {
259 VideoSource *currentVideoSource = playlistModel->getVideoSource();
260 setVideoSource(currentVideoSource, false);
263 bool MediaView::canGoBack() {
264 return getHistoryIndex() > 0;
267 void MediaView::goBack() {
268 if (history.size() > 1) {
269 int currentIndex = getHistoryIndex();
270 if (currentIndex > 0) {
271 VideoSource *previousVideoSource = history.at(currentIndex - 1);
272 setVideoSource(previousVideoSource, false, true);
277 bool MediaView::canGoForward() {
278 int currentIndex = getHistoryIndex();
279 return currentIndex >= 0 && currentIndex < history.size() - 1;
282 void MediaView::goForward() {
283 if (canGoForward()) {
284 int currentIndex = getHistoryIndex();
285 VideoSource *nextVideoSource = history.at(currentIndex + 1);
286 setVideoSource(nextVideoSource, false);
290 int MediaView::getHistoryIndex() {
291 return history.lastIndexOf(playlistModel->getVideoSource());
294 void MediaView::appear() {
295 playlistView->setFocus();
296 Video *currentVideo = playlistModel->activeVideo();
298 MainWindow::instance()->setWindowTitle(
299 currentVideo->title() + " - " + Constants::NAME);
300 MainWindow::instance()->showMessage(currentVideo->description());
304 void MediaView::disappear() {
308 void MediaView::handleError(QString message) {
309 qWarning() << __PRETTY_FUNCTION__ << message;
310 #ifdef APP_PHONON_SEEK
313 QTimer::singleShot(500, this, SLOT(startPlaying()));
318 void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/) {
319 if (newState == Phonon::PlayingState)
320 videoAreaWidget->showVideo();
321 else if (newState == Phonon::ErrorState) {
322 qWarning() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
323 if (mediaObject->errorType() == Phonon::FatalError)
324 handleError(mediaObject->errorString());
329 void MediaView::pause() {
331 switch( mediaObject->state() ) {
332 case Phonon::PlayingState:
333 mediaObject->pause();
342 QRegExp MediaView::wordRE(QString s) {
343 return QRegExp("\\W" + s + "\\W?", Qt::CaseInsensitive);
346 void MediaView::stop() {
349 while (!history.isEmpty()) {
350 VideoSource *videoSource = history.takeFirst();
351 if (!videoSource->parent()) delete videoSource;
354 playlistModel->abortSearch();
355 videoAreaWidget->clear();
356 videoAreaWidget->update();
358 playlistView->selectionModel()->clearSelection();
360 downloadItem->stop();
363 currentVideoSize = 0;
365 The::globalActions()->value("refine-search")->setChecked(false);
366 updateSubscriptionAction(0, false);
367 #ifdef APP_ACTIVATION
371 foreach (QAction *action, currentVideoActions)
372 action->setEnabled(false);
374 QAction *a = The::globalActions()->value("download");
375 a->setEnabled(false);
376 a->setVisible(false);
381 currentVideoId.clear();
383 #ifndef APP_PHONON_SEEK
384 QSlider *slider = MainWindow::instance()->getSlider();
385 slider->setEnabled(false);
389 if (snapshotSettings) {
390 delete snapshotSettings;
391 snapshotSettings = 0;
395 const QString & MediaView::getCurrentVideoId() {
396 return currentVideoId;
399 void MediaView::activeRowChanged(int row) {
408 downloadItem->stop();
411 currentVideoSize = 0;
414 Video *video = playlistModel->videoAt(row);
417 videoAreaWidget->showLoading(video);
419 connect(video, SIGNAL(gotStreamUrl(QUrl)),
420 SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
421 connect(video, SIGNAL(errorStreamUrl(QString)),
422 SLOT(skip()), Qt::UniqueConnection);
423 video->loadStreamUrl();
425 // video title in titlebar
426 MainWindow::instance()->setWindowTitle(video->title() + " - " + Constants::NAME);
427 MainWindow::instance()->showMessage(video->description());
429 // ensure active item is visible
431 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
432 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
435 // enable/disable actions
436 The::globalActions()->value("download")->setEnabled(
437 DownloadManager::instance()->itemForVideo(video) == 0);
438 The::globalActions()->value("previous")->setEnabled(row > 0);
439 The::globalActions()->value("stopafterthis")->setEnabled(true);
440 The::globalActions()->value("related-videos")->setEnabled(true);
442 bool enableDownload = video->license() == Video::LicenseCC;
443 #ifdef APP_ACTIVATION
444 enableDownload = enableDownload || Activation::instance().isLegacy();
447 enableDownload = true;
449 QAction *a = The::globalActions()->value("download");
450 a->setEnabled(enableDownload);
451 a->setVisible(enableDownload);
453 updateSubscriptionAction(video, YTChannel::isSubscribed(video->channelId()));
455 foreach (QAction *action, currentVideoActions)
456 action->setEnabled(true);
458 #ifndef APP_PHONON_SEEK
459 QSlider *slider = MainWindow::instance()->getSlider();
460 slider->setEnabled(false);
464 if (snapshotSettings) {
465 delete snapshotSettings;
466 snapshotSettings = 0;
469 // see you in gotStreamUrl...
472 void MediaView::gotStreamUrl(QUrl streamUrl) {
474 if (!streamUrl.isValid()) {
479 Video *video = static_cast<Video *>(sender());
481 qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
484 video->disconnect(this);
486 currentVideoId = video->id();
488 #ifdef APP_PHONON_SEEK
489 mediaObject->setCurrentSource(streamUrl);
495 // ensure we always have videos ahead
496 playlistModel->searchNeeded();
498 // ensure active item is visible
499 int row = playlistModel->activeRow();
501 QModelIndex index = playlistModel->index(row, 0, QModelIndex());
502 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
505 #ifdef APP_ACTIVATION
506 if (!Activation::instance().isActivated())
507 demoTimer->start(180000);
511 Extra::notify(video->title(), video->channelTitle(), video->formattedDuration());
514 ChannelAggregator::instance()->videoWatched(video);
517 void MediaView::downloadStatusChanged() {
518 // qDebug() << __PRETTY_FUNCTION__;
519 switch(downloadItem->status()) {
521 // qDebug() << "Downloading";
522 if (downloadItem->offset() == 0) startPlaying();
525 // qDebug() << "Seeking to" << downloadItem->offset();
526 mediaObject->seek(offsetToTime(downloadItem->offset()));
532 // qDebug() << "Starting";
535 // qDebug() << "Finished" << mediaObject->state();
536 #ifdef APP_PHONON_SEEK
537 MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
541 // qDebug() << "Failed";
545 // qDebug() << "Idle";
550 void MediaView::startPlaying() {
551 // qDebug() << __PRETTY_FUNCTION__;
558 if (downloadItem->offset() == 0) {
559 currentVideoSize = downloadItem->bytesTotal();
560 // qDebug() << "currentVideoSize" << currentVideoSize;
564 QString source = downloadItem->currentFilename();
565 qDebug() << "Playing" << source << QFile::exists(source);
567 mediaObject->setCurrentSource(QUrl::fromLocalFile(source));
570 #ifdef APP_PHONON_SEEK
571 MainWindow::instance()->getSeekSlider()->setEnabled(false);
573 QSlider *slider = MainWindow::instance()->getSlider();
574 slider->setEnabled(true);
578 void MediaView::itemActivated(const QModelIndex &index) {
579 if (playlistModel->rowExists(index.row())) {
581 // if it's the current video, just rewind and play
582 Video *activeVideo = playlistModel->activeVideo();
583 Video *video = playlistModel->videoAt(index.row());
584 if (activeVideo && video && activeVideo == video) {
585 // mediaObject->seek(0);
590 } else playlistModel->setActiveRow(index.row());
592 // the user doubleclicked on the "Search More" item
594 playlistModel->searchMore();
595 playlistView->selectionModel()->clearSelection();
599 void MediaView::skipVideo() {
600 // skippedVideo is useful for DELAYED skip operations
601 // in order to be sure that we're skipping the video we wanted
602 // and not another one
604 if (playlistModel->activeVideo() != skippedVideo) {
605 qDebug() << "Skip of video canceled";
608 int nextRow = playlistModel->rowForVideo(skippedVideo);
610 if (nextRow == -1) return;
611 playlistModel->setActiveRow(nextRow);
615 void MediaView::skip() {
616 int nextRow = playlistModel->nextRow();
617 if (nextRow == -1) return;
618 playlistModel->setActiveRow(nextRow);
621 void MediaView::skipBackward() {
622 int prevRow = playlistModel->previousRow();
623 if (prevRow == -1) return;
624 playlistModel->setActiveRow(prevRow);
627 void MediaView::aboutToFinish() {
629 qint64 currentTime = mediaObject->currentTime();
630 qint64 totalTime = mediaObject->totalTime();
631 qDebug() << __PRETTY_FUNCTION__ << currentTime << totalTime;
632 if (totalTime < 1 || currentTime + 10000 < totalTime) {
633 // QTimer::singleShot(500, this, SLOT(playbackResume()));
634 mediaObject->seek(currentTime);
640 void MediaView::playbackFinished() {
644 const qint64 totalTime = mediaObject->totalTime();
645 const qint64 currentTime = mediaObject->currentTime();
646 qDebug() << __PRETTY_FUNCTION__ << mediaObject->currentTime() << totalTime;
647 // add 10 secs for imprecise Phonon backends (VLC, Xine)
648 if (totalTime < 1 || (currentTime > 0 && currentTime + 10000 < totalTime)) {
649 // mediaObject->seek(currentTime);
650 QTimer::singleShot(500, this, SLOT(playbackResume()));
652 QAction* stopAfterThisAction = The::globalActions()->value("stopafterthis");
653 if (stopAfterThisAction->isChecked()) {
654 stopAfterThisAction->setChecked(false);
660 void MediaView::playbackResume() {
663 const qint64 currentTime = mediaObject->currentTime();
664 qDebug() << __PRETTY_FUNCTION__ << currentTime;
666 mediaObject->seek(currentTime);
671 void MediaView::openWebPage() {
672 Video* video = playlistModel->activeVideo();
675 mediaObject->pause();
677 QDesktopServices::openUrl(video->webpage());
680 void MediaView::copyWebPage() {
681 Video* video = playlistModel->activeVideo();
683 QString address = video->webpage();
684 QApplication::clipboard()->setText(address);
685 QString message = tr("You can now paste the YouTube link into another application");
686 MainWindow::instance()->showMessage(message);
689 void MediaView::copyVideoLink() {
690 Video* video = playlistModel->activeVideo();
692 QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
693 QString message = tr("You can now paste the video stream URL into another application")
694 + ". " + tr("The link will be valid only for a limited time.");
695 MainWindow::instance()->showMessage(message);
698 void MediaView::openInBrowser() {
699 Video* video = playlistModel->activeVideo();
702 mediaObject->pause();
704 QDesktopServices::openUrl(video->getStreamUrl());
707 void MediaView::removeSelected() {
708 if (!playlistView->selectionModel()->hasSelection()) return;
709 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
710 playlistModel->removeIndexes(indexes);
713 void MediaView::selectVideos(QList<Video*> videos) {
714 foreach (Video *video, videos) {
715 QModelIndex index = playlistModel->indexForVideo(video);
716 playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
717 playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
721 void MediaView::selectionChanged(const QItemSelection & /*selected*/,
722 const QItemSelection & /*deselected*/) {
723 const bool gotSelection = playlistView->selectionModel()->hasSelection();
724 The::globalActions()->value("remove")->setEnabled(gotSelection);
725 The::globalActions()->value("moveUp")->setEnabled(gotSelection);
726 The::globalActions()->value("moveDown")->setEnabled(gotSelection);
729 void MediaView::moveUpSelected() {
730 if (!playlistView->selectionModel()->hasSelection()) return;
732 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
733 qStableSort(indexes.begin(), indexes.end());
734 playlistModel->move(indexes, true);
736 // set current index after row moves to something more intuitive
737 int row = indexes.first().row();
738 playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>1?row:1),
739 QItemSelectionModel::NoUpdate);
742 void MediaView::moveDownSelected() {
743 if (!playlistView->selectionModel()->hasSelection()) return;
745 QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
746 qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
747 playlistModel->move(indexes, false);
749 // set current index after row moves to something more intuitive
750 // (respect 1 static item on bottom)
751 int row = indexes.first().row()+1, max = playlistModel->rowCount() - 2;
752 playlistView->selectionModel()->setCurrentIndex(
753 playlistModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
756 void MediaView::setPlaylistVisible(bool visible) {
757 if (splitter->widget(0)->isVisible() == visible) return;
758 splitter->widget(0)->setVisible(visible);
759 playlistView->setFocus();
762 bool MediaView::isPlaylistVisible() {
763 return splitter->widget(0)->isVisible();
766 void MediaView::saveSplitterState() {
768 settings.setValue("splitter", splitter->saveState());
771 #ifdef APP_ACTIVATION
773 static QPushButton *continueButton;
775 void MediaView::demoMessage() {
777 if (mediaObject->state() != Phonon::PlayingState) return;
778 mediaObject->pause();
781 QMessageBox msgBox(this);
782 msgBox.setIconPixmap(QPixmap(":/images/app.png").scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
783 msgBox.setText(tr("This is just the demo version of %1.").arg(Constants::NAME));
784 msgBox.setInformativeText(tr("It allows you to test the application and see if it works for you."));
785 msgBox.setModal(true);
786 // make it a "sheet" on the Mac
787 msgBox.setWindowModality(Qt::WindowModal);
789 continueButton = msgBox.addButton("5", QMessageBox::RejectRole);
790 continueButton->setEnabled(false);
791 QPushButton *buyButton = msgBox.addButton(tr("Get the full version"), QMessageBox::ActionRole);
793 QTimeLine *timeLine = new QTimeLine(6000, this);
794 timeLine->setCurveShape(QTimeLine::LinearCurve);
795 timeLine->setFrameRange(5, 0);
796 connect(timeLine, SIGNAL(frameChanged(int)), SLOT(updateContinueButton(int)));
801 if (msgBox.clickedButton() == buyButton) {
802 MainWindow::instance()->showActivationView();
807 demoTimer->start(600000);
814 void MediaView::updateContinueButton(int value) {
816 continueButton->setText(tr("Continue"));
817 continueButton->setEnabled(true);
819 continueButton->setText(QString::number(value));
825 void MediaView::downloadVideo() {
826 Video* video = playlistModel->activeVideo();
828 DownloadManager::instance()->addItem(video);
829 The::globalActions()->value("downloads")->setVisible(true);
830 QString message = tr("Downloading %1").arg(video->title());
831 MainWindow::instance()->showMessage(message);
835 void MediaView::snapshot() {
836 qint64 currentTime = mediaObject->currentTime() / 1000;
838 QImage image = videoWidget->snapshot();
839 if (image.isNull()) {
840 qWarning() << "Null snapshot";
844 // QPixmap pixmap = QPixmap::grabWindow(videoWidget->winId());
845 QPixmap pixmap = QPixmap::fromImage(image.scaled(videoWidget->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
846 videoAreaWidget->showSnapshotPreview(pixmap);
848 Video* video = playlistModel->activeVideo();
851 QString location = SnapshotSettings::getCurrentLocation();
853 if (!dir.exists()) dir.mkpath(location);
854 QString basename = video->title();
855 QString format = video->duration() > 3600 ? "h_mm_ss" : "m_ss";
856 basename += " (" + QTime().addSecs(currentTime).toString(format) + ")";
857 basename = DataUtils::stringToFilename(basename);
858 QString filename = location + "/" + basename + ".png";
859 qDebug() << filename;
860 image.save(filename, "PNG");
862 if (snapshotSettings) delete snapshotSettings;
863 snapshotSettings = new SnapshotSettings(videoWidget);
864 snapshotSettings->setSnapshot(pixmap, filename);
865 QStatusBar *statusBar = MainWindow::instance()->statusBar();
867 Extra::fadeInWidget(statusBar, statusBar);
869 statusBar->clearMessage();
870 statusBar->insertPermanentWidget(0, snapshotSettings);
871 snapshotSettings->show();
875 void MediaView::fullscreen() {
876 videoAreaWidget->setParent(0);
877 videoAreaWidget->showFullScreen();
880 void MediaView::startDownloading() {
881 Video *video = playlistModel->activeVideo();
883 Video *videoCopy = video->clone();
885 downloadItem->stop();
888 QString tempFile = Temporary::filename();
889 downloadItem = new DownloadItem(videoCopy, video->getStreamUrl(), tempFile, this);
890 connect(downloadItem, SIGNAL(statusChanged()),
891 SLOT(downloadStatusChanged()), Qt::UniqueConnection);
892 connect(downloadItem, SIGNAL(bufferProgress(int)),
893 loadingWidget, SLOT(bufferStatus(int)), Qt::UniqueConnection);
894 // connect(downloadItem, SIGNAL(finished()), SLOT(itemFinished()));
895 connect(video, SIGNAL(errorStreamUrl(QString)),
896 SLOT(handleError(QString)), Qt::UniqueConnection);
897 connect(downloadItem, SIGNAL(error(QString)),
898 SLOT(handleError(QString)), Qt::UniqueConnection);
899 downloadItem->start();
902 void MediaView::sliderMoved(int value) {
904 #ifndef APP_PHONON_SEEK
906 if (currentVideoSize <= 0 || !downloadItem || !mediaObject->isSeekable())
909 QSlider *slider = MainWindow::instance()->getSlider();
910 if (slider->isSliderDown()) return;
912 qint64 offset = (currentVideoSize * value) / slider->maximum();
914 bool needsDownload = downloadItem->needsDownload(offset);
916 if (downloadItem->isBuffered(offset)) {
917 qint64 realOffset = downloadItem->blankAtOffset(offset);
918 if (offset < currentVideoSize)
919 downloadItem->seekTo(realOffset, false);
920 mediaObject->seek(offsetToTime(offset));
922 mediaObject->pause();
923 downloadItem->seekTo(offset);
926 // qDebug() << "simple seek";
927 mediaObject->seek(offsetToTime(offset));
933 qint64 MediaView::offsetToTime(qint64 offset) {
935 const qint64 totalTime = mediaObject->totalTime();
936 return ((offset * totalTime) / currentVideoSize);
940 void MediaView::findVideoParts() {
943 Video* video = playlistModel->activeVideo();
946 QString query = video->title();
948 static QString optionalSpace = "\\s*";
949 static QString staticCounterSeparators = "[\\/\\-]";
950 QString counterSeparators = "( of | " +
951 tr("of", "Used in video parts, as in '2 of 3'") +
952 " |" + staticCounterSeparators + ")";
954 // numbers from 1 to 15
955 static QString counterNumber = "([1-9]|1[0-5])";
957 // query.remove(QRegExp(counterSeparators + optionalSpace + counterNumber));
958 query.remove(QRegExp(counterNumber + optionalSpace +
959 counterSeparators + optionalSpace + counterNumber));
960 query.remove(wordRE("pr?t\\.?" + optionalSpace + counterNumber));
961 query.remove(wordRE("ep\\.?" + optionalSpace + counterNumber));
962 query.remove(wordRE("part" + optionalSpace + counterNumber));
963 query.remove(wordRE("episode" + optionalSpace + counterNumber));
964 query.remove(wordRE(tr("part", "This is for video parts, as in 'Cool video - part 1'") +
965 optionalSpace + counterNumber));
966 query.remove(wordRE(tr("episode",
967 "This is for video parts, as in 'Cool series - episode 1'") +
968 optionalSpace + counterNumber));
969 query.remove(QRegExp("[\\(\\)\\[\\]]"));
971 #define NUMBERS "one|two|three|four|five|six|seven|eight|nine|ten"
973 QRegExp englishNumberRE = QRegExp(QLatin1String(".*(") + NUMBERS + ").*",
974 Qt::CaseInsensitive);
975 // bool numberAsWords = englishNumberRE.exactMatch(query);
976 query.remove(englishNumberRE);
978 QRegExp localizedNumberRE = QRegExp(QLatin1String(".*(") + tr(NUMBERS) + ").*",
979 Qt::CaseInsensitive);
980 // if (!numberAsWords) numberAsWords = localizedNumberRE.exactMatch(query);
981 query.remove(localizedNumberRE);
983 SearchParams *searchParams = new SearchParams();
984 searchParams->setTransient(true);
985 searchParams->setKeywords(query);
986 searchParams->setChannelId(video->channelId());
989 if (!numberAsWords) {
990 qDebug() << "We don't have number as words";
991 // searchParams->setSortBy(SearchParams::SortByNewest);
992 // TODO searchParams->setReverseOrder(true);
993 // TODO searchParams->setMax(50);
997 search(searchParams);
1001 void MediaView::relatedVideos() {
1002 Video* video = playlistModel->activeVideo();
1004 YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource();
1005 singleVideoSource->setVideo(video->clone());
1006 singleVideoSource->setAsyncDetails(true);
1007 setVideoSource(singleVideoSource);
1008 The::globalActions()->value("related-videos")->setEnabled(false);
1011 void MediaView::shareViaTwitter() {
1012 Video* video = playlistModel->activeVideo();
1014 QUrl url("https://twitter.com/intent/tweet");
1016 QUrlQueryHelper urlHelper(url);
1017 urlHelper.addQueryItem("via", "minitubeapp");
1018 urlHelper.addQueryItem("text", video->title());
1019 urlHelper.addQueryItem("url", video->webpage());
1021 QDesktopServices::openUrl(url);
1024 void MediaView::shareViaFacebook() {
1025 Video* video = playlistModel->activeVideo();
1027 QUrl url("https://www.facebook.com/sharer.php");
1029 QUrlQueryHelper urlHelper(url);
1030 urlHelper.addQueryItem("t", video->title());
1031 urlHelper.addQueryItem("u", video->webpage());
1033 QDesktopServices::openUrl(url);
1036 void MediaView::shareViaBuffer() {
1037 Video* video = playlistModel->activeVideo();
1039 QUrl url("http://bufferapp.com/add");
1041 QUrlQueryHelper urlHelper(url);
1042 urlHelper.addQueryItem("via", "minitubeapp");
1043 urlHelper.addQueryItem("text", video->title());
1044 urlHelper.addQueryItem("url", video->webpage());
1045 urlHelper.addQueryItem("picture", video->thumbnailUrl());
1047 QDesktopServices::openUrl(url);
1050 void MediaView::shareViaEmail() {
1051 Video* video = playlistModel->activeVideo();
1053 QUrl url("mailto:");
1055 QUrlQueryHelper urlHelper(url);
1056 urlHelper.addQueryItem("subject", video->title());
1057 const QString body = video->title() + "\n" +
1058 video->webpage() + "\n\n" +
1059 tr("Sent from %1").arg(Constants::NAME) + "\n" +
1061 urlHelper.addQueryItem("body", body);
1063 QDesktopServices::openUrl(url);
1066 void MediaView::authorPushed(QModelIndex index) {
1067 Video* video = playlistModel->videoAt(index.row());
1070 QString channelId = video->channelId();
1071 // if (channelId.isEmpty()) channelId = video->channelTitle();
1072 if (channelId.isEmpty()) return;
1074 SearchParams *searchParams = new SearchParams();
1075 searchParams->setChannelId(channelId);
1076 searchParams->setSortBy(SearchParams::SortByNewest);
1079 search(searchParams);
1082 void MediaView::updateSubscriptionAction(Video *video, bool subscribed) {
1083 QAction *subscribeAction = The::globalActions()->value("subscribe-channel");
1085 QString subscribeTip;
1086 QString subscribeText;
1088 subscribeText = subscribeAction->property("originalText").toString();
1089 subscribeAction->setEnabled(false);
1090 } else if (subscribed) {
1091 subscribeText = tr("Unsubscribe from %1").arg(video->channelTitle());
1092 subscribeTip = subscribeText;
1093 subscribeAction->setEnabled(true);
1095 subscribeText = tr("Subscribe to %1").arg(video->channelTitle());
1096 subscribeTip = subscribeText;
1097 subscribeAction->setEnabled(true);
1099 subscribeAction->setText(subscribeText);
1100 subscribeAction->setStatusTip(subscribeTip);
1104 static QIcon tintedIcon;
1105 if (tintedIcon.isNull()) {
1107 sizes << QSize(16, 16);
1108 tintedIcon = IconUtils::tintedIcon("bookmark-new", QColor(254, 240, 0), sizes);
1110 subscribeAction->setIcon(tintedIcon);
1112 subscribeAction->setIcon(IconUtils::icon("bookmark-remove"));
1115 subscribeAction->setIcon(IconUtils::icon("bookmark-new"));
1118 IconUtils::setupAction(subscribeAction);
1121 void MediaView::toggleSubscription() {
1122 Video *video = playlistModel->activeVideo();
1124 QString userId = video->channelId();
1125 if (userId.isEmpty()) return;
1126 bool subscribed = YTChannel::isSubscribed(userId);
1127 if (subscribed) YTChannel::unsubscribe(userId);
1128 else YTChannel::subscribe(userId);
1129 updateSubscriptionAction(video, !subscribed);