mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2025-09-05 10:12:46 +00:00
Initial commit
Features so far: - Login is working, both on back-end and GUI-wise - Saving and reusing login tokens is working - The home page is mostly functional - Show details can be received and displayed in a basic manner Following features are taken into account, but have not been fully implemented: - Support for multiple accounts/servers - Securely saving login tokens
This commit is contained in:
commit
53b3eac213
40 changed files with 2375 additions and 0 deletions
22
qml/3rdparty.xml
Normal file
22
qml/3rdparty.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<includes>
|
||||
<include>
|
||||
<name>Storeman</name>
|
||||
<url>https://github.com/mentaljam/harbour-storeman/tree/f64314e7f72550faf35f95f046b52cee42501cf8</url>
|
||||
<type>SNIPPET</type>
|
||||
<license>
|
||||
<type>MIT</type>
|
||||
<copyright>Copyright (c) 2017 Petr Tsymbarovich</copyright>
|
||||
<text>licenses/MIT.txt</text>
|
||||
</license>
|
||||
</include>
|
||||
<include>
|
||||
<name>Hutspot</name>
|
||||
<url>https://github.com/sailfish-spotify/hutspot/tree/22787baa6603b5235a3c9e6a65778e0485dfcd7b</url>
|
||||
<type>SNIPPET</type>
|
||||
<license>
|
||||
<type>MIT</type>
|
||||
<copyright>Copyright (c) 2019 sailfish-spotify contributors</copyright>
|
||||
<text>licenses/MIT.txt</text>
|
||||
</license>
|
||||
</include>
|
||||
</includes>
|
43
qml/components/GlassyBackground.qml
Normal file
43
qml/components/GlassyBackground.qml
Normal file
|
@ -0,0 +1,43 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
import QtGraphicalEffects 1.0
|
||||
|
||||
Rectangle {
|
||||
property alias source: backgroundImage.source
|
||||
property alias sourceSize: backgroundImage.sourceSize
|
||||
property real dimmedOpacity: Theme.opacityFaint
|
||||
readonly property alias status: backgroundImage.status
|
||||
color: Theme.colorScheme == Theme.DarkOnLight ? "#fff" : "#000"
|
||||
z: -1
|
||||
opacity: status == Image.Ready ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 300 } }
|
||||
|
||||
Image {
|
||||
id: backgroundImage
|
||||
cache: true
|
||||
smooth: false
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
}
|
||||
|
||||
FastBlur {
|
||||
anchors.fill: backgroundImage
|
||||
source: backgroundImage
|
||||
opacity: dimmedOpacity
|
||||
radius: 100
|
||||
}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
fillMode: Image.Tile
|
||||
source: "image://theme/graphic-shader-texture"
|
||||
opacity: 0.1
|
||||
visible: parent.visible
|
||||
}
|
||||
|
||||
function clear() {
|
||||
//source = ""
|
||||
}
|
||||
}
|
55
qml/components/LibraryItemDelegate.qml
Normal file
55
qml/components/LibraryItemDelegate.qml
Normal file
|
@ -0,0 +1,55 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
BackgroundItem {
|
||||
id: root
|
||||
property alias poster: posterImage.source
|
||||
property alias title: titleText.text
|
||||
property bool landscape: false
|
||||
width: Screen.width / 3
|
||||
height: landscape ? width / 4 * 3 : width / 2 * 3
|
||||
|
||||
RemoteImage {
|
||||
id: posterImage
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: posterImage
|
||||
color: Theme.rgba(Theme.highlightBackgroundColor, Theme.highlightBackgroundOpacity)
|
||||
visible: root.highlighted
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: titleText.height * 1.5 + Theme.paddingSmall * 2
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "transparent"; }
|
||||
GradientStop { position: 1.0; color: Theme.highlightDimmerColor }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: titleText
|
||||
anchors {
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
leftMargin: Theme.paddingMedium
|
||||
rightMargin: Theme.paddingMedium
|
||||
bottomMargin: Theme.paddingSmall
|
||||
}
|
||||
truncationMode: TruncationMode.Fade
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
76
qml/components/MoreSection.qml
Normal file
76
qml/components/MoreSection.qml
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* File taken from Storeman. See ../3rdparty.xml for licensing information
|
||||
*/
|
||||
|
||||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property alias text: label.text
|
||||
property alias textAlignment: label.horizontalAlignment
|
||||
property bool busy: false
|
||||
property int depth: 0
|
||||
readonly property color _color: enabled ? highlighted ? Theme.highlightColor : Theme.primaryColor : Theme.secondaryColor
|
||||
default property alias content: container.data
|
||||
|
||||
implicitHeight: backgroundItem.height + container.height
|
||||
width: parent.width
|
||||
|
||||
BackgroundItem {
|
||||
id: backgroundItem
|
||||
width: parent.width
|
||||
height: Theme.itemSizeMedium
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.rgba(Theme.highlightBackgroundColor, 0.15) }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: image.left
|
||||
verticalCenter: parent.verticalCenter
|
||||
leftMargin: Theme.horizontalPageMargin + depth * Theme.paddingLarge
|
||||
rightMargin: Theme.paddingMedium
|
||||
}
|
||||
horizontalAlignment: Text.AlignRight
|
||||
truncationMode: TruncationMode.Fade
|
||||
color: _color
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
anchors {
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
}
|
||||
visible: root.enabled && !root.busy
|
||||
source: "image://theme/icon-m-right?" + _color
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
id: busyIndicator
|
||||
running: root.busy
|
||||
anchors.centerIn: image
|
||||
size: BusyIndicatorSize.Small
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: container
|
||||
anchors {
|
||||
top: backgroundItem.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
width: parent.width
|
||||
height: children[0].height
|
||||
}
|
||||
}
|
18
qml/components/PlainLabel.qml
Normal file
18
qml/components/PlainLabel.qml
Normal file
|
@ -0,0 +1,18 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
/**
|
||||
* A label with the most commonly used settings set
|
||||
*/
|
||||
Label {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
}
|
||||
color: Theme.highlightColor
|
||||
linkColor: Theme.primaryColor
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
29
qml/components/RemoteImage.qml
Normal file
29
qml/components/RemoteImage.qml
Normal file
|
@ -0,0 +1,29 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
Image {
|
||||
property string fallbackImage
|
||||
property bool usingFallbackImage
|
||||
|
||||
BusyIndicator {
|
||||
anchors.centerIn: parent
|
||||
running: parent.status == Image.Loading
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fallbackBackground
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Theme.highlightColor; }
|
||||
GradientStop { position: 1.0; color: Theme.highlightDimmerColor; }
|
||||
}
|
||||
visible: parent.status == Image.Error
|
||||
}
|
||||
|
||||
Image {
|
||||
id: fallbackImageItem
|
||||
anchors.centerIn: parent
|
||||
visible: parent.status == Image.Error
|
||||
source: fallbackImage ? fallbackImage : "image://theme/icon-m-question"
|
||||
}
|
||||
}
|
34
qml/components/UserGridDelegate.qml
Normal file
34
qml/components/UserGridDelegate.qml
Normal file
|
@ -0,0 +1,34 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
GridItem {
|
||||
id: root
|
||||
property string image
|
||||
property alias name: nameLabel.text
|
||||
RemoteImage {
|
||||
id: userImage
|
||||
anchors.fill: parent
|
||||
source: root.image ? root.image : "image://theme/icon-m-contact?" + ((root.highlighted || root.down) ? Theme.highlightColor : Theme.primaryColor)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: "transparent" }
|
||||
GradientStop { position: 1.0; color: Theme.overlayBackgroundColor }
|
||||
}
|
||||
}
|
||||
Label {
|
||||
id: nameLabel
|
||||
anchors {
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
right: parent.right
|
||||
left: parent.left
|
||||
bottom: parent.bottom
|
||||
bottomMargin: Theme.paddingSmall
|
||||
}
|
||||
text: qsTr("Other account")
|
||||
color: (root.highlighted || root.down) ? Theme.highlightColor : Theme.secondaryColor
|
||||
}
|
||||
}
|
22
qml/cover/CoverPage.qml
Normal file
22
qml/cover/CoverPage.qml
Normal file
|
@ -0,0 +1,22 @@
|
|||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
CoverBackground {
|
||||
Label {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("My Cover")
|
||||
}
|
||||
|
||||
CoverActionList {
|
||||
id: coverAction
|
||||
|
||||
CoverAction {
|
||||
iconSource: "image://theme/icon-cover-next"
|
||||
}
|
||||
|
||||
CoverAction {
|
||||
iconSource: "image://theme/icon-cover-pause"
|
||||
}
|
||||
}
|
||||
}
|
65
qml/harbour-sailfin.qml
Normal file
65
qml/harbour-sailfin.qml
Normal file
|
@ -0,0 +1,65 @@
|
|||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
import Nemo.Notifications 1.0
|
||||
|
||||
import "components"
|
||||
import "pages"
|
||||
|
||||
ApplicationWindow {
|
||||
id: appWindow
|
||||
property bool isInSetup: false
|
||||
property bool _hasInitialized: false
|
||||
//property alias backdrop: backdrop
|
||||
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onNetworkError: errorNotification.show("Network error: " + error)
|
||||
onConnectionFailed: errorNotification.show("Connect error: " + error)
|
||||
//onConnectionSuccess: errorNotification.show("Success: " + loginMessage)
|
||||
}
|
||||
|
||||
/*GlassyBackground {
|
||||
id: backdrop
|
||||
anchors.fill: parent
|
||||
opacity: status == Image.Ready ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 300 } }
|
||||
|
||||
function clear() {
|
||||
source = ""
|
||||
}
|
||||
}*/
|
||||
|
||||
initialPage: Component {
|
||||
MainPage {
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onSetupRequired: {
|
||||
if (!isInSetup) {
|
||||
isInSetup = true;
|
||||
pageStack.replace(Qt.resolvedUrl("pages/AddServerPage.qml"), {"backNavigation": false});
|
||||
}
|
||||
}
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (status == PageStatus.Active && !_hasInitialized) {
|
||||
_hasInitialized = true;
|
||||
ApiClient.initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cover: Qt.resolvedUrl("cover/CoverPage.qml")
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
Notification {
|
||||
id: errorNotification
|
||||
previewSummary: "foo"
|
||||
isTransient: true
|
||||
|
||||
function show(data) {
|
||||
previewSummary = data;
|
||||
publish();
|
||||
}
|
||||
}
|
||||
}
|
17
qml/licenses/MIT.txt
Normal file
17
qml/licenses/MIT.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
40
qml/pages/AddServerConnectingPage.qml
Normal file
40
qml/pages/AddServerConnectingPage.qml
Normal file
|
@ -0,0 +1,40 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
Page {
|
||||
property string serverName
|
||||
property string serverAddress
|
||||
property Page firstPage
|
||||
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
|
||||
BusyLabel {
|
||||
text: qsTr("Connecting to %1").arg(serverName)
|
||||
running: true
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status == PageStatus.Active) {
|
||||
console.log("Connecting page active");
|
||||
ApiClient.setupConnection();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onConnectionSuccess: {
|
||||
console.log("Login success: " + loginMessage);
|
||||
pageStack.replace(Qt.resolvedUrl("LoginDialog.qml"), {"loginMessage": loginMessage, "firstPage": firstPage});
|
||||
}
|
||||
onConnectionFailed: function(error) {
|
||||
console.log("Connection failed : " + error)
|
||||
pageStack.pop();
|
||||
}
|
||||
onNetworkError: {
|
||||
console.log("ConnectingPage: popping page!")
|
||||
pageStack.pop();
|
||||
}
|
||||
}
|
||||
}
|
89
qml/pages/AddServerPage.qml
Normal file
89
qml/pages/AddServerPage.qml
Normal file
|
@ -0,0 +1,89 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
Dialog {
|
||||
id: dialogRoot
|
||||
allowedOrientations: Orientation.All
|
||||
// Picks the address of the ComboBox if selected, otherwise the manual address entry
|
||||
readonly property string address: serverSelect.currentItem._address
|
||||
readonly property bool addressCorrect: serverSelect.currentIndex > 0 || manualAddress.acceptableInput
|
||||
readonly property string serverName: serverSelect.currentItem._name
|
||||
|
||||
|
||||
acceptDestination: AddServerConnectingPage {
|
||||
id: connectingPage
|
||||
serverName: dialogRoot.serverName
|
||||
serverAddress: address
|
||||
firstPage: dialogRoot
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
DialogHeader {
|
||||
acceptText: qsTr("Connect")
|
||||
title: qsTr("Connect to Jellyfin")
|
||||
}
|
||||
|
||||
ServerDiscoveryModel {
|
||||
id: serverModel
|
||||
}
|
||||
|
||||
|
||||
ComboBox {
|
||||
id: serverSelect
|
||||
label: qsTr("Server")
|
||||
description: qsTr("Sailfin will try to search for Jellyfin servers on your local network automatically")
|
||||
|
||||
menu: ContextMenu {
|
||||
MenuItem {
|
||||
// Special values are cool, aren't they?
|
||||
readonly property string _address: manualAddress.text
|
||||
readonly property string _name: manualAddress.text
|
||||
text: qsTr("enter address manually")
|
||||
}
|
||||
Repeater {
|
||||
model: serverModel
|
||||
delegate: MenuItem {
|
||||
readonly property string _address: address
|
||||
readonly property string _name: name
|
||||
text: qsTr("%1 - %2").arg(name).arg(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: manualAddress
|
||||
width: parent.width
|
||||
clip: true
|
||||
|
||||
label: qsTr("Server address")
|
||||
placeholderText: qsTr("e.g. https://demo.jellyfin.org")
|
||||
|
||||
enabled: serverSelect.currentIndex == 0
|
||||
visible: enabled
|
||||
|
||||
inputMethodHints: Qt.ImhUrlCharactersOnly
|
||||
validator: RegExpValidator {
|
||||
regExp: /^https?:\/\/[a-zA-Z0-9-._~:/?#\[\]\@\!\$\&\'\(\)\*\+\,\;\=]+$/m
|
||||
}
|
||||
|
||||
EnterKey.enabled: addressCorrect
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
|
||||
EnterKey.onClicked: dialogRoot.tryConnect()
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: serverModel.refresh()
|
||||
canAccept: addressCorrect
|
||||
|
||||
function tryConnect() {
|
||||
console.log("Hi there!")
|
||||
ApiClient.baseUrl = address;
|
||||
//ApiClient.setupConnection()
|
||||
//fakeTimer.start()
|
||||
}
|
||||
|
||||
onDone: tryConnect()
|
||||
}
|
138
qml/pages/DetailBasePage.qml
Normal file
138
qml/pages/DetailBasePage.qml
Normal file
|
@ -0,0 +1,138 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: pageRoot
|
||||
property string itemId: ""
|
||||
property var itemData: ({})
|
||||
property bool _loading: true
|
||||
readonly property bool _hasLogo: itemData.ImageTags.Logo !== undefined
|
||||
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
|
||||
|
||||
on_BackdropImagesChanged: updateBackdrop()
|
||||
on_ParentBackdropImagesChanged: updateBackdrop()
|
||||
|
||||
function updateBackdrop() {
|
||||
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]
|
||||
} else if (_parentBackdropImages && _parentBackdropImages.length > 0) {
|
||||
console.log(parentId)
|
||||
backdrop.source = ApiClient.baseUrl + "/Items/" + itemData.ParentBackdropItemId + "/Images/Backdrop/0?tag=" + _parentBackdropImages[0]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
allowedOrientations: Orientation.All
|
||||
GlassyBackground {
|
||||
id: backdrop
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: content.height
|
||||
|
||||
Column {
|
||||
id: content
|
||||
width: parent.width
|
||||
|
||||
PageHeader {
|
||||
title: itemData.Name
|
||||
visible: !_hasLogo
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.paddingLarge
|
||||
}
|
||||
RemoteImage {
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
source: _hasLogo ? ApiClient.baseUrl + "/Items/" + itemId + "/Images/Logo?tag=" + _logo : undefined
|
||||
}
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.paddingLarge
|
||||
}
|
||||
visible: _hasLogo
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.paddingLarge
|
||||
}
|
||||
|
||||
PlainLabel {
|
||||
id: overviewText
|
||||
text: itemData.Overview
|
||||
visible: text.length > 0
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: overviewText.visible
|
||||
width: 1
|
||||
height: Theme.paddingLarge
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors {
|
||||
//left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
}
|
||||
spacing: Theme.paddingMedium
|
||||
IconButton {
|
||||
id: favouriteButton
|
||||
icon.source: "image://theme/icon-m-favorite"
|
||||
}
|
||||
IconButton {
|
||||
id: playButton
|
||||
icon.source: "image://theme/icon-l-play"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PageBusyIndicator {
|
||||
running: pageRoot._loading
|
||||
}
|
||||
|
||||
onItemIdChanged: {
|
||||
itemData = {}
|
||||
if (itemId.length > 0) {
|
||||
pageRoot._loading = true
|
||||
ApiClient.fetchItem(itemId)
|
||||
}
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status == PageStatus.Deactivating) {
|
||||
backdrop.clear()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onItemFetched: {
|
||||
if (itemId === pageRoot.itemId) {
|
||||
console.log(JSON.stringify(result))
|
||||
pageRoot.itemData = result
|
||||
pageRoot._loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
104
qml/pages/LegalPage.qml
Normal file
104
qml/pages/LegalPage.qml
Normal file
|
@ -0,0 +1,104 @@
|
|||
import QtQuick 2.6
|
||||
import QtQuick.XmlListModel 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
SilicaFlickable {
|
||||
anchors.fill: parent
|
||||
contentHeight: content.height
|
||||
|
||||
Column {
|
||||
id: content
|
||||
width: parent.width
|
||||
|
||||
XmlListModel {
|
||||
id: licencesModel
|
||||
source: Qt.resolvedUrl("../3rdparty.xml")
|
||||
query: "/includes/include"
|
||||
XmlRole { name: "name"; query: "name/string()" }
|
||||
XmlRole { name: "type"; query: "type/string()" }
|
||||
XmlRole { name: "url"; query: "url/string()" }
|
||||
XmlRole { name: "copyright"; query: "license/copyright/string()" }
|
||||
XmlRole { name: "licenseUrl"; query: "license/text/string()" }
|
||||
XmlRole { name: "licenseType"; query: "license/type/string()" }
|
||||
}
|
||||
|
||||
PageHeader {
|
||||
title: qsTr("Legal")
|
||||
}
|
||||
|
||||
PlainLabel {
|
||||
text: qsTr("The Sailfin application contains some code from other projects. Without them, Sailfin would " +
|
||||
"not be possible!")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: licencesModel
|
||||
Column {
|
||||
width: parent.width
|
||||
SectionHeader {
|
||||
text: name
|
||||
}
|
||||
|
||||
PlainLabel {
|
||||
color: Theme.secondaryHighlightColor
|
||||
text: {
|
||||
switch(type) {
|
||||
case "SNIPPET":
|
||||
return qsTr("This program contains small snippets of code taken from <a href=\"%1\">%2</a>, which " +
|
||||
"is licensed under the %3 license:")
|
||||
.arg(model.url).arg(model.name).arg(model.licenseType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.paddingLarge
|
||||
}
|
||||
|
||||
SilicaFlickable {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
}
|
||||
height: licenseLabel.contentHeight
|
||||
contentWidth: licenseLabel.contentWidth
|
||||
clip: true
|
||||
|
||||
Label {
|
||||
id: licenseLabel
|
||||
color: Theme.secondaryHighlightColor
|
||||
font.family: "monospace"
|
||||
font.pixelSize: Theme.fontSizeExtraSmall
|
||||
wrapMode: Text.NoWrap
|
||||
|
||||
Component.onCompleted: {
|
||||
var xhr = new XMLHttpRequest;
|
||||
xhr.open("GET", Qt.resolvedUrl("../" + model.licenseUrl)); // set Method and File
|
||||
console.log(Qt.resolvedUrl("../" + model.licenseUrl))
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === XMLHttpRequest.DONE){ // if request_status == DONE
|
||||
var response = model.copyright + "\n\n" + xhr.responseText;
|
||||
console.log(response);
|
||||
licenseLabel.text = response
|
||||
}
|
||||
}
|
||||
xhr.send(); // begin the request
|
||||
}
|
||||
}
|
||||
HorizontalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
||||
}
|
127
qml/pages/LoginDialog.qml
Normal file
127
qml/pages/LoginDialog.qml
Normal file
|
@ -0,0 +1,127 @@
|
|||
import QtQuick 2.6
|
||||
import Sailfish.Silica 1.0
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
import "../components"
|
||||
|
||||
Dialog {
|
||||
property string loginMessage
|
||||
property Page firstPage
|
||||
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
|
||||
acceptDestination: Page {
|
||||
BusyLabel {
|
||||
text: qsTr("Logging in as %1").arg(username.text)
|
||||
running: true
|
||||
}
|
||||
onStatusChanged: {
|
||||
if(status == PageStatus.Active) {
|
||||
ApiClient.authenticate(username.text, password.text, true)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onAuthenticatedChanged: {
|
||||
if (ApiClient.authenticated) {
|
||||
console.log("authenticated!")
|
||||
pageStack.replaceAbove(pageStack.previousPage(firstPage), Qt.resolvedUrl("MainPage.qml"))
|
||||
}
|
||||
}
|
||||
onAuthenticationError: {
|
||||
pageStack.completeAnimation()
|
||||
pageStack.pop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PublicUserModel {
|
||||
id: userModel
|
||||
apiClient: ApiClient
|
||||
Component.onCompleted: reload();
|
||||
}
|
||||
|
||||
DialogHeader {
|
||||
id: dialogHeader
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
acceptText: qsTr("Login");
|
||||
}
|
||||
SilicaFlickable {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: dialogHeader.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
contentHeight: column.height
|
||||
clip: true
|
||||
|
||||
VerticalScrollDecorator {}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
width: parent.width
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
Repeater {
|
||||
model: userModel
|
||||
delegate: UserGridDelegate {
|
||||
name: model.name
|
||||
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.id).arg(model.primaryImageTag) : null
|
||||
highlighted: model.name == username.text
|
||||
onClicked: {
|
||||
username.text = model.name
|
||||
password.focus = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
text: qsTr("Credentials")
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: username
|
||||
width: parent.width
|
||||
placeholderText: qsTr("Username")
|
||||
label: qsTr("Username")
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-next"
|
||||
EnterKey.onClicked: password.focus = true
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: password
|
||||
width: parent.width
|
||||
|
||||
placeholderText: qsTr("Password")
|
||||
label: qsTr("password")
|
||||
echoMode: TextInput.Password
|
||||
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
|
||||
EnterKey.onClicked: login()
|
||||
}
|
||||
|
||||
SectionHeader {
|
||||
text: qsTr("Login message")
|
||||
}
|
||||
Label {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: Theme.horizontalPageMargin
|
||||
rightMargin: Theme.horizontalPageMargin
|
||||
}
|
||||
text: loginMessage
|
||||
wrapMode: Text.WordWrap
|
||||
color: Theme.highlightColor
|
||||
}
|
||||
}
|
||||
}
|
||||
canAccept: username.text.length > 0
|
||||
|
||||
/*onAccepted: {
|
||||
pageStack.replace(Qt.resolvedUrl("MainPage.qml"))
|
||||
}*/
|
||||
}
|
125
qml/pages/MainPage.qml
Normal file
125
qml/pages/MainPage.qml
Normal file
|
@ -0,0 +1,125 @@
|
|||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
import nl.netsoj.chris.Jellyfin 1.0
|
||||
|
||||
import "../components"
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
// The effective value will be restricted by ApplicationWindow.allowedOrientations
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
property bool _modelsLoaded: false
|
||||
|
||||
Connections {
|
||||
target: ApiClient
|
||||
onAuthenticatedChanged: {
|
||||
if (authenticated && !_modelsLoaded) loadModels();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (ApiClient.authenticated && _modelsLoaded) {
|
||||
loadModels();
|
||||
}
|
||||
}
|
||||
|
||||
// To enable PullDownMenu, place our content in a SilicaFlickable
|
||||
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("LegalPage.qml"))
|
||||
}
|
||||
MenuItem {
|
||||
text: qsTr("Settings")
|
||||
onClicked: pageStack.push(Qt.resolvedUrl("SecondPage.qml"))
|
||||
}
|
||||
}
|
||||
|
||||
// Tell SilicaFlickable the height of its content.
|
||||
contentHeight: column.height
|
||||
|
||||
|
||||
// Place our content in a Column. The PageHeader is always placed at the top
|
||||
// of the page, followed by our content.
|
||||
Column {
|
||||
id: column
|
||||
|
||||
width: page.width
|
||||
//spacing: Theme.paddingLarge
|
||||
UserViewModel {
|
||||
id: mediaLibraryModel2
|
||||
apiClient: ApiClient
|
||||
}
|
||||
|
||||
MoreSection {
|
||||
text: "Kijken hervatten"
|
||||
enabled: false
|
||||
}
|
||||
MoreSection {
|
||||
text: "Volgende"
|
||||
}
|
||||
|
||||
UserViewModel {
|
||||
id: mediaLibraryModel
|
||||
apiClient: ApiClient
|
||||
}
|
||||
Repeater {
|
||||
model: mediaLibraryModel
|
||||
MoreSection {
|
||||
text: model.name
|
||||
busy: userItemModel.status != ApiModel.Ready
|
||||
|
||||
SilicaListView {
|
||||
clip: true
|
||||
height: count > 0 ? Screen.width / 4 : 0
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 300 }
|
||||
}
|
||||
width: parent.width
|
||||
model: userItemModel
|
||||
orientation: ListView.Horizontal
|
||||
delegate: LibraryItemDelegate {
|
||||
property string id: model.id
|
||||
title: model.name
|
||||
poster: ApiClient.baseUrl + "/Items/" + model.id + "/Images/Primary?maxHeight=" + height + "&tag=" + model.imageTags["Primary"]
|
||||
landscape: true
|
||||
|
||||
onClicked: {
|
||||
pageStack.push(Qt.resolvedUrl("DetailBasePage.qml"), {"itemId": model.id})
|
||||
}
|
||||
}
|
||||
HorizontalScrollDecorator {}
|
||||
UserItemModel {
|
||||
id: userItemModel
|
||||
apiClient: ApiClient
|
||||
parentId: model.id
|
||||
limit: 12
|
||||
}
|
||||
Connections {
|
||||
target: mediaLibraryModel
|
||||
onStatusChanged: {
|
||||
if (status == ApiModel.Ready) {
|
||||
userItemModel.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadModels() {
|
||||
_modelsLoaded = true;
|
||||
mediaLibraryModel.reload()
|
||||
}
|
||||
}
|
30
qml/pages/SecondPage.qml
Normal file
30
qml/pages/SecondPage.qml
Normal file
|
@ -0,0 +1,30 @@
|
|||
import QtQuick 2.0
|
||||
import Sailfish.Silica 1.0
|
||||
|
||||
Page {
|
||||
id: page
|
||||
|
||||
// The effective value will be restricted by ApplicationWindow.allowedOrientations
|
||||
allowedOrientations: Orientation.All
|
||||
|
||||
SilicaListView {
|
||||
id: listView
|
||||
model: 20
|
||||
anchors.fill: parent
|
||||
header: PageHeader {
|
||||
title: qsTr("Nested Page")
|
||||
}
|
||||
delegate: BackgroundItem {
|
||||
id: delegate
|
||||
|
||||
Label {
|
||||
x: Theme.horizontalPageMargin
|
||||
text: qsTr("Item") + " " + index
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
|
||||
}
|
||||
onClicked: console.log("Clicked " + index)
|
||||
}
|
||||
VerticalScrollDecorator {}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue