diff --git a/common.pri b/common.pri new file mode 100644 index 0000000..17cb998 --- /dev/null +++ b/common.pri @@ -0,0 +1,17 @@ +QT += qml quick network dbus +CONFIG += c++11 + +SOURCES += \ + src/main.cpp \ + src/ircchat.cpp \ + src/tools.cpp \ + src/qmlsettings.cpp \ + src/message.cpp \ + src/messagelistmodel.cpp + +HEADERS += \ + src/ircchat.h \ + src/tools.h \ + src/qmlsettings.h \ + src/message.h \ + src/messagelistmodel.h diff --git a/harbour-twitchtube.pro b/harbour-twitchtube.pro index c34cf07..5eb1e9a 100644 --- a/harbour-twitchtube.pro +++ b/harbour-twitchtube.pro @@ -12,65 +12,34 @@ # The name of your application TARGET = harbour-twitchtube -CONFIG += sailfishapp c++11 +CONFIG += sailfishapp -QT += network dbus +DEFINES += OS_SAILFISH -icons.path = /usr/share/icons/hicolor -icons.files = icons/* -INSTALLS += icons +include(common.pri) -SOURCES += \ - src/harbour-twitchtube.cpp \ - src/ircchat.cpp \ - src/tools.cpp \ - src/qmlsettings.cpp \ - src/message.cpp \ - src/messagelistmodel.cpp +RESOURCES += sailfish-ui/TwitchTube.qrc -OTHER_FILES += \ - translations/*.ts \ - qml/pages/GamesPage.qml \ - qml/pages/ChannelsPage.qml \ - qml/pages/StreamPage.qml \ - qml/js/httphelper.js \ - qml/pages/SettingsPage.qml \ - qml/pages/SearchPage.qml \ - qml/pages/LoginPage.qml \ - qml/pages/FollowedPage.qml \ - qml/harbour-twitchtube.qml \ - harbour-twitchtube.desktop \ - rpm/harbour-twitchtube.spec \ - rpm/harbour-twitchtube.yaml \ - qml/pages/elements/Categories.qml \ - qml/pages/QualityChooserPage.qml \ - rpm/harbour-twitchtube.changes \ - qml/images/heart.png \ - qml/images/icon.png \ - qml/pages/elements/GamesGrid.qml \ - qml/pages/elements/ChannelsGrid.qml \ - qml/pages/GameChannelsPage.qml \ - qml/pages/FollowedGamesPage.qml \ - qml/cover/NavigationCover.qml \ - qml/cover/StreamCover.qml \ - qml/images/heart_crossed.png +QML_FILES += $$files(sailfish-ui/*.qml,true) \ + $$files(sailfish-ui/*.js,true) \ + $$files(sailfish-ui/*.png,true) + +CONF_FILES += rpm/harbour-twitchtube.spec \ + rpm/harbour-twitchtube.yaml \ + rpm/harbour-twitchtube.changes + +TRANSLATIONS += $$files(translations/*.ts) + +OTHER_FILES += $${CONF_FILES} \ + $${QML_FILES} \ + harbour-twitchtube.desktop + +SAILFISHAPP_ICONS = 86x86 108x108 128x128 256x256 # to disable building translations every time, comment out the # following CONFIG line CONFIG += sailfishapp_i18n -#TRANSLATIONS += translations/harbour-twitchtube-ru.ts - -HEADERS += \ - src/ircchat.h \ - src/tools.h \ - src/qmlsettings.h \ - src/message.h \ - src/messagelistmodel.h DISTFILES += \ - icons/108x108/apps/harbour-twitchtube.png \ - icons/128x128/apps/harbour-twitchtube.png \ - icons/256x256/apps/harbour-twitchtube.png \ - icons/86x86/apps/harbour-twitchtube.png \ - qml/pages/elements/GridWrapper.qml + sailfish-ui/Main.qml diff --git a/icons/108x108/apps/harbour-twitchtube.png b/icons/108x108/harbour-twitchtube.png similarity index 100% rename from icons/108x108/apps/harbour-twitchtube.png rename to icons/108x108/harbour-twitchtube.png diff --git a/icons/128x128/apps/harbour-twitchtube.png b/icons/128x128/harbour-twitchtube.png similarity index 100% rename from icons/128x128/apps/harbour-twitchtube.png rename to icons/128x128/harbour-twitchtube.png diff --git a/icons/256x256/apps/harbour-twitchtube.png b/icons/256x256/harbour-twitchtube.png similarity index 100% rename from icons/256x256/apps/harbour-twitchtube.png rename to icons/256x256/harbour-twitchtube.png diff --git a/icons/86x86/apps/harbour-twitchtube.png b/icons/86x86/harbour-twitchtube.png similarity index 100% rename from icons/86x86/apps/harbour-twitchtube.png rename to icons/86x86/harbour-twitchtube.png diff --git a/rpm/harbour-twitchtube.spec b/rpm/harbour-twitchtube.spec index cc79ef3..1667330 100644 --- a/rpm/harbour-twitchtube.spec +++ b/rpm/harbour-twitchtube.spec @@ -69,9 +69,6 @@ desktop-file-install --delete-original \ %{_bindir} %{_datadir}/%{name} %{_datadir}/applications/%{name}.desktop -%{_datadir}/icons/hicolor/86x86/apps/%{name}.png -%{_datadir}/icons/hicolor/108x108/apps/%{name}.png -%{_datadir}/icons/hicolor/128x128/apps/%{name}.png -%{_datadir}/icons/hicolor/256x256/apps/%{name}.png +%{_datadir}/icons/hicolor/*/apps/%{name}.png # >> files # << files diff --git a/rpm/harbour-twitchtube.yaml b/rpm/harbour-twitchtube.yaml index e108d94..66329c8 100644 --- a/rpm/harbour-twitchtube.yaml +++ b/rpm/harbour-twitchtube.yaml @@ -42,10 +42,7 @@ Files: - '%{_bindir}' - '%{_datadir}/%{name}' - '%{_datadir}/applications/%{name}.desktop' - - '%{_datadir}/icons/hicolor/86x86/apps/%{name}.png' - - '%{_datadir}/icons/hicolor/108x108/apps/%{name}.png' - - '%{_datadir}/icons/hicolor/128x128/apps/%{name}.png' - - '%{_datadir}/icons/hicolor/256x256/apps/%{name}.png' + - '%{_datadir}/icons/hicolor/*/apps/%{name}.png' # For more information about yaml and what's supported in Sailfish OS # build system, please see https://wiki.merproject.org/wiki/Spectacle diff --git a/sailfish-icon.svg b/sailfish-icon.svg new file mode 100644 index 0000000..c97c2f4 --- /dev/null +++ b/sailfish-icon.svg @@ -0,0 +1,229 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/qml/harbour-twitchtube.qml b/sailfish-ui/Main.qml similarity index 100% rename from qml/harbour-twitchtube.qml rename to sailfish-ui/Main.qml diff --git a/sailfish-ui/TwitchTube.qrc b/sailfish-ui/TwitchTube.qrc new file mode 100755 index 0000000..8848bf6 --- /dev/null +++ b/sailfish-ui/TwitchTube.qrc @@ -0,0 +1,26 @@ + + + Main.qml + pages/ChannelsPage.qml + pages/GameChannelsPage.qml + pages/GamesPage.qml + pages/StreamPage.qml + pages/FollowedPage.qml + pages/LoginPage.qml + pages/QualityChooserPage.qml + pages/SearchPage.qml + pages/SettingsPage.qml + pages/FollowedGamesPage.qml + pages/elements/Categories.qml + pages/elements/ChannelsGrid.qml + pages/elements/GamesGrid.qml + pages/elements/GridWrapper.qml + cover/NavigationCover.qml + cover/StreamCover.qml + js/httphelper.js + images/heart.png + images/heart_crossed.png + images/icon.png + + + diff --git a/qml/cover/NavigationCover.qml b/sailfish-ui/cover/NavigationCover.qml similarity index 100% rename from qml/cover/NavigationCover.qml rename to sailfish-ui/cover/NavigationCover.qml diff --git a/qml/cover/StreamCover.qml b/sailfish-ui/cover/StreamCover.qml similarity index 100% rename from qml/cover/StreamCover.qml rename to sailfish-ui/cover/StreamCover.qml diff --git a/qml/images/heart.png b/sailfish-ui/images/heart.png similarity index 100% rename from qml/images/heart.png rename to sailfish-ui/images/heart.png diff --git a/heart.svg b/sailfish-ui/images/heart.svg similarity index 79% rename from heart.svg rename to sailfish-ui/images/heart.svg index 68293cf..58cf121 100644 --- a/heart.svg +++ b/sailfish-ui/images/heart.svg @@ -21,7 +21,14 @@ inkscape:export-ydpi="22.5">image/svg+xml + + + + + + + + #endif +#ifdef OS_SAILFISH #include +#endif #include #include @@ -76,21 +78,32 @@ int main(int argc, char *argv[]) // // To display the view, call "show()" (will show fullscreen on device). +#ifdef OS_SAILFISH QGuiApplication *app(SailfishApp::application(argc, argv)); QCoreApplication::setOrganizationName("harbour-twitchtube"); QCoreApplication::setApplicationName("harbour-twitchtube"); - qmlRegisterType("harbour.twitchtube.ircchat", 1, 0, "IrcChat"); qmlRegisterType("harbour.twitchtube.ircchat", 1, 0, "MessageListModel"); qmlRegisterType("harbour.twitchtube.settings", 1, 0, "Setting"); QQuickView *view(SailfishApp::createView()); +#else + QGuiApplication *app = new QGuiApplication(argc, argv); + QCoreApplication::setOrganizationName("twitchtube.aldrog"); + QCoreApplication::setApplicationName("twitchtube.aldrog"); + qmlRegisterType("aldrog.twitchtube.ircchat", 1, 0, "IrcChat"); + qmlRegisterType("aldrog.twitchtube.ircchat", 1, 0, "MessageListModel"); + qmlRegisterType("aldrog.twitchtube.settings", 1, 0, "Setting"); + + QQuickView *view = new QQuickView(); +#endif registerSettings(view); Tools *tools = new Tools(); view->rootContext()->setContextProperty("cpptools", tools); - view->setSource(SailfishApp::pathTo("qml/harbour-twitchtube.qml")); + view->setSource(QUrl("qrc:///Main.qml")); + view->setResizeMode(QQuickView::SizeRootObjectToView); view->show(); return app->exec(); } diff --git a/src/tools.cpp b/src/tools.cpp old mode 100644 new mode 100755 index 5b959a9..67056b2 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -22,17 +22,24 @@ #include #include #include + +#ifdef OS_SAILFISH #include +#endif Tools::Tools(QObject *parent) : - QObject(parent), - mceReqInterface("com.nokia.mce", + QObject(parent) +#ifdef OS_SAILFISH + , mceReqInterface("com.nokia.mce", "/com/nokia/mce/request", "com.nokia.mce.request", QDBusConnection::connectToBus(QDBusConnection::SystemBus, "system")) +#endif { +#ifdef OS_SAILFISH pauseRefresher = new QTimer(); connect(pauseRefresher, SIGNAL(timeout()), this, SLOT(refreshPause())); +#endif } Tools::~Tools() { } @@ -45,6 +52,8 @@ Tools::~Tools() { } int Tools::clearCookies() { QStringList dataPaths = QStandardPaths::standardLocations(QStandardPaths::DataLocation); if(dataPaths.size()) { + qDebug() << QDir(dataPaths[0]).entryList(); +#ifdef OS_SAILFISH QDir webData(QDir(dataPaths.at(0)).filePath(".QtWebKit")); if(webData.exists()) { if(webData.removeRecursively()) @@ -54,10 +63,22 @@ int Tools::clearCookies() { } else return 1; +#elif OS_UBUNTU + QDir webData(QDir(dataPaths.at(0))); + if(webData.exists()) { + if(webData.removeRecursively()) + return 0; + else + return -1; + } + else + return 1; +#endif } return -2; } +#ifdef OS_SAILFISH // true - screen blanks (default) // false - no blanking void Tools::setBlankingMode(bool state) @@ -80,3 +101,4 @@ void Tools::refreshPause() { mceReqInterface.call(QLatin1String("req_display_blanking_pause")); } +#endif diff --git a/src/tools.h b/src/tools.h old mode 100644 new mode 100755 index a64cf5c..9c19a95 --- a/src/tools.h +++ b/src/tools.h @@ -22,10 +22,13 @@ #include #include + +#ifdef OS_SAILFISH #include #include const int PAUSE_PERIOD = 50000; //ms +#endif class Tools : public QObject { @@ -35,12 +38,18 @@ class Tools : public QObject ~Tools(); Q_INVOKABLE int clearCookies(); +#ifdef OS_SAILFISH Q_INVOKABLE void setBlankingMode(bool state); +#endif public slots: +#ifdef OS_SAILFISH void refreshPause(); +#endif protected: +#ifdef OS_SAILFISH QDBusInterface mceReqInterface; QTimer* pauseRefresher; +#endif }; #endif // TOOLS_H diff --git a/translations/harbour-twitchtube-ru.ts b/translations/harbour-twitchtube-ru.ts deleted file mode 100644 index 8b738fc..0000000 --- a/translations/harbour-twitchtube-ru.ts +++ /dev/null @@ -1,117 +0,0 @@ - - - - - ChannelsPage - - Settings - Настройки - - - Search - Поиск - - - Following - Любимые - - - Games - Игры - - - Channels - Каналы - - - Load more - Загрузить ещё - - - Live Channels - Не точно - Прямые трансляции - - - - GamesPage - - Settings - Настройки - - - Search - Поиск - - - Following - Любимые - - - Channels - Каналы - - - Load more - Загрузить ещё - - - Popular Games - Популярные игры - - - - SearchPage - - Search channels - Поиск каналов - - - Settings - Настройки - - - Following - Любимые - - - Games - Игры - - - Channels - Каналы - - - - SettingsPage - - Twitch Settings - Настройки Twitch - - - Apply - Применить - - - Game posters quality - Качество изображений игр - - - High - Высокое - - - Medium - Среднее - - - Low - Низкое - - - Stream previews quality - Качество изображений трансляций - - - diff --git a/ubuntu-icon.svg b/ubuntu-icon.svg new file mode 100644 index 0000000..6264d7a --- /dev/null +++ b/ubuntu-icon.svg @@ -0,0 +1,231 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/ubuntu-twitchtube.pro b/ubuntu-twitchtube.pro new file mode 100644 index 0000000..50b3290 --- /dev/null +++ b/ubuntu-twitchtube.pro @@ -0,0 +1,44 @@ +TEMPLATE = app +TARGET = TwitchTube + +load(ubuntu-click) + +include(common.pri) + +# specify the manifest file, this file is required for click +# packaging and for the IDE to create runconfigurations +UBUNTU_MANIFEST_FILE=ubuntu/manifest.json.in + +# specify translation domain, this must be equal with the +# app name in the manifest file +UBUNTU_TRANSLATION_DOMAIN="twitchtube.aldrog" + +DEFINES += OS_UBUNTU + +RESOURCES += ubuntu-ui/TwitchTube.qrc + +QML_FILES += $$files(ubuntu-ui/*.qml,true) \ + $$files(ubuntu-ui/*.js,true) + +CONF_FILES += ubuntu/TwitchTube.apparmor \ + ubuntu/TwitchTube.png + +OTHER_FILES += $${CONF_FILES} \ + $${QML_FILES} \ + ubuntu/TwitchTube.desktop + +#specify where the config files are installed to +config_files.path = /TwitchTube +config_files.files += $${CONF_FILES} +INSTALLS += config_files + +#install the desktop file, a translated version is +#automatically created in the build directory +desktop_file.path = /TwitchTube +desktop_file.files = ubuntu/TwitchTube.desktop +desktop_file.CONFIG += no_check_exist +INSTALLS+=desktop_file + +# Default rules for deployment. +target.path = $${UBUNTU_CLICK_BINARY_PATH} +INSTALLS += target diff --git a/ubuntu-ui/Main.qml b/ubuntu-ui/Main.qml new file mode 100644 index 0000000..bf7c35f --- /dev/null +++ b/ubuntu-ui/Main.qml @@ -0,0 +1,63 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "pages" +import "js/httphelper.js" as HTTP + +MainView { + id: mainWindow + + property string username + + signal userChanged + + // objectName for functional testing purposes (autopilot-qt5) + objectName: "mainView" + + // Note! applicationName needs to match the "name" field of the click manifest + applicationName: "twitchtube.aldrog" + + width: units.gu(100) + height: units.gu(75) + + Component.onCompleted: { + if(authToken.value) { + HTTP.getRequest("https://api.twitch.tv/kraken/user?oauth_token=" + authToken.value, function(data) { + if(data) { + var user = JSON.parse(data) + username = user.name + console.log("Successfully received username") + } + }) + } + + pageStack.push(startingPage) + } + + PageStack { + id: pageStack + } + + GamesPage { + id: startingPage + visible: false + } +} diff --git a/ubuntu-ui/TwitchTube.qrc b/ubuntu-ui/TwitchTube.qrc new file mode 100755 index 0000000..8b538fd --- /dev/null +++ b/ubuntu-ui/TwitchTube.qrc @@ -0,0 +1,23 @@ + + + Main.qml + pages/ChannelsPage.qml + pages/GameChannelsPage.qml + pages/GamesPage.qml + pages/FollowedPage.qml + pages/FollowedGamesPage.qml + pages/SearchPage.qml + pages/StreamPage.qml + pages/QualityChooserPage.qml + pages/SettingsPage.qml + pages/LoginPage.qml + pages/elements/Categories.qml + pages/elements/ChannelsGrid.qml + pages/elements/GamesGrid.qml + pages/elements/GridWrapper.qml + js/httphelper.js + images/heart.png + images/heart_crossed.png + + + diff --git a/ubuntu-ui/cover/NavigationCover.qml b/ubuntu-ui/cover/NavigationCover.qml new file mode 100755 index 0000000..7b1f7ab --- /dev/null +++ b/ubuntu-ui/cover/NavigationCover.qml @@ -0,0 +1,30 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.1 +import Sailfish.Silica 1.0 + +CoverBackground { + property string status: pageStack.currentPage.navStatus + + CoverPlaceholder { + icon.source: "../images/icon.png" + text: status + } +} diff --git a/ubuntu-ui/cover/StreamCover.qml b/ubuntu-ui/cover/StreamCover.qml new file mode 100755 index 0000000..fe75a56 --- /dev/null +++ b/ubuntu-ui/cover/StreamCover.qml @@ -0,0 +1,143 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.1 +import Sailfish.Silica 1.0 +import QtMultimedia 5.0 +import "../js/httphelper.js" as HTTP + +// The most of this code was taken from Sailfish Silica components + +CoverBackground { + id: root + + property string channel: mainWindow.currentChannel + + onChannelChanged: { + HTTP.getRequest("https://api.twitch.tv/kraken/streams/" + channel, function(data) { + if(data) { + var stream = JSON.parse(data).stream + streamPreview.source = stream.preview.template.replace("{height}", root.height).replace("{width}", ~~(root.height*16/9)) + statusContainer.text = stream.channel.status + } + }) + } + + CoverActionList { + CoverAction { + iconSource: mainWindow.playing ? "image://theme/icon-m-speaker-mute" : "image://theme/icon-m-speaker" + onTriggered: { + if(mainWindow.playing) + mainWindow.stopAudio() + else + mainWindow.playAudio() + } + } + } + + Item { + id: glassTextureItem + visible: false + width: glassTextureImage.width + height: glassTextureImage.height + Image { + id: glassTextureImage + opacity: 0.1 + scale: Theme.pixelRatio + source: "image://theme/graphic-shader-texture" + Behavior on opacity { FadeAnimation { duration: 200 } } + } + } + + Image { + id: streamPreview + anchors.fill: parent + visible: false + fillMode: Image.PreserveAspectCrop + + onSourceChanged: { + // Workaround -- seems to be necessary for the ShaderEffect to update the texture + wallpaperEffect.wallpaperTexture = null + wallpaperEffect.wallpaperTexture = streamPreview + } + } + + ShaderEffect { + id: wallpaperEffect + anchors.fill: parent + + property real horizontalOffset: -(root.height*16/9) / 2 + root.width / 2 + property real verticalOffset: 0 + + visible: streamPreview.source != "" + + // offset normalized to effect size + property size offset: Qt.size(horizontalOffset / width, verticalOffset / height) + + // ratio of effect size vs home wallpaper size + property real ratio: 1 + property size sizeRatio: Qt.size((9/16)*width/height, 1) + + // glass texture size + property size glassTextureSizeInv: Qt.size(1.0/glassTextureImage.sourceSize.width, -1.0/glassTextureImage.sourceSize.height) + + property Image wallpaperTexture: streamPreview + property variant glassTexture: ShaderEffectSource { + hideSource: true + sourceItem: glassTextureItem + wrapMode: ShaderEffectSource.Repeat + } + + opacity: 0.8 + + // Enable blending in compositor (for events view etc..) + blending: true + + vertexShader: " + uniform highp mat4 qt_Matrix; + uniform highp vec2 offset; + uniform highp vec2 sizeRatio; + attribute highp vec4 qt_Vertex; + attribute highp vec2 qt_MultiTexCoord0; + varying highp vec2 qt_TexCoord0; + void main() { + qt_TexCoord0 = (qt_MultiTexCoord0 - offset) * sizeRatio; + gl_Position = qt_Matrix * qt_Vertex; + } + " + + fragmentShader: " + uniform sampler2D wallpaperTexture; + uniform sampler2D glassTexture; + uniform highp vec2 glassTextureSizeInv; + uniform lowp float qt_Opacity; + varying highp vec2 qt_TexCoord0; + void main() { + lowp vec4 wp = texture2D(wallpaperTexture, qt_TexCoord0); + lowp vec4 tx = texture2D(glassTexture, gl_FragCoord.xy * glassTextureSizeInv); + gl_FragColor = vec4(0.8*wp.rgb + tx.rgb, 1.0)" + (blending ? "*qt_Opacity" : "") + "; + } + " + } + + CoverPlaceholder { + id: statusContainer + icon.source: "../images/icon.png" + } +} diff --git a/ubuntu-ui/images/heart.png b/ubuntu-ui/images/heart.png new file mode 100755 index 0000000..81ef011 Binary files /dev/null and b/ubuntu-ui/images/heart.png differ diff --git a/ubuntu-ui/images/heart_crossed.png b/ubuntu-ui/images/heart_crossed.png new file mode 100755 index 0000000..d34a331 Binary files /dev/null and b/ubuntu-ui/images/heart_crossed.png differ diff --git a/ubuntu-ui/images/icon.png b/ubuntu-ui/images/icon.png new file mode 100755 index 0000000..b5e6f54 Binary files /dev/null and b/ubuntu-ui/images/icon.png differ diff --git a/ubuntu-ui/js/httphelper.js b/ubuntu-ui/js/httphelper.js new file mode 100644 index 0000000..1dc375d --- /dev/null +++ b/ubuntu-ui/js/httphelper.js @@ -0,0 +1,95 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ +.pragma library + +function getRequest(url, callback, publicAPI) { + var request = new XMLHttpRequest() + request.open("GET", url) + if(url.indexOf("https://api.twitch.tv/kraken") === 0) { + // Kraken API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } else if(publicAPI) { + // Experimental API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } + request.onreadystatechange = function() { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status && request.status === 200) { + callback(request.responseText) + } else { + console.log("Error accessing url", url) + console.log("HTTP:", request.status, request.statusText) + callback(false) + } + } + } + request.send() +} + +function putRequest(url, callback, publicAPI) { + var request = new XMLHttpRequest() + request.open("PUT", url) + if(url.indexOf("https://api.twitch.tv/kraken") === 0) { + // Kraken API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } else if(publicAPI) { + // Experimental API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } + request.onreadystatechange = function() { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status && request.status === 200) { + callback(request.responseText) + } else { + console.log("HTTP:", request.status, request.statusText) + callback(false) + } + } + } + request.send() +} + +function deleteRequest(url, callback, publicAPI) { + var request = new XMLHttpRequest() + request.open("DELETE", url) + if(url.indexOf("https://api.twitch.tv/kraken") === 0) { + // Kraken API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } else if(publicAPI) { + // Experimental API + request.setRequestHeader("Accept", "application/vnd.twitchtv.v3+json") + request.setRequestHeader("Client-ID", "n57dx0ypqy48ogn1ac08buvoe13bnsu") + } + request.onreadystatechange = function() { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status && request.status === 200) { + callback(request.responseText) + } else { + console.log("HTTP:", request.status, request.statusText) + callback(request.status) + } + } + } + request.send() +} diff --git a/ubuntu-ui/pages/ChannelsPage.qml b/ubuntu-ui/pages/ChannelsPage.qml new file mode 100755 index 0000000..e45b8e2 --- /dev/null +++ b/ubuntu-ui/pages/ChannelsPage.qml @@ -0,0 +1,62 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + // Status for NavigationCover + property string navStatus: qsTr("Channels") + + header: PageHeader { + title: qsTr("Top Channels") + flickable: mainContainer + + leadingActionBar.actions: categories.actions + Categories { + id: categories + channels: false + } + } + + GridWrapper { + id: mainContainer + grids: [ + ChannelsGrid { + id: gridChannels + + function loadContent() { + var url = "https://api.twitch.tv/kraken/streams?limit=" + countOnPage + "&offset=" + offset + HTTP.getRequest(url,function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.streams) + channels.append(result.streams[i]) + } + }) + } + }] + } +} diff --git a/ubuntu-ui/pages/FollowedGamesPage.qml b/ubuntu-ui/pages/FollowedGamesPage.qml new file mode 100755 index 0000000..c99e97f --- /dev/null +++ b/ubuntu-ui/pages/FollowedGamesPage.qml @@ -0,0 +1,60 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + // Status for NavigationCover + property string navStatus: qsTr("Following") + + header: PageHeader { + title: qsTr("Followed Games") + flickable: mainContainer + } + + GridWrapper { + id: mainContainer + + grids: [ + GamesGrid { + id: gridGames + + function loadContent() { + var url = "https://api.twitch.tv/api/users/" + mainWindow.username + "/follows/games?limit=" + countOnPage + "&offset=" + offset + console.log(url) + HTTP.getRequest(url,function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.follows) + games.append(result.follows[i]) + } + }) + } + + parameters: { "fromFollowings": true } + }] + } +} diff --git a/ubuntu-ui/pages/FollowedPage.qml b/ubuntu-ui/pages/FollowedPage.qml new file mode 100755 index 0000000..2725461 --- /dev/null +++ b/ubuntu-ui/pages/FollowedPage.qml @@ -0,0 +1,77 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + // Status for NavigationCover + property string navStatus: qsTr("Following") + property bool showOffline: false + + header: PageHeader { + title: qsTr("Followed Channels") + flickable: mainContainer + + leadingActionBar.actions: categories.actions + Categories { + id: categories + games: false + } + + trailingActionBar.actions: [ + Action { + iconName: "view-expand" + onTriggered: pageStack.push(Qt.resolvedUrl("FollowedGamesPage.qml")) + } + ] + } + + GridWrapper { + id: mainContainer + grids: [ + ChannelsGrid { + id: gridChannels + + function loadContent() { + var url = "https://api.twitch.tv/kraken/streams/followed?limit=" + countOnPage + "&offset=" + offset + "&oauth_token=" + authToken.value + console.log(url) + HTTP.getRequest(url, function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.streams) + channels.append(result.streams[i]) + } + }) + } + + onLoadMoreAvailableChanged: { + if(!loadMoreAvailable && !showOffline) { + showOffline = true + } + } + }] + } +} diff --git a/ubuntu-ui/pages/GameChannelsPage.qml b/ubuntu-ui/pages/GameChannelsPage.qml new file mode 100755 index 0000000..c3b49cd --- /dev/null +++ b/ubuntu-ui/pages/GameChannelsPage.qml @@ -0,0 +1,96 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import QtGraphicalEffects 1.0 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + property string game + property bool followed + property bool fromFollowings: false + // Status for NavigationCover + property string navStatus: game + + function checkIfFollowed() { + followed = false + if(mainWindow.username) { + HTTP.getRequest("https://api.twitch.tv/api/users/" + mainWindow.username + "/follows/games/" + game, function(data) { + if(data) + followed = true + else + followed = false + }) + } + } + + header: PageHeader { + title: qsTr("Top Games") + flickable: mainContainer + + trailingActionBar.actions: [ + Action { + enabled: mainWindow.username + iconName: followed ? "starred" : "non-starred" + name: followed ? qsTr("Unfollow") : qsTr("Follow") + + Component.onCompleted: checkIfFollowed() + onEnabledChanged: checkIfFollowed() + onTriggered: { + if(!followed) + HTTP.putRequest("https://api.twitch.tv/api/users/" + username + "/follows/games/" + game + "?oauth_token=" + authToken.value, function(data) { + if(data) + followed = true + }) + else + HTTP.deleteRequest("https://api.twitch.tv/api/users/" + username + "/follows/games/" + game + "?oauth_token=" + authToken.value, function(data) { + if(data === 204) + followed = false + }) + } + } + ] + } + + GridWrapper { + id: mainContainer + + grids: [ + ChannelsGrid { + id: gridChannels + + function loadContent() { + var url = "https://api.twitch.tv/kraken/streams?limit=" + countOnPage + "&offset=" + offset + encodeURI("&game=" + game) + HTTP.getRequest(url,function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.streams) + channels.append(result.streams[i]) + } + }) + } + }] + } +} diff --git a/ubuntu-ui/pages/GamesPage.qml b/ubuntu-ui/pages/GamesPage.qml new file mode 100755 index 0000000..f9ee871 --- /dev/null +++ b/ubuntu-ui/pages/GamesPage.qml @@ -0,0 +1,64 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + // Status for NavigationCover + property string navStatus: qsTr("Games") + + header: PageHeader { + title: qsTr("Top Games") + flickable: mainContainer + + leadingActionBar.actions: categories.actions + Categories { + id: categories + games: false + } + } + + GridWrapper { + id: mainContainer + + grids: [ + GamesGrid { + id: gridGames + + function loadContent() { + var url = "https://api.twitch.tv/kraken/games/top?limit=" + countOnPage + "&offset=" + offset + console.log(url) + HTTP.getRequest(url,function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.top) + games.append(result.top[i].game) + } + }) + } + }] + } +} diff --git a/ubuntu-ui/pages/LoginPage.qml b/ubuntu-ui/pages/LoginPage.qml new file mode 100755 index 0000000..3264692 --- /dev/null +++ b/ubuntu-ui/pages/LoginPage.qml @@ -0,0 +1,80 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Ubuntu.Web 0.2 + +Page { + id: page + + property bool needExit: false + // Status for NavigationCover + property string navStatus: qsTr("Settings") + + header: PageHeader { + id: head + title: qsTr("Log into Twitch account") + } + + WebView { + id: twitchLogin + + anchors { + top: head.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + + onUrlChanged: { + var newurl = url.toString() + if(newurl.indexOf("http://localhost") === 0) { + var params = newurl.substring(newurl.lastIndexOf('/') + 1) + if(params.indexOf("#access_token") >= 0) { + authToken.value = params.split('=')[1].split('&')[0] + mainWindow.userChanged() + } + pageStack.pop() + } + } + + onNavigationRequested: { + var rurl = request.url.toString() + console.log(request) + console.log(rurl) + console.log(url) + if(rurl.indexOf("http://localhost") === 0) { + var params = rurl.substring(rurl.lastIndexOf('/') + 1) + if(params.indexOf("#access_token") >= 0) { + authToken.value = params.split('=')[1].split('&')[0] + } + if(status === PageStatus.Activating) + needExit = true + else + pageStack.pop() + } + else + request.action = WebView.AcceptRequest; + } + url: encodeURI("https://api.twitch.tv/kraken/oauth2/authorize?response_type=token&client_id=n57dx0ypqy48ogn1ac08buvoe13bnsu&redirect_uri=http://localhost&scope=user_read user_follows_edit chat_login") + } + + //onStatusChanged: if(status === PageStatus.Active && needExit) pageStack.pop() +} diff --git a/ubuntu-ui/pages/QualityChooserPage.qml b/ubuntu-ui/pages/QualityChooserPage.qml new file mode 100755 index 0000000..8458aa2 --- /dev/null +++ b/ubuntu-ui/pages/QualityChooserPage.qml @@ -0,0 +1,160 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 + +Page { + id: page + + property var qualities: ["chunked", "high", "medium", "low", "mobile"] + property bool chatOnly + property bool audioOnly + + signal accepted + + header: PageHeader { + id: header + + title: qsTr("Stream properties") + flickable: mainContainer + trailingActionBar.actions: [ + Action { + text: qsTr("Apply") + iconName: "ok" + + onTriggered: { + streamQuality.value = qualities[qualityChooser.currentIndex] + chatOnly = chatOnlySwitch.checked + audioOnly = audioOnlySwitch.checked + accepted() + console.log("accepted") + pageStack.pop() + } + }, + + Action { + text: qsTr("Cancel") + iconName: "close" + + onTriggered: pageStack.pop() + } + ] + } + + Flickable { + id: mainContainer + anchors.fill: parent + contentHeight: header.height + optionsContainer.height + + Column { + id: optionsContainer + + anchors { + top: header.bottom + left: parent.left + right: parent.right + } + + ListItem { + width: parent.width + height: qualityChooser.height + units.gu(4) + + OptionSelector { + id: qualityChooser + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: units.gu(2) + } + + text: qsTr("Quality") + model: [ + qsTr("Source"), + qsTr("High"), + qsTr("Medium"), + qsTr("Low"), + qsTr("Mobile") + ] + selectedIndex: qualities.indexOf(streamQuality.value) + } + } + + ListItem { + width: parent.width + + Label { + anchors { + left: parent.left + top: parent.top + margins: units.gu(2) + } + + text: qsTr("Chat only") + } + + Switch { + id: chatOnlySwitch + anchors { + right: parent.right + top: parent.top + margins: units.gu(2) + } + checked: chatOnly + + onCheckedChanged: { + if(checked) + audioOnlySwitch.checked = false + } + } + } + + ListItem { + width: parent.width + + Label { + anchors { + left: parent.left + top: parent.top + margins: units.gu(2) + } + + text: qsTr("Audio only") + } + + Switch { + id: audioOnlySwitch + anchors { + right: parent.right + top: parent.top + margins: units.gu(2) + } + checked: audioOnly + + onCheckedChanged: { + if(checked) + chatOnlySwitch.checked = false + } + } + } + } + } +} diff --git a/ubuntu-ui/pages/SearchPage.qml b/ubuntu-ui/pages/SearchPage.qml new file mode 100755 index 0000000..7f58e45 --- /dev/null +++ b/ubuntu-ui/pages/SearchPage.qml @@ -0,0 +1,90 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "elements" +import "../js/httphelper.js" as HTTP + +Page { + id: page + + // Status for NavigationCover + property string navStatus: qsTr("Search") + + property string querry: "" + + header: PageHeader { + + flickable: mainContainer + + contents: TextField { + id: searchQuerry + + anchors.fill: parent + anchors.margins: units.gu(1) + + hasClearButton: true + placeholderText: qsTr("Search channels") + onTextChanged: { + gridResults.channels.clear() + gridResults.offset = 0 + page.querry = text + gridResults.loadContent() + } + } + + leadingActionBar.actions: categories.actions + Categories { + id: categories + search: false + } + } + + GridWrapper { + id: mainContainer + grids: [ + ChannelsGrid { + id: gridResults + + function loadContent() { + if(querry) { + var url = "https://api.twitch.tv/kraken/search/streams?q=" + querry + "&limit=" + countOnPage + "&offset=" + offset + console.log(url) + HTTP.getRequest(url,function(data) { + if (data) { + offset += countOnPage + var result = JSON.parse(data) + totalCount = result._total + for (var i in result.streams) + channels.append(result.streams[i]) + } + }) + } + else { + totalCount = 0 + } + } + autoLoad: false + + // This prevents search field from loosing focus when grid changes + currentIndex: -1 + }] + } +} diff --git a/ubuntu-ui/pages/SettingsPage.qml b/ubuntu-ui/pages/SettingsPage.qml new file mode 100755 index 0000000..5f05816 --- /dev/null +++ b/ubuntu-ui/pages/SettingsPage.qml @@ -0,0 +1,246 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import "../js/httphelper.js" as HTTP + +Page { + id: page + + property var imageSizes: ["large", "medium", "small"] + property string name + // Status for NavigationCover + property string navStatus: qsTr("Settings") + + function getName() { + if(authToken.value) { + HTTP.getRequest("https://api.twitch.tv/kraken/user?oauth_token=" + authToken.value, function(data) { + var user = JSON.parse(data) + name = user.display_name + mainWindow.username = user.name + }) + } else { + name = "" + mainWindow.username = "" + } + } + + Component.onCompleted: getName() + Connections { + target: mainWindow + onUserChanged: getName() + } + + header: PageHeader { + id: header + + title: qsTr("Settings") + flickable: mainContainer + trailingActionBar.actions: [ + Action { + text: qsTr("Apply") + iconName: "ok" + + onTriggered: { + gameImageSize.value = imageSizes[gameQ.selectedIndex] + channelImageSize.value = imageSizes[previewQ.selectedIndex] + showBroadcastTitles.value = streamTitles.checked + chatFlowBtT.value = chatTtB.checked + pageStack.pop() + } + }, + Action { + text: qsTr("Cancel") + iconName: "close" + + onTriggered: pageStack.pop() + } + ] + } + + Flickable { + id: mainContainer + anchors.fill: parent + contentHeight: header.height + settingsContainer.height + units.gu(2) // for bottom margin + + Column { + id: settingsContainer + + anchors { + top: parent.top + left: parent.left + right: parent.right + } + + ListItem { + id: login + + width: parent.width + height: lblAcc1.height + lblAcc2.height + 2*units.gu(2) + units.gu(1) + + trailingActions: ListItemActions { + actions: [ + Action { + id: accountAction + text: !authToken.value ? qsTr("Log in") : qsTr("Log out") + iconName: "go-next" + + onTriggered: { + console.log("old token:", authToken.value) + if(!authToken.value) { + var lpage = pageStack.push(Qt.resolvedUrl("LoginPage.qml")) + } + else { + authToken.value = "" + console.log("Cookie cleaning script result code:", cpptools.clearCookies()) + mainWindow.userChanged() + } + } + } + ] + } + + onClicked: accountAction.trigger() + + Label { + id: lblAcc1 + + anchors { top: parent.top + left: parent.left + right: parent.right + topMargin: units.gu(2) + leftMargin: units.gu(2) + rightMargin: units.gu(2) + } + text: !authToken.value ? qsTr("Not logged in") : (qsTr("Logged in as ") + name) + font.pixelSize: FontUtils.sizeToPixels("medium") + } + + Label { + id: lblAcc2 + + anchors { bottom: parent.bottom + left: parent.left + right: parent.right + bottomMargin: units.gu(2) + leftMargin: units.gu(2) + rightMargin: units.gu(2) + } + text: !authToken.value ? qsTr("Log in") : qsTr("Log out") + color: UbuntuColors.coolGrey + font.pixelSize: FontUtils.sizeToPixels("small") + } + } + + ListItem { + width: parent.width + + Label { + anchors { + left: parent.left + top: parent.top + margins: units.gu(2) + } + + text: qsTr("Show broadcast titles") } + Switch { + id: streamTitles + anchors { + right: parent.right + top: parent.top + margins: units.gu(2) + } + checked: showBroadcastTitles.value + } + } + + ListItem { + width: parent.width + + Label { + anchors { + left: parent.left + top: parent.top + margins: units.gu(2) + } + + text: qsTr("Chat flows from bottom to top") } + + Switch { + id: chatTtB + anchors { + right: parent.right + top: parent.top + margins: units.gu(2) + } + checked: chatFlowBtT.value + } + } + + ListItem { + width: parent.width + height: gameQ.height + units.gu(4) + + OptionSelector { + id: gameQ + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: units.gu(2) + } + + text: qsTr("Game posters quality") + model: [ + qsTr("High"), + qsTr("Medium"), + qsTr("Low") + ] + selectedIndex: imageSizes.indexOf(gameImageSize.value) + } + } + + ListItem { + width: parent.width + height: previewQ.height + units.gu(4) + + OptionSelector { + id: previewQ + + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: units.gu(2) + } + + text: qsTr("Stream previews quality") + model: [ + qsTr("High"), + qsTr("Medium"), + qsTr("Low") + ] + selectedIndex: imageSizes.indexOf(channelImageSize.value) + } + } + } + } +} diff --git a/ubuntu-ui/pages/StreamPage.qml b/ubuntu-ui/pages/StreamPage.qml new file mode 100755 index 0000000..059a2b5 --- /dev/null +++ b/ubuntu-ui/pages/StreamPage.qml @@ -0,0 +1,368 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import QtMultimedia 5.0 +import aldrog.twitchtube.ircchat 1.0 +import "../js/httphelper.js" as HTTP + +Page { + id: page + + property var url + property string channel + property string username + property bool followed + property bool chatMode: false + property bool audioMode: false + property bool active: Qt.application.active + property bool isLandscape: width > height + property bool isPortrait: !isLandscape + property bool fullscreenConditions: isLandscape && main.visibleArea.yPosition === 0 && !main.moving && !state && video.visible + + function findUrl(s, q) { + for (var x in s) { + if (s[x].substring(0,4) === "http" && s[x].indexOf(q) >= 0) + return s[x] + } + } + + function loadStreamInfo() { + HTTP.getRequest("http://api.twitch.tv/api/channels/" + channel + "/access_token", function (tokendata) { + if (tokendata) { + var token = JSON.parse(tokendata) + HTTP.getRequest(encodeURI("http://usher.twitch.tv/api/channel/hls/" + channel + ".json?allow_source=true&allow_audio_only=true&sig=" + token.sig + "&token=" + token.token + "&type=any"), function (data) { + if (data) { + var videourls = data.split('\n') + url = { + chunked: findUrl(videourls, "chunked"), + high: findUrl(videourls, "high"), + medium: findUrl(videourls, "medium"), + low: findUrl(videourls, "low"), + mobile: findUrl(videourls, "mobile"), + audio: findUrl(videourls, "audio_only") + } + video.play() + mainWindow.audioUrl = url.audio + } + }) + } + }) + } + + function checkFollow() { + if(mainWindow.username) { + HTTP.getRequest("https://api.twitch.tv/kraken/users/" + mainWindow.username + "/follows/channels/" + channel, function(data) { + if(data) + return true + }) + } + return false + } + + onChatModeChanged: { + if(chatMode) + video.stop() + } + +// onStatusChanged: { +// if(status === PageStatus.Activating) { +// mainWindow.currentChannel = channel +// mainWindow.cover = Qt.resolvedUrl("../cover/StreamCover.qml") +// cpptools.setBlankingMode(false) +// } +// if(status === PageStatus.Deactivating) { +// if (_navigation === PageNavigation.Back) { +// mainWindow.cover = Qt.resolvedUrl("../cover/NavigationCover.qml") +// } +// cpptools.setBlankingMode(true) +// } +// } + +// onActiveChanged: { +// if(page.status === PageStatus.Active) { +// if(active) { +// mainWindow.stopAudio() +// video.play() +// if(!twitchChat.connected) { +// twitchChat.reopenSocket() +// twitchChat.join(channel) +// } +// } +// else { +// video.pause() +// if(audioMode) +// mainWindow.playAudio() +// if(twitchChat.connected) +// twitchChat.disconnect() +// } +// } +// } + + Component.onCompleted: { + loadStreamInfo() + followed = checkFollow() + } + + header: PageHeader { + title: channel + flickable: main + + trailingActionBar.actions: [ + Action { + enabled: mainWindow.username + iconName: followed ? "starred" : "non-starred" + name: followed ? qsTr("Unfollow") : qsTr("Follow") + + onTriggered: { + if(!followed) { + HTTP.putRequest("https://api.twitch.tv/kraken/users/" + username + "/follows/channels/" + channel + "?oauth_token=" + authToken.value, function(data) { + if(data) + followed = true + }) + } else { + HTTP.deleteRequest("https://api.twitch.tv/kraken/users/" + username + "/follows/channels/" + channel + "?oauth_token=" + authToken.value, function(data) { + if(data === 204) + followed = false + }) + } + } + }, + + Action { + iconName: "settings" + name: qsTr("Quality") + onTriggered: { + pageStack.push(streamSettings, { chatOnly: chatMode, audioOnly: audioMode, channel: channel }) + } + } + ] + } + + QualityChooserPage { + id: streamSettings + onAccepted: { + chatMode = chatOnly + audioMode = audioOnly + console.log("Chat mode", chatMode) + console.log("Audio mode", audioMode) + } + } + + Timer { + id: fullscreenTimer + + interval: 3000 + running: fullscreenConditions + onTriggered: page.state = "fullscreen" + } + + Flickable { + id: main + + anchors.fill: parent + contentHeight: isPortrait ? page.height : (chatMode ? page.height : (5/3 * page.height)) + //onContentHeightChanged: console.log(contentHeight, height + Screen.width, Screen.width, chat.height) + +// PullDownMenu { +// id: streamMenu + +// MenuItem { +// text: qsTr("Follow") +// onClicked: HTTP.putRequest("https://api.twitch.tv/kraken/users/" + username + "/follows/channels/" + channel + "?oauth_token=" + authToken.value, function(data) { +// if(data) +// followed = true +// }) +// visible: mainWindow.username && !followed +// } + +// MenuItem { +// text: qsTr("Unfollow") +// onClicked: HTTP.deleteRequest("https://api.twitch.tv/kraken/users/" + username + "/follows/channels/" + channel + "?oauth_token=" + authToken.value, function(data) { +// if(data === 204) +// followed = false +// }) +// visible: mainWindow.username && followed +// } + +// MenuItem { +// text: qsTr("Quality") +// onClicked: { +// var dialog = pageStack.push(Qt.resolvedUrl("QualityChooserPage.qml"), { chatOnly: chatMode, audioOnly: audioMode, channel: channel }) +// dialog.accepted.connect(function() { +// chatMode = dialog.chatOnly +// audioMode = dialog.audioOnly +// }) +// } +// } +// } + + Rectangle { + id: videoBackground + + color: "black" + anchors.top: parent.top; anchors.left: parent.left; anchors.right: parent.right + height: (!chatMode && !audioMode) ? (page.width * 9/16) : 0 + visible: (!chatMode && !audioMode) + + Video { + id: video + + anchors.fill: parent + source: audioMode ? url["audio"] : url[streamQuality.value] + + onErrorChanged: console.error("video error:", errorString) + + ActivityIndicator { + anchors.centerIn: parent + running: video.playbackState !== MediaPlayer.PlayingState + } + + MouseArea { + anchors.fill: parent + onClicked: { + page.state = !page.state ? "fullscreen" : "" + console.log(page.state) + } + } + } + } + + TextField { + id: chatMessage + + anchors { + left: parent.left + right: parent.right + top: chatFlowBtT.value ? videoBackground.bottom : undefined + bottom: chatFlowBtT.value ? undefined : parent.bottom + topMargin: chatMode ? Theme.paddingLarge : Theme.paddingMedium + bottomMargin: Theme.paddingMedium + } + // Maybe it's better to replace ternary operators with if else blocks + placeholderText: twitchChat.connected ? (twitchChat.anonymous ? qsTr("Please log in to send messages") : qsTr("Type your message here")) : qsTr("Chat is not available") + enabled: twitchChat.connected && !twitchChat.anonymous + inputMask: "X" + onAccepted: { + twitchChat.sendMessage(text) + text = "" + } + } + + ListView { + id: chat + + anchors { + left: parent.left + right: parent.right + top: chatFlowBtT.value ? chatMessage.bottom : videoBackground.bottom + bottom: chatFlowBtT.value ? parent.bottom : chatMessage.top + //topMargin: (chatMode && !chatFlowBtT.value) ? 0 : Theme.paddingMedium + //bottomMargin: 0//chatFlowBtT.value ? Theme.paddingLarge : Theme.paddingMedium + } + + highlightRangeMode: count > 0 ? ListView.StrictlyEnforceRange : ListView.NoHighlightRange + //preferredHighlightBegin: chat.height - currentItem.height + preferredHighlightEnd: chat.height + clip: true + verticalLayoutDirection: chatFlowBtT.value ? ListView.BottomToTop : ListView.TopToBottom + + model: twitchChat.messages + delegate: Item { + height: lbl.height + width: chat.width + + ListView.onAdd: { + if(chat.currentIndex >= chat.count - 3) { + chat.currentIndex = chat.count - 1 + } + } + + Label { + id: lbl + + anchors { + left: parent.left + right: parent.right + leftMargin: Theme.horizontalPageMargin + rightMargin: Theme.horizontalPageMargin + } + + text: richTextMessage + textFormat: Text.RichText + wrapMode: Text.WordWrap + color: isNotice ? UbuntuColors.orange : UbuntuColors.ash + } + } + + IrcChat { + id: twitchChat + + name: mainWindow.username + password: 'oauth:' + authToken.value + anonymous: !mainWindow.username + textSize: 14 + + Component.onCompleted: { + twitchChat.join(channel) + } + + onErrorOccured: { + console.log("Chat error: ", errorDescription) + } + + onConnectedChanged: { + console.log(connected) + } + } + } + } + + states: State { + name: "fullscreen" + PropertyChanges { + target: main + contentHeight: page.height + } + + PropertyChanges { + target: chatMessage + visible: false + } + + PropertyChanges { + target: chat + visible: false + } + +// PropertyChanges { +// target: streamMenu +// visible: false +// active: false +// } + + PropertyChanges { + target: mainWindow + // special flag only supported by Unity8/MIR so far that hides the shell's + // top panel in Staged mode + flags: Qt.Window | 0x00800000 + } + } +} diff --git a/ubuntu-ui/pages/elements/Categories.qml b/ubuntu-ui/pages/elements/Categories.qml new file mode 100755 index 0000000..a71421d --- /dev/null +++ b/ubuntu-ui/pages/elements/Categories.qml @@ -0,0 +1,73 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 + +Item { + property bool search: true + property bool following: true + property bool channels: true + property bool games: true + + property list actions: [ + Action { + text: qsTr("Games") + visible: games + onTriggered: { + pageStack.pop() + pageStack.push(Qt.resolvedUrl("../GamesPage.qml")) + } + }, + + Action { + text: qsTr("Channels") + visible: channels + onTriggered: { + pageStack.pop() + pageStack.push(Qt.resolvedUrl("../ChannelsPage.qml")) + } + }, + + Action { + text: qsTr("Following") + visible: following && authToken.value + onTriggered: { + pageStack.pop() + pageStack.push(Qt.resolvedUrl("../FollowedPage.qml")) + } + }, + + Action { + text: qsTr("Search") + visible: search + onTriggered: { + pageStack.pop() + pageStack.push(Qt.resolvedUrl("../SearchPage.qml")) + } + }, + + Action { + text: qsTr("Settings") + onTriggered: { + pageStack.push(Qt.resolvedUrl("../SettingsPage.qml")) + } + } + ] +} diff --git a/ubuntu-ui/pages/elements/ChannelsGrid.qml b/ubuntu-ui/pages/elements/ChannelsGrid.qml new file mode 100755 index 0000000..154a793 --- /dev/null +++ b/ubuntu-ui/pages/elements/ChannelsGrid.qml @@ -0,0 +1,100 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 + +GridView { + id: grid + + property alias channels: grid.model + property bool loadMoreAvailable: offset < totalCount + property int row: 3 + // In brackets must be row lengths for portrait and landscape orientations + property int countOnPage: (2*3) * 3 + property int offset: 0 + property int totalCount: 0 + property bool autoLoad: true + property var parameters: ({}) + + height: childrenRect.height + + Component.onCompleted: { + if(autoLoad) + loadContent() + } + + anchors { + left: parent.left + right: parent.right + } + interactive: false + + model: ListModel { id: channelsList } + cellWidth: width/row + // 5:8 is the actual aspect ratio of previews + cellHeight: cellWidth * 5/8 + + delegate: Empty { + id: delegate + + width: grid.cellWidth + height: grid.cellHeight + + onClicked: { + var properties = parameters + properties.channel = channel.name + pageStack.push (Qt.resolvedUrl("../StreamPage.qml"), properties) + } + + Image { + id: previewImage + + source: preview[channelImageSize.value] + anchors.fill: parent + anchors.margins: units.gu(1) + } + + Label { + id: name + + anchors { + left: previewImage.left; leftMargin: units.gu(2) + right: previewImage.right; rightMargin: units.gu(1) + topMargin: units.gu(1) + } + text: channel.display_name + color: UbuntuColors.lightGrey + } + + Label { + id: title + + visible: showBroadcastTitles.value + anchors { + left: previewImage.left; leftMargin: units.gu(2) + right: previewImage.right; rightMargin: units.gu(1) + top: name.bottom; topMargin: -units.gu(1) + } + text: channel.status + color: UbuntuColors.silk + } + } +} diff --git a/ubuntu-ui/pages/elements/GamesGrid.qml b/ubuntu-ui/pages/elements/GamesGrid.qml new file mode 100755 index 0000000..8307a2e --- /dev/null +++ b/ubuntu-ui/pages/elements/GamesGrid.qml @@ -0,0 +1,86 @@ +/* + * Copyright © 2015-2016 Andrew Penkrat + * + * This file is part of TwitchTube. + * + * TwitchTube is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * TwitchTube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with TwitchTube. If not, see . + */ + +import QtQuick 2.4 +import Ubuntu.Components 1.3 +import Ubuntu.Components.ListItems 1.3 + +GridView { + id: grid + + property alias games: grid.model + property int row: 4 + // In brackets must be row lengths for portrait and landscape orientations + property int countOnPage: (2*4) * 2 + property int offset: 0 + property int totalCount: 0 + property bool autoLoad: true + property var parameters: ({}) + + Component.onCompleted: { + if(autoLoad) + loadContent() + } + + height: childrenRect.height + anchors { + left: parent.left + right: parent.right + } + + interactive: false + + model: ListModel { id: gameList } + cellWidth: width/row + // 18:13 is the actual aspect ratio of previews + cellHeight: cellWidth * 18/13 + + delegate: Empty { + id: delegate + + width: grid.cellWidth + height: grid.cellHeight + onClicked: { + var properties = parameters + properties.game = name + pageStack.push (Qt.resolvedUrl("../GameChannelsPage.qml"), properties) + } + + Image { + id: logo + + anchors.fill: parent + anchors.margins: units.gu(1) + fillMode: Image.PreserveAspectCrop + source: box[gameImageSize.value] + } + + Label { + id: gameName + + anchors { + left: parent.left; leftMargin: units.gu(2) + right: parent.right; rightMargin: units.gu(2) + topMargin: units.gu(1) + } + text: name + color: UbuntuColors.silk + } + } +} diff --git a/ubuntu-ui/pages/elements/GridWrapper.qml b/ubuntu-ui/pages/elements/GridWrapper.qml new file mode 100755 index 0000000..79a1969 --- /dev/null +++ b/ubuntu-ui/pages/elements/GridWrapper.qml @@ -0,0 +1,37 @@ +import QtQuick 2.4 +import Ubuntu.Components 1.3 + +Flickable { + id: root + + property alias grids: container.data + //property alias header: mainHeader + + anchors.fill: parent + contentHeight: container.height + + Column { + id: container + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: units.gu(2) + anchors.rightMargin: units.gu(2) + +// PageHeader { +// id: mainHeader +// } + } + +// PushUpMenu { +// id: loadMoreMenu +// enabled: grids[grids.length - 1].offset < grids[grids.length - 1].totalCount +// visible: grids[grids.length - 1].offset < grids[grids.length - 1].totalCount + +// MenuItem { +// text: qsTr("Load more") +// onClicked: { +// grids[grids.length - 1].loadContent() +// } +// } +// } +} diff --git a/ubuntu/TwitchTube.apparmor b/ubuntu/TwitchTube.apparmor new file mode 100644 index 0000000..51ef8ef --- /dev/null +++ b/ubuntu/TwitchTube.apparmor @@ -0,0 +1,8 @@ +{ + "policy_groups": [ + "video", + "webview", + "networking" + ], + "policy_version": 1.3 +} diff --git a/ubuntu/TwitchTube.desktop b/ubuntu/TwitchTube.desktop new file mode 100644 index 0000000..2ba508b --- /dev/null +++ b/ubuntu/TwitchTube.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=TwitchTube +Exec=TwitchTube +Icon=TwitchTube/TwitchTube.png +Terminal=false +Type=Application +X-Ubuntu-Touch=true diff --git a/ubuntu/TwitchTube.png b/ubuntu/TwitchTube.png new file mode 100644 index 0000000..7e6d884 Binary files /dev/null and b/ubuntu/TwitchTube.png differ diff --git a/ubuntu/manifest.json.in b/ubuntu/manifest.json.in new file mode 100644 index 0000000..0c87e01 --- /dev/null +++ b/ubuntu/manifest.json.in @@ -0,0 +1,15 @@ +{ + "name": "twitchtube.aldrog", + "description": "3rd party client for Twitch streaming service", + "architecture": "@CLICK_ARCH@", + "title": "TwitchTube", + "hooks": { + "TwitchTube": { + "apparmor": "TwitchTube/TwitchTube.apparmor", + "desktop": "TwitchTube/TwitchTube.desktop" + } + }, + "version": "0.1", + "maintainer": "Andrew Penkrat ", + "framework": "ubuntu-sdk-15.04.5" +}