From 67c8621d6f4f9e4a57a85ab67f5aed1856b42243 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Sat, 26 Sep 2020 23:29:45 +0200 Subject: [PATCH] Added settings, logout and improved error states --- harbour-sailfin.pro | 2 + qml/components/IconListItem.qml | 27 ++++++++++++ qml/components/RemoteImage.qml | 1 + qml/harbour-sailfin.qml | 8 ++++ qml/pages/DetailPage.qml | 24 +++++++---- qml/pages/MainPage.qml | 52 +++++++++++++++++------ qml/pages/SettingsPage.qml | 75 +++++++++++++++++++++++++++++++++ src/credentialmanager.cpp | 2 +- src/jellyfinapiclient.cpp | 35 ++++++++------- src/jellyfinapiclient.h | 20 +++++++++ translations/harbour-sailfin.ts | 60 ++++++++++++++++++++++++-- 11 files changed, 264 insertions(+), 42 deletions(-) create mode 100644 qml/components/IconListItem.qml create mode 100644 qml/pages/SettingsPage.qml diff --git a/harbour-sailfin.pro b/harbour-sailfin.pro index 182a3db..7df1b53 100644 --- a/harbour-sailfin.pro +++ b/harbour-sailfin.pro @@ -32,6 +32,7 @@ DISTFILES += \ qml/Constants.qml \ qml/Utils.js \ qml/components/GlassyBackground.qml \ + qml/components/IconListItem.qml \ qml/components/LibraryItemDelegate.qml \ qml/components/MoreSection.qml \ qml/components/PlainLabel.qml \ @@ -54,6 +55,7 @@ DISTFILES += \ qml/pages/MainPage.qml \ qml/pages/AboutPage.qml \ qml/harbour-sailfin.qml \ + qml/pages/SettingsPage.qml \ qml/pages/VideoPage.qml \ qml/pages/setup/AddServerConnectingPage.qml \ qml/pages/setup/LoginDialog.qml \ diff --git a/qml/components/IconListItem.qml b/qml/components/IconListItem.qml new file mode 100644 index 0000000..e8fdc31 --- /dev/null +++ b/qml/components/IconListItem.qml @@ -0,0 +1,27 @@ +import QtQuick 2.0 +import Sailfish.Silica 1.0 + +BackgroundItem { + property alias text: label.text + property alias iconSource: icon.source + HighlightImage { + id: icon + anchors { + top: parent.top + topMargin: Theme.paddingMedium + left: parent.left + leftMargin: Theme.horizontalPageMargin + bottom: parent.bottom + bottomMargin: Theme.paddingMedium + } + } + + Label { + id: label + anchors { + left: icon.right + leftMargin: Theme.paddingMedium + verticalCenter: parent.verticalCenter + } + } +} diff --git a/qml/components/RemoteImage.qml b/qml/components/RemoteImage.qml index 2cd9bb4..257458b 100644 --- a/qml/components/RemoteImage.qml +++ b/qml/components/RemoteImage.qml @@ -7,6 +7,7 @@ import Sailfish.Silica 1.0 HighlightImage { property string fallbackImage property bool usingFallbackImage + asynchronous: true BusyIndicator { anchors.centerIn: parent diff --git a/qml/harbour-sailfin.qml b/qml/harbour-sailfin.qml index a3691b3..8500264 100644 --- a/qml/harbour-sailfin.qml +++ b/qml/harbour-sailfin.qml @@ -13,6 +13,14 @@ ApplicationWindow { property bool _hasInitialized: false // The global mediaPlayer instance readonly property MediaPlayer mediaPlayer: _mediaPlayer + property url backgroundSource + onBackgroundSourceChanged: { + if (backgroundSource) { + appWindow._overlayBackgroundSource.backgroundItem.source = backgroundSource + } else { + appWindow._overlayBackgroundSource.backgroundItem.source = Theme.backgroundImage + } + } // Data of the currently selected item. For use on the cover. property var itemData diff --git a/qml/pages/DetailPage.qml b/qml/pages/DetailPage.qml index b66167f..8b9a114 100644 --- a/qml/pages/DetailPage.qml +++ b/qml/pages/DetailPage.qml @@ -21,7 +21,7 @@ Page { readonly property string _logo: itemData.ImageTags.Logo readonly property var _backdropImages: itemData.BackdropImageTags readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags - readonly property string parentId: itemData.ParentId + readonly property string parentId: itemData.ParentId || "" on_BackdropImagesChanged: updateBackdrop() on_ParentBackdropImagesChanged: updateBackdrop() @@ -30,10 +30,13 @@ Page { if (_backdropImages && _backdropImages.length > 0) { var rand = Math.floor(Math.random() * (_backdropImages.length - 0.001)) console.log("Random: ", rand) - backdrop.source = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" + _backdropImages[rand] + "&maxHeight" + height + //backdrop.source = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" + _backdropImages[rand] + "&maxHeight" + height + appWindow.backgroundSource = ApiClient.baseUrl + "/Items/" + itemId + "/Images/Backdrop/" + rand + "?tag=" + _backdropImages[rand] + "&maxHeight" + height } else if (_parentBackdropImages && _parentBackdropImages.length > 0) { console.log(parentId) - backdrop.source = ApiClient.baseUrl + "/Items/" + itemData.ParentBackdropItemId + "/Images/Backdrop/0?tag=" + _parentBackdropImages[0] + //backdrop.source = ApiClient.baseUrl + "/Items/" + itemData.ParentBackdropItemId + "/Images/Backdrop/0?tag=" + _parentBackdropImages[0] + appWindow.backgroundSource = ApiClient.baseUrl + "/Items/" + itemData.ParentBackdropItemId + "/Images/Backdrop/0?tag=" + _parentBackdropImages[0] + Theme.backgroundGlowColor } } @@ -53,7 +56,7 @@ Page { width: parent.width PageHeader { - title: itemData.Name + title: itemData.Name || qsTr("Loading") visible: !_hasLogo } @@ -67,7 +70,7 @@ Page { anchors { horizontalCenter: parent.horizontalCenter } - source: _hasLogo ? ApiClient.baseUrl + "/Items/" + itemId + "/Images/Logo?tag=" + _logo : undefined + source: _hasLogo ? ApiClient.baseUrl + "/Items/" + itemId + "/Images/Logo?tag=" + _logo : "" } Item { width: 1 @@ -95,6 +98,8 @@ Page { return Qt.resolvedUrl("../components/itemdetails/SeasonDetails.qml") case "Episode": return Qt.resolvedUrl("../components/itemdetails/EpisodeDetails.qml") + case undefined: + return "" default: return Qt.resolvedUrl("../components/itemdetails/UnsupportedDetails.qml") } @@ -114,7 +119,7 @@ Page { onItemIdChanged: { itemData = {} - if (itemId.length > 0) { + if (itemId.length && PageStatus.Active) { pageRoot._loading = true ApiClient.fetchItem(itemId) } @@ -125,8 +130,11 @@ Page { backdrop.clear() //appWindow.itemData = ({}) } - if (status == PageStatus.Active && itemData) { - appWindow.itemData = itemData + if (status == PageStatus.Active) { + if (itemId) { + ApiClient.fetchItem(itemId) + } + } } diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index 8f9e470..814ed1c 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -16,15 +16,20 @@ Page { id: page allowedOrientations: Orientation.All + ViewPlaceholder { + + } + SilicaFlickable { anchors.fill: parent // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView PullDownMenu { MenuItem { - text: qsTr("About") - onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) + text: qsTr("Settings") + onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) } + busy: mediaLibraryModel.status == ApiModel.Loading } // Tell SilicaFlickable the height of its content. @@ -60,7 +65,7 @@ Page { MoreSection { text: model.name busy: userItemModel.status != ApiModel.Ready - property string collectionType: model.collectionType + property string collectionType: model.collectionType || "" onHeaderClicked: pageStack.push(Qt.resolvedUrl("DetailPage.qml"), {"itemId": model.id}) @@ -117,30 +122,49 @@ Page { } } } + Column { + width: parent.width + visible: mediaLibraryModel.status == ApiModel.Error + PageHeader { + title: qsTr("Network error") + //clickable: false + } + + PlainLabel { + text: qsTr("An error has occurred. Please try again.") + } + Item { width: 1; height: Theme.paddingLarge } + Button { + text: qsTr("Retry") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: loadModels(true) + } + Item { width: 1; height: Theme.paddingLarge } + } } } onStatusChanged: { if (status == PageStatus.Active) { appWindow.itemData = null - if (!_modelsLoaded && ApiClient.authenticated) loadModels() + loadModels(false) } } Connections { target: ApiClient - onAuthenticatedChanged: { - if (authenticated /*&& !_modelsLoaded*/) loadModels(); - } + onAuthenticatedChanged: loadModels(false) } - Component.onCompleted: { - if (ApiClient.authenticated && _modelsLoaded) { - loadModels(); + + /** + * Loads models if not laoded. Set force to true to reload models + * even if loaded. + */ + function loadModels(force) { + if (force || (ApiClient.authenticated && !_modelsLoaded)) { + _modelsLoaded = true; + mediaLibraryModel.reload() } - } - function loadModels() { - _modelsLoaded = true; - mediaLibraryModel.reload() } } diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml new file mode 100644 index 0000000..ac35701 --- /dev/null +++ b/qml/pages/SettingsPage.qml @@ -0,0 +1,75 @@ +import QtQuick 2.6 +import Sailfish.Silica 1.0 + +import nl.netsoj.chris.Jellyfin 1.0 + +import "../components" + +Page { + id: settingsPage + + SilicaFlickable { + anchors.fill: parent + contentHeight: content.height + + Column { + id: content + width: parent.width + + RemorsePopup { + id: remorse + } + + PageHeader { + //: Header of Settings page + title: qsTr("Settings") + } + + + SectionHeader { + text: qsTr("Session") + } + + PlainLabel { + text: qsTr("Server") + } + + PlainLabel { + text: ApiClient.baseUrl + color: Theme.secondaryHighlightColor + } + + Item { width: 1; height: Theme.paddingMedium; } + + PlainLabel { + text: qsTr("User id") + } + + PlainLabel { + text: ApiClient.userId + color: Theme.secondaryHighlightColor + } + + Item { width: 1; height: Theme.paddingLarge; } + + ButtonLayout { + Button { + text: qsTr("Log out") + onClicked: remorse.execute(qsTr("Logging out"), ApiClient.deleteSession) + } + } + + SectionHeader { + //: Other settings + text: qsTr("Other") + } + + IconListItem { + text: qsTr("About Sailfin") + iconSource: "image://theme/icon-m-about" + onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) + } + } + } + +} diff --git a/src/credentialmanager.cpp b/src/credentialmanager.cpp index e027731..31700ef 100644 --- a/src/credentialmanager.cpp +++ b/src/credentialmanager.cpp @@ -33,7 +33,7 @@ void FallbackCredentialsManager::get(const QString &server, const QString &user) } void FallbackCredentialsManager::remove(const QString &server, const QString &user) { - m_settings.remove(urlToGroupName(server) + "/" + user); + m_settings.remove(urlToGroupName(server) + "/users/" + user); } void FallbackCredentialsManager::listServers() const { diff --git a/src/jellyfinapiclient.cpp b/src/jellyfinapiclient.cpp index 09cfaaf..4978b7d 100644 --- a/src/jellyfinapiclient.cpp +++ b/src/jellyfinapiclient.cpp @@ -132,12 +132,7 @@ void ApiClient::setupConnection() { } rep->deleteLater(); }); - connect(rep, static_cast(&QNetworkReply::error), - this, [rep, this](QNetworkReply::NetworkError error) { - qDebug() << "Error from URL: " << rep->url(); - emit this->networkError(error); - rep->deleteLater(); - }); + setDefaultErrorHandler(rep); } void ApiClient::getBrandingConfiguration() { @@ -162,11 +157,7 @@ void ApiClient::getBrandingConfiguration() { } rep->deleteLater(); }); - connect(rep, static_cast(&QNetworkReply::error), - this, [rep, this](QNetworkReply::NetworkError error) { - emit this->networkError(error); - rep->deleteLater(); - }); + setDefaultErrorHandler(rep); } void ApiClient::authenticate(QString username, QString password, bool storeCredentials) { @@ -195,8 +186,17 @@ void ApiClient::authenticate(QString username, QString password, bool storeCrede } rep->deleteLater(); }); - connect(rep, static_cast(&QNetworkReply::error), - this, &ApiClient::defaultNetworkErrorHandler); + setDefaultErrorHandler(rep); +} + +void ApiClient::deleteSession() { + QNetworkReply *rep = post("/Sessions/Logout"); + connect(rep, &QNetworkReply::finished, this, [rep, this] { + m_credManager->remove(m_baseUrl, m_userId); + this->setAuthenticated(false); + emit this->setupRequired(); + rep->deleteLater(); + }); } void ApiClient::fetchItem(const QString &id) { @@ -209,6 +209,11 @@ void ApiClient::fetchItem(const QString &id) { } rep->deleteLater(); }); + connect(rep, static_cast(&QNetworkReply::error), + this, [id, rep, this](QNetworkReply::NetworkError error) { + emit this->itemFetchFailed(id, error); + rep->deleteLater(); + }); } void ApiClient::postCapabilities() { @@ -221,8 +226,7 @@ void ApiClient::postCapabilities() { capabilities["IconUrl"] = "https://chris.netsoj.nl/static/img/logo.png"; capabilities["DeviceProfile"] = m_deviceProfile; QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities)); - connect(rep, static_cast(&QNetworkReply::error), - this, &ApiClient::defaultNetworkErrorHandler); + setDefaultErrorHandler(rep); } void ApiClient::generateDeviceProfile() { @@ -244,6 +248,7 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) { QObject *signalSender = sender(); QNetworkReply *rep = dynamic_cast(signalSender); if (rep != nullptr && statusCode(rep) == 401) { + this->setAuthenticated(false); emit this->authenticationError(ApiError::INVALID_PASSWORD); } else { emit this->networkError(error); diff --git a/src/jellyfinapiclient.h b/src/jellyfinapiclient.h index c4a4637..198520b 100644 --- a/src/jellyfinapiclient.h +++ b/src/jellyfinapiclient.h @@ -106,6 +106,7 @@ signals: void userIdChanged(QString userId); void itemFetched(const QString &itemId, const QJsonObject &result); + void itemFetchFailed(const QString &itemId, const QNetworkReply::NetworkError error); public slots: /** @@ -120,6 +121,12 @@ public slots: */ void setupConnection(); void authenticate(QString username, QString password, bool storeCredentials = false); + + /** + * @brief Logs the user out and clears the session. + */ + void deleteSession(); + void fetchItem(const QString &id); /** @@ -157,6 +164,7 @@ protected: * is a big mess and should be safely contained in it's own file. */ void generateDeviceProfile(); + QString &token() { return m_token; } private: @@ -206,6 +214,18 @@ private: static inline int statusCode(QNetworkReply *rep) { return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); } + + /** + * @brief Sets the error handler of a reply to this classes default error handler + * @param rep The reply to set the error handler on. + * + * Motivation for this helper is because I forget the correct signature each time, with all the + * funky casts. + */ + void setDefaultErrorHandler(QNetworkReply *rep) { + connect(rep, static_cast(&QNetworkReply::error), + this, &ApiClient::defaultNetworkErrorHandler); + } }; } // NS Jellyfin diff --git a/translations/harbour-sailfin.ts b/translations/harbour-sailfin.ts index 7800e44..ff900a3 100644 --- a/translations/harbour-sailfin.ts +++ b/translations/harbour-sailfin.ts @@ -65,6 +65,13 @@ + + DetailPage + + Loading + + + EpisodeDetails @@ -127,10 +134,6 @@ MainPage - - About - - Resume watching @@ -139,6 +142,22 @@ Next up + + Settings + + + + Network error + + + + An error has occurred. Please try again. + + + + Retry + + SeasonDetails @@ -156,6 +175,39 @@ + + SettingsPage + + Settings + Header of Settings page + + + + Other + Other settings + + + + About Sailfin + + + + Session + + + + Server + + + + User id + + + + Log out + + + UnsupportedDetails