1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2025-09-04 01:42:44 +00:00

WIP: Add playlists/queues and add support for Sailfish back

This commit is contained in:
Chris Josten 2021-07-31 15:06:17 +02:00
parent fbc154fb56
commit 86672be051
89 changed files with 1637 additions and 849 deletions

View file

@ -18,25 +18,27 @@ set(harbour-sailfin_SOURCES
src/harbour-sailfin.cpp)
set(sailfin_QML_SOURCES
qml/ApiClient.qml
qml/Constants.qml
qml/Utils.js
qml/components/music/NarrowAlbumCover.qml
qml/components/music/WideAlbumCover.qml
qml/components/music/SongDelegate.qml
qml/components/videoplayer/VideoError.qml
qml/components/videoplayer/VideoHud.qml
qml/components/music/NarrowAlbumCover.qml
qml/components/music/WideAlbumCover.qml
qml/components/music/SongDelegate.qml
qml/components/videoplayer/VideoError.qml
qml/components/videoplayer/VideoHud.qml
qml/components/IconListItem.qml
qml/components/LibraryItemDelegate.qml
qml/components/JItem.qml
qml/components/LibraryItemDelegate.qml
qml/components/MoreSection.qml
qml/components/PlainLabel.qml
qml/components/PlaybackBar.qml
qml/components/PlayQueue.qml
qml/components/PlayToolbar.qml
qml/components/PlaybackBar.qml
qml/components/PlayQueue.qml
qml/components/PlayToolbar.qml
qml/components/RemoteImage.qml
qml/components/Shim.qml
qml/components/UserGridDelegate.qml
qml/components/VideoPlayer.qml
qml/components/VideoTrackSelector.qml
qml/components/VideoTrackSelector.qml
qml/cover/CoverPage.qml
qml/cover/PosterCover.qml
qml/cover/VideoCover.qml
@ -51,21 +53,22 @@ set(sailfin_QML_SOURCES
qml/pages/itemdetails/EpisodePage.qml
qml/pages/itemdetails/FilmPage.qml
qml/pages/itemdetails/MusicAlbumPage.qml
qml/pages/itemdetails/PhotoPage.qml
qml/pages/itemdetails/PhotoPage.qml
qml/pages/itemdetails/SeasonPage.qml
qml/pages/itemdetails/SeriesPage.qml
qml/pages/itemdetails/UnsupportedPage.qml
qml/pages/itemdetails/VideoPage.qml
qml/pages/settings/DebugPage.qml
qml/pages/settings/DebugPage.qml
qml/pages/setup/AddServerConnectingPage.qml
qml/pages/setup/LoginDialog.qml
qml/pages/setup/AddServerPage.qml
qml/pages/setup/LoginDialog.qml
qml/qmldir)
add_executable(harbour-sailfin ${harbour-sailfin_SOURCES} ${sailfin_QML_SOURCES})
target_link_libraries(harbour-sailfin PRIVATE Qt5::Gui Qt5::Qml Qt5::Quick SailfishApp::SailfishApp
# Note: this may break when the compiler changes. -rdynamic and -pie seem to be needed for the
# invoker/booster to work
jellyfin-qt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
JellyfinQt "-Wl,-rpath,${CMAKE_INSTALL_LIBDIR} -rdynamic -pie")
target_compile_definitions(harbour-sailfin
PRIVATE $<$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>:QT_QML_DEBUG>)

View file

@ -3,7 +3,7 @@ Type=Application
Version=1.1
X-Nemo-Application-Type=silica-qt5
Icon=harbour-sailfin
Exec=harbour-sailfin --attempt-sandbox
Exec=harbour-sailfin
Name=Sailfin

View file

@ -0,0 +1,7 @@
pragma Singleton
import QtQuick 2.6
import nl.netsoj.chris.Jellyfin 1.0 as J
J.ApiClient {
supportedCommands: [J.GeneralCommandType.Play, J.GeneralCommandType.DisplayContent, J.GeneralCommandType.DisplayMessage]
}

View file

@ -68,6 +68,7 @@ function itemModelImageUrl(baseUrl, itemId, tag, type, options) {
}
function usePortraitCover(itemType) {
console.log("ItemType: " + itemType)
return ["Series", "Movie", "tvshows", "movies"].indexOf(itemType) >= 0
}

View file

@ -0,0 +1,9 @@
import QtQuick 2.6
import nl.netsoj.chris.Jellyfin 1.0 as J
// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible.
// Since J.Item clashses with the QtQuick item type, this is a workaround until
// Sailfish OS upgrades to a Qt > 5.8. Maybe in 2023?
J.Item {
}

View file

@ -27,6 +27,7 @@ import Nemo.KeepAlive 1.2
import "components"
import "pages"
import "." as D
ApplicationWindow {
id: appWindow
@ -35,14 +36,13 @@ ApplicationWindow {
//readonly property MediaPlayer mediaPlayer: _mediaPlayer
readonly property PlaybackManager playbackManager: _playbackManager
// Data of the currently selected item. For use on the cover.
property JellyfinItem itemData
// Due QTBUG-10822, declarartions such as `property J.Item foo` are not possible.
property QtObject itemData
// Id of the collection currently browsing. For use on the cover.
property string collectionId
// Bad way to implement settings, but it'll do for now.
property bool showDebugInfo: true
property bool _hidePlaybackBar: false
bottomMargin: playbackBar.visibleSize
@ -52,14 +52,14 @@ ApplicationWindow {
initialPage: Component {
MainPage {
Connections {
target: ApiClient
target: D.ApiClient
// Replace the MainPage if no server was set up.
}
onStatusChanged: {
if (status == PageStatus.Active && !_hasInitialized) {
_hasInitialized = true;
ApiClient.restoreSavedSession();
D.ApiClient.restoreSavedSession();
}
}
}
@ -94,7 +94,7 @@ ApplicationWindow {
PlaybackManager {
id: _playbackManager
apiClient: ApiClient
apiClient: D.ApiClient
audioIndex: 0
autoOpen: true
}
@ -118,7 +118,7 @@ ApplicationWindow {
//FIXME: proper error handling
Connections {
target: ApiClient
target: D.ApiClient
onNetworkError: errorNotification.show("Network error: " + error)
onConnectionFailed: errorNotification.show("Connect error: " + error)
//onConnectionSuccess: errorNotification.show("Success: " + loginMessage)

View file

@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components"
import ".."
Page {
id: page

View file

@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.0
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components"
import "../"
@ -32,7 +32,7 @@ Page {
/// True if the models on this page already have been loaded and don't necessarily need a refresh
property bool _modelsLoaded: false
id: page
id: mainPage
allowedOrientations: Orientation.All
// This component is reused both in the normal state and error state
@ -49,7 +49,7 @@ Page {
text: qsTr("Reload")
onClicked: loadModels(true)
}
busy: mediaLibraryModel.status == ApiModel.Loading
busy: userViewsLoader.status === J.UsersViewsLoader.Loading
}
}
@ -67,29 +67,33 @@ Page {
// of the page, followed by our content.
Column {
id: column
width: page.width
width: mainPage.width
UserViewModel {
id: mediaLibraryModel2
apiClient: ApiClient
J.ItemModel {
id: mediaLibraryModel
loader: J.UsersViewsLoader {
id: mediaLibraryLoader
apiClient: ApiClient
}
}
MoreSection {
//- Section header for films and TV shows that an user hasn't completed yet.
text: qsTr("Resume watching")
clickable: false
busy: userResumeModel.status === ApiModel.Loading
//busy: userResumeModel.status === J.ApiModel.Loading
Loader {
width: parent.width
sourceComponent: carrouselView
property alias itemModel: userResumeModel
property string collectionType: "series"
UserItemResumeModel {
J.ItemModel {
id: userResumeModel
apiClient: ApiClient
// Resume model
/*apiClient: ApiClient
limit: 12
recursive: true
recursive: true*/
}
}
}
@ -97,7 +101,7 @@ Page {
//- Section header for next episodes in a TV show that an user was watching.
text: qsTr("Next up")
clickable: false
busy: showNextUpModel.status === ApiModel.Loading
//busy: showNextUpModel.status === .Loading
Loader {
width: parent.width
@ -105,23 +109,18 @@ Page {
property alias itemModel: showNextUpModel
property string collectionType: "series"
ShowNextUpModel {
J.ItemModel {
id: showNextUpModel
apiClient: ApiClient
limit: 12
/*apiClient: ApiClient
limit: 12*/
}
}
}
UserViewModel {
id: mediaLibraryModel
apiClient: ApiClient
}
Repeater {
model: mediaLibraryModel
MoreSection {
text: model.name
busy: userItemModel.status !== ApiModel.Ready
busy: userItemModel.status !== J.UsersViewsLoader.Ready
onHeaderClicked: pageStack.push(Qt.resolvedUrl("itemdetails/CollectionPage.qml"), {"itemId": model.jellyfinId})
Loader {
@ -130,11 +129,12 @@ Page {
property alias itemModel: userItemModel
property string collectionType: model.collectionType || ""
UserItemLatestModel {
J.ItemModel {
id: userItemModel
apiClient: ApiClient
parentId: jellyfinId
limit: 16
loader: J.LatestMediaLoader {
apiClient: ApiClient
parentId: jellyfinId
}
}
Connections {
target: mediaLibraryModel
@ -184,8 +184,8 @@ Page {
if (force || (ApiClient.authenticated && !_modelsLoaded)) {
_modelsLoaded = true;
mediaLibraryModel.reload()
userResumeModel.reload()
showNextUpModel.reload()
//userResumeModel.reload()
//showNextUpModel.reload()
}
}
@ -236,11 +236,11 @@ Page {
states: [
State {
name: "default"
when: mediaLibraryModel2.status !== ApiModel.Error
when: mediaLibraryLoader.status !== J.UsersViewsLoader.Error
},
State {
name: "error"
when: mediaLibraryModel2.status === ApiModel.Error
when: mediaLibraryLoader.status === J.UsersViewsLoader.Error
PropertyChanges {
target: errorFlickable

View file

@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../components"
@ -57,9 +57,9 @@ Page {
rightMargin: Theme.horizontalPageMargin
}
height: user.implicitHeight + server.implicitHeight + Theme.paddingMedium
User {
QtObject {
id: loggedInUser
apiClient: ApiClient
//apiClient: ApiClient
}
RemoteImage {
id: userIcon

View file

@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../.."
@ -32,14 +32,14 @@ import "../.."
*/
Page {
id: pageRoot
property alias itemId: jItem.jellyfinId
property alias itemData: jItem
property string itemId: ""
property alias itemData: jItemLoader.data
//property string itemId: ""
//property var itemData: ({})
property bool _loading: jItem.status === "Loading"
readonly property bool hasLogo: (typeof itemData.ImageTags !== "undefined") && (typeof itemData.ImageTags["Logo"] !== "undefined")
property bool _loading: jItemLoader.status === J.ItemLoader.Loading
readonly property bool hasLogo: (typeof itemData.imageTags !== "undefined") && (typeof itemData.imageTags["Logo"] !== "undefined")
property string _chosenBackdropImage: ""
readonly property string parentId: itemData.ParentId || ""
readonly property string parentId: itemData.parentId || ""
function updateBackdrop() {
var rand = 0;
@ -62,13 +62,13 @@ Page {
SilicaFlickable {
anchors.fill: parent
contentHeight: errorContent.height
visible: jItem.status == JellyfinItem.Error
visible: jItemLoader.status === J.ItemLoader.Error
PullDownMenu {
busy: jItem.status == JellyfinItem.Loading
busy: jItemLoader.status === J.ItemLoader.Loading
MenuItem {
text: qsTr("Retry")
onClicked: jItem.reload()
onClicked: jItemLoader.reload()
}
}
@ -79,28 +79,33 @@ Page {
ViewPlaceholder {
enabled: true
text: qsTr("An error has occured")
hintText: jItem.errorString
hintText: jItemLoader.errorString
}
}
}
JellyfinItem {
id: jItem
J.ItemLoader {
id: jItemLoader
apiClient: ApiClient
itemId: pageRoot.itemId
onStatusChanged: {
//console.log("Status changed: " + newStatus, JSON.stringify(jItem))
if (status == JellyfinItem.Ready) {
console.log("Status changed: " + newStatus, JSON.stringify(jItemLoader.data))
if (status === J.ItemLoader.Ready) {
updateBackdrop()
}
}
}
Label {
text: "ItemLoader status=%1, \nitemId=%2\nitemData=%3".arg(jItemLoader.status).arg(jItemLoader.itemId).arg(jItemLoader.data)
}
onStatusChanged: {
if (status == PageStatus.Deactivating) {
//appWindow.itemData = ({})
}
if (status == PageStatus.Active) {
appWindow.itemData = jItem
//appWindow.itemData = jItemLoader.data
}
}
}

View file

@ -19,7 +19,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../.."
import "../../components"
@ -27,12 +27,13 @@ import "../../components"
BaseDetailPage {
id: pageRoot
UserItemModel {
J.ItemModel {
id: collectionModel
apiClient: ApiClient
parentId: itemData.jellyfinId
sortBy: ["SortName"]
onParentIdChanged: reload()
//sortBy: ["SortName"]
loader: J.UserItemsLoader {
apiClient: ApiClient
parentId: itemData.jellyfinId
}
}
SilicaGridView {
@ -42,7 +43,7 @@ BaseDetailPage {
cellWidth: Constants.libraryDelegateWidth
cellHeight: Utils.usePortraitCover(itemData.collectionType) ? Constants.libraryDelegatePosterHeight
: Constants.libraryDelegateHeight
visible: itemData.status !== JellyfinItem.Error
visible: itemData.status !== J.ItemLoader.Error
header: PageHeader {
title: itemData.name || qsTr("Loading")
@ -54,7 +55,7 @@ BaseDetailPage {
text: qsTr("Sort by")
onClicked: pageStack.push(sortPageComponent)
}
busy: collectionModel.status === ApiModel.Loading
busy: collectionModel.status === J.UserItemsLoader.Loading
}
delegate: GridItem {
RemoteImage {

View file

@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../.."

View file

@ -20,7 +20,7 @@ import QtQuick 2.6
import Sailfish.Silica 1.0
import QtQuick.Layouts 1.1
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../../components/music"
@ -33,13 +33,15 @@ BaseDetailPage {
readonly property bool _twoColumns: albumPageRoot.width / Theme.pixelRatio >= 800
UserItemModel {
J.ItemModel {
id: collectionModel
apiClient: ApiClient
sortBy: ["SortName"]
fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"]
parentId: itemData.jellyfinId
onParentIdChanged: reload()
loader: J.UserItemsLoader {
apiClient: ApiClient
//sortBy: ["SortName"]
//fields: ["ItemCounts","PrimaryImageAspectRatio","BasicSyncInfo","CanDelete","MediaSourceCount"]
parentId: itemData.jellyfinId
onParentIdChanged: reload()
}
}
RowLayout {
anchors.fill: parent
@ -50,9 +52,9 @@ BaseDetailPage {
visible: _twoColumns
Layout.minimumWidth: 1000 / Theme.pixelRatio
Layout.fillHeight: true
source: visible
/*source: visible
? "../../components/music/WideAlbumCover.qml" : ""
onLoaded: bindAlbum(item)
onLoaded: bindAlbum(item)*/
}
Item {height: 1; width: Theme.horizontalPageMargin; visible: wideAlbumCover.visible; }
SilicaListView {
@ -62,8 +64,8 @@ BaseDetailPage {
model: collectionModel
header: Loader {
width: parent.width
source: "../../components/music/NarrowAlbumCover.qml"
onLoaded: bindAlbum(item)
/*source: "../../components/music/NarrowAlbumCover.qml"
onLoaded: bindAlbum(item)*/
}
section {
property: "parentIndexNumber"
@ -84,14 +86,8 @@ BaseDetailPage {
}
}
Connections {
target: itemData
onAlbumArtistsChanged: {
}
}
function bindAlbum(item) {
item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})})
//item.albumArt = Qt.binding(function(){ return Utils.itemImageUrl(ApiClient.baseUrl, itemData, "Primary", {"maxWidth": parent.width})})
item.name = Qt.binding(function(){ return itemData.name})
item.releaseYear = Qt.binding(function() { return itemData.productionYear})
item.albumArtist = Qt.binding(function() { return itemData.albumArtist})

View file

@ -20,7 +20,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../.."
@ -33,11 +33,11 @@ BaseDetailPage {
property alias subtitle: pageHeader.description
default property alias _data: content.data
property real _playbackProsition: itemData.userData.playbackPositionTicks
readonly property bool _userdataReady: itemData.status == JellyfinItem.Ready && itemData.userData != null
readonly property bool _userdataReady: itemData.status === J.ItemLoader.Ready && itemData.userData !== null
SilicaFlickable {
anchors.fill: parent
contentHeight: content.height + Theme.paddingLarge
visible: itemData.status !== JellyfinItem.Error
visible: itemData.status !== J.ItemLoader.Error
VerticalScrollDecorator {}
@ -84,7 +84,7 @@ BaseDetailPage {
Connections {
target: itemData
onStatusChanged: {
if (status == JellyfinItem.Ready) {
if (status === J.ItemLoader.Ready) {
console.log(itemData.mediaStreams)
}
}

View file

@ -19,9 +19,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../.."
Page {
id: page
@ -53,7 +54,7 @@ Page {
label: qsTr("Connection state")
value: {
var stateText
switch( ApiClient.websocket.state) {
switch(ApiClient.websocket.state) {
case 0:
//- Socket state
stateText = qsTr("Unconnected");

View file

@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../.."
/**
* Page to indicate that the application is connecting to a server.

View file

@ -18,7 +18,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../"
/**
* Dialog showed when the user has to connect to a Jellyfin server.
@ -49,7 +51,7 @@ Dialog {
title: qsTr("Connect to Jellyfin")
}
ServerDiscoveryModel {
J.ServerDiscoveryModel {
id: serverModel
}

View file

@ -18,9 +18,10 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../../"
/**
* Page where the user can login on their server. Is displayed after the AddServerPage successfully connected
@ -31,7 +32,7 @@ Dialog {
id: loginDialog
property string loginMessage
property Page firstPage
property User selectedUser: null
property QtObject /*User*/ selectedUser: null
property string error
@ -65,11 +66,13 @@ Dialog {
}
}
PublicUserModel {
QtObject { id: userModel; }
/*PublicUserModel {
id: userModel
apiClient: ApiClient
Component.onCompleted: reload();
}
}*/
DialogHeader {
id: dialogHeader
@ -97,7 +100,7 @@ Dialog {
width: parent.width
Repeater {
id: userRepeater
model: userModel
model: 0 //userModel
delegate: UserGridDelegate {
name: model.name
image: model.primaryImageTag ? "%1/Users/%2/Images/Primary?tag=%3".arg(ApiClient.baseUrl).arg(model.jellyfinId).arg(model.primaryImageTag) : ""

View file

@ -16,4 +16,5 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
singleton Constants 1.0 Constants.qml
singleton ApiClient 1.0 ApiClient.qml
Utils 1.0 Utils.js

View file

@ -60,14 +60,14 @@ int main(int argc, char *argv[]) {
QCommandLineParser cmdParser;
cmdParser.addHelpOption();
cmdParser.addVersionOption();
QCommandLineOption sandboxOption("attempt-sandbox", app->translate("Command line argument description", "Try to start with FireJail."));
QCommandLineOption sandboxOption("no-attempt-sandbox", app->translate("Command line argument description", "Try to not start with FireJail."));
if (canSanbox) {
cmdParser.addOption(sandboxOption);
}
cmdParser.process(*app);
if (canSanbox && cmdParser.isSet(sandboxOption)) {
qDebug() << "Restarting in Sanbox mode";
if (canSanbox && !cmdParser.isSet(sandboxOption)) {
qDebug() << "Restarting in sandbox mode";
QProcess::execute(QString(SANDBOX_PROGRAM),
QStringList() << "-p" << "harbour-sailfin.desktop" << "/usr/bin/harbour-sailfin");
return 0;