1
0
Fork 0
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:
Chris Josten 2020-09-15 16:53:13 +02:00
commit 53b3eac213
40 changed files with 2375 additions and 0 deletions

22
qml/3rdparty.xml Normal file
View 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>

View 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 = ""
}
}

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

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

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

View 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"
}
}

View 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
View 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
View 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
View 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.

View 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();
}
}
}

View 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()
}

View 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
View 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
View 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
View 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
View 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 {}
}
}