-2.0
+2.0 - January ?? 2013
+- YouTube categories and "standard feeds": "Most Popular", "Featured", etc
+- Country selection for YouTube categories and feeds
- Autoupdate on Mac and Windows
+- Related videos are now appended to the video when pasting a YouTube link
+- "Show 10 More" with a single click
+- Play video in the playlist with a single click on its thumbnail
+- OS X Mountain Lion notifications on video start
+- Fixed some YouTube links not working when pasted in the searchbox
+- Fixed playlist drag'n'drop
+- Fixed system language settings detection
- Fixed clicking on channel names not working in some cases
+- Fixed incorrect number of downloads in status bar
1.9 - September 27, 2012
- Adapted to YouTube changes
-CONFIG += release
+CONFIG += debug
TEMPLATE = app
VERSION = 2.0
DEFINES += APP_VERSION="$$VERSION"
DEFINES += QT_USE_FAST_OPERATOR_PLUS
DEFINES += QT_STRICT_ITERATORS
-# TODO Saner string behaviour
-# DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII
TARGET = minitube
-QT += network xml phonon declarative
+QT += network xml phonon
include(src/qtsingleapplication/qtsingleapplication.pri)
HEADERS += \
- src/youtubesearch.h \
src/video.h \
- src/youtubestreamreader.h \
src/searchlineedit.h \
src/urllineedit.h \
src/spacer.h \
src/videomimedata.h \
src/global.h \
src/updatechecker.h \
- src/playlistwidget.h \
src/searchparams.h \
src/minisplitter.h \
src/loadingwidget.h \
src/downloadmodel.h \
src/downloadlistview.h \
src/downloadsettings.h \
- src/youtubesuggest.h \
src/suggester.h \
src/channelsuggest.h \
src/temporary.h \
src/sidebarwidget.h \
src/homeview.h \
src/aboutview.h \
- src/listmodel.h \
src/mainwindow.h \
src/mediaview.h \
src/searchview.h \
src/view.h \
- src/categoriesview.h \
src/userview.h \
- src/youtubecategories.h
+ src/playlistmodel.h \
+ src/videosource.h \
+ src/ytsearch.h \
+ src/ytstandardfeed.h \
+ src/standardfeedsview.h \
+ src/ytregions.h \
+ src/ytcategories.h \
+ src/ytfeedreader.h \
+ src/ytsuggester.h \
+ src/videosourcewidget.h \
+ src/regionsview.h \
+ src/ytsinglevideosource.h \
+ src/sidebarheader.h
SOURCES += src/main.cpp \
- src/youtubesearch.cpp \
- src/youtubestreamreader.cpp \
src/searchlineedit.cpp \
src/urllineedit.cpp \
src/spacer.cpp \
src/videomimedata.cpp \
src/updatechecker.cpp \
src/networkaccess.cpp \
- src/playlistwidget.cpp \
src/searchparams.cpp \
src/minisplitter.cpp \
src/loadingwidget.cpp \
src/downloadmodel.cpp \
src/downloadlistview.cpp \
src/downloadsettings.cpp \
- src/youtubesuggest.cpp \
src/channelsuggest.cpp \
src/temporary.cpp \
src/segmentedcontrol.cpp \
src/homeview.cpp \
src/mainwindow.cpp \
src/mediaview.cpp \
- src/listmodel.cpp \
src/aboutview.cpp \
src/searchview.cpp \
- src/categoriesview.cpp \
src/userview.cpp \
src/playlistitemdelegate.cpp \
- src/youtubecategories.cpp
+ src/playlistmodel.cpp \
+ src/videosource.cpp \
+ src/ytsearch.cpp \
+ src/ytstandardfeed.cpp \
+ src/standardfeedsview.cpp \
+ src/ytregions.cpp \
+ src/ytcategories.cpp \
+ src/ytfeedreader.cpp \
+ src/ytsuggester.cpp \
+ src/videosourcewidget.cpp \
+ src/regionsview.cpp \
+ src/ytsinglevideosource.cpp \
+ src/sidebarheader.cpp
RESOURCES += resources.qrc
DESTDIR = build/target/
OBJECTS_DIR = build/obj/
icon512.files += data/512x512/minitube.png
}
mac|win32:include(local/local.pri)
-
-OTHER_FILES += \
- qml/categories.qml
<file>images/search-sortBy.png</file>
<file>images/search-quality.png</file>
<file>images/search-duration.png</file>
- <file>qml/categories.qml</file>
+ <file>flags/dz.png</file>
+ <file>flags/ar.png</file>
+ <file>flags/au.png</file>
+ <file>flags/be.png</file>
+ <file>flags/br.png</file>
+ <file>flags/ca.png</file>
+ <file>flags/cl.png</file>
+ <file>flags/co.png</file>
+ <file>flags/cz.png</file>
+ <file>flags/eg.png</file>
+ <file>flags/fr.png</file>
+ <file>flags/de.png</file>
+ <file>flags/gh.png</file>
+ <file>flags/gr.png</file>
+ <file>flags/hk.png</file>
+ <file>flags/hu.png</file>
+ <file>flags/in.png</file>
+ <file>flags/id.png</file>
+ <file>flags/ie.png</file>
+ <file>flags/il.png</file>
+ <file>flags/it.png</file>
+ <file>flags/jp.png</file>
+ <file>flags/jo.png</file>
+ <file>flags/ke.png</file>
+ <file>flags/my.png</file>
+ <file>flags/mx.png</file>
+ <file>flags/ma.png</file>
+ <file>flags/nl.png</file>
+ <file>flags/nz.png</file>
+ <file>flags/ng.png</file>
+ <file>flags/pe.png</file>
+ <file>flags/ph.png</file>
+ <file>flags/pl.png</file>
+ <file>flags/ru.png</file>
+ <file>flags/sa.png</file>
+ <file>flags/sg.png</file>
+ <file>flags/za.png</file>
+ <file>flags/kr.png</file>
+ <file>flags/es.png</file>
+ <file>flags/se.png</file>
+ <file>flags/tw.png</file>
+ <file>flags/tn.png</file>
+ <file>flags/tr.png</file>
+ <file>flags/ug.png</file>
+ <file>flags/ae.png</file>
+ <file>flags/gb.png</file>
+ <file>flags/ye.png</file>
+ <file>style.css</file>
</qresource>
</RCC>
#include "downloadmanager.h"
#include "downloaditem.h"
#include "video.h"
-#include "listmodel.h"
+#include "playlistmodel.h"
DownloadModel::DownloadModel(DownloadManager *downloadManager, QObject *parent) :
QAbstractListModel(parent),
#include "downloadlistview.h"
#include "downloaditem.h"
#include "downloadsettings.h"
-#include "listmodel.h"
+#include "playlistmodel.h"
#include "playlistitemdelegate.h"
#include "segmentedcontrol.h"
--- /dev/null
+#include "homeview.h"
+#include "segmentedcontrol.h"
+#include "searchview.h"
+#include "standardfeedsview.h"
+#include "userview.h"
+#include "mainwindow.h"
+#include "mediaview.h"
+#include "ytstandardfeed.h"
+
+HomeView::HomeView(QWidget *parent) : QWidget(parent) {
+ standardFeedsView = 0;
+ userView = 0;
+
+ QBoxLayout *layout = new QVBoxLayout(this);
+ layout->setMargin(0);
+ layout->setSpacing(0);
+
+ setupBar();
+ layout->addWidget(bar);
+
+ stackedWidget = new QStackedWidget();
+ layout->addWidget(stackedWidget);
+
+ searchView = new SearchView();
+ connect(searchView, SIGNAL(search(SearchParams*)),
+ MainWindow::instance(), SLOT(showMedia(SearchParams*)));
+ stackedWidget->addWidget(searchView);
+}
+
+void HomeView::setupBar() {
+ bar = new SegmentedControl(this);
+
+ QAction *action = new QAction(tr("Search"), this);
+ action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_1));
+ action->setStatusTip(tr("Find videos and channels by keyword"));
+ connect(action, SIGNAL(triggered()), SLOT(showSearch()));
+ bar->addAction(action);
+ bar->setCheckedAction(action);
+
+ action = new QAction(tr("Categories"), this);
+ action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_2));
+ action->setStatusTip(tr("Browse videos by category"));
+ connect(action, SIGNAL(triggered()), SLOT(showStandardFeeds()));
+ bar->addAction(action);
+
+ /*
+ action = new QAction(tr("User"), this);
+ action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_3));
+ action->setStatusTip(tr("Your favorite videos, subscriptions and playlists"));
+ connect(action, SIGNAL(triggered()), SLOT(showUser()));
+ bar->addAction(action);
+ */
+
+ foreach (QAction* action, bar->actions()) {
+ // action->setEnabled(false);
+ addAction(action);
+ action->setAutoRepeat(false);
+ if (!action->shortcut().isEmpty())
+ action->setStatusTip(
+ action->statusTip() + " (" +
+ action->shortcut().toString(QKeySequence::NativeText) + ")");
+ }
+}
+
+void HomeView::showWidget(QWidget *widget) {
+ QWidget* currentWidget = stackedWidget->currentWidget();
+ if (currentWidget == widget) return;
+ QMetaObject::invokeMethod(currentWidget, "disappear");
+ currentWidget->setEnabled(false);
+ stackedWidget->setCurrentWidget(widget);
+ widget->setEnabled(true);
+ QMetaObject::invokeMethod(widget, "appear");
+ bar->setCheckedAction(stackedWidget->currentIndex());
+ // autoChosenView = false;
+ widget->setFocus();
+}
+
+void HomeView::appear() {
+ QMetaObject::invokeMethod(stackedWidget->currentWidget(), "appear");
+}
+
+void HomeView::disappear() {
+ QMetaObject::invokeMethod(stackedWidget->currentWidget(), "disappear");
+}
+
+void HomeView::showSearch() {
+ showWidget(searchView);
+}
+
+void HomeView::showStandardFeeds() {
+ if (!standardFeedsView) {
+ standardFeedsView = new StandardFeedsView();
+ connect(standardFeedsView, SIGNAL(activated(VideoSource*)),
+ MainWindow::instance(),
+ SLOT(showMedia(VideoSource*)));
+ stackedWidget->addWidget(standardFeedsView);
+ }
+ showWidget(standardFeedsView);
+}
+
+void HomeView::showUser() {
+ if (!userView) {
+ userView = new UserView(this);
+ stackedWidget->addWidget(userView);
+ }
+ showWidget(userView);
+}
--- /dev/null
+#ifndef HOMEVIEW_H
+#define HOMEVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class SegmentedControl;
+class SearchView;
+class StandardFeedsView;
+class UserView;
+
+class HomeView : public QWidget, public View {
+
+ Q_OBJECT
+
+public:
+ HomeView(QWidget *parent = 0);
+ void appear();
+ void disappear();
+ void showWidget(QWidget *widget);
+ SearchView* getSearchView() { return searchView; }
+ StandardFeedsView* getStandardFeedsView() { return standardFeedsView; }
+
+public slots:
+ void showSearch();
+ void showStandardFeeds();
+ void showUser();
+
+private:
+ void setupBar();
+ SegmentedControl *bar;
+ QStackedWidget *stackedWidget;
+
+ SearchView *searchView;
+ StandardFeedsView *standardFeedsView;
+ UserView* userView;
+
+};
+
+#endif // HOMEVIEW_H
#ifndef Q_WS_X11
Extra::appSetup(&app);
+#else
+ QFile cssFile(":/style.css");
+ cssFile.open(QFile::ReadOnly);
+ QString styleSheet = QLatin1String(cssFile.readAll());
+ app.setStyleSheet(styleSheet);
#endif
- const QString locale = QLocale::system().name();
-
// qt translations
QTranslator qtTranslator;
- qtTranslator.load("qt_" + locale,
+ qtTranslator.load("qt_" + QLocale::system().name(),
QLibraryInfo::location(QLibraryInfo::TranslationsPath));
app.installTranslator(&qtTranslator);
#else
QString dataDir = "";
#endif
- QString localeDir = qApp->applicationDirPath() + QDir::separator() + "locale";
+ QString localeDir = qApp->applicationDirPath() + "/locale";
if (!QDir(localeDir).exists()) {
- localeDir = dataDir + QDir::separator() + "locale";
+ localeDir = dataDir + "/locale";
}
// qDebug() << "Using locale dir" << localeDir << locale;
QTranslator translator;
- translator.load(locale, localeDir);
+ translator.load(QLocale::system(), localeDir);
app.installTranslator(&translator);
QTextCodec::setCodecForTr(QTextCodec::codecForName("utf8"));
#include "videodefinition.h"
#include "fontutils.h"
#include "globalshortcuts.h"
+#include "searchparams.h"
+#include "videosource.h"
+#include "ytsearch.h"
#ifdef Q_WS_X11
#include "gnomeglobalshortcutbackend.h"
#endif
#include "macutils.h"
#endif
#include "downloadmanager.h"
-#include "youtubesuggest.h"
+#include "ytsuggester.h"
#include "updatechecker.h"
#include "temporary.h"
#ifdef APP_MAC
#include "activationview.h"
#include "activationdialog.h"
#endif
+#include "ytregions.h"
+#include "regionsview.h"
+#include "standardfeedsview.h"
static MainWindow *singleton = 0;
}
MainWindow::MainWindow() :
- updateChecker(0),
- aboutView(0),
- downloadView(0),
- mediaObject(0),
- audioOutput(0),
- m_fullscreen(false) {
+ updateChecker(0),
+ aboutView(0),
+ downloadView(0),
+ regionsView(0),
+ mediaObject(0),
+ audioOutput(0),
+ m_fullscreen(false) {
singleton = this;
views->addWidget(homeView);
// TODO make this lazy
- mediaView = new MediaView(this);
+ mediaView = MediaView::instance();
mediaView->setEnabled(false);
views->addWidget(mediaView);
createStatusBar();
initPhonon();
- mediaView->setSlider(seekSlider);
mediaView->setMediaObject(mediaObject);
// remove that useless menu/toolbar context menu
action = new QAction(tr("&Refine Search..."), this);
action->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_R));
action->setCheckable(true);
+ action->setEnabled(false);
actions->insert("refine-search", action);
+ action = new QAction(YTRegions::worldwideRegion().name, this);
+ actions->insert("worldwide-region", action);
+
+ action = new QAction(YTRegions::localRegion().name, this);
+ actions->insert("local-region", action);
+
+ action = new QAction(tr("More..."), this);
+ actions->insert("more-region", action);
+
#ifdef APP_ACTIVATION
Extra::createActivationAction(tr("Buy %1...").arg(Constants::NAME));
#endif
seekSlider->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
mainToolBar->addWidget(seekSlider);
-/*
+ /*
mainToolBar->addWidget(new Spacer());
slider = new QSlider(this);
slider->setOrientation(Qt::Horizontal);
QSlider* volumeQSlider = volumeSlider->findChild<QSlider*>();
if (volumeQSlider)
volumeQSlider->setStatusTip(tr("Press %1 to raise the volume, %2 to lower it").arg(
- volumeUpAct->shortcut().toString(QKeySequence::NativeText), volumeDownAct->shortcut().toString(QKeySequence::NativeText)));
+ volumeUpAct->shortcut().toString(QKeySequence::NativeText), volumeDownAct->shortcut().toString(QKeySequence::NativeText)));
// this makes the volume slider smaller
volumeSlider->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
mainToolBar->addWidget(volumeSlider);
toolbarSearch = new SearchLineEdit(this);
#endif
toolbarSearch->setMinimumWidth(toolbarSearch->fontInfo().pixelSize()*15);
- toolbarSearch->setSuggester(new YouTubeSuggest(this));
+ toolbarSearch->setSuggester(new YTSuggester(this));
connect(toolbarSearch, SIGNAL(search(const QString&)), this, SLOT(startToolbarSearch(const QString&)));
connect(toolbarSearch, SIGNAL(suggestionAccepted(const QString&)), SLOT(startToolbarSearch(const QString&)));
toolbarSearch->setStatusTip(searchFocusAct->statusTip());
}
void MainWindow::createStatusBar() {
- QToolBar* toolBar = new QToolBar(this);
- statusToolBar = toolBar;
- toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- toolBar->setIconSize(QSize(16, 16));
- toolBar->addAction(The::globalActions()->value("downloads"));
- toolBar->addAction(The::globalActions()->value("definition"));
- statusBar()->addPermanentWidget(toolBar);
+ statusToolBar = new QToolBar(this);
+ statusToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ statusToolBar->setIconSize(QSize(16, 16));
+ statusToolBar->addAction(The::globalActions()->value("downloads"));
+
+ regionButton = new QToolButton(this);
+ regionButton->setStatusTip(tr("Choose your content location"));
+ regionButton->setIcon(QtIconLoader::icon("go-down"));
+ regionButton->setIconSize(QSize(16, 16));
+ regionButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ regionAction = statusToolBar->addWidget(regionButton);
+ regionAction->setVisible(false);
+
+ QAction *localAction = The::globalActions()->value("local-region");
+ if (!localAction->text().isEmpty()) {
+ regionButton->setPopupMode(QToolButton::InstantPopup);
+ QMenu *regionMenu = new QMenu(this);
+ regionMenu->addAction(The::globalActions()->value("worldwide-region"));
+ regionMenu->addAction(localAction);
+ regionMenu->addSeparator();
+ QAction *moreRegionsAction = The::globalActions()->value("more-region");
+ regionMenu->addAction(moreRegionsAction);
+ connect(moreRegionsAction, SIGNAL(triggered()), SLOT(showRegionsView()));
+ regionButton->setMenu(regionMenu);
+ } else {
+ connect(regionButton, SIGNAL(clicked()), SLOT(showRegionsView()));
+ }
+
+ /* Stupid code that generates the QRC items
+ foreach(YTRegion r, YTRegions::list())
+ qDebug() << QString("<file>flags/%1.png</file>").arg(r.id.toLower());
+ */
+
+ statusToolBar->addAction(The::globalActions()->value("definition"));
+
+ statusBar()->addPermanentWidget(statusToolBar);
statusBar()->show();
}
if (settings.contains("geometry")) {
restoreGeometry(settings.value("geometry").toByteArray());
#ifdef APP_MAC
- MacSupport::fixGeometry(this);
+ MacSupport::fixGeometry(this);
#endif
} else {
setGeometry(100, 100, 1000, 500);
newView->appear();
QHash<QString,QVariant> metadata = newView->metadata();
QString title = metadata.value("title").toString();
- if (!title.isEmpty()) title += " - ";
- setWindowTitle(title + Constants::NAME);
+ if (title.isEmpty()) title = Constants::NAME;
+ else title += QLatin1String(" - ") + Constants::NAME;
+ setWindowTitle(title);
QString desc = metadata.value("description").toString();
if (!desc.isEmpty()) showMessage(desc);
}
widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
#ifndef Q_WS_X11
- if (transition) Extra::fadeInWidget(oldWidget, widget);
+ if (transition && oldWidget != mediaView)
+ Extra::fadeInWidget(oldWidget, widget);
#endif
history->push(widget);
showWidget(mediaView);
}
+void MainWindow::showMedia(VideoSource *videoSource) {
+ mediaView->setVideoSource(videoSource);
+ showWidget(mediaView);
+}
+
void MainWindow::stateChanged(Phonon::State newState, Phonon::State /* oldState */) {
// qDebug() << "Phonon state: " << newState;
}
break;
- case Phonon::PlayingState:
+ case Phonon::PlayingState:
pauseAct->setEnabled(true);
pauseAct->setIcon(QtIconLoader::icon("media-playback-pause"));
pauseAct->setText(tr("&Pause"));
// stopAct->setEnabled(true);
break;
- case Phonon::StoppedState:
+ case Phonon::StoppedState:
pauseAct->setEnabled(false);
// stopAct->setEnabled(false);
break;
- case Phonon::PausedState:
+ case Phonon::PausedState:
pauseAct->setEnabled(true);
pauseAct->setIcon(QtIconLoader::icon("media-playback-start"));
pauseAct->setText(tr("&Play"));
// stopAct->setEnabled(true);
break;
- case Phonon::BufferingState:
- case Phonon::LoadingState:
+ case Phonon::BufferingState:
+ case Phonon::LoadingState:
pauseAct->setEnabled(false);
currentTime->clear();
totalTime->clear();
// stopAct->setEnabled(true);
break;
- default:
+ default:
;
}
}
if (show) {
stopAct->setShortcuts(QList<QKeySequence>() << QKeySequence(Qt::Key_MediaStop));
The::globalActions()->value("downloads")->setShortcuts(
- QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_J)
- << QKeySequence(Qt::Key_Escape));
+ QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_J)
+ << QKeySequence(Qt::Key_Escape));
} else {
The::globalActions()->value("downloads")->setShortcuts(
- QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_J));
+ QList<QKeySequence>() << QKeySequence(Qt::CTRL + Qt::Key_J));
stopAct->setShortcuts(QList<QKeySequence>() << QKeySequence(Qt::Key_Escape) << QKeySequence(Qt::Key_MediaStop));
}
if (urls.isEmpty())
return;
QUrl url = urls.first();
- QString videoId = YouTubeSearch::videoIdFromUrl(url.toString());
+ QString videoId = YTSearch::videoIdFromUrl(url.toString());
if (!videoId.isNull())
event->acceptProposedAction();
}
if (urls.isEmpty())
return;
QUrl url = urls.first();
- QString videoId = YouTubeSearch::videoIdFromUrl(url.toString());
+ QString videoId = YTSearch::videoIdFromUrl(url.toString());
if (!videoId.isNull()) {
setWindowTitle(url.toString());
SearchParams *searchParams = new SearchParams();
action->setEnabled(false);
}
#endif
+
+void MainWindow::showRegionsView() {
+ if (!regionsView) {
+ regionsView = new RegionsView(this);
+ connect(regionsView, SIGNAL(regionChanged()),
+ homeView->getStandardFeedsView(), SLOT(load()));
+ views->addWidget(regionsView);
+ }
+ showWidget(regionsView);
+}
class SearchLineEdit;
class UpdateChecker;
class SearchParams;
+class VideoSource;
class MainWindow : public QMainWindow {
void readSettings();
void writeSettings();
static void printHelp();
+ MediaView* getMediaView() { return mediaView; }
+ QToolButton* getRegionButton() { return regionButton; }
+ QAction* getRegionAction() { return regionAction; }
public slots:
void showHome(bool transition = true);
void showMedia(SearchParams *params);
+ void showMedia(VideoSource *videoSource);
+ void showRegionsView();
void restore();
void messageReceived(const QString &message);
void quit();
MediaView *mediaView;
QWidget *aboutView;
QWidget *downloadView;
+ QWidget *regionsView;
// actions
QAction *addGadgetAct;
QMenu *playlistMenu;
QMenu *helpMenu;
- // toolbar
+ // toolbar & statusbar
QToolBar *mainToolBar;
SearchLineEdit *toolbarSearch;
QToolBar *statusToolBar;
+ QToolButton *regionButton;
+ QAction *regionAction;
// phonon
Phonon::SeekSlider *seekSlider;
QLabel *currentTime;
QLabel *totalTime;
+ // fullscreen
bool m_fullscreen;
bool m_maximized;
-
QTimer *mouseTimer;
-
};
#endif
#include "mediaview.h"
+#include "playlistmodel.h"
#include "playlistview.h"
-#include "playlistitemdelegate.h"
+#include "loadingwidget.h"
+#include "videoareawidget.h"
#include "networkaccess.h"
#include "videowidget.h"
#include "minisplitter.h"
#include "downloaditem.h"
#include "mainwindow.h"
#include "temporary.h"
-#include "sidebarwidget.h"
-#include "playlistwidget.h"
#include "refinesearchwidget.h"
#include "sidebarwidget.h"
+#include "sidebarheader.h"
#ifdef APP_MAC
#include "macfullscreen.h"
+#include "macutils.h"
#endif
#ifdef APP_ACTIVATION
#include "activation.h"
#endif
+#include "videosource.h"
+#include "ytsearch.h"
+#include "searchparams.h"
+#include "ytsinglevideosource.h"
namespace The {
NetworkAccess* http();
-}
-
-namespace The {
QMap<QString, QAction*>* globalActions();
QMap<QString, QMenu*>* globalMenus();
QNetworkAccessManager* networkAccessManager();
}
-MediaView::MediaView(QWidget *parent) : QWidget(parent) {
+MediaView* MediaView::instance() {
+ static MediaView *i = new MediaView();
+ return i;
+}
+MediaView::MediaView(QWidget *parent) : QWidget(parent) {
reallyStopped = false;
downloadItem = 0;
- QBoxLayout *layout = new QVBoxLayout();
+ QBoxLayout *layout = new QVBoxLayout(this);
layout->setMargin(0);
- splitter = new MiniSplitter(this);
+ splitter = new MiniSplitter();
splitter->setChildrenCollapsible(false);
- listView = new PlaylistView(this);
- listView->setItemDelegate(new PlaylistItemDelegate(this));
- listView->setSelectionMode(QAbstractItemView::ExtendedSelection);
-
- // dragndrop
- listView->setDragEnabled(true);
- listView->setAcceptDrops(true);
- listView->setDropIndicatorShown(true);
- listView->setDragDropMode(QAbstractItemView::DragDrop);
-
- // cosmetics
- listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
- listView->setFrameShape( QFrame::NoFrame );
- listView->setAttribute(Qt::WA_MacShowFocusRect, false);
- listView->setMinimumSize(320,240);
- listView->setUniformItemSizes(true);
-
+ playlistView = new PlaylistView(this);
// respond to the user doubleclicking a playlist item
- connect(listView, SIGNAL(activated(const QModelIndex &)), this, SLOT(itemActivated(const QModelIndex &)));
+ connect(playlistView, SIGNAL(activated(const QModelIndex &)),
+ SLOT(itemActivated(const QModelIndex &)));
- listModel = new ListModel(this);
- connect(listModel, SIGNAL(activeRowChanged(int)), this, SLOT(activeRowChanged(int)));
+ playlistModel = new PlaylistModel();
+ connect(playlistModel, SIGNAL(activeRowChanged(int)),
+ SLOT(activeRowChanged(int)));
// needed to restore the selection after dragndrop
- connect(listModel, SIGNAL(needSelectionFor(QList<Video*>)), this, SLOT(selectVideos(QList<Video*>)));
- listView->setModel(listModel);
+ connect(playlistModel, SIGNAL(needSelectionFor(QList<Video*>)),
+ SLOT(selectVideos(QList<Video*>)));
+ playlistView->setModel(playlistModel);
- connect(listView->selectionModel(),
- SIGNAL(selectionChanged ( const QItemSelection & , const QItemSelection & )),
- this, SLOT(selectionChanged ( const QItemSelection & , const QItemSelection & )));
+ connect(playlistView->selectionModel(),
+ SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection &)),
+ SLOT(selectionChanged(const QItemSelection &, const QItemSelection &)));
- connect(listView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
+ connect(playlistView, SIGNAL(authorPushed(QModelIndex)), SLOT(authorPushed(QModelIndex)));
sidebar = new SidebarWidget(this);
- sidebar->setPlaylist(listView);
+ sidebar->setPlaylist(playlistView);
connect(sidebar->getRefineSearchWidget(), SIGNAL(searchRefined()),
SLOT(searchAgain()));
- connect(listModel, SIGNAL(haveSuggestions(const QStringList &)),
+ connect(playlistModel, SIGNAL(haveSuggestions(const QStringList &)),
sidebar, SLOT(showSuggestions(const QStringList &)));
connect(sidebar, SIGNAL(suggestionAccepted(QString)),
MainWindow::instance(), SLOT(startToolbarSearch(QString)));
videoAreaWidget->setMinimumSize(320,240);
videoWidget = new Phonon::VideoWidget(this);
videoAreaWidget->setVideoWidget(videoWidget);
- videoAreaWidget->setListModel(listModel);
+ videoAreaWidget->setListModel(playlistModel);
loadingWidget = new LoadingWidget(this);
videoAreaWidget->setLoadingWidget(loadingWidget);
splitter->addWidget(videoAreaWidget);
layout->addWidget(splitter);
- setLayout(layout);
splitter->setStretchFactor(0, 1);
splitter->setStretchFactor(1, 6);
errorTimer->setInterval(3000);
connect(errorTimer, SIGNAL(timeout()), SLOT(skipVideo()));
- workaroundTimer = new QTimer(this);
- workaroundTimer->setSingleShot(true);
- workaroundTimer->setInterval(3000);
- connect(workaroundTimer, SIGNAL(timeout()), SLOT(timerPlay()));
-
#ifdef APP_ACTIVATION
demoTimer = new QTimer(this);
demoTimer->setSingleShot(true);
}
void MediaView::initialize() {
- connect(videoAreaWidget, SIGNAL(doubleClicked()), The::globalActions()->value("fullscreen"), SLOT(trigger()));
-
- /*
- videoAreaWidget->setContextMenuPolicy(Qt::CustomContextMenu);
- connect(videoAreaWidget, SIGNAL(customContextMenuRequested(QPoint)),
- this, SLOT(showVideoContextMenu(QPoint)));
- */
+ connect(videoAreaWidget, SIGNAL(doubleClicked()),
+ The::globalActions()->value("fullscreen"), SLOT(trigger()));
QAction* refineSearchAction = The::globalActions()->value("refine-search");
connect(refineSearchAction, SIGNAL(toggled(bool)),
void MediaView::setMediaObject(Phonon::MediaObject *mediaObject) {
this->mediaObject = mediaObject;
- Phonon::createPath(this->mediaObject, videoWidget);
- connect(mediaObject, SIGNAL(finished()), this, SLOT(playbackFinished()));
+ Phonon::createPath(mediaObject, videoWidget);
+ connect(mediaObject, SIGNAL(finished()), SLOT(playbackFinished()));
connect(mediaObject, SIGNAL(stateChanged(Phonon::State, Phonon::State)),
- this, SLOT(stateChanged(Phonon::State, Phonon::State)));
+ SLOT(stateChanged(Phonon::State, Phonon::State)));
connect(mediaObject, SIGNAL(currentSourceChanged(Phonon::MediaSource)),
- this, SLOT(currentSourceChanged(Phonon::MediaSource)));
- // connect(mediaObject, SIGNAL(bufferStatus(int)), loadingWidget, SLOT(bufferStatus(int)));
+ SLOT(currentSourceChanged(Phonon::MediaSource)));
connect(mediaObject, SIGNAL(aboutToFinish()), SLOT(aboutToFinish()));
}
+SearchParams* MediaView::getSearchParams() {
+ VideoSource *videoSource = playlistModel->getVideoSource();
+ if (videoSource && videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
+ YTSearch *search = dynamic_cast<YTSearch *>(videoSource);
+ return search->getSearchParams();
+ }
+ return 0;
+}
+
void MediaView::search(SearchParams *searchParams) {
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ QString videoId = YTSearch::videoIdFromUrl(searchParams->keywords());
+ if (!videoId.isEmpty()) {
+ qDebug() << "single video";
+ YTSingleVideoSource *singleVideoSource = new YTSingleVideoSource(this);
+ singleVideoSource->setVideoId(videoId);
+ setVideoSource(singleVideoSource);
+ return;
+ }
+ }
+ }
+ setVideoSource(new YTSearch(searchParams, this));
+}
+
+void MediaView::setVideoSource(VideoSource *videoSource, bool addToHistory) {
reallyStopped = false;
#ifdef APP_ACTIVATION
demoTimer->stop();
#endif
- workaroundTimer->stop();
errorTimer->stop();
- this->searchParams = searchParams;
+ if (addToHistory) {
+ int currentIndex = getHistoryIndex();
+ if (currentIndex >= 0 && currentIndex < history.size() - 1) {
+ for (int i = currentIndex + 1; i < history.size(); i++) {
+ VideoSource *vs = history.takeAt(i);
+ if (!vs->parent()) delete vs;
+ }
+ }
+ history.append(videoSource);
+ }
- // start serching for videos
- listModel->search(searchParams);
+ playlistModel->setVideoSource(videoSource);
sidebar->showPlaylist();
- listView->setFocus();
-
- QString keyword = searchParams->keywords();
- QString display = keyword;
- if (keyword.startsWith("http://") || keyword.startsWith("https://")) {
- int separator = keyword.indexOf("|");
- if (separator > 0 && separator + 1 < keyword.length()) {
- display = keyword.mid(separator+1);
+ sidebar->getRefineSearchWidget()->setSearchParams(getSearchParams());
+ sidebar->hideSuggestions();
+ sidebar->getHeader()->updateInfo();
+
+ SearchParams *searchParams = getSearchParams();
+ bool isChannel = searchParams && !searchParams->author().isEmpty();
+ playlistView->setClickableAuthors(!isChannel);
+}
+
+void MediaView::searchAgain() {
+ VideoSource *currentVideoSource = playlistModel->getVideoSource();
+ setVideoSource(currentVideoSource, false);
+}
+
+bool MediaView::canGoBack() {
+ return getHistoryIndex() > 0;
+}
+
+void MediaView::goBack() {
+ if (history.size() > 1) {
+ int currentIndex = getHistoryIndex();
+ if (currentIndex > 0) {
+ VideoSource *previousVideoSource = history.at(currentIndex - 1);
+ setVideoSource(previousVideoSource, false);
}
}
+}
- sidebar->getRefineSearchWidget()->setSearchParams(searchParams);
- sidebar->hideSuggestions();
+bool MediaView::canGoForward() {
+ int currentIndex = getHistoryIndex();
+ return currentIndex >= 0 && currentIndex < history.size() - 1;
+}
+void MediaView::goForward() {
+ if (canGoForward()) {
+ int currentIndex = getHistoryIndex();
+ VideoSource *nextVideoSource = history.at(currentIndex + 1);
+ setVideoSource(nextVideoSource, false);
+ }
}
-void MediaView::searchAgain() {
- search(searchParams);
+int MediaView::getHistoryIndex() {
+ return history.lastIndexOf(playlistModel->getVideoSource());
}
void MediaView::appear() {
- listView->setFocus();
+ playlistView->setFocus();
}
void MediaView::disappear() {
- timerPlayFlag = true;
+
}
void MediaView::handleError(QString /* message */) {
-
QTimer::singleShot(500, this, SLOT(startPlaying()));
-
- /*
- videoAreaWidget->showError(message);
- skippedVideo = listModel->activeVideo();
- // recover from errors by skipping to the next video
- errorTimer->start(2000);
- */
}
void MediaView::stateChanged(Phonon::State newState, Phonon::State /*oldState*/) {
- // qDebug() << "Phonon state: " << newState;
- // slider->setEnabled(newState == Phonon::PlayingState);
-
- switch (newState) {
-
- case Phonon::ErrorState:
+ if (newState == Phonon::PlayingState)
+ videoAreaWidget->showVideo();
+ else if (newState == Phonon::ErrorState) {
qDebug() << "Phonon error:" << mediaObject->errorString() << mediaObject->errorType();
if (mediaObject->errorType() == Phonon::FatalError)
handleError(mediaObject->errorString());
- break;
-
- case Phonon::PlayingState:
- // qDebug("playing");
- videoAreaWidget->showVideo();
- break;
-
- case Phonon::StoppedState:
- // qDebug("stopped");
- // play() has already been called when setting the source
- // but Phonon on Linux needs a little more help to start playback
- // if (!reallyStopped) mediaObject->play();
-
-#ifdef APP_MAC
- // Workaround for Mac playback start problem
- if (!timerPlayFlag) {
- // workaroundTimer->start();
- }
-#endif
-
- break;
-
- case Phonon::PausedState:
- qDebug("paused");
- break;
-
- case Phonon::BufferingState:
- qDebug("buffering");
- break;
-
- case Phonon::LoadingState:
- qDebug("loading");
- break;
-
}
}
void MediaView::pause() {
- // qDebug() << "pause() called" << mediaObject->state();
-
switch( mediaObject->state() ) {
case Phonon::PlayingState:
mediaObject->pause();
mediaObject->play();
break;
}
-
}
QRegExp MediaView::wordRE(QString s) {
}
void MediaView::stop() {
- listModel->abortSearch();
+ playlistModel->abortSearch();
reallyStopped = true;
mediaObject->stop();
videoAreaWidget->clear();
- workaroundTimer->stop();
errorTimer->stop();
- listView->selectionModel()->clearSelection();
+ playlistView->selectionModel()->clearSelection();
if (downloadItem) {
downloadItem->stop();
delete downloadItem;
downloadItem = 0;
}
The::globalActions()->value("refine-search")->setChecked(false);
+
+ while (!history.isEmpty()) {
+ VideoSource *videoSource = history.takeFirst();
+ if (!videoSource->parent()) delete videoSource;
+ }
}
void MediaView::activeRowChanged(int row) {
if (reallyStopped) return;
- Video *video = listModel->videoAt(row);
+ Video *video = playlistModel->videoAt(row);
if (!video) return;
- // now that we have a new video to play
- // stop all the timers
- workaroundTimer->stop();
+ // Related videos without video interruption
+ if (row == 0) {
+ VideoSource *videoSource = playlistModel->getVideoSource();
+ if (videoSource && !history.isEmpty() &&
+ mediaObject->state() == Phonon::PlayingState &&
+ videoSource->metaObject()->className() == QLatin1String("YTSingleVideoSource")) {
+ if (playlistModel->videoAt(row)->title() == downloadItem->getVideo()->title())
+ return;
+ }
+ }
+
errorTimer->stop();
+ videoAreaWidget->showLoading(video);
+
mediaObject->stop();
if (downloadItem) {
downloadItem->stop();
delete downloadItem;
downloadItem = 0;
}
- // slider->setMinimum(0);
-
- // immediately show the loading widget
- videoAreaWidget->showLoading(video);
- connect(video, SIGNAL(gotStreamUrl(QUrl)), SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
- // TODO handle signal in a proper slot and impl item error status
- connect(video, SIGNAL(errorStreamUrl(QString)), SLOT(handleError(QString)), Qt::UniqueConnection);
+ connect(video, SIGNAL(gotStreamUrl(QUrl)),
+ SLOT(gotStreamUrl(QUrl)), Qt::UniqueConnection);
+ connect(video, SIGNAL(errorStreamUrl(QString)),
+ SLOT(handleError(QString)), Qt::UniqueConnection);
video->loadStreamUrl();
- // reset the timer flag
- timerPlayFlag = false;
-
// video title in the statusbar
MainWindow::instance()->showMessage(video->title());
// ensure active item is visible
// int row = listModel->activeRow();
if (row != -1) {
- QModelIndex index = listModel->index(row, 0, QModelIndex());
- listView->scrollTo(index, QAbstractItemView::EnsureVisible);
+ QModelIndex index = playlistModel->index(row, 0, QModelIndex());
+ playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
}
// enable/disable actions
Video *video = static_cast<Video *>(sender());
if (!video) {
- qDebug() << "Cannot get sender";
+ qDebug() << "Cannot get sender in" << __PRETTY_FUNCTION__;
return;
}
video->disconnect(this);
connect(downloadItem, SIGNAL(error(QString)), SLOT(handleError(QString)), Qt::UniqueConnection);
downloadItem->start();
+#ifdef Q_WS_MAC
+ if (mac::canNotify())
+ mac::notify(video->title(), video->author(), video->formattedDuration());
+#endif
}
/*
// qDebug() << "Finished" << mediaObject->state();
// if (mediaObject->state() == Phonon::StoppedState) startPlaying();
#ifdef Q_WS_X11
- seekSlider->setEnabled(mediaObject->isSeekable());
+ MainWindow::instance()->getSeekSlider()->setEnabled(mediaObject->isSeekable());
#endif
break;
case Failed:
mediaObject->setCurrentSource(source);
mediaObject->play();
#ifdef Q_WS_X11
- seekSlider->setEnabled(false);
+ MainWindow::instance()->getSeekSlider()->setEnabled(false);
#endif
// ensure we always have 10 videos ahead
- listModel->searchNeeded();
+ playlistModel->searchNeeded();
// ensure active item is visible
- int row = listModel->activeRow();
+ int row = playlistModel->activeRow();
if (row != -1) {
- QModelIndex index = listModel->index(row, 0, QModelIndex());
- listView->scrollTo(index, QAbstractItemView::EnsureVisible);
+ QModelIndex index = playlistModel->index(row, 0, QModelIndex());
+ playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
}
#ifdef APP_ACTIVATION
}
void MediaView::itemActivated(const QModelIndex &index) {
- if (listModel->rowExists(index.row())) {
+ if (playlistModel->rowExists(index.row())) {
// if it's the current video, just rewind and play
- Video *activeVideo = listModel->activeVideo();
- Video *video = listModel->videoAt(index.row());
+ Video *activeVideo = playlistModel->activeVideo();
+ Video *video = playlistModel->videoAt(index.row());
if (activeVideo && video && activeVideo == video) {
mediaObject->seek(0);
mediaObject->play();
- } else listModel->setActiveRow(index.row());
+ } else playlistModel->setActiveRow(index.row());
- // the user doubleclicked on the "Search More" item
+ // the user doubleclicked on the "Search More" item
} else {
- listModel->searchMore();
- listView->selectionModel()->clearSelection();
+ playlistModel->searchMore();
+ playlistView->selectionModel()->clearSelection();
}
}
// in order to be sure that we're skipping the video we wanted
// and not another one
if (skippedVideo) {
- if (listModel->activeVideo() != skippedVideo) {
+ if (playlistModel->activeVideo() != skippedVideo) {
qDebug() << "Skip of video canceled";
return;
}
- int nextRow = listModel->rowForVideo(skippedVideo);
+ int nextRow = playlistModel->rowForVideo(skippedVideo);
nextRow++;
if (nextRow == -1) return;
- listModel->setActiveRow(nextRow);
+ playlistModel->setActiveRow(nextRow);
}
}
void MediaView::skip() {
- int nextRow = listModel->nextRow();
+ int nextRow = playlistModel->nextRow();
if (nextRow == -1) return;
- listModel->setActiveRow(nextRow);
+ playlistModel->setActiveRow(nextRow);
}
void MediaView::skipBackward() {
- int prevRow = listModel->previousRow();
+ int prevRow = playlistModel->previousRow();
if (prevRow == -1) return;
- listModel->setActiveRow(prevRow);
+ playlistModel->setActiveRow(prevRow);
}
void MediaView::aboutToFinish() {
}
void MediaView::openWebPage() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
mediaObject->pause();
QDesktopServices::openUrl(video->webpage());
}
void MediaView::copyWebPage() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QString address = video->webpage().toString();
QApplication::clipboard()->setText(address);
}
void MediaView::copyVideoLink() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QApplication::clipboard()->setText(video->getStreamUrl().toEncoded());
QString message = tr("You can now paste the video stream URL into another application")
}
void MediaView::removeSelected() {
- if (!listView->selectionModel()->hasSelection()) return;
- QModelIndexList indexes = listView->selectionModel()->selectedIndexes();
- listModel->removeIndexes(indexes);
+ if (!playlistView->selectionModel()->hasSelection()) return;
+ QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
+ playlistModel->removeIndexes(indexes);
}
void MediaView::selectVideos(QList<Video*> videos) {
foreach (Video *video, videos) {
- QModelIndex index = listModel->indexForVideo(video);
- listView->selectionModel()->select(index, QItemSelectionModel::Select);
- listView->scrollTo(index, QAbstractItemView::EnsureVisible);
+ QModelIndex index = playlistModel->indexForVideo(video);
+ playlistView->selectionModel()->select(index, QItemSelectionModel::Select);
+ playlistView->scrollTo(index, QAbstractItemView::EnsureVisible);
}
}
void MediaView::selectionChanged(const QItemSelection & /*selected*/, const QItemSelection & /*deselected*/) {
- const bool gotSelection = listView->selectionModel()->hasSelection();
+ const bool gotSelection = playlistView->selectionModel()->hasSelection();
The::globalActions()->value("remove")->setEnabled(gotSelection);
The::globalActions()->value("moveUp")->setEnabled(gotSelection);
The::globalActions()->value("moveDown")->setEnabled(gotSelection);
}
void MediaView::moveUpSelected() {
- if (!listView->selectionModel()->hasSelection()) return;
+ if (!playlistView->selectionModel()->hasSelection()) return;
- QModelIndexList indexes = listView->selectionModel()->selectedIndexes();
+ QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
qStableSort(indexes.begin(), indexes.end());
- listModel->move(indexes, true);
+ playlistModel->move(indexes, true);
// set current index after row moves to something more intuitive
int row = indexes.first().row();
- listView->selectionModel()->setCurrentIndex(listModel->index(row>1?row:1), QItemSelectionModel::NoUpdate);
+ playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>1?row:1), QItemSelectionModel::NoUpdate);
}
void MediaView::moveDownSelected() {
- if (!listView->selectionModel()->hasSelection()) return;
+ if (!playlistView->selectionModel()->hasSelection()) return;
- QModelIndexList indexes = listView->selectionModel()->selectedIndexes();
+ QModelIndexList indexes = playlistView->selectionModel()->selectedIndexes();
qStableSort(indexes.begin(), indexes.end(), qGreater<QModelIndex>());
- listModel->move(indexes, false);
+ playlistModel->move(indexes, false);
// set current index after row moves to something more intuitive (respect 1 static item on bottom)
- int row = indexes.first().row()+1, max = listModel->rowCount() - 2;
- listView->selectionModel()->setCurrentIndex(listModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
-}
-
-void MediaView::showVideoContextMenu(QPoint point) {
- The::globalMenus()->value("video")->popup(videoWidget->mapToGlobal(point));
-}
-
-void MediaView::searchMostRelevant() {
- searchParams->setSortBy(SearchParams::SortByRelevance);
- search(searchParams);
-}
-
-void MediaView::searchMostRecent() {
- searchParams->setSortBy(SearchParams::SortByNewest);
- search(searchParams);
-}
-
-void MediaView::searchMostViewed() {
- searchParams->setSortBy(SearchParams::SortByViewCount);
- search(searchParams);
+ int row = indexes.first().row()+1, max = playlistModel->rowCount() - 2;
+ playlistView->selectionModel()->setCurrentIndex(playlistModel->index(row>max?max:row), QItemSelectionModel::NoUpdate);
}
void MediaView::setPlaylistVisible(bool visible) {
if (splitter->widget(0)->isVisible() == visible) return;
splitter->widget(0)->setVisible(visible);
- listView->setFocus();
+ playlistView->setFocus();
}
bool MediaView::isPlaylistVisible() {
return splitter->widget(0)->isVisible();
}
-void MediaView::timerPlay() {
- // Workaround Phonon bug on Mac OSX
- // qDebug() << mediaObject->currentTime();
- if (mediaObject->currentTime() <= 0 && mediaObject->state() == Phonon::PlayingState) {
- // qDebug() << "Mac playback workaround";
- mediaObject->pause();
- // QTimer::singleShot(1000, mediaObject, SLOT(play()));
- mediaObject->play();
- }
-}
-
void MediaView::saveSplitterState() {
QSettings settings;
settings.setValue("splitter", splitter->saveState());
#endif
void MediaView::downloadVideo() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
DownloadManager::instance()->addItem(video);
The::globalActions()->value("downloads")->setVisible(true);
void MediaView::seekTo(int value) {
qDebug() << __func__;
mediaObject->pause();
- workaroundTimer->stop();
errorTimer->stop();
// mediaObject->clear();
void MediaView::findVideoParts() {
// parts
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QString query = video->title();
}
void MediaView::shareViaTwitter() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QUrl url("https://twitter.com/intent/tweet");
url.addQueryItem("via", "minitubeapp");
}
void MediaView::shareViaFacebook() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QUrl url("https://www.facebook.com/sharer.php");
url.addQueryItem("t", video->title());
}
void MediaView::shareViaBuffer() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QUrl url("http://bufferapp.com/add");
url.addQueryItem("via", "minitubeapp");
url.addQueryItem("text", video->title());
url.addQueryItem("url", video->webpage().toString());
- if (!video->thumbnailUrls().isEmpty())
- url.addQueryItem("picture", video->thumbnailUrls().first().toString());
+ url.addQueryItem("picture", video->thumbnailUrl());
QDesktopServices::openUrl(url);
}
void MediaView::shareViaEmail() {
- Video* video = listModel->activeVideo();
+ Video* video = playlistModel->activeVideo();
if (!video) return;
QUrl url("mailto:");
url.addQueryItem("subject", video->title());
}
void MediaView::authorPushed(QModelIndex index) {
- Video* video = listModel->videoAt(index.row());
+ Video* video = playlistModel->videoAt(index.row());
if (!video) return;
QString channel = video->authorUri();
#include <phonon/videowidget.h>
#include <phonon/seekslider.h>
#include "view.h"
-#include "listmodel.h"
-#include "segmentedcontrol.h"
-#include "searchparams.h"
-#include "loadingwidget.h"
-#include "videoareawidget.h"
+class Video;
+class PlaylistModel;
+class SearchParams;
+class LoadingWidget;
+class VideoAreaWidget;
class DownloadItem;
class PlaylistView;
class SidebarWidget;
+class VideoSource;
namespace The {
QMap<QString, QAction*>* globalActions();
}
class MediaView : public QWidget, public View {
+
Q_OBJECT
public:
- MediaView(QWidget *parent);
+ static MediaView* instance();
void initialize();
void appear();
void disappear();
void setMediaObject(Phonon::MediaObject *mediaObject);
- void setSlider(Phonon::SeekSlider *slider) { this->seekSlider = slider; }
+ const QList<VideoSource*> & getHistory() { return history; }
+ int getHistoryIndex();
+ PlaylistModel* getPlaylistModel() { return playlistModel; }
public slots:
void search(SearchParams *searchParams);
+ void setVideoSource(VideoSource *videoSource, bool addToHistory = true);
void pause();
void stop();
void skip();
void snapshot();
void fullscreen();
void findVideoParts();
+ bool canGoBack();
+ void goBack();
+ bool canGoForward();
+ void goForward();
private slots:
// list/model
// phonon
void stateChanged(Phonon::State newState, Phonon::State oldState);
void currentSourceChanged(const Phonon::MediaSource source);
- void showVideoContextMenu(QPoint point);
void aboutToFinish();
- // bar
- void searchMostRelevant();
- void searchMostRecent();
- void searchMostViewed();
- // timer
- void timerPlay();
#ifdef APP_ACTIVATION
void demoMessage();
void updateContinueButton(int);
*/
private:
+ MediaView(QWidget *parent = 0);
+ SearchParams* getSearchParams();
static QRegExp wordRE(QString s);
- SearchParams *searchParams;
-
QSplitter *splitter;
-
SidebarWidget *sidebar;
- PlaylistView *listView;
- ListModel *listModel;
-
- // sortBar
- SegmentedControl *sortBar;
- QAction *mostRelevantAction;
- QAction *mostRecentAction;
- QAction *mostViewedAction;
+ PlaylistView *playlistView;
+ PlaylistModel *playlistModel;
+ VideoAreaWidget *videoAreaWidget;
+ LoadingWidget *loadingWidget;
// phonon
Phonon::MediaObject *mediaObject;
Phonon::VideoWidget *videoWidget;
- Phonon::SeekSlider *seekSlider;
- // loadingWidget
- VideoAreaWidget *videoAreaWidget;
- LoadingWidget *loadingWidget;
-
- bool timerPlayFlag;
bool reallyStopped;
-
QTimer *errorTimer;
- QTimer *workaroundTimer;
Video *skippedVideo;
#ifdef APP_ACTIVATION
#endif
DownloadItem *downloadItem;
-
+ QList<VideoSource*> history;
};
#endif // __MEDIAVIEW_H__
#include "playlistitemdelegate.h"
-#include "listmodel.h"
+#include "playlistmodel.h"
#include "fontutils.h"
#include "downloaditem.h"
#include "iconloader/qticonloader.h"
#include "videodefinition.h"
-
-#include <QFontMetricsF>
-#include <QPainter>
-#include <QHash>
+#include "video.h"
const qreal PlaylistItemDelegate::THUMB_HEIGHT = 90.0;
const qreal PlaylistItemDelegate::THUMB_WIDTH = 120.0;
const Video *video = videoPointer.data();
// thumb
- if (!video->thumbnail().isNull()) {
- painter->drawImage(QRect(0, 0, THUMB_WIDTH, THUMB_HEIGHT), video->thumbnail());
-
- // play icon overlayed on the thumb
- if (isActive)
- paintPlayIcon(painter);
-
- // time
- QString timeString;
- int duration = video->duration();
- if ( duration > 3600 )
- timeString = QTime().addSecs(duration).toString("h:mm:ss");
- else
- timeString = QTime().addSecs(duration).toString("m:ss");
- drawTime(painter, timeString, line);
+ painter->drawPixmap(0, 0, THUMB_WIDTH, THUMB_HEIGHT, video->thumbnail());
- }
+ // play icon overlayed on the thumb
+ if (isActive)
+ paintPlayIcon(painter);
+
+ // time
+ drawTime(painter, video->formattedDuration(), line);
if (isActive) painter->setFont(boldFont);
-#ifndef PRETTYITEMDELEGATE_H
-#define PRETTYITEMDELEGATE_H
+#ifndef PLAYLISTITEMDELEGATE_H
+#define PLAYLISTITEMDELEGATE_H
-#include <QModelIndex>
-#include <QStyledItemDelegate>
-
-class QPainter;
-class QProgressBar;
+#include <QtGui>
class PlaylistItemDelegate : public QStyledItemDelegate {
-#include "listmodel.h"
+#include "playlistmodel.h"
#include "videomimedata.h"
+#include "videosource.h"
+#include "ytsearch.h"
+#include "video.h"
+#include "searchparams.h"
-#define MAX_ITEMS 10
+static const int maxItems = 10;
static const QString recentKeywordsKey = "recentKeywords";
static const QString recentChannelsKey = "recentChannels";
-ListModel::ListModel(QWidget *parent) : QAbstractListModel(parent) {
- youtubeSearch = 0;
+PlaylistModel::PlaylistModel(QWidget *parent) : QAbstractListModel(parent) {
+ videoSource = 0;
searching = false;
canSearchMore = true;
m_activeVideo = 0;
m_activeRow = -1;
skip = 1;
+ max = 0;
hoveredRow = -1;
authorHovered = false;
authorPressed = false;
}
-ListModel::~ListModel() {
- delete youtubeSearch;
-}
-
-int ListModel::rowCount(const QModelIndex &/*parent*/) const {
+int PlaylistModel::rowCount(const QModelIndex &/*parent*/) const {
int count = videos.size();
// add the message item
return count;
}
-QVariant ListModel::data(const QModelIndex &index, int role) const {
+QVariant PlaylistModel::data(const QModelIndex &index, int role) const {
int row = index.row();
case Qt::StatusTipRole:
if (!errorMessage.isEmpty()) return errorMessage;
if (searching) return tr("Searching...");
- if (canSearchMore) return tr("Show %1 More").arg(MAX_ITEMS);
+ if (canSearchMore) return tr("Show %1 More").arg(maxItems);
if (videos.isEmpty()) return tr("No videos");
else return tr("No more videos");
case Qt::TextAlignmentRole:
return QVariant();
}
-void ListModel::setActiveRow( int row) {
+void PlaylistModel::setActiveRow( int row) {
if ( rowExists( row ) ) {
m_activeRow = row;
}
-int ListModel::nextRow() const {
+int PlaylistModel::nextRow() const {
int nextRow = m_activeRow + 1;
if (rowExists(nextRow))
return nextRow;
return -1;
}
-int ListModel::previousRow() const {
+int PlaylistModel::previousRow() const {
int prevRow = m_activeRow - 1;
if (rowExists(prevRow))
return prevRow;
return -1;
}
-Video* ListModel::videoAt( int row ) const {
+Video* PlaylistModel::videoAt( int row ) const {
if ( rowExists( row ) )
return videos.at( row );
return 0;
}
-Video* ListModel::activeVideo() const {
+Video* PlaylistModel::activeVideo() const {
return m_activeVideo;
}
-void ListModel::search(SearchParams *searchParams) {
-
- // delete current videos
+void PlaylistModel::setVideoSource(VideoSource *videoSource) {
while (!videos.isEmpty())
delete videos.takeFirst();
+
m_activeVideo = 0;
m_activeRow = -1;
skip = 1;
- errorMessage.clear();
reset();
- // (re)initialize the YouTubeSearch
- if (youtubeSearch) delete youtubeSearch;
- youtubeSearch = new YouTubeSearch();
- connect(youtubeSearch, SIGNAL(gotVideo(Video*)), this, SLOT(addVideo(Video*)));
- connect(youtubeSearch, SIGNAL(finished(int)), this, SLOT(searchFinished(int)));
- connect(youtubeSearch, SIGNAL(error(QString)), this, SLOT(searchError(QString)));
+ this->videoSource = videoSource;
+ connect(videoSource, SIGNAL(gotVideo(Video*)),
+ SLOT(addVideo(Video*)), Qt::UniqueConnection);
+ connect(videoSource, SIGNAL(finished(int)),
+ SLOT(searchFinished(int)), Qt::UniqueConnection);
+ connect(videoSource, SIGNAL(error(QString)),
+ SLOT(searchError(QString)), Qt::UniqueConnection);
- this->searchParams = searchParams;
- searching = true;
- youtubeSearch->search(searchParams, MAX_ITEMS, skip);
- skip += MAX_ITEMS;
+ searchMore();
}
-void ListModel::searchMore(int max) {
+void PlaylistModel::searchMore(int max) {
if (searching) return;
searching = true;
+ this->max = max;
errorMessage.clear();
- youtubeSearch->search(searchParams, max, skip);
+ videoSource->loadVideos(max, skip);
skip += max;
}
-void ListModel::searchMore() {
- searchMore(MAX_ITEMS);
+void PlaylistModel::searchMore() {
+ searchMore(maxItems);
}
-void ListModel::searchNeeded() {
+void PlaylistModel::searchNeeded() {
int remainingRows = videos.size() - m_activeRow;
- int rowsNeeded = MAX_ITEMS - remainingRows;
+ int rowsNeeded = maxItems - remainingRows;
if (rowsNeeded > 0)
searchMore(rowsNeeded);
}
-void ListModel::abortSearch() {
+void PlaylistModel::abortSearch() {
while (!videos.isEmpty())
delete videos.takeFirst();
reset();
- youtubeSearch->abort();
+ videoSource->abort();
searching = false;
}
-void ListModel::searchFinished(int total) {
+void PlaylistModel::searchFinished(int total) {
searching = false;
- canSearchMore = total > 0;
+ canSearchMore = total >= max;
// update the message item
- emit dataChanged( createIndex( MAX_ITEMS, 0 ), createIndex( MAX_ITEMS, columnCount() - 1 ) );
+ emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
- if (!youtubeSearch->getSuggestions().isEmpty()) {
- emit haveSuggestions(youtubeSearch->getSuggestions());
- }
+ if (!videoSource->getSuggestions().isEmpty())
+ emit haveSuggestions(videoSource->getSuggestions());
}
-void ListModel::searchError(QString message) {
+void PlaylistModel::searchError(QString message) {
errorMessage = message;
// update the message item
- emit dataChanged( createIndex( MAX_ITEMS, 0 ), createIndex( MAX_ITEMS, columnCount() - 1 ) );
+ emit dataChanged( createIndex( maxItems, 0 ), createIndex( maxItems, columnCount() - 1 ) );
}
-void ListModel::addVideo(Video* video) {
+void PlaylistModel::addVideo(Video* video) {
- connect(video, SIGNAL(gotThumbnail()), this, SLOT(updateThumbnail()));
+ connect(video, SIGNAL(gotThumbnail()), SLOT(updateThumbnail()), Qt::UniqueConnection);
+ video->loadThumbnail();
beginInsertRows(QModelIndex(), videos.size(), videos.size());
videos << video;
if (!settings.value("manualplay", false).toBool())
setActiveRow(0);
- // save keyword
- QString query = searchParams->keywords();
- if (!query.isEmpty() && !searchParams->isTransient()) {
- if (query.startsWith("http://")) {
- // Save the video title
- query += "|" + videos.first()->title();
+ if (videoSource->metaObject()->className() == QLatin1String("YTSearch")) {
+
+ static const int maxRecentElements = 10;
+
+ YTSearch *search = dynamic_cast<YTSearch *>(videoSource);
+ SearchParams *searchParams = search->getSearchParams();
+
+ // save keyword
+ QString query = searchParams->keywords();
+ if (!query.isEmpty() && !searchParams->isTransient()) {
+ if (query.startsWith("http://")) {
+ // Save the video title
+ query += "|" + videos.first()->title();
+ }
+ QStringList keywords = settings.value(recentKeywordsKey).toStringList();
+ keywords.removeAll(query);
+ keywords.prepend(query);
+ while (keywords.size() > maxRecentElements)
+ keywords.removeLast();
+ settings.setValue(recentKeywordsKey, keywords);
}
- QStringList keywords = settings.value(recentKeywordsKey).toStringList();
- keywords.removeAll(query);
- keywords.prepend(query);
- while (keywords.size() > 10)
- keywords.removeLast();
- settings.setValue(recentKeywordsKey, keywords);
- }
- // save channel
- QString channel = searchParams->author();
- if (!channel.isEmpty() && !searchParams->isTransient()) {
- QSettings settings;
- QStringList channels = settings.value(recentChannelsKey).toStringList();
- channels.removeAll(channel);
- channels.prepend(channel);
- while (channels.size() > 10)
- channels.removeLast();
- settings.setValue(recentChannelsKey, channels);
+ // save channel
+ QString channel = searchParams->author();
+ if (!channel.isEmpty() && !searchParams->isTransient()) {
+ if (!video->authorUri().isEmpty())
+ channel = video->authorUri() + "|" + video->author();
+ QStringList channels = settings.value(recentChannelsKey).toStringList();
+ channels.removeAll(channel);
+ channels.prepend(channel);
+ while (channels.size() > maxRecentElements)
+ channels.removeLast();
+ settings.setValue(recentChannelsKey, channels);
+ }
}
}
}
-void ListModel::updateThumbnail() {
+void PlaylistModel::updateThumbnail() {
Video *video = static_cast<Video *>(sender());
if (!video) {
/**
* This function does not free memory
*/
-bool ListModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
+bool PlaylistModel::removeRows(int position, int rows, const QModelIndex & /*parent*/) {
beginRemoveRows(QModelIndex(), position, position+rows-1);
for (int row = 0; row < rows; ++row) {
videos.removeAt(position);
return true;
}
-void ListModel::removeIndexes(QModelIndexList &indexes) {
+void PlaylistModel::removeIndexes(QModelIndexList &indexes) {
QList<Video*> originalList(videos);
QList<Video*> delitems;
foreach (QModelIndex index, indexes) {
-Qt::DropActions ListModel::supportedDropActions() const {
+Qt::DropActions PlaylistModel::supportedDropActions() const {
return Qt::MoveAction;
}
-Qt::ItemFlags ListModel::flags(const QModelIndex &index) const {
+Qt::ItemFlags PlaylistModel::flags(const QModelIndex &index) const {
if (index.isValid())
if (index.row() == videos.size()) {
// don't drag the "show 10 more" item
- return Qt::ItemIsEnabled;
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
} else return (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled);
return Qt::ItemIsDropEnabled;
}
-QStringList ListModel::mimeTypes() const {
+QStringList PlaylistModel::mimeTypes() const {
QStringList types;
types << "application/x-minitube-video";
return types;
}
-QMimeData* ListModel::mimeData( const QModelIndexList &indexes ) const {
+QMimeData* PlaylistModel::mimeData( const QModelIndexList &indexes ) const {
VideoMimeData* mime = new VideoMimeData();
foreach( const QModelIndex &it, indexes ) {
return mime;
}
-bool ListModel::dropMimeData(const QMimeData *data,
- Qt::DropAction action, int row, int column,
- const QModelIndex &parent) {
+bool PlaylistModel::dropMimeData(const QMimeData *data,
+ Qt::DropAction action, int row, int column,
+ const QModelIndex &parent) {
if (action == Qt::IgnoreAction)
return true;
}
-int ListModel::rowForVideo(Video* video) {
+int PlaylistModel::rowForVideo(Video* video) {
return videos.indexOf(video);
}
-QModelIndex ListModel::indexForVideo(Video* video) {
+QModelIndex PlaylistModel::indexForVideo(Video* video) {
return createIndex(videos.indexOf(video), 0);
}
-void ListModel::move(QModelIndexList &indexes, bool up) {
+void PlaylistModel::move(QModelIndexList &indexes, bool up) {
QList<Video*> movedVideos;
foreach (QModelIndex index, indexes) {
/* row hovering */
-void ListModel::setHoveredRow(int row) {
+void PlaylistModel::setHoveredRow(int row) {
int oldRow = hoveredRow;
hoveredRow = row;
emit dataChanged( createIndex( oldRow, 0 ), createIndex( oldRow, columnCount() - 1 ) );
emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
}
-void ListModel::clearHover() {
+void PlaylistModel::clearHover() {
emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
hoveredRow = -1;
}
/* clickable author */
-void ListModel::enterAuthorHover() {
+void PlaylistModel::enterAuthorHover() {
if (authorHovered) return;
authorHovered = true;
updateAuthor();
}
-void ListModel::exitAuthorHover() {
+void PlaylistModel::exitAuthorHover() {
if (!authorHovered) return;
authorHovered = false;
updateAuthor();
setHoveredRow(hoveredRow);
}
-void ListModel::enterAuthorPressed() {
+void PlaylistModel::enterAuthorPressed() {
if (authorPressed) return;
authorPressed = true;
updateAuthor();
}
-void ListModel::exitAuthorPressed() {
+void PlaylistModel::exitAuthorPressed() {
if (!authorPressed) return;
authorPressed = false;
updateAuthor();
}
-void ListModel::updateAuthor() {
+void PlaylistModel::updateAuthor() {
emit dataChanged( createIndex( hoveredRow, 0 ), createIndex( hoveredRow, columnCount() - 1 ) );
}
-#ifndef LISTMODEL_H
-#define LISTMODEL_H
+#ifndef PLAYLISTMODEL_H
+#define PLAYLISTMODEL_H
-#include "video.h"
-#include "youtubesearch.h"
-#include "searchparams.h"
+#include <QtGui>
+
+class Video;
+class VideoSource;
enum DataRoles {
ItemTypeRole = Qt::UserRole,
ItemTypeShowMore
};
-class ListModel : public QAbstractListModel {
+class PlaylistModel : public QAbstractListModel {
Q_OBJECT
public:
+ PlaylistModel(QWidget *parent = 0);
- ListModel(QWidget *parent);
- ~ListModel();
-
- // inherited from QAbstractListModel
int rowCount(const QModelIndex &parent = QModelIndex()) const;
- // int rowCount( const QModelIndex& parent = QModelIndex() ) const { Q_UNUSED( parent ); return videos.size(); }
- int columnCount( const QModelIndex& parent = QModelIndex() ) const { Q_UNUSED( parent ); return 4; }
+ int columnCount( const QModelIndex& parent = QModelIndex() ) const { Q_UNUSED( parent ); return 1; }
QVariant data(const QModelIndex &index, int role) const;
bool removeRows(int position, int rows, const QModelIndex &parent);
Qt::DropAction action, int row, int column,
const QModelIndex &parent);
- // custom methods
void setActiveRow( int row );
bool rowExists( int row ) const { return (( row >= 0 ) && ( row < videos.size() ) ); }
int activeRow() const { return m_activeRow; } // returns -1 if there is no active row
Video* videoAt( int row ) const;
Video* activeVideo() const;
- // video search methods
- void search(SearchParams *searchParams);
+ VideoSource* getVideoSource() { return videoSource; }
+ void setVideoSource(VideoSource *videoSource);
void abortSearch();
-
public slots:
void searchMore();
void searchNeeded();
private:
void searchMore(int max);
- YouTubeSearch *youtubeSearch;
- SearchParams *searchParams;
+ VideoSource *videoSource;
bool searching;
bool canSearchMore;
QList<Video*> videos;
int skip;
+ int max;
- // the row being played
int m_activeRow;
Video *m_activeVideo;
--- /dev/null
+#include "playlistsuggest.h"
+#include <QtXml>
+#include "networkaccess.h"
+
+namespace The {
+NetworkAccess* http();
+}
+
+struct Playlist {
+ QString id;
+ QString title;
+ QString summary;
+ QString author;
+ int videoCount;
+};
+
+PlaylistSuggest::PlaylistSuggest(QObject *parent) : Suggester() {
+
+}
+
+void PlaylistSuggest::suggest(QString query) {
+ QUrl url("http://gdata.youtube.com/feeds/api/playlists/snippets");
+ url.addQueryItem("v", "2");
+ url.addQueryItem("q", query);
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(handleNetworkData(QByteArray)));
+}
+
+void PlaylistSuggest::handleNetworkData(QByteArray data) {
+ QList<Playlist> playlists;
+
+ QXmlStreamReader xml(data);
+ while (!xml.atEnd()) {
+ xml.readNext();
+ if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == "entry") {
+
+ Playlist playlist = {};
+
+ while (xml.readNextStartElement()) {
+ if (xml.name() == "title") {
+ playlist.title = xml.readElementText();
+ }
+ else if (xml.name() == "summary") {
+ playlist.summary = xml.readElementText();
+ }
+ else if (xml.name() == "author") {
+ while (xml.readNextStartElement()) {
+ if (xml.name() == "name") {
+ playlist.author = xml.readElementText();
+ break;
+ }
+ }
+ }
+ else if (xml.name() == "playlistId") {
+ playlist.id = xml.readElementText();
+ }
+ else if (xml.name() == "countHint") {
+ playlist.videoCount = xml.readElementText().toInt();
+ }
+ }
+
+ playlists << playlist;
+
+ }
+ }
+
+ // emit ready(choices);
+}
+
+/* model */
+
+class PlaylistSuggestModel : public QAbstractListModel {
+
+ Q_OBJECT
+
+public:
+ PlaylistSuggestModel(QWidget *parent) : QAbstractListModel(parent) { }
+ int rowCount(const QModelIndex &parent = QModelIndex()) const {
+ Q_UNUSED(parent);
+ return list.size();
+ }
+ int columnCount( const QModelIndex& parent = QModelIndex() ) const {
+ Q_UNUSED(parent);
+ return 1;
+ }
+ QVariant data(const QModelIndex &index, int role) const {
+ Q_UNUSED(index);
+ Q_UNUSED(role);
+ return QVariant();
+ }
+ QList<Playlist> list;
+
+};
+
+/* delegate */
+
+class PlaylistSuggestDelegate : public QStyledItemDelegate {
+
+ Q_OBJECT
+
+public:
+ PlaylistSuggestDelegate(QObject* parent);
+ QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const {
+ return QSize(0, 100);
+ }
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const {
+ QStyleOptionViewItemV4 opt = QStyleOptionViewItemV4(option);
+ initStyleOption(&opt, index);
+ opt.text = "";
+ opt.widget->style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
+
+ painter->save();
+ painter->translate(option.rect.topLeft());
+
+ QRect line(0, 0, option.rect.width(), option.rect.height());
+
+ painter->restore();
+
+ }
+
+};
--- /dev/null
+#ifndef PLAYLISTSUGGEST_H
+#define PLAYLISTSUGGEST_H
+
+#include <QtGui>
+#include "suggester.h"
+
+class PlaylistSuggest : public Suggester {
+
+ Q_OBJECT
+
+public:
+ PlaylistSuggest(QObject *parent = 0);
+ void suggest(QString query);
+
+signals:
+ void ready(QStringList);
+
+private slots:
+ void handleNetworkData(QByteArray response);
+
+};
+
+#endif // PLAYLISTSUGGEST_H
#include "playlistview.h"
-#include "listmodel.h"
+#include "playlistmodel.h"
#include "playlistitemdelegate.h"
PlaylistView::PlaylistView(QWidget *parent) : QListView(parent) {
- connect(this, SIGNAL(entered(const QModelIndex &)), SLOT(itemEntered(const QModelIndex &)));
+ clickableAuthors = true;
+
+ setItemDelegate(new PlaylistItemDelegate(this));
+ setSelectionMode(QAbstractItemView::ExtendedSelection);
+
+ // dragndrop
+ setDragEnabled(true);
+ setAcceptDrops(true);
+ setDropIndicatorShown(true);
+ setDragDropMode(QAbstractItemView::DragDrop);
+
+ // cosmetics
+ setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
+ setFrameShape(QFrame::NoFrame);
+ setAttribute(Qt::WA_MacShowFocusRect, false);
+ setMinimumSize(320, 240);
+ setUniformItemSizes(true);
+
+ connect(this, SIGNAL(entered(const QModelIndex &)),
+ SLOT(itemEntered(const QModelIndex &)));
setMouseTracking(true);
}
void PlaylistView::itemEntered(const QModelIndex &index) {
- ListModel *listModel = dynamic_cast<ListModel *>(model());
+ PlaylistModel *listModel = dynamic_cast<PlaylistModel *>(model());
if (listModel) listModel->setHoveredRow(index.row());
}
void PlaylistView::leaveEvent(QEvent * /* event */) {
- ListModel *listModel = dynamic_cast<ListModel *>(model());
+ PlaylistModel *listModel = dynamic_cast<PlaylistModel *>(model());
if (listModel) listModel->clearHover();
}
void PlaylistView::mouseMoveEvent(QMouseEvent *event) {
QListView::mouseMoveEvent(event);
- QWidget::mouseMoveEvent(event);
-
- if (isHoveringAuthor(event)) {
-
- // check for special "message" item
- ListModel *listModel = dynamic_cast<ListModel *>(model());
- if (listModel && listModel->rowCount() == indexAt(event->pos()).row())
- return;
+ // QWidget::mouseMoveEvent(event);
+ if (isHoveringThumbnail(event)) {
+ setCursor(Qt::PointingHandCursor);
+ } else if (isShowMoreItem(indexAt(event->pos()))) {
+ setCursor(Qt::PointingHandCursor);
+ } else if (isHoveringAuthor(event)) {
QMetaObject::invokeMethod(model(), "enterAuthorHover");
setCursor(Qt::PointingHandCursor);
} else {
QMetaObject::invokeMethod(model(), "exitAuthorHover");
unsetCursor();
}
-
}
void PlaylistView::mousePressEvent(QMouseEvent *event) {
- if (event->button() == Qt::LeftButton
- && isHoveringAuthor(event)) {
- QMetaObject::invokeMethod(model(), "enterAuthorPressed");
- event->ignore();
- } else {
- QListView::mousePressEvent(event);
- }
+ if (event->button() == Qt::LeftButton) {
+ if (isHoveringThumbnail(event)) {
+ event->accept();
+ } else if (isHoveringAuthor(event)) {
+ QMetaObject::invokeMethod(model(), "enterAuthorPressed");
+ event->ignore();
+ } else QListView::mousePressEvent(event);
+ } else QListView::mousePressEvent(event);
}
void PlaylistView::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
QMetaObject::invokeMethod(model(), "exitAuthorPressed");
- if (isHoveringAuthor(event))
- emit authorPushed(indexAt(event->pos()));
+ const QModelIndex index = indexAt(event->pos());
+ if (isHoveringThumbnail(event)) {
+ emit activated(index);
+ unsetCursor();
+ } else if (isHoveringAuthor(event)) {
+ emit authorPushed(index);
+ } else if (isShowMoreItem(index)) {
+ PlaylistModel *listModel = dynamic_cast<PlaylistModel *>(model());
+ listModel->searchMore();
+ unsetCursor();
+ }
+
} else {
QListView::mousePressEvent(event);
}
}
bool PlaylistView::isHoveringAuthor(QMouseEvent *event) {
+ if (!clickableAuthors) return false;
+
const QModelIndex itemIndex = indexAt(event->pos());
const QRect itemRect = visualRect(itemIndex);
// qDebug() << " itemRect.x()" << itemRect.x();
return ret;
}
+
+bool PlaylistView::isHoveringThumbnail(QMouseEvent *event) {
+ const QModelIndex index = indexAt(event->pos());
+ const QRect itemRect = visualRect(index);
+ QRect thumbRect(0, 0, 120, 90);
+ const int x = event->x() - itemRect.x() - thumbRect.x();
+ const int y = event->y() - itemRect.y() - thumbRect.y();
+ return x > 0 && x < thumbRect.width() && y > 0 && y < thumbRect.height();
+}
+
+bool PlaylistView::isShowMoreItem(const QModelIndex &index) {
+ return model()->rowCount() > 1 &&
+ model()->rowCount() == index.row() + 1;
+}
public:
PlaylistView(QWidget *parent = 0);
+ void setClickableAuthors(bool enabled) { clickableAuthors = enabled; }
protected:
void leaveEvent(QEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
- bool isHoveringAuthor(QMouseEvent *event);
signals:
void authorPushed(QModelIndex index);
private slots:
void itemEntered(const QModelIndex &index);
+private:
+ bool isHoveringAuthor(QMouseEvent *event);
+ bool isShowMoreItem(const QModelIndex &index);
+ bool isHoveringThumbnail(QMouseEvent *event);
+
+ bool clickableAuthors;
+
};
#endif // PLAYLISTVIEW_H
--- /dev/null
+#include "regionsview.h"
+#include "ytregions.h"
+#include "mainwindow.h"
+
+RegionsView::RegionsView(QWidget *parent) : QWidget(parent) {
+ QBoxLayout *l = new QVBoxLayout(this);
+ l->setMargin(30);
+ l->setSpacing(30);
+
+ layout = new QGridLayout();
+ layout->setMargin(0);
+ layout->setSpacing(0);
+ l->addLayout(layout);
+
+ addRegion(YTRegions::worldwideRegion());
+ foreach(YTRegion region, YTRegions::list())
+ addRegion(region);
+
+ doneButton = new QPushButton(tr("Done"));
+ doneButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ doneButton->setDefault(true);
+ doneButton->setProperty("custom", true);
+ doneButton->setProperty("important", true);
+ doneButton->setProperty("big", true);
+ connect(doneButton, SIGNAL(clicked()), MainWindow::instance(), SLOT(goBack()));
+ l->addWidget(doneButton, 0, Qt::AlignCenter);
+}
+
+void RegionsView::addRegion(const YTRegion ®ion) {
+ QPushButton *button = new QPushButton(region.name);
+ button->setProperty("regionId", region.id);
+ button->setCheckable(true);
+ button->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
+ button->setFocusPolicy(Qt::StrongFocus);
+ button->setIcon(YTRegions::iconForRegionId(region.id));
+ connect(button, SIGNAL(clicked()), SLOT(buttonClicked()));
+ const int i = layout->count();
+ static const int rows = 10;
+ layout->addWidget(button, i % rows, i / rows);
+}
+
+void RegionsView::appear() {
+ doneButton->setFocus();
+
+ QString currentRegionId = YTRegions::currentRegionId();
+ for (int i = 0; i < layout->count(); i++) {
+ QLayoutItem *item = layout->itemAt(i);
+ QPushButton *b = static_cast<QPushButton*>(item->widget());
+ QString regionId = b->property("regionId").toString();
+ b->setChecked(currentRegionId == regionId);
+ }
+}
+
+void RegionsView::buttonClicked() {
+ QObject* o = sender();
+ QString regionId = o->property("regionId").toString();
+ YTRegions::setRegion(regionId);
+ emit regionChanged();
+ doneButton->click();
+
+ // uncheck other buttons
+ /*
+ for (int i = 0; i < layout->count(); i++) {
+ QLayoutItem *item = layout->itemAt(i);
+ QPushButton *b = static_cast<QPushButton*>(item->widget());
+ if (b != o && b->isChecked()) b->setChecked(false);
+ }
+ */
+}
--- /dev/null
+#ifndef REGIONSVIEW_H
+#define REGIONSVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class YTRegion;
+
+class RegionsView : public QWidget, public View {
+
+ Q_OBJECT
+
+public:
+ RegionsView(QWidget *parent = 0);
+ void appear();
+
+signals:
+ void regionChanged();
+
+private slots:
+ void buttonClicked();
+
+private:
+ void addRegion(const YTRegion ®ion);
+ QGridLayout *layout;
+ QPushButton *doneButton;
+
+};
+
+#endif // REGIONSVIEW_H
#include "searchparams.h"
-SearchParams::SearchParams() {
+SearchParams::SearchParams(QObject *parent) : QObject(parent) {
m_transient = false;
m_sortBy = SortByRelevance;
m_duration = DurationAny;
TimeMonth
};
- SearchParams();
+ SearchParams(QObject *parent = 0);
const QString keywords() const { return m_keywords; }
void setKeywords( QString keywords ) { m_keywords = keywords; }
int time() const { return m_time; }
void setTime( int time ) { m_time = time; }
+ bool operator==(const SearchParams &other) const {
+ return m_keywords == other.keywords() &&
+ m_author == other.author();
+ }
+
public slots:
void setParam(QString name, QVariant value);
#include "constants.h"
#include "fontutils.h"
#include "searchparams.h"
-#include "youtubesuggest.h"
+#include "ytsuggester.h"
#include "channelsuggest.h"
#ifdef APP_MAC
#include "searchlineedit_mac.h"
connect(queryEdit, SIGNAL(textChanged(const QString &)), SLOT(textChanged(const QString &)));
connect(queryEdit, SIGNAL(suggestionAccepted(const QString&)), SLOT(watch(const QString&)));
- youtubeSuggest = new YouTubeSuggest(this);
+ youtubeSuggest = new YTSuggester(this);
channelSuggest = new ChannelSuggest(this);
searchTypeChanged(0);
foreach (QString keyword, keywords) {
QString link = keyword;
QString display = keyword;
- if (keyword.startsWith("http://") || keyword.startsWith("https://")) {
- int separator = keyword.indexOf("|");
- if (separator > 0 && separator + 1 < keyword.length()) {
- link = keyword.left(separator);
- display = keyword.mid(separator+1);
- }
+ int separator = keyword.indexOf('|');
+ if (separator > 0 && separator + 1 < keyword.length()) {
+ link = keyword.left(separator);
+ display = keyword.mid(separator+1);
}
QLabel *itemLabel = new QLabel("<a href=\"" + link
+ "\" style=\"color:palette(text); text-decoration:none\">"
class SearchLineEdit;
class SearchParams;
-class YouTubeSuggest;
+class YTSuggester;
class ChannelSuggest;
class SearchView : public QWidget, public View {
Q_OBJECT
public:
- SearchView(QWidget *parent);
+ SearchView(QWidget *parent = 0);
void updateRecentKeywords();
void updateRecentChannels();
QHash<QString, QVariant> metadata() {
public slots:
void appear();
+ void disappear() { }
void watch(QString query);
void watchChannel(QString channel);
void watchKeywords(QString query);
void searchTypeChanged(int index);
private:
- YouTubeSuggest *youtubeSuggest;
+ YTSuggester *youtubeSuggest;
ChannelSuggest *channelSuggest;
QComboBox *typeCombo;
void SegmentedControl::leaveEvent(QEvent *event) {
QWidget::leaveEvent(event);
- // status tip
- MainWindow::instance()->statusBar()->clearMessage();
+ // status tip
+ // static_cast<QMainWindow*>(window())->statusBar()->clearMessage();
d->hoveredAction = 0;
d->pressedAction = 0;
update();
--- /dev/null
+#include "sidebarheader.h"
+#include "iconloader/qticonloader.h"
+#include "mediaview.h"
+#include "videosource.h"
+#include "fontutils.h"
+
+SidebarHeader::SidebarHeader(QWidget *parent) : QToolBar(parent) { }
+
+void SidebarHeader::setup() {
+ static bool isSetup = false;
+ if (isSetup) return;
+ isSetup = true;
+
+ backAction = new QAction(
+ QtIconLoader::icon("go-previous"),
+ tr("&Back"), this);
+ connect(backAction, SIGNAL(triggered()), MediaView::instance(), SLOT(goBack()));
+ addAction(backAction);
+
+ forwardAction = new QAction(
+ QtIconLoader::icon("go-next"),
+ tr("&Back"), this);
+ connect(forwardAction, SIGNAL(triggered()), MediaView::instance(), SLOT(goForward()));
+ addAction(forwardAction);
+
+ /*
+ QWidget *spacerWidget = new QWidget(this);
+ spacerWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+ spacerWidget->setVisible(true);
+ addWidget(spacerWidget);
+ */
+}
+
+QSize SidebarHeader::minimumSizeHint (void) const {
+ return(QSize(1, QFontMetrics(font()).height() * 1.9));
+}
+
+void SidebarHeader::updateInfo() {
+ setup();
+
+ QList<VideoSource*> history = MediaView::instance()->getHistory();
+ int currentIndex = MediaView::instance()->getHistoryIndex();
+
+ bool canGoForward = MediaView::instance()->canGoForward();
+ forwardAction->setVisible(canGoForward);
+ if (canGoForward) {
+ VideoSource *nextVideoSource = history.at(currentIndex + 1);
+ forwardAction->setStatusTip(
+ tr("Forward to %1")
+ .arg(nextVideoSource->getName()));
+ }
+
+ bool canGoBack = MediaView::instance()->canGoBack();
+ bool backVisible = canGoForward || canGoBack;
+ backAction->setVisible(backVisible);
+ backAction->setEnabled(canGoBack);
+ if (canGoBack) {
+ VideoSource *previousVideoSource = history.at(currentIndex - 1);
+ backAction->setStatusTip(
+ tr("Back to %1")
+ .arg(previousVideoSource->getName()));
+ }
+
+ VideoSource *currentVideoSource = history.at(currentIndex);
+ connect(currentVideoSource, SIGNAL(nameChanged(QString)),
+ SLOT(updateTitle(QString)), Qt::UniqueConnection);
+ setTitle(currentVideoSource->getName());
+}
+
+void SidebarHeader::updateTitle(QString title) {
+ sender()->disconnect(this);
+ setTitle(title);
+}
+
+void SidebarHeader::setTitle(QString title) {
+ this->title = title;
+ update();
+}
+
+void SidebarHeader::paintEvent(QPaintEvent *event) {
+ QToolBar::paintEvent(event);
+ if (title.isEmpty()) return;
+ QPainter p(this);
+ p.setFont(FontUtils::smallBold());
+ p.setPen(Qt::white);
+
+ const QRect r = rect();
+
+ QString t = title;
+ QRect textBox = p.boundingRect(r, Qt::AlignCenter, t);
+ int i = 1;
+ static const int margin = 100;
+ while (textBox.width() > r.width() - margin) {
+ t = t.left(t.length() - i) + "...";
+ textBox = p.boundingRect(r, Qt::AlignCenter, t);
+ i++;
+ }
+
+ p.drawText(r, Qt::AlignCenter, t);
+}
--- /dev/null
+#ifndef SIDEBARHEADER_H
+#define SIDEBARHEADER_H
+
+#include <QtGui>
+
+class SidebarHeader : public QToolBar {
+
+ Q_OBJECT
+
+public:
+ SidebarHeader(QWidget *parent = 0);
+ void updateInfo();
+
+protected:
+ QSize minimumSizeHint() const;
+ void paintEvent(QPaintEvent *event);
+
+private slots:
+ void updateTitle(QString title);
+
+private:
+ void setup();
+ void setTitle(QString title);
+
+ QAction *backAction;
+ QAction * forwardAction;
+ QString title;
+};
+
+#endif // SIDEBARHEADER_H
#include "sidebarwidget.h"
#include "refinesearchbutton.h"
#include "refinesearchwidget.h"
+#include "sidebarheader.h"
#ifndef Q_WS_X11
#include "extra.h"
#endif
playlist = 0;
QBoxLayout *layout = new QVBoxLayout(this);
- layout->setSpacing(1);
+ layout->setSpacing(0);
layout->setMargin(0);
+ sidebarHeader = new SidebarHeader();
+ layout->addWidget(sidebarHeader);
+
// hidden message widget
messageLabel = new QLabel(this);
messageLabel->setMargin(10);
void SidebarWidget::showPlaylist() {
setup();
stackedWidget->setCurrentWidget(playlist);
+ The::globalActions()->value("refine-search")->setChecked(false);
}
void SidebarWidget::showRefineSearchWidget() {
+ if (!refineSearchWidget->isEnabled()) return;
refineSearchWidget->setDirty(false);
stackedWidget->setCurrentWidget(refineSearchWidget);
refineSearchWidget->setFocus();
}
void SidebarWidget::showRefineSearchButton() {
+ if (!refineSearchWidget->isEnabled()) return;
refineSearchButton->move(
playlist->viewport()->width() - refineSearchButton->minimumWidth(),
height() - refineSearchButton->minimumHeight());
class RefineSearchButton;
class RefineSearchWidget;
+class SidebarHeader;
class SidebarWidget : public QWidget {
void setPlaylist(QListView *playlist);
void showPlaylist();
RefineSearchWidget* getRefineSearchWidget() { return refineSearchWidget; }
+ SidebarHeader* getHeader() { return sidebarHeader; }
void hideSuggestions();
public slots:
RefineSearchWidget *refineSearchWidget;
QTimer *mouseTimer;
QLabel *messageLabel;
-
+ SidebarHeader *sidebarHeader;
};
#endif // SIDEBARWIDGET_H
--- /dev/null
+#include "standardfeedsview.h"
+#include "videosourcewidget.h"
+#include "ytcategories.h"
+#include "ytstandardfeed.h"
+#include "ytregions.h"
+#include "mainwindow.h"
+
+namespace The {
+QMap<QString, QAction*>* globalActions();
+}
+
+static const int cols = 5;
+
+StandardFeedsView::StandardFeedsView(QWidget *parent) : QWidget(parent),
+ layout(0) {
+ QPalette p = palette();
+ p.setBrush(QPalette::Window, Qt::black);
+ setPalette(p);
+ setAutoFillBackground(true);
+
+ connect(The::globalActions()->value("worldwide-region"), SIGNAL(triggered()),
+ SLOT(selectWorldwideRegion()));
+
+ connect(The::globalActions()->value("local-region"), SIGNAL(triggered()),
+ SLOT(selectLocalRegion()));
+
+ /*
+ QAction *regionAction = MainWindow::instance()->getRegionAction();
+ connect(regionAction, SIGNAL(changed()), SLOT(load()));
+ */
+}
+
+void StandardFeedsView::load() {
+ YTCategories *youTubeCategories = new YTCategories(this);
+ connect(youTubeCategories, SIGNAL(categoriesLoaded(const QList<YTCategory> &)),
+ SLOT(layoutCategories(const QList<YTCategory> &)));
+ youTubeCategories->loadCategories();
+
+ if (layout) {
+ while (QLayoutItem *item = layout->takeAt(0)) {
+ delete item->widget();
+ delete item;
+ }
+ delete layout;
+ }
+
+ layout = new QGridLayout(this);
+ layout->setMargin(0);
+ layout->setSpacing(1);
+
+ QList<YTStandardFeed*> feeds = getMainFeeds();
+ foreach(YTStandardFeed *feed, feeds)
+ addVideoSourceWidget(feed);
+
+ YTRegion region = YTRegions::currentRegion();
+ QToolButton *regionButton = MainWindow::instance()->getRegionButton();
+ regionButton->setText(region.name);
+ regionButton->setIcon(YTRegions::iconForRegionId(region.id));
+}
+
+void StandardFeedsView::layoutCategories(const QList<YTCategory> &categories) {
+ QString regionId = YTRegions::currentRegionId();
+ foreach(YTCategory category, categories) {
+ // assign a parent to this VideoSource so it won't be deleted by MediaView
+ YTStandardFeed *feed = new YTStandardFeed(this);
+ feed->setCategory(category.term);
+ feed->setLabel(category.label);
+ feed->setRegionId(regionId);
+ feed->setFeedId("most_popular");
+ addVideoSourceWidget(feed);
+ }
+}
+
+void StandardFeedsView::addVideoSourceWidget(VideoSource *videoSource) {
+ VideoSourceWidget *w = new VideoSourceWidget(videoSource);
+ connect(w, SIGNAL(activated(VideoSource*)),
+ SIGNAL(activated(VideoSource*)));
+ int i = layout->count();
+ layout->addWidget(w, i / cols, i % cols);
+}
+
+QList<YTStandardFeed*> StandardFeedsView::getMainFeeds() {
+ QList<YTStandardFeed*> feeds;
+
+ feeds << buildStardardFeed("most_popular", tr("Most Popular"))
+ << buildStardardFeed("recently_featured", tr("Featured"))
+ << buildStardardFeed("most_shared", tr("Most Shared"))
+ << buildStardardFeed("most_discussed", tr("Most Discussed"))
+ << buildStardardFeed("top_rated", tr("Top Rated"));
+
+ return feeds;
+}
+
+YTStandardFeed* StandardFeedsView::buildStardardFeed(QString feedId, QString label) {
+ YTStandardFeed *feed = new YTStandardFeed(this);
+ feed->setFeedId(feedId);
+ feed->setLabel(label);
+ feed->setRegionId(YTRegions::currentRegionId());
+ return feed;
+}
+
+void StandardFeedsView::appear() {
+ setFocus();
+ if (!layout) load();
+ QAction *regionAction = MainWindow::instance()->getRegionAction();
+ regionAction->setVisible(true);
+}
+
+void StandardFeedsView::disappear() {
+ QAction *regionAction = MainWindow::instance()->getRegionAction();
+ regionAction->setVisible(false);
+}
+
+void StandardFeedsView::selectWorldwideRegion() {
+ YTRegions::setRegion(YTRegions::worldwideRegion().id);
+ load();
+}
+
+void StandardFeedsView::selectLocalRegion() {
+ YTRegions::setRegion(YTRegions::localRegion().id);
+ load();
+}
+
+
--- /dev/null
+#ifndef CATEGORIESVIEW_H
+#define CATEGORIESVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class VideoSource;
+class YTCategory;
+class YTStandardFeed;
+
+class StandardFeedsView : public QWidget, public View {
+
+ Q_OBJECT
+
+public:
+ StandardFeedsView(QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *standardFeed);
+
+public slots:
+ void appear();
+ void disappear();
+ void load();
+
+private slots:
+ void layoutCategories(const QList<YTCategory> &categories);
+ void selectWorldwideRegion();
+ void selectLocalRegion();
+
+private:
+ void addVideoSourceWidget(VideoSource *videoSource);
+ QList<YTStandardFeed*> getMainFeeds();
+ YTStandardFeed* buildStardardFeed(QString feedId, QString label);
+ QGridLayout *layout;
+
+};
+
+#endif // CATEGORIESVIEW_H
--- /dev/null
+#include "userview.h"
+
+UserView::UserView(QWidget *parent) : QWidget(parent) {
+ layout = new QGridLayout(this);
+}
--- /dev/null
+#ifndef USERVIEW_H
+#define USERVIEW_H
+
+#include <QtGui>
+#include "view.h"
+
+class VideoSource;
+
+class UserView : public QWidget, public View {
+
+ Q_OBJECT
+
+public:
+ UserView(QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *standardFeed);
+
+private:
+ QGridLayout *layout;
+
+};
+
+#endif // USERVIEW_H
cloneVideo->m_webpage = m_webpage;
cloneVideo->m_streamUrl = m_streamUrl;
cloneVideo->m_thumbnail = m_thumbnail;
- cloneVideo->m_thumbnailUrls = m_thumbnailUrls;
+ cloneVideo->m_thumbnailUrl = m_thumbnailUrl;
+ cloneVideo->m_mediumThumbnailUrl = m_mediumThumbnailUrl;
cloneVideo->m_duration = m_duration;
cloneVideo->m_published = m_published;
cloneVideo->m_viewCount = m_viewCount;
return cloneVideo;
}
-void Video::preloadThumbnail() {
- if (m_thumbnailUrls.isEmpty()) return;
- QObject *reply = The::http()->get(m_thumbnailUrls.first());
+void Video::loadThumbnail() {
+ QObject *reply = The::http()->get(m_thumbnailUrl);
connect(reply, SIGNAL(data(QByteArray)), SLOT(setThumbnail(QByteArray)));
}
void Video::setThumbnail(QByteArray bytes) {
- m_thumbnail = QImage::fromData(bytes);
+ m_thumbnail.loadFromData(bytes);
emit gotThumbnail();
}
-const QImage Video::thumbnail() const {
- return m_thumbnail;
+void Video::loadMediumThumbnail() {
+ if (m_mediumThumbnailUrl.isEmpty()) return;
+ QObject *reply = The::http()->get(m_mediumThumbnailUrl);
+ connect(reply, SIGNAL(data(QByteArray)), SIGNAL(gotMediumThumbnail(QByteArray)));
}
void Video::loadStreamUrl() {
// Get Video ID
// youtube-dl line 428
// QRegExp re("^((?:http://)?(?:\\w+\\.)?youtube\\.com/(?:(?:v/)|(?:(?:watch(?:\\.php)?)?\\?(?:.+&)?v=)))?([0-9A-Za-z_-]+)(?(1).+)?$");
- QRegExp re("^http://www\\.youtube\\.com/watch\\?v=([0-9A-Za-z_-]+).*");
+ QRegExp re("^https?://www\\.youtube\\.com/watch\\?v=([0-9A-Za-z_-]+).*");
bool match = re.exactMatch(m_webpage.toString());
if (!match || re.numCaptures() < 1) {
+ qDebug() << QString("Cannot get video id for %1").arg(m_webpage.toString());
emit errorStreamUrl(QString("Cannot get video id for %1").arg(m_webpage.toString()));
loadingStreamUrl = false;
return;
// see you in gotHeadHeaders()
}
+
+
+QString Video::formattedDuration() const {
+ QString format = m_duration > 3600 ? "h:mm:ss" : "m:ss";
+ return QTime().addSecs(m_duration).toString(format);
+}
const QUrl webpage() const { return m_webpage; }
void setWebpage( QUrl webpage ) { m_webpage = webpage; }
- QList<QUrl> thumbnailUrls() const { return m_thumbnailUrls; }
- void addThumbnailUrl(QUrl url) {
- m_thumbnailUrls << url;
- }
+ void loadThumbnail();
+ const QPixmap & thumbnail() const { return m_thumbnail; }
- void preloadThumbnail();
- const QImage thumbnail() const;
+ QString thumbnailUrl() { return m_thumbnailUrl; }
+ void setThumbnailUrl(QString url) { m_thumbnailUrl = url; }
+
+ void loadMediumThumbnail();
+ QString mediumThumbnailUrl() { return m_mediumThumbnailUrl; }
+ void setMediumThumbnailUrl(QString url) { m_mediumThumbnailUrl = url; }
int duration() const { return m_duration; }
void setDuration( int duration ) { m_duration = duration; }
+ QString formattedDuration() const;
int viewCount() const { return m_viewCount; }
void setViewCount( int viewCount ) { m_viewCount = viewCount; }
QString id() { return videoId; }
-public slots:
- void setThumbnail(QByteArray bytes);
-
signals:
void gotThumbnail();
+ void gotMediumThumbnail(QByteArray bytes);
void gotStreamUrl(QUrl streamUrl);
void errorStreamUrl(QString message);
private slots:
+ void setThumbnail(QByteArray bytes);
void gotVideoInfo(QByteArray);
void errorVideoInfo(QNetworkReply*);
void scrapeWebPage(QByteArray);
QString m_authorUri;
QUrl m_webpage;
QUrl m_streamUrl;
- QImage m_thumbnail;
- QList<QUrl> m_thumbnailUrls;
+ QPixmap m_thumbnail;
+ QString m_thumbnailUrl;
+ QString m_mediumThumbnailUrl;
int m_duration;
QDateTime m_published;
int m_viewCount;
- // The YouTube video id
- // This is needed by the gotVideoInfo callback
QString videoId;
-
QString videoToken;
int definitionCode;
snapshotPreview = new QLabel(this);
stackedLayout->addWidget(snapshotPreview);
- setLayout(vLayout);
- setAcceptDrops(true);
-
+ setAcceptDrops(true);
setMouseTracking(true);
}
#include <QWidget>
#include "video.h"
#include "loadingwidget.h"
-#include "listmodel.h"
+#include "playlistmodel.h"
class VideoAreaWidget : public QWidget {
Q_OBJECT
public:
- VideoAreaWidget(QWidget *parent);
+ VideoAreaWidget(QWidget *parent = 0);
void setVideoWidget(QWidget *videoWidget);
void setLoadingWidget(LoadingWidget *loadingWidget);
void showLoading(Video* video);
void showVideo();
void showError(QString message);
void clear();
- void setListModel(ListModel *listModel) {
+ void setListModel(PlaylistModel *listModel) {
this->listModel = listModel;
}
void showSnapshotPreview(QPixmap pixmap);
QStackedLayout *stackedLayout;
QWidget *videoWidget;
LoadingWidget *loadingWidget;
- ListModel *listModel;
+ PlaylistModel *listModel;
QLabel *messageLabel;
QLabel *snapshotPreview;
--- /dev/null
+#include "videosource.h"
+
+void VideoSource::setParam(QString name, QVariant value) {
+ bool success = setProperty(name.toUtf8(), value);
+ if (!success) qWarning() << "Failed to set property" << name << value.toString();
+}
--- /dev/null
+#ifndef VIDEOSOURCE_H
+#define VIDEOSOURCE_H
+
+#include <QtCore>
+
+class Video;
+
+class VideoSource : public QObject {
+
+ Q_OBJECT
+
+public:
+ VideoSource(QObject *parent = 0) : QObject(parent) { }
+ virtual void loadVideos(int max, int skip) = 0;
+ virtual void abort() = 0;
+ virtual const QStringList & getSuggestions() = 0;
+ virtual QString getName() = 0;
+
+public slots:
+ void setParam(QString name, QVariant value);
+
+signals:
+ void gotVideo(Video *video);
+ void finished(int total);
+ void error(QString message);
+ void nameChanged(QString name);
+
+};
+
+#endif // VIDEOSOURCE_H
--- /dev/null
+#include "videosourcewidget.h"
+#include "videosource.h"
+#include "video.h"
+#include "fontutils.h"
+
+VideoSourceWidget::VideoSourceWidget(VideoSource *videoSource, QWidget *parent)
+ : QWidget(parent),
+ videoSource(videoSource),
+ hovered(false),
+ pressed(false) {
+
+ setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ setCursor(Qt::PointingHandCursor);
+ setFocusPolicy(Qt::StrongFocus);
+
+ connect(videoSource, SIGNAL(gotVideo(Video*)),
+ SLOT(previewVideo(Video*)), Qt::UniqueConnection);
+ videoSource->loadVideos(1, 1);
+}
+
+void VideoSourceWidget::activate() {
+ emit activated(videoSource);
+}
+
+void VideoSourceWidget::previewVideo(Video *video) {
+ videoSource->disconnect();
+ this->video = video;
+ connect(video, SIGNAL(gotMediumThumbnail(QByteArray)),
+ SLOT(setPixmapData(QByteArray)), Qt::UniqueConnection);
+ video->loadMediumThumbnail();
+}
+
+void VideoSourceWidget::setPixmapData(QByteArray bytes) {
+ video->deleteLater();
+ video = 0;
+ pixmap.loadFromData(bytes);
+ update();
+}
+
+QPixmap VideoSourceWidget::playPixmap() {
+ const int s = height() / 2;
+ const int padding = s / 8;
+ QPixmap playIcon = QPixmap(s, s);
+ playIcon.fill(Qt::transparent);
+ QPainter painter(&playIcon);
+ QPolygon polygon;
+ polygon << QPoint(padding, padding)
+ << QPoint(s - padding, s / 2)
+ << QPoint(padding, s - padding);
+ painter.setRenderHints(QPainter::Antialiasing, true);
+
+ // QColor color = pressed ? Qt::black : Qt::white;
+ QColor color = Qt::white;
+ painter.setBrush(color);
+ QPen pen;
+ pen.setColor(color);
+ pen.setWidth(10);
+ pen.setJoinStyle(Qt::RoundJoin);
+ pen.setCapStyle(Qt::RoundCap);
+ painter.setPen(pen);
+ painter.drawPolygon(polygon);
+ return playIcon;
+}
+
+void VideoSourceWidget::paintEvent(QPaintEvent *) {
+ if (pixmap.isNull()) return;
+
+ QPainter p(this);
+
+ const int w = width();
+ const int h = height();
+
+ int xOffset = 0;
+ int xOrigin = 0;
+ int wDiff = pixmap.width() - w;
+ if (wDiff > 0) xOffset = wDiff / 2;
+ else xOrigin = -wDiff / 2;
+ int yOffset = 0;
+ int yOrigin = 0;
+ int hDiff = pixmap.height() - h;
+ if (hDiff > 0) yOffset = hDiff / 4;
+ else yOrigin = -hDiff / 2;
+ p.drawPixmap(xOrigin, yOrigin, pixmap, xOffset, yOffset, w, h);
+
+ if (hovered) {
+ QPixmap play = playPixmap();
+ p.save();
+ p.setOpacity(.5);
+ p.drawPixmap(
+ (w - play.width()) / 2,
+ (h * 2/3 - play.height()) / 2,
+ play
+ );
+ p.restore();
+ }
+
+ QRect nameBox = rect();
+ nameBox.adjust(0, 0, 0, -h*2/3);
+ nameBox.translate(0, h - nameBox.height());
+ p.save();
+ p.setPen(Qt::NoPen);
+ p.setBrush(QColor(0, 0, 0, 128));
+ p.drawRect(nameBox);
+ p.restore();
+
+ QString name = videoSource->getName();
+ bool tooBig = false;
+ p.save();
+ p.setFont(FontUtils::medium());
+ QRect textBox = p.boundingRect(nameBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setFont(font());
+ textBox = p.boundingRect(nameBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+ if (textBox.height() > nameBox.height()) {
+ p.setClipRect(nameBox);
+ tooBig = true;
+ }
+ }
+ p.setPen(Qt::white);
+ if (tooBig)
+ p.drawText(nameBox, Qt::AlignHCenter | Qt::AlignTop | Qt::TextWordWrap, name);
+ else
+ p.drawText(textBox, Qt::AlignCenter | Qt::TextWordWrap, name);
+ p.restore();
+
+ if (hasFocus()) {
+ p.save();
+ QPen pen;
+ pen.setBrush(palette().highlight());
+ pen.setWidth(2);
+ p.setPen(pen);
+ p.drawRect(rect());
+ p.restore();
+ }
+}
+
+void VideoSourceWidget::mouseMoveEvent (QMouseEvent *event) {
+ QWidget::mouseMoveEvent(event);
+ hovered = rect().contains(event->pos());
+}
+
+void VideoSourceWidget::mousePressEvent(QMouseEvent *event) {
+ QWidget::mousePressEvent(event);
+ if (event->button() != Qt::LeftButton) return;
+ pressed = true;
+ update();
+}
+
+void VideoSourceWidget::mouseReleaseEvent(QMouseEvent *event) {
+ QWidget::mouseReleaseEvent(event);
+ if (event->button() != Qt::LeftButton) return;
+ pressed = false;
+ if (hovered) emit activated(videoSource);
+}
+
+void VideoSourceWidget::leaveEvent(QEvent *event) {
+ QWidget::leaveEvent(event);
+ hovered = false;
+ update();
+}
+
+void VideoSourceWidget::enterEvent(QEvent *event) {
+ QWidget::enterEvent(event);
+ hovered = true;
+ update();
+}
+
+void VideoSourceWidget::keyReleaseEvent(QKeyEvent *event) {
+ if (event->key() == Qt::Key_Return)
+ emit activated(videoSource);
+}
--- /dev/null
+#ifndef VIDEOSOURCEWIDGET_H
+#define VIDEOSOURCEWIDGET_H
+
+#include <QtGui>
+
+class Video;
+class VideoSource;
+
+class VideoSourceWidget : public QWidget {
+
+ Q_OBJECT
+
+public:
+ VideoSourceWidget(VideoSource *videoSource, QWidget *parent = 0);
+
+signals:
+ void activated(VideoSource *videoSource);
+
+protected:
+ void paintEvent(QPaintEvent *);
+ void mouseMoveEvent(QMouseEvent *event);
+ void mousePressEvent(QMouseEvent *event);
+ void mouseReleaseEvent(QMouseEvent *event);
+ void enterEvent(QEvent *event);
+ void leaveEvent(QEvent *event);
+ void keyReleaseEvent(QKeyEvent *event);
+
+private slots:
+ void activate();
+ void previewVideo(Video*);
+ void setPixmapData(QByteArray bytes);
+
+private:
+ QPixmap playPixmap();
+ VideoSource *videoSource;
+ QPixmap pixmap;
+ Video *video;
+
+ bool hovered;
+ bool pressed;
+};
+
+#endif // VIDEOSOURCEWIDGET_H
--- /dev/null
+#include "ytcategories.h"
+#include "networkaccess.h"
+#include <QtXml>
+
+namespace The {
+NetworkAccess* http();
+}
+
+YTCategories::YTCategories(QObject *parent) : QObject(parent) { }
+
+void YTCategories::loadCategories() {
+ QString url = "http://gdata.youtube.com/schemas/2007/categories.cat?hl=";
+#if QT_VERSION >= 0x040800
+ url += QLocale::system().uiLanguages().first();
+#else
+ url += QLocale::system().name().replace('_', '-');
+#endif
+ qDebug() << url;
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseCategories(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTCategories::parseCategories(QByteArray bytes) {
+ QList<YTCategory> categories;
+
+ QXmlStreamReader xml(bytes);
+ while (!xml.atEnd()) {
+ xml.readNext();
+ if (xml.isStartElement() && xml.name() == "category") {
+ QString term = xml.attributes().value("term").toString();
+ QString label = xml.attributes().value("label").toString();
+ while(xml.readNextStartElement())
+ if (xml.name() == "assignable") {
+ YTCategory category;
+ category.term = term;
+ category.label = label;
+ categories << category;
+ } else xml.skipCurrentElement();
+ }
+ }
+
+ if (xml.hasError()) {
+ emit error(xml.errorString());
+ return;
+ }
+
+ emit categoriesLoaded(categories);
+}
+
+void YTCategories::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+}
--- /dev/null
+#ifndef YTCATEGORIES_H
+#define YTCATEGORIES_H
+
+#include <QtNetwork>
+
+struct YTCategory {
+ QString term;
+ QString label;
+};
+
+class YTCategories : public QObject {
+
+ Q_OBJECT
+
+public:
+ YTCategories(QObject *parent = 0);
+ void loadCategories();
+
+signals:
+ void categoriesLoaded(const QList<YTCategory> &);
+ void error(QString message);
+
+private slots:
+ void parseCategories(QByteArray bytes);
+ void requestError(QNetworkReply *reply);
+
+};
+
+#endif // YTCATEGORIES_H
-#include "youtubestreamreader.h"
-#include <QtGui>
-
-
-YouTubeStreamReader::YouTubeStreamReader() {
-
-}
-
-bool YouTubeStreamReader::read(QByteArray data) {
- addData(data);
+#include "ytfeedreader.h"
+#include "video.h"
+YTFeedReader::YTFeedReader(const QByteArray &bytes) : QXmlStreamReader(bytes) {
while (!atEnd()) {
readNext();
- if (isStartElement()) {
- if (name() == "feed") {
- while (!atEnd()) {
- readNext();
- if (isStartElement() && name() == "entry") {
- readEntry();
- } else if (name() == "link"
- && attributes().value("rel").toString()
- == "http://schemas.google.com/g/2006#spellcorrection") {
- suggestions << attributes().value("title").toString();
- }
- }
- }
+ if (isStartElement() && name() == "entry") {
+ readEntry();
+ } else if (name() == "link"
+ && attributes().value("rel").toString()
+ == "http://schemas.google.com/g/2006#spellcorrection") {
+ suggestions << attributes().value("title").toString();
}
}
-
- return !error();
-}
-
-void YouTubeStreamReader::readMediaGroup() {
-
}
-void YouTubeStreamReader::readEntry() {
+void YTFeedReader::readEntry() {
Video* video = new Video();
- // qDebug(" *** ENTRY ***");
while (!atEnd()) {
readNext();
if (isStartElement()) {
if (name() == "link"
- && attributes().value("rel").toString() == "alternate"
- && attributes().value("type").toString() == "text/html"
- ) {
+ && attributes().value("rel").toString() == "alternate"
+ && attributes().value("type").toString() == "text/html"
+ ) {
QString webpage = attributes().value("href").toString();
webpage.remove("&feature=youtube_gdata");
video->setWebpage(QUrl(webpage));
if (isStartElement()) {
if (name() == "thumbnail") {
// qDebug() << "Thumb: " << attributes().value("url").toString();
- // video->thumbnailUrls() << QUrl(attributes().value("url").toString());
- video->addThumbnailUrl(QUrl(attributes().value("url").toString()));
+ QStringRef name = attributes().value("yt:name");
+ if (name == "default")
+ video->setThumbnailUrl(
+ attributes().value("url").toString());
+ else if (name == "hqdefault")
+ video->setMediumThumbnailUrl(
+ attributes().value("url").toString());
}
else if (name() == "title") {
QString title = readElementText();
}
-QList<Video*> YouTubeStreamReader::getVideos() {
+const QList<Video *> &YTFeedReader::getVideos() {
return videos;
}
-const QStringList & YouTubeStreamReader::getSuggestions() const {
+const QStringList & YTFeedReader::getSuggestions() const {
return suggestions;
}
-#ifndef YOUTUBESTREAMREADER_H
-#define YOUTUBESTREAMREADER_H
+#ifndef YTFEEDREADER_H
+#define YTFEEDREADER_H
-#include <QXmlStreamReader>
-#include <QBuffer>
-#include "video.h"
+#include <QtXml>
+
+class Video;
+
+class YTFeedReader : public QXmlStreamReader {
-class YouTubeStreamReader : public QXmlStreamReader
-{
public:
- YouTubeStreamReader();
- bool read(QByteArray data);
- QList<Video*> getVideos();
+ YTFeedReader(const QByteArray &bytes);
+ const QList<Video*> & getVideos();
const QStringList & getSuggestions() const;
private:
- void readMediaGroup();
void readEntry();
QList<Video*> videos;
QStringList suggestions;
};
-#endif // YOUTUBESTREAMREADER_H
+#endif // YTFEEDREADER_H
--- /dev/null
+#include "ytregions.h"
+
+YTRegions::YTRegions() : QObject() { }
+
+const QList<YTRegion> & YTRegions::list() {
+ static QList<YTRegion> list;
+ if (list.isEmpty()) {
+ list << r(tr("Algeria"), "DZ")
+ << r(tr("Argentina"), "AR")
+ << r(tr("Australia"), "AU")
+ << r(tr("Belgium"), "BE")
+ << r(tr("Brazil"), "BR")
+ << r(tr("Canada"), "CA")
+ << r(tr("Chile"), "CL")
+ << r(tr("Colombia"), "CO")
+ << r(tr("Czech Republic"), "CZ")
+ << r(tr("Egypt"), "EG")
+ << r(tr("France"), "FR")
+ << r(tr("Germany"), "DE")
+ << r(tr("Ghana"), "GH")
+ << r(tr("Greece"), "GR")
+ << r(tr("Hong Kong"), "HK")
+ << r(tr("Hungary"), "HU")
+ << r(tr("India"), "IN")
+ << r(tr("Indonesia"), "ID")
+ << r(tr("Ireland"), "IE")
+ << r(tr("Israel"), "IL")
+ << r(tr("Italy"), "IT")
+ << r(tr("Japan"), "JP")
+ << r(tr("Jordan"), "JO")
+ << r(tr("Kenya"), "KE")
+ << r(tr("Malaysia"), "MY")
+ << r(tr("Mexico"), "MX")
+ << r(tr("Morocco"), "MA")
+ << r(tr("Netherlands"), "NL")
+ << r(tr("New Zealand"), "NZ")
+ << r(tr("Nigeria"), "NG")
+ << r(tr("Peru"), "PE")
+ << r(tr("Philippines"), "PH")
+ << r(tr("Poland"), "PL")
+ << r(tr("Russia"), "RU")
+ << r(tr("Saudi Arabia"), "SA")
+ << r(tr("Singapore"), "SG")
+ << r(tr("South Africa"), "ZA")
+ << r(tr("South Korea"), "KR")
+ << r(tr("Spain"), "ES")
+ << r(tr("Sweden"), "SE")
+ << r(tr("Taiwan"), "TW")
+ << r(tr("Tunisia"), "TN")
+ << r(tr("Turkey"), "TR")
+ << r(tr("Uganda"), "UG")
+ << r(tr("United Arab Emirates"), "AE")
+ << r(tr("United Kingdom"), "GB")
+ << r(tr("Yemen"), "YE");
+/*
+ list << r(QLocale::Algeria, "DZ")
+ << r(QLocale::Argentina, "AR")
+ << r(QLocale::Australia, "AU")
+ << r(QLocale::Belgium, "BE")
+ << r(QLocale::Brazil, "BR")
+ << r(QLocale::Canada, "CA")
+ << r(QLocale::Chile, "CL")
+ << r(QLocale::Colombia, "CO")
+ << r(QLocale::CzechRepublic, "CZ")
+ << r(QLocale::Egypt, "EG")
+ << r(QLocale::France, "FR")
+ << r(QLocale::Germany, "DE")
+ << r(QLocale::Ghana, "GH")
+ << r(QLocale::Greece, "GR")
+ << r(QLocale::HongKong, "HK")
+ << r(QLocale::Hungary, "HU")
+ << r(QLocale::India, "IN")
+ << r(QLocale::Indonesia, "ID")
+ << r(QLocale::Ireland, "IE")
+ << r(QLocale::Israel, "IL")
+ << r(QLocale::Italy, "IT")
+ << r(QLocale::Japan, "JP")
+ << r(QLocale::Jordan, "JO")
+ << r(QLocale::Kenya, "KE")
+ << r(QLocale::Malaysia, "MY")
+ << r(QLocale::Mexico, "MX")
+ << r(QLocale::Morocco, "MA")
+ << r(QLocale::Netherlands, "NL")
+ << r(QLocale::NewZealand, "NZ")
+ << r(QLocale::Nigeria, "NG")
+ << r(QLocale::Peru, "PE")
+ << r(QLocale::Philippines, "PH")
+ << r(QLocale::Poland, "PL")
+ << r(QLocale::RussianFederation, "RU")
+ << r(QLocale::SaudiArabia, "SA")
+ << r(QLocale::Singapore, "SG")
+ << r(QLocale::SouthAfrica, "ZA")
+ << r(QLocale::RepublicOfKorea, "KR")
+ << r(QLocale::Spain, "ES")
+ << r(QLocale::Sweden, "SE")
+ << r(QLocale::Taiwan, "TW")
+ << r(QLocale::Tunisia, "TN")
+ << r(QLocale::Turkey, "TR")
+ << r(QLocale::Uganda, "UG")
+ << r(QLocale::UnitedArabEmirates, "AE")
+ << r(QLocale::UnitedKingdom, "GB")
+ << r(QLocale::Yemen, "YE");
+ */
+ qSort(list);
+ }
+ return list;
+}
+
+YTRegion YTRegions::r(QString name, QString id) {
+ YTRegion r = {id, name};
+ return r;
+}
+
+const YTRegion & YTRegions::localRegion() {
+ static YTRegion region;
+ if (region.name.isEmpty()) {
+ QString country = QLocale::system().name().right(2);
+ foreach (YTRegion r, list())
+ if (r.id == country) {
+ region = r;
+ break;
+ } // else qDebug() << r.id << country;
+ }
+ return region;
+}
+
+const YTRegion & YTRegions::worldwideRegion() {
+ static YTRegion region = {"", tr("Worldwide")};
+ return region;
+}
+
+void YTRegions::setRegion(QString regionId) {
+ QSettings settings;
+ settings.setValue("regionId", regionId);
+}
+
+QString YTRegions::currentRegionId() {
+ QSettings settings;
+ return settings.value("regionId").toString();
+}
+
+YTRegion YTRegions::currentRegion() {
+ return regionById(currentRegionId());
+}
+
+YTRegion YTRegions::regionById(QString id) {
+ if (id.isEmpty()) return worldwideRegion();
+ YTRegion region;
+ foreach (YTRegion r, list())
+ if (r.id == id) {
+ region = r;
+ break;
+ }
+ if (region.name.isEmpty()) return worldwideRegion();
+ return region;
+}
+
+QIcon YTRegions::iconForRegionId(QString regionId) {
+ if (regionId.isEmpty()) return QIcon(":flags/worldwide.png");
+ return QIcon(":flags/" + regionId.toLower() + ".png");
+}
--- /dev/null
+#ifndef YTREGIONS_H
+#define YTREGIONS_H
+
+#include <QtGui>
+
+struct YTRegion {
+ QString id;
+ QString name;
+ bool operator<(const YTRegion &other) const {
+ return name < other.name;
+ }
+};
+
+class YTRegions : public QObject {
+
+public:
+ static const QList<YTRegion> & list();
+ static const YTRegion & localRegion();
+ static const YTRegion & worldwideRegion();
+ static void setRegion(QString regionId);
+ static QString currentRegionId();
+ static YTRegion currentRegion();
+ static QIcon iconForRegionId(QString regionId);
+
+private:
+ static YTRegion r(QString name, QString id);
+ static YTRegion regionById(QString id);
+ YTRegions();
+
+};
+
+#endif // YTREGIONS_H
--- /dev/null
+#include "ytsearch.h"
+#include "ytfeedreader.h"
+#include "constants.h"
+#include "networkaccess.h"
+#include "searchparams.h"
+#include "video.h"
+
+namespace The {
+ NetworkAccess* http();
+}
+
+YTSearch::YTSearch(SearchParams *searchParams, QObject *parent) :
+ VideoSource(parent),
+ searchParams(searchParams) {
+ searchParams->setParent(this);
+}
+
+void YTSearch::loadVideos(int max, int skip) {
+ this->aborted = false;
+
+ QUrl url("https://gdata.youtube.com/feeds/api/videos/");
+ url.addQueryItem("v", "2");
+
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(skip));
+
+ if (!searchParams->keywords().isEmpty()) {
+ if (searchParams->keywords().startsWith("http://") ||
+ searchParams->keywords().startsWith("https://")) {
+ url.addQueryItem("q", YTSearch::videoIdFromUrl(searchParams->keywords()));
+ } else url.addQueryItem("q", searchParams->keywords());
+ }
+
+ if (!searchParams->author().isEmpty())
+ url.addQueryItem("author", searchParams->author());
+
+ switch (searchParams->sortBy()) {
+ case SearchParams::SortByNewest:
+ url.addQueryItem("orderby", "published");
+ break;
+ case SearchParams::SortByViewCount:
+ url.addQueryItem("orderby", "viewCount");
+ break;
+ case SearchParams::SortByRating:
+ url.addQueryItem("orderby", "rating");
+ break;
+ }
+
+ switch (searchParams->duration()) {
+ case SearchParams::DurationShort:
+ url.addQueryItem("duration", "short");
+ break;
+ case SearchParams::DurationMedium:
+ url.addQueryItem("duration", "medium");
+ break;
+ case SearchParams::DurationLong:
+ url.addQueryItem("duration", "long");
+ break;
+ }
+
+ switch (searchParams->time()) {
+ case SearchParams::TimeToday:
+ url.addQueryItem("time", "today");
+ break;
+ case SearchParams::TimeWeek:
+ url.addQueryItem("time", "this_week");
+ break;
+ case SearchParams::TimeMonth:
+ url.addQueryItem("time", "this_month");
+ break;
+ }
+
+ switch (searchParams->quality()) {
+ case SearchParams::QualityHD:
+ url.addQueryItem("hd", "true");
+ break;
+ }
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parseResults(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTSearch::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSearch::getSuggestions() {
+ return suggestions;
+}
+
+QString YTSearch::getName() {
+ if (!name.isEmpty()) return name;
+ if (!searchParams->keywords().isEmpty()) return searchParams->keywords();
+ return QString();
+}
+
+void YTSearch::parseResults(QByteArray data) {
+ if (aborted) return;
+
+ YTFeedReader reader(data);
+ QList<Video*> videos = reader.getVideos();
+ suggestions = reader.getSuggestions();
+
+ if (!searchParams->author().isEmpty()) {
+ if (videos.isEmpty()) name = searchParams->author();
+ else name = videos.first()->author();
+ emit nameChanged(name);
+ }
+
+ foreach (Video *video, videos)
+ emit gotVideo(video);
+
+ emit finished(videos.size());
+}
+
+void YTSearch::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+}
+
+QString YTSearch::videoIdFromUrl(QString url) {
+ QRegExp re = QRegExp("^.*[\\?&]v=([^&#]+).*$");
+ if (re.exactMatch(url)) return re.cap(1);
+ re = QRegExp("^.*://.*/([^&#\\?]+).*$");
+ if (re.exactMatch(url)) return re.cap(1);
+ return QString();
+}
--- /dev/null
+#ifndef YTSEARCH_H
+#define YTSEARCH_H
+
+#include <QtNetwork>
+#include "videosource.h"
+
+class SearchParams;
+class Video;
+
+class YTSearch : public VideoSource {
+
+ Q_OBJECT
+
+public:
+ YTSearch(SearchParams *params, QObject *parent = 0);
+ void loadVideos(int max, int skip);
+ virtual void abort();
+ virtual const QStringList & getSuggestions();
+ static QString videoIdFromUrl(QString url);
+ QString getName();
+ SearchParams* getSearchParams() const { return searchParams; }
+
+ bool operator==(const YTSearch &other) const {
+ return searchParams == other.getSearchParams();
+ }
+
+private slots:
+ void parseResults(QByteArray data);
+ void requestError(QNetworkReply *reply);
+
+private:
+ SearchParams *searchParams;
+ bool aborted;
+ QStringList suggestions;
+ QString name;
+};
+
+#endif // YTSEARCH_H
--- /dev/null
+#include "ytsinglevideosource.h"
+#include <QtXml>
+#include "networkaccess.h"
+#include "video.h"
+#include "ytfeedreader.h"
+
+namespace The {
+NetworkAccess* http();
+}
+
+YTSingleVideoSource::YTSingleVideoSource(QObject *parent) : VideoSource(parent) {
+ skip = 0;
+ max = 0;
+}
+
+void YTSingleVideoSource::loadVideos(int max, int skip) {
+ aborted = false;
+ this->skip = skip;
+ this->max = max;
+
+ QString s;
+ if (skip == 1) s = "https://gdata.youtube.com/feeds/api/videos/" + videoId;
+ else s = QString("https://gdata.youtube.com/feeds/api/videos/%1/related").arg(videoId);
+ QUrl url(s);
+ url.addQueryItem("v", "2");
+
+ if (skip != 1) {
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(skip-1));
+ }
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parse(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTSingleVideoSource::abort() {
+ aborted = true;
+}
+
+const QStringList & YTSingleVideoSource::getSuggestions() {
+ QStringList *l = new QStringList();
+ return *l;
+}
+
+QString YTSingleVideoSource::getName() {
+ return QString();
+}
+
+void YTSingleVideoSource::parse(QByteArray data) {
+ if (aborted) return;
+
+ YTFeedReader reader(data);
+ QList<Video*> videos = reader.getVideos();
+
+ foreach (Video *video, videos)
+ emit gotVideo(video);
+
+ if (skip == 1) loadVideos(max - 1, 2);
+ else if (skip == 2) emit finished(videos.size() + 1);
+ else emit finished(videos.size());
+}
+
+void YTSingleVideoSource::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+}
--- /dev/null
+#ifndef YTSINGLEVIDEOSOURCE_H
+#define YTSINGLEVIDEOSOURCE_H
+
+#include <QtNetwork>
+#include "videosource.h"
+
+class YTSingleVideoSource : public VideoSource {
+
+ Q_OBJECT
+
+public:
+ YTSingleVideoSource(QObject *parent = 0);
+ void loadVideos(int max, int skip);
+ void abort();
+ const QStringList & getSuggestions();
+ QString getName();
+
+ void setVideoId(QString videoId) { this->videoId = videoId; }
+
+private slots:
+ void parse(QByteArray data);
+ void requestError(QNetworkReply *reply);
+
+private:
+ QString videoId;
+ bool aborted;
+ int skip;
+ int max;
+};
+
+#endif // YTSINGLEVIDEOSOURCE_H
--- /dev/null
+#include "ytstandardfeed.h"
+#include <QtXml>
+#include "networkaccess.h"
+#include "video.h"
+#include "ytfeedreader.h"
+
+namespace The {
+NetworkAccess* http();
+}
+
+YTStandardFeed::YTStandardFeed(QObject *parent)
+ : VideoSource(parent),
+ aborted(false) { }
+
+void YTStandardFeed::loadVideos(int max, int skip) {
+ aborted = false;
+
+ QString s = "https://gdata.youtube.com/feeds/api/standardfeeds/";
+ if (!regionId.isEmpty()) s += regionId + "/";
+ s += feedId;
+ if (!category.isEmpty()) s += "_" + category;
+
+ QUrl url(s);
+ url.addQueryItem("v", "2");
+
+ if (feedId != "most_shared")
+ url.addQueryItem("time", "today");
+
+ url.addQueryItem("max-results", QString::number(max));
+ url.addQueryItem("start-index", QString::number(skip));
+
+ QObject *reply = The::http()->get(url);
+ connect(reply, SIGNAL(data(QByteArray)), SLOT(parse(QByteArray)));
+ connect(reply, SIGNAL(error(QNetworkReply*)), SLOT(requestError(QNetworkReply*)));
+}
+
+void YTStandardFeed::abort() {
+ aborted = true;
+}
+
+const QStringList & YTStandardFeed::getSuggestions() {
+ QStringList *l = new QStringList();
+ return *l;
+}
+
+void YTStandardFeed::parse(QByteArray data) {
+ if (aborted) return;
+
+ YTFeedReader reader(data);
+ QList<Video*> videos = reader.getVideos();
+
+ foreach (Video *video, videos)
+ emit gotVideo(video);
+
+ emit finished(videos.size());
+}
+
+void YTStandardFeed::requestError(QNetworkReply *reply) {
+ emit error(reply->errorString());
+}
--- /dev/null
+#ifndef YTSTANDARDFEED_H
+#define YTSTANDARDFEED_H
+
+#include <QtNetwork>
+#include "videosource.h"
+
+class YTStandardFeed : public VideoSource {
+
+ Q_OBJECT
+
+public:
+ YTStandardFeed(QObject *parent = 0);
+
+ QString getFeedId() { return feedId; }
+ void setFeedId(QString feedId) { this->feedId = feedId; }
+
+ QString getRegionId() { return regionId; }
+ void setRegionId(QString regionId) { this->regionId = regionId; }
+
+ QString getCategory() { return category; }
+ void setCategory(QString category) { this->category = category; }
+
+ QString getLabel() { return label; }
+ void setLabel(QString label) { this->label = label; }
+
+ void loadVideos(int max, int skip);
+ void abort();
+ const QStringList & getSuggestions();
+ QString getName() { return label; }
+
+private slots:
+ void parse(QByteArray data);
+ void requestError(QNetworkReply *reply);
+
+private:
+ QString feedId;
+ QString regionId;
+ QString category;
+ QString label;
+ bool aborted;
+};
+
+#endif // YTSTANDARDFEED_H
-#include "youtubesuggest.h"
+#include "ytsuggester.h"
#include <QtXml>
#include "networkaccess.h"
NetworkAccess* http();
}
-YouTubeSuggest::YouTubeSuggest(QObject *parent) : Suggester() {
+YTSuggester::YTSuggester(QObject *parent) : Suggester() {
}
-void YouTubeSuggest::suggest(QString query) {
+void YTSuggester::suggest(QString query) {
+ if (query.startsWith("http")) return;
+
+#if QT_VERSION >= 0x040800
+ QString locale = QLocale::system().uiLanguages().first();
+#else
QString locale = QLocale::system().name().replace("_", "-");
+#endif
+
// case for system locales such as "C"
if (locale.length() < 2) {
locale = "en-US";
connect(reply, SIGNAL(data(QByteArray)), SLOT(handleNetworkData(QByteArray)));
}
-void YouTubeSuggest::handleNetworkData(QByteArray response) {
+void YTSuggester::handleNetworkData(QByteArray response) {
QStringList choices;
QXmlStreamReader xml(response);
-#ifndef YOUTUBESUGGEST_H
-#define YOUTUBESUGGEST_H
+#ifndef YTSUGGESTER_H
+#define YTSUGGESTER_H
#include <QtCore>
-
#include "suggester.h"
-class YouTubeSuggest : public Suggester {
+class YTSuggester : public Suggester {
Q_OBJECT
public:
- YouTubeSuggest(QObject *parent = 0);
+ YTSuggester(QObject *parent = 0);
void suggest(QString query);
signals:
};
-#endif // YOUTUBESUGGEST_H
+#endif // YTSUGGESTER_H
--- /dev/null
+RegionsView QPushButton[regionId] {
+ margin: 5px;
+ padding: 10px;
+ text-align: left;
+ vertical-align: middle;
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 10px;
+}
+
+RegionsView QPushButton[regionId=""] {
+ font-weight: bold;
+}
+
+RegionsView QPushButton[regionId]:hover {
+ border: 1px solid rgba(0,0,0,32);
+ background: rgba(0,0,0,16);
+}
+
+RegionsView QPushButton[regionId]:focus {
+ border: 1px solid palette(highlight);
+ background: rgba(0,0,0,16);
+}
+
+RegionsView QPushButton[regionId]:checked {
+ color: #fff;
+ border: 1px solid rgba(0,0,0,64);
+ background: qradialgradient(cx: 0.5, cy: 0,
+ fx: 0.5, fy: 0,
+ radius: 1.35, stop: 0 rgba(0,0,0,128), stop: 1 rgba(0,0,0,64));
+}
+
+SidebarHeader {
+ background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+ stop: 0 #262626, stop: 1 #3c3c3c);
+ border: 0;
+ padding: 0;
+ margin: 0;
+ spacing: 0;
+}
+
+SidebarHeader QToolButton {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ spacing: 0;
+ height: 20;
+ width: 24;
+}
+
+SidebarHeader QPushButton {
+ background: transparent;
+ color: #fff;
+ text-align: center;
+}
+
+SidebarHeader QComboBox::drop-down {
+ width: 0;
+ border-style: none;
+}