1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-05-18 12:02:41 +00:00

Added settings, logout and improved error states

This commit is contained in:
Chris Josten 2020-09-26 23:29:45 +02:00
parent edb514bf2d
commit 67c8621d6f
11 changed files with 264 additions and 42 deletions

View file

@ -32,6 +32,7 @@ DISTFILES += \
qml/Constants.qml \ qml/Constants.qml \
qml/Utils.js \ qml/Utils.js \
qml/components/GlassyBackground.qml \ qml/components/GlassyBackground.qml \
qml/components/IconListItem.qml \
qml/components/LibraryItemDelegate.qml \ qml/components/LibraryItemDelegate.qml \
qml/components/MoreSection.qml \ qml/components/MoreSection.qml \
qml/components/PlainLabel.qml \ qml/components/PlainLabel.qml \
@ -54,6 +55,7 @@ DISTFILES += \
qml/pages/MainPage.qml \ qml/pages/MainPage.qml \
qml/pages/AboutPage.qml \ qml/pages/AboutPage.qml \
qml/harbour-sailfin.qml \ qml/harbour-sailfin.qml \
qml/pages/SettingsPage.qml \
qml/pages/VideoPage.qml \ qml/pages/VideoPage.qml \
qml/pages/setup/AddServerConnectingPage.qml \ qml/pages/setup/AddServerConnectingPage.qml \
qml/pages/setup/LoginDialog.qml \ qml/pages/setup/LoginDialog.qml \

View file

@ -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
}
}
}

View file

@ -7,6 +7,7 @@ import Sailfish.Silica 1.0
HighlightImage { HighlightImage {
property string fallbackImage property string fallbackImage
property bool usingFallbackImage property bool usingFallbackImage
asynchronous: true
BusyIndicator { BusyIndicator {
anchors.centerIn: parent anchors.centerIn: parent

View file

@ -13,6 +13,14 @@ ApplicationWindow {
property bool _hasInitialized: false property bool _hasInitialized: false
// The global mediaPlayer instance // The global mediaPlayer instance
readonly property MediaPlayer mediaPlayer: _mediaPlayer 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. // Data of the currently selected item. For use on the cover.
property var itemData property var itemData

View file

@ -21,7 +21,7 @@ Page {
readonly property string _logo: itemData.ImageTags.Logo readonly property string _logo: itemData.ImageTags.Logo
readonly property var _backdropImages: itemData.BackdropImageTags readonly property var _backdropImages: itemData.BackdropImageTags
readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags readonly property var _parentBackdropImages: itemData.ParentBackdropImageTags
readonly property string parentId: itemData.ParentId readonly property string parentId: itemData.ParentId || ""
on_BackdropImagesChanged: updateBackdrop() on_BackdropImagesChanged: updateBackdrop()
on_ParentBackdropImagesChanged: updateBackdrop() on_ParentBackdropImagesChanged: updateBackdrop()
@ -30,10 +30,13 @@ Page {
if (_backdropImages && _backdropImages.length > 0) { if (_backdropImages && _backdropImages.length > 0) {
var rand = Math.floor(Math.random() * (_backdropImages.length - 0.001)) var rand = Math.floor(Math.random() * (_backdropImages.length - 0.001))
console.log("Random: ", rand) 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) { } else if (_parentBackdropImages && _parentBackdropImages.length > 0) {
console.log(parentId) 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 width: parent.width
PageHeader { PageHeader {
title: itemData.Name title: itemData.Name || qsTr("Loading")
visible: !_hasLogo visible: !_hasLogo
} }
@ -67,7 +70,7 @@ Page {
anchors { anchors {
horizontalCenter: parent.horizontalCenter 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 { Item {
width: 1 width: 1
@ -95,6 +98,8 @@ Page {
return Qt.resolvedUrl("../components/itemdetails/SeasonDetails.qml") return Qt.resolvedUrl("../components/itemdetails/SeasonDetails.qml")
case "Episode": case "Episode":
return Qt.resolvedUrl("../components/itemdetails/EpisodeDetails.qml") return Qt.resolvedUrl("../components/itemdetails/EpisodeDetails.qml")
case undefined:
return ""
default: default:
return Qt.resolvedUrl("../components/itemdetails/UnsupportedDetails.qml") return Qt.resolvedUrl("../components/itemdetails/UnsupportedDetails.qml")
} }
@ -114,7 +119,7 @@ Page {
onItemIdChanged: { onItemIdChanged: {
itemData = {} itemData = {}
if (itemId.length > 0) { if (itemId.length && PageStatus.Active) {
pageRoot._loading = true pageRoot._loading = true
ApiClient.fetchItem(itemId) ApiClient.fetchItem(itemId)
} }
@ -125,8 +130,11 @@ Page {
backdrop.clear() backdrop.clear()
//appWindow.itemData = ({}) //appWindow.itemData = ({})
} }
if (status == PageStatus.Active && itemData) { if (status == PageStatus.Active) {
appWindow.itemData = itemData if (itemId) {
ApiClient.fetchItem(itemId)
}
} }
} }

View file

@ -16,15 +16,20 @@ Page {
id: page id: page
allowedOrientations: Orientation.All allowedOrientations: Orientation.All
ViewPlaceholder {
}
SilicaFlickable { SilicaFlickable {
anchors.fill: parent anchors.fill: parent
// PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView // PullDownMenu and PushUpMenu must be declared in SilicaFlickable, SilicaListView or SilicaGridView
PullDownMenu { PullDownMenu {
MenuItem { MenuItem {
text: qsTr("About") text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("AboutPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
} }
busy: mediaLibraryModel.status == ApiModel.Loading
} }
// Tell SilicaFlickable the height of its content. // Tell SilicaFlickable the height of its content.
@ -60,7 +65,7 @@ Page {
MoreSection { MoreSection {
text: model.name text: model.name
busy: userItemModel.status != ApiModel.Ready 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}) 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: { onStatusChanged: {
if (status == PageStatus.Active) { if (status == PageStatus.Active) {
appWindow.itemData = null appWindow.itemData = null
if (!_modelsLoaded && ApiClient.authenticated) loadModels() loadModels(false)
} }
} }
Connections { Connections {
target: ApiClient target: ApiClient
onAuthenticatedChanged: { onAuthenticatedChanged: loadModels(false)
if (authenticated /*&& !_modelsLoaded*/) loadModels();
}
} }
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()
} }
} }

View file

@ -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"))
}
}
}
}

View file

@ -33,7 +33,7 @@ void FallbackCredentialsManager::get(const QString &server, const QString &user)
} }
void FallbackCredentialsManager::remove(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 { void FallbackCredentialsManager::listServers() const {

View file

@ -132,12 +132,7 @@ void ApiClient::setupConnection() {
} }
rep->deleteLater(); rep->deleteLater();
}); });
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), setDefaultErrorHandler(rep);
this, [rep, this](QNetworkReply::NetworkError error) {
qDebug() << "Error from URL: " << rep->url();
emit this->networkError(error);
rep->deleteLater();
});
} }
void ApiClient::getBrandingConfiguration() { void ApiClient::getBrandingConfiguration() {
@ -162,11 +157,7 @@ void ApiClient::getBrandingConfiguration() {
} }
rep->deleteLater(); rep->deleteLater();
}); });
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), setDefaultErrorHandler(rep);
this, [rep, this](QNetworkReply::NetworkError error) {
emit this->networkError(error);
rep->deleteLater();
});
} }
void ApiClient::authenticate(QString username, QString password, bool storeCredentials) { void ApiClient::authenticate(QString username, QString password, bool storeCredentials) {
@ -195,8 +186,17 @@ void ApiClient::authenticate(QString username, QString password, bool storeCrede
} }
rep->deleteLater(); rep->deleteLater();
}); });
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), setDefaultErrorHandler(rep);
this, &ApiClient::defaultNetworkErrorHandler); }
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) { void ApiClient::fetchItem(const QString &id) {
@ -209,6 +209,11 @@ void ApiClient::fetchItem(const QString &id) {
} }
rep->deleteLater(); rep->deleteLater();
}); });
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, [id, rep, this](QNetworkReply::NetworkError error) {
emit this->itemFetchFailed(id, error);
rep->deleteLater();
});
} }
void ApiClient::postCapabilities() { void ApiClient::postCapabilities() {
@ -221,8 +226,7 @@ void ApiClient::postCapabilities() {
capabilities["IconUrl"] = "https://chris.netsoj.nl/static/img/logo.png"; capabilities["IconUrl"] = "https://chris.netsoj.nl/static/img/logo.png";
capabilities["DeviceProfile"] = m_deviceProfile; capabilities["DeviceProfile"] = m_deviceProfile;
QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities)); QNetworkReply *rep = post("/Sessions/Capabilities/Full", QJsonDocument(capabilities));
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), setDefaultErrorHandler(rep);
this, &ApiClient::defaultNetworkErrorHandler);
} }
void ApiClient::generateDeviceProfile() { void ApiClient::generateDeviceProfile() {
@ -244,6 +248,7 @@ void ApiClient::defaultNetworkErrorHandler(QNetworkReply::NetworkError error) {
QObject *signalSender = sender(); QObject *signalSender = sender();
QNetworkReply *rep = dynamic_cast<QNetworkReply *>(signalSender); QNetworkReply *rep = dynamic_cast<QNetworkReply *>(signalSender);
if (rep != nullptr && statusCode(rep) == 401) { if (rep != nullptr && statusCode(rep) == 401) {
this->setAuthenticated(false);
emit this->authenticationError(ApiError::INVALID_PASSWORD); emit this->authenticationError(ApiError::INVALID_PASSWORD);
} else { } else {
emit this->networkError(error); emit this->networkError(error);

View file

@ -106,6 +106,7 @@ signals:
void userIdChanged(QString userId); void userIdChanged(QString userId);
void itemFetched(const QString &itemId, const QJsonObject &result); void itemFetched(const QString &itemId, const QJsonObject &result);
void itemFetchFailed(const QString &itemId, const QNetworkReply::NetworkError error);
public slots: public slots:
/** /**
@ -120,6 +121,12 @@ public slots:
*/ */
void setupConnection(); void setupConnection();
void authenticate(QString username, QString password, bool storeCredentials = false); 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); void fetchItem(const QString &id);
/** /**
@ -157,6 +164,7 @@ protected:
* is a big mess and should be safely contained in it's own file. * is a big mess and should be safely contained in it's own file.
*/ */
void generateDeviceProfile(); void generateDeviceProfile();
QString &token() { return m_token; } QString &token() { return m_token; }
private: private:
@ -206,6 +214,18 @@ private:
static inline int statusCode(QNetworkReply *rep) { static inline int statusCode(QNetworkReply *rep) {
return rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); 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<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
this, &ApiClient::defaultNetworkErrorHandler);
}
}; };
} // NS Jellyfin } // NS Jellyfin

View file

@ -65,6 +65,13 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>DetailPage</name>
<message>
<source>Loading</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>EpisodeDetails</name> <name>EpisodeDetails</name>
<message> <message>
@ -127,10 +134,6 @@
</context> </context>
<context> <context>
<name>MainPage</name> <name>MainPage</name>
<message>
<source>About</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Resume watching</source> <source>Resume watching</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -139,6 +142,22 @@
<source>Next up</source> <source>Next up</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Network error</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>An error has occurred. Please try again.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Retry</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>SeasonDetails</name> <name>SeasonDetails</name>
@ -156,6 +175,39 @@
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>SettingsPage</name>
<message>
<source>Settings</source>
<extracomment>Header of Settings page</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>Other</source>
<extracomment>Other settings</extracomment>
<translation type="unfinished"></translation>
</message>
<message>
<source>About Sailfin</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Session</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Server</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>User id</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Log out</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>UnsupportedDetails</name> <name>UnsupportedDetails</name>
<message> <message>