diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3223568..01ad104 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -9,6 +9,10 @@ cmake_policy(SET CMP0048 NEW)
option(SAILFISHOS "Build SailfishOS version of application")
option(PLATFORM_QTQUICK "Build QtQuick version of application")
+if (NOT SAILFIN_VERSION)
+ set(SAILFIN_VERSION "1.0.0")
+endif()
+
if(SAILFISHOS)
# Hardcode this less?
set(CMAKE_INSTALL_LIBDIR "${CMAKE_INSTALL_PREFIX}/share/harbour-sailfin/lib")
diff --git a/core/src/jellyfinapiclient.cpp b/core/src/jellyfinapiclient.cpp
index 2fbfdab..196a989 100644
--- a/core/src/jellyfinapiclient.cpp
+++ b/core/src/jellyfinapiclient.cpp
@@ -184,7 +184,7 @@ void ApiClient::authenticate(QString username, QString password, bool storeCrede
requestData["Username"] = username;
requestData["Pw"] = password;
- QNetworkReply *rep = post("/Users/Authenticatebyname", QJsonDocument(requestData));
+ QNetworkReply *rep = post("/Users/authenticatebyname", QJsonDocument(requestData));
connect(rep, &QNetworkReply::finished, this, [rep, username, storeCredentials, this]() {
int status = rep->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qDebug() << "Got reply with status code " << status;
@@ -202,6 +202,12 @@ void ApiClient::authenticate(QString username, QString password, bool storeCrede
if (storeCredentials) {
m_credManager->store(this->m_baseUrl, this->m_userId, this->m_token);
}
+ } else if(status >= 400 && status < 500) {
+ if (status == 401) {
+ emit authenticationError(ApiError::INVALID_PASSWORD);
+ } else {
+ emit authenticationError(ApiError::UNEXPECTED_STATUS);
+ }
}
rep->deleteLater();
});
diff --git a/core/src/jellyfindeviceprofile.cpp b/core/src/jellyfindeviceprofile.cpp
index 9b7342d..a70c18b 100644
--- a/core/src/jellyfindeviceprofile.cpp
+++ b/core/src/jellyfindeviceprofile.cpp
@@ -85,7 +85,7 @@ QJsonObject DeviceProfile::generateProfile() {
QJsonObject {
JsonPair("Property", "IsSecondaryAudio"),
JsonPair("Condition", "Equals"),
- JsonPair("Value", false),
+ JsonPair("Value", "false"),
JsonPair("IsRequired", false)
}
}),
@@ -97,7 +97,7 @@ QJsonObject DeviceProfile::generateProfile() {
QJsonObject {
JsonPair("Property", "IsAnamorphic"),
JsonPair("Condition", "NotEquals"),
- JsonPair("Value", true),
+ JsonPair("Value", "true"),
JsonPair("IsRequired", false)
},
QJsonObject {
@@ -109,13 +109,13 @@ QJsonObject DeviceProfile::generateProfile() {
QJsonObject {
JsonPair("Property", "VideoLevel"),
JsonPair("Condition", "LessThanEqual"),
- JsonPair("Value", 51),
+ JsonPair("Value", "51"),
JsonPair("IsRequired", false)
},
QJsonObject {
JsonPair("Property", "IsInterlaced"),
JsonPair("Condition", "NotEquals"),
- JsonPair("Value", true),
+ JsonPair("Value", "true"),
JsonPair("IsRequired", false)
}
}),
@@ -130,8 +130,8 @@ QJsonObject DeviceProfile::generateProfile() {
transcoding1["BreakOnNonKeyFrames"] =true;
transcoding1["Container"] = "ts";
transcoding1["Context"] = "Streaming";
- transcoding1["MaxAudioChannels"] = 2;
- transcoding1["MinSegments"] = 1;
+ transcoding1["MaxAudioChannels"] = "2";
+ transcoding1["MinSegments"] = "1";
transcoding1["Protocol"] = "hls";
transcoding1["Type"] = "Audio";
transcodingProfiles.append(transcoding1);
@@ -142,7 +142,7 @@ QJsonObject DeviceProfile::generateProfile() {
JsonPair("BreakOnNonKeyFrames", true),
JsonPair("Container", "ts"),
JsonPair("Context", "Streaming"),
- JsonPair("MaxAudioChannels", 2),
+ JsonPair("MaxAudioChannels", "2"),
JsonPair("MinSegments", 1),
JsonPair("Protocol", "hls"),
JsonPair("Type", "Video"),
@@ -168,8 +168,8 @@ QJsonObject DeviceProfile::generateProfile() {
JsonPair("VideoCodec", hlsVideoCodecs.join(",")),
JsonPair("Context", "Streaming"),
JsonPair("Protocol", "hls"),
- JsonPair("MaxAudioChannels", 2),
- JsonPair("MinSegments", 1),
+ JsonPair("MaxAudioChannels", "2"),
+ JsonPair("MinSegments", "1"),
JsonPair("BreakOnNonKeyFrames", true)
});
}
diff --git a/qtquick/qml.qrc b/qtquick/qml.qrc
index 74e3502..6307c82 100644
--- a/qtquick/qml.qrc
+++ b/qtquick/qml.qrc
@@ -14,5 +14,7 @@
qml/components/+sailfinstyle/Background.qml
qml/components/BusyDialog.qml
qml/pages/setup/LoginPage.qml
+ qml/pages/MainPage.qml
+ qml/pages/DetailPage.qml
diff --git a/qtquick/qml/main.qml b/qtquick/qml/main.qml
index 90f4470..0418b9f 100644
--- a/qtquick/qml/main.qml
+++ b/qtquick/qml/main.qml
@@ -1,6 +1,5 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
-import QtGraphicalEffects 1.12
import QtQuick.Window 2.12
import nl.netsoj.chris.Jellyfin 1.0
@@ -31,11 +30,13 @@ ApplicationWindow {
}
_oldDepth = depth
}
+ initialItem: Qt.resolvedUrl("pages/MainPage.qml")
+ Keys.onEscapePressed: pop()
}
Connections {
target: ApiClient
- onSetupRequired: pageStack.push(Qt.resolvedUrl("pages/setup/ServerSelectPage.qml"));
+ onSetupRequired: pageStack.replace(Qt.resolvedUrl("pages/setup/ServerSelectPage.qml"));
}
Component.onCompleted: {
diff --git a/qtquick/qml/pages/DetailPage.qml b/qtquick/qml/pages/DetailPage.qml
new file mode 100644
index 0000000..bbff247
--- /dev/null
+++ b/qtquick/qml/pages/DetailPage.qml
@@ -0,0 +1,37 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Window 2.12
+
+import nl.netsoj.chris.Jellyfin 1.0
+
+import "../components"
+import "../.."
+
+Page {
+ property bool _modelsLoaded: false
+ property StackView stackView: StackView.view
+ property string itemId
+ property alias jellyfinItem: jellyfinItem
+ header: ToolBar {
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: SailfinStyle.fontSizeLarge
+ text: jellyfinItem.name
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: stackView.pop()
+ }
+ JellyfinItem {
+ id: jellyfinItem
+ jellyfinId: itemId
+ apiClient: ApiClient
+ }
+
+ Image {
+ anchors.centerIn: parent
+ source: ApiClient.baseUrl + "/Items/" + itemId + "/Images/Primary?tag=" + jellyfinItem.tag
+ }
+}
diff --git a/qtquick/qml/pages/MainPage.qml b/qtquick/qml/pages/MainPage.qml
new file mode 100644
index 0000000..d373ad2
--- /dev/null
+++ b/qtquick/qml/pages/MainPage.qml
@@ -0,0 +1,99 @@
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Window 2.12
+
+import nl.netsoj.chris.Jellyfin 1.0
+
+import "../components"
+import "../.."
+
+Page {
+ property bool _modelsLoaded: false
+ property StackView stackView: StackView.view
+ header: ToolBar {
+ Label {
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: SailfinStyle.fontSizeLarge
+ text: qsTr("Main page")
+ }
+ }
+
+ UserViewModel {
+ id: mediaLibraryModel
+ apiClient: ApiClient
+ }
+
+ ScrollView {
+ anchors.fill: parent
+ contentHeight: content.height
+ Column {
+ id: content
+ width: parent.width
+ Repeater {
+ model: mediaLibraryModel
+ Column {
+ width: parent.width
+ UserItemLatestModel {
+ id: userItemModel
+ apiClient: ApiClient
+ parentId: model.id
+ limit: 16
+ }
+ Label {
+ text: model.name
+ }
+
+ ListView {
+ width: parent.width
+ height: SailfinStyle.unit * 20
+ orientation: ListView.Horizontal
+ model: userItemModel
+ delegate: ItemDelegate {
+ width: 12 * SailfinStyle.unit
+ height: 20 * SailfinStyle.unit
+ Image {
+ anchors.fill: parent
+ source: ApiClient.baseUrl + "/Items/" + model.id + "/Images/Primary?tag=" + model.tag
+ }
+ Label {
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.right: parent.right
+ text: model.name
+ }
+ onClicked: stackView.push(Qt.resolvedUrl("DetailPage.qml"), {"itemId": model.id})
+ }
+ }
+ Connections {
+ target: mediaLibraryModel
+ onStatusChanged: {
+ if (mediaLibraryModel.status == UserItemModel.Ready) {
+ userItemModel.reload()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 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()
+ }
+ }
+
+ Connections {
+ target: ApiClient
+ onAuthenticatedChanged: {
+ if (authenticated) {
+ loadModels(false)
+ }
+ }
+ }
+}
diff --git a/qtquick/qml/pages/setup/LoginPage.qml b/qtquick/qml/pages/setup/LoginPage.qml
index 704515d..c09e3aa 100644
--- a/qtquick/qml/pages/setup/LoginPage.qml
+++ b/qtquick/qml/pages/setup/LoginPage.qml
@@ -1,10 +1,14 @@
import QtQuick 2.12
import QtQuick.Controls 2.12
+import nl.netsoj.chris.Jellyfin 1.0
+
+import "../../components"
import "../../.."
Page {
property string loginMessage
+ property StackView stackView: StackView.view
header: ToolBar {
Label {
anchors.horizontalCenter: parent.horizontalCenter
@@ -20,11 +24,27 @@ Page {
TextField {
id: usernameField
width: parent.width
+ placeholderText: qsTr("Username")
EnterKey.type: Qt.EnterKeyNext
}
TextField {
id: passwordField
width: parent.width
+ placeholderText: qsTr("Password")
+ echoMode: TextInput.Password
+ }
+
+ Label {
+ id: loginError
+ width: parent.width
+ wrapMode: Text.WordWrap
+ text: qsTr("Invalid username/password")
+ visible: false
+ }
+
+ Button {
+ text: qsTr("Login")
+ onClicked: login()
}
Label {
width: parent.width
@@ -32,4 +52,29 @@ Page {
wrapMode: Text.WordWrap
}
}
+
+ BusyDialog {
+ id: busyDialog
+ anchors.centerIn: Overlay.overlay
+ title: qsTr("Logging in as %1").arg(usernameField.text)
+ }
+
+ function login() {
+ busyDialog.open()
+ ApiClient.authenticate(usernameField.text, passwordField.text, true)
+ }
+
+ Connections {
+ target: ApiClient
+ onAuthenticatedChanged: {
+ busyDialog.close()
+ if (authenticated) {
+ stackView.replace(null, Qt.resolvedUrl("../MainPage.qml"), {})
+ }
+ }
+ onAuthenticationError: {
+ busyDialog.close()
+ loginError.visible = true
+ }
+ }
}