1
0
Fork 0
mirror of https://github.com/HenkKalkwater/harbour-sailfin.git synced 2024-06-26 11:33:50 +00:00

sailfish: add LiveTvChannels page

This ocmmit adds a LiveTvChannels page for displaying the programs that
are now playing.

The section Live TV Channels on the main page now shows the TV channel
list in order of the channel number.

Additionally, it fixes an issue in ApiModel, where it would not reload
when a new loader was assigned. This is now fixed and some code on pages
that worked around this fix has been removed.
This commit is contained in:
Chris Josten 2024-06-03 22:11:10 +02:00
parent edcd3a93af
commit 57b67292fd
23 changed files with 344 additions and 54 deletions

1
core/3rdparty/qtpromise vendored Submodule

@ -0,0 +1 @@
Subproject commit f7639e921ee7b0d20de4ac0da67c0f69e0692101

View file

@ -239,23 +239,33 @@ bool setRequestStartIndex(P &parameters, int startIndex) {
#ifndef JELLYFIN_APIMODEL_CPP
extern template bool setRequestStartIndex(Loader::GetUserViewsParams &params, int startIndex);
extern template void setRequestLimit(Loader::GetUserViewsParams &params, int limit);
extern template QList<DTO::BaseItemDto> extractRecords(const DTO::BaseItemDtoQueryResult &result);
extern template int extractTotalRecordCount(const DTO::BaseItemDtoQueryResult &result);
extern template QList<DTO::BaseItemDto> extractRecords(const QList<DTO::BaseItemDto> &result);
extern template int extractTotalRecordCount(const QList<DTO::BaseItemDto> &result);
extern template void setRequestLimit(Loader::GetLatestMediaParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetLatestMediaParams &params, int offset);
extern template void setRequestLimit(Loader::GetItemsByUserIdParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetItemsByUserIdParams &params, int offset);
extern template void setRequestLimit(Loader::GetResumeItemsParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetResumeItemsParams &params, int offset);
extern template void setRequestLimit(Loader::GetPublicUsersParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetPublicUsersParams &params, int offset);
extern template void setRequestLimit(Loader::GetNextUpParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetNextUpParams &params, int offset);
extern template void setRequestLimit(Loader::GetAlbumArtistsParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetAlbumArtistsParams &params, int offset);
extern template void setRequestLimit(Loader::GetLiveTvChannelsParams &params, int limit);
extern template bool setRequestStartIndex(Loader::GetLiveTvChannelsParams &params, int offset);
extern template QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result);
extern template int extractTotalRecordCount(const QList<DTO::UserDto> &result);
#endif
@ -519,6 +529,7 @@ public:
BaseApiModel::setLoader(newLoader);
BaseApiModel::disconnectOldLoader(m_loader);
m_loader = castedLoader;
reload();
} else {
qWarning() << "Somehow set a BaseModelLoader on ApiModel instead of a ModelLoader<T>";
}

View file

@ -178,9 +178,12 @@ public:
Q_PROPERTY(int albumCount READ albumCount NOTIFY albumCountChanged)
Q_PROPERTY(int artistCount READ artistCount NOTIFY artistCountChanged)
Q_PROPERTY(int musicVideoCount READ musicVideoCount NOTIFY musicVideoCountChanged)
Q_PROPERTY(QString mediaType READ mediaType READ mediaType NOTIFY mediaTypeChanged)
Q_PROPERTY(QString mediaType READ mediaType NOTIFY mediaTypeChanged)
Q_PROPERTY(QDateTime endDate READ endDate NOTIFY endDateChanged)
Q_PROPERTY(QDateTime startDate READ startDate NOTIFY startDateChanged)
Q_PROPERTY(int width READ width NOTIFY widthChanged)
Q_PROPERTY(int height READ height NOTIFY heightChanged)
Q_PROPERTY(Jellyfin::ViewModel::Item *currentProgram READ currentProgram NOTIFY currentProgramChanged)
QString jellyfinId() const { return m_data->jellyfinId(); }
QString name() const { return m_data->name(); }
@ -225,6 +228,9 @@ public:
QStringList backdropImageTags() const { return m_data->backdropImageTags(); }
QJsonObject imageBlurHashes() const { return m_data->imageBlurHashes(); }
QString mediaType() const { return m_data->mediaType(); }
QDateTime endDate() const { return m_data->endDate(); }
QDateTime startDate() const { return m_data->startDate(); }
Item *currentProgram() const { return m_currentProgram; }
int trailerCount() const { return m_data->trailerCount().value_or(0); }
int movieCount() const { return m_data->movieCount().value_or(0); }
@ -308,8 +314,11 @@ signals:
void artistCountChanged(int newArtistCount);
void musicVideoCountChanged(int newMusicVideoCount);
void mediaTypeChanged(const QString &newMediaType);
void endDateChanged();
void startDateChanged();
void widthChanged(int newWidth);
void heightChanged(int newHeight);
void currentProgramChanged();
protected:
void setUserData(DTO::UserItemDataDto &newData);
void setUserData(QSharedPointer<DTO::UserItemDataDto> newData);
@ -322,6 +331,7 @@ protected:
QObjectList m_videoStreams;
QObjectList m_subtitleStreams;
QObjectList m_artistItems;
Item *m_currentProgram = nullptr;
private slots:
void onUserDataChanged(const DTO::UserItemDataDto &userData);
};

View file

@ -327,6 +327,32 @@ public:
FWDLISTPROP(int, years, Years);
};
using LiveTvChannelsLoaderBase = AbstractUserParameterLoader<Model::Item, DTO::BaseItemDto, DTO::BaseItemDtoQueryResult, Jellyfin::Loader::GetLiveTvChannelsParams>;
class LiveTvChannelsLoader : public LiveTvChannelsLoaderBase {
Q_OBJECT
public:
explicit LiveTvChannelsLoader(QObject *parent = nullptr);
FWDPROP(Jellyfin::DTO::ChannelTypeClass::Value, type, Type)
FWDPROP(bool, isMovie, IsMovie)
FWDPROP(bool, isSeries, IsSeries)
FWDPROP(bool, isNews, IsNews)
FWDPROP(bool, isKids, IsKids)
FWDPROP(bool, isSports, IsSports)
FWDPROP(bool, isFavorite, IsFavorite)
FWDPROP(bool, isLiked, IsLiked)
FWDPROP(bool, isDisliked, IsDisliked)
FWDPROP(bool, enableImages, EnableImages)
FWDPROP(int, imageTypeLimit, ImageTypeLimit)
FWDLISTPROP(Jellyfin::DTO::ImageTypeClass::Value, enableImageTypes, EnableImageTypes)
FWDLISTPROP(Jellyfin::DTO::ItemFieldsClass::Value, fields, Fields)
FWDPROP(bool, enableUserData, EnableUserData)
FWDPROP(QStringList, sortBy, SortBy)
FWDPROP(Jellyfin::DTO::SortOrderClass::Value, sortOrder, SortOrder)
FWDPROP(bool, enableFavoriteSorting, EnableFavoriteSorting)
FWDPROP(bool, addCurrentProgram, AddCurrentProgram)
};
/**
* @brief Base class for each model that works with items.
*/
@ -369,6 +395,10 @@ public:
userDataLastPlayedDate,
userDataPlayed,
userDataKey,
currentProgramName,
currentProgramOverview,
currentProgramStartDate,
currentProgramEndDate,
jellyfinExtendModelAfterHere = Qt::UserRole + 300 // Should be enough for now
};
@ -410,6 +440,10 @@ public:
JFRN(userDataLastPlayedDate),
JFRN(userDataPlayed),
JFRN(userDataKey),
JFRN(currentProgramName),
JFRN(currentProgramOverview),
JFRN(currentProgramStartDate),
JFRN(currentProgramEndDate),
};
}
QVariant data(const QModelIndex &index, int role) const override;

View file

@ -194,6 +194,18 @@ bool setRequestStartIndex(Loader::GetAlbumArtistsParams &params, int offset) {
return true;
}
template<>
void setRequestLimit(Loader::GetLiveTvChannelsParams &params, int limit) {
params.setLimit(limit);
}
template<>
bool setRequestStartIndex(Loader::GetLiveTvChannelsParams &params, int offset) {
params.setStartIndex(offset);
return true;
}
template<>
QList<DTO::UserDto> extractRecords(const QList<DTO::UserDto> &result) {
return result;

View file

@ -83,6 +83,7 @@ void JellyfinPlugin::registerTypes(const char *uri) {
qmlRegisterType<ViewModel::NextUpLoader>(uri, 1, 0, "NextUpLoader");
qmlRegisterType<ViewModel::PublicUsersLoader>(uri, 1, 0, "PublicUsersLoader");
qmlRegisterType<ViewModel::AlbumArtistLoader>(uri, 1, 0, "AlbumArtistLoader");
qmlRegisterType<ViewModel::LiveTvChannelsLoader>(uri, 1, 0, "LiveTvChannelsLoader");
// Enumerations
qmlRegisterUncreatableType<Jellyfin::DTO::GeneralCommandTypeClass>(uri, 1, 0, "GeneralCommandType", "Is an enum");

View file

@ -34,9 +34,7 @@ Item::Item(QObject *parent, QSharedPointer<Model::Item> data)
: QObject(parent),
m_data(data),
m_userData(new UserData(this)){
connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged);
m_userData->setData(data->userData());
updateMediaStreams();
this->setData(data);
}
void Item::setData(QSharedPointer<Model::Item> newData) {
@ -50,8 +48,18 @@ void Item::setData(QSharedPointer<Model::Item> newData) {
connect(m_data.data(), &Model::Item::userDataChanged, this, &Item::onUserDataChanged);
updateMediaStreams();
setUserData(m_data->userData());
if (m_data->currentProgram().isNull()) {
m_currentProgram = nullptr;
} else {
QSharedPointer<DTO::BaseItemDto> dataDto = m_data->currentProgram();
QSharedPointer<Model::Item> data = QSharedPointer<Model::Item>::create(*dataDto.data());
m_currentProgram = new Item(this, data);
}
emit currentProgramChanged();
}
emit userDataChanged(m_userData);
}

View file

@ -16,18 +16,19 @@
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "JellyfinQt/viewmodel/itemmodel.h"
#include <JellyfinQt/viewmodel/itemmodel.h>
#include "JellyfinQt/viewmodel/item.h"
#include <JellyfinQt/viewmodel/item.h>
#include "JellyfinQt/loader/http/artists.h"
#include "JellyfinQt/loader/http/items.h"
#include "JellyfinQt/loader/http/userlibrary.h"
#include "JellyfinQt/loader/http/userviews.h"
#include "JellyfinQt/loader/http/tvshows.h"
#include <JellyfinQt/loader/http/artists.h>
#include <JellyfinQt/loader/http/items.h>
#include <JellyfinQt/loader/http/livetv.h>
#include <JellyfinQt/loader/http/userlibrary.h>
#include <JellyfinQt/loader/http/userviews.h>
#include <JellyfinQt/loader/http/tvshows.h>
#include "JellyfinQt/viewmodel/userdata.h"
#include "JellyfinQt/viewmodel/utils.h"
#include <JellyfinQt/viewmodel/userdata.h>
#include <JellyfinQt/viewmodel/utils.h>
#define JF_CASE(roleName) case roleName: \
try { \
@ -64,6 +65,9 @@ NextUpLoader::NextUpLoader(QObject *parent)
AlbumArtistLoader::AlbumArtistLoader(QObject *parent)
: AlbumArtistLoaderBase(new Jellyfin::Loader::HTTP::GetAlbumArtistsLoader(), parent) {}
LiveTvChannelsLoader::LiveTvChannelsLoader(QObject *parent)
: LiveTvChannelsLoaderBase(new Jellyfin::Loader::HTTP::GetLiveTvChannelsLoader(), parent) {}
ItemModel::ItemModel(QObject *parent)
: ApiModel<Model::Item>(parent) {
connect(this, &QAbstractItemModel::rowsInserted, this, &ItemModel::onInsertItems);
@ -126,6 +130,30 @@ QVariant ItemModel::data(const QModelIndex &index, int role) const {
return QVariant(item->userData()->played());
case RoleNames::userDataKey:
return QVariant(item->userData()->key());
case RoleNames::currentProgramName:
if (item->currentProgram()) {
return QVariant(item->currentProgram()->name());
} else {
return QVariant();
}
case RoleNames::currentProgramOverview:
if (item->currentProgram()) {
return QVariant(item->currentProgram()->overview());
} else {
return QVariant();
}
case RoleNames::currentProgramStartDate:
if (item->currentProgram()) {
return QVariant(item->currentProgram()->startDate());
} else {
return QVariant();
}
case RoleNames::currentProgramEndDate:
if (item->currentProgram()) {
return QVariant(item->currentProgram()->endDate());
} else {
return QVariant();
}
default:
return QVariant();
}

View file

@ -12,6 +12,12 @@
# * date Author's Name <author's email> version-release
# - Summary of changes
#
* ??? ??? ? ???? Chris Josten <chris@netsoj.nl> ?.?.?-?
- New features:
- Added a page that shows a list of Live TV channels with details about currently running
programs.
- Changes:
- TV CHannels on the main page are now sorted by channel number, instead of recently added
* Tue Jan 2 2024 Chris Josten <chris@netsoj.nl> 0.5.0-1
- New features
- Allow remote controlling other Jellyfin clients. See the pulley item on the main screen named

View file

@ -56,10 +56,12 @@ set(sailfin_QML_SOURCES
qml/pages/itemdetails/CollectionPage.qml
qml/pages/itemdetails/EpisodePage.qml
qml/pages/itemdetails/FilmPage.qml
qml/pages/itemdetails/MusicAlbumPage.qml
qml/pages/itemdetails/LiveTvChannelPage.qml
qml/pages/itemdetails/LiveTvChannelsPage.qml
qml/pages/itemdetails/MusicAlbumPage.qml
qml/pages/itemdetails/MusicArtistPage.qml
qml/pages/itemdetails/MusicLibraryPage.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

View file

@ -115,6 +115,8 @@ function getPageUrl(mediaType, itemType, isFolder) {
return Qt.resolvedUrl("pages/itemdetails/MusicAlbumPage.qml")
case "photo":
return Qt.resolvedUrl("pages/itemdetails/PhotoPage.qml")
case "tvchannel":
return Qt.resolvedUrl("pages/itemdetails/LiveTvChannelPage.qml")
case "collectionfolder":
// TODO: support for other collection folders
switch(mediaType.toLowerCase()) {
@ -124,6 +126,8 @@ function getPageUrl(mediaType, itemType, isFolder) {
// FALLTRHOUGH
default:
switch (mediaType ? mediaType.toLowerCase() : isFolder ? "folder" : "") {
case "livetv":
return Qt.resolvedUrl("pages/itemdetails/LiveTvChannelsPage.qml")
case "folder":
return Qt.resolvedUrl("pages/itemdetails/CollectionPage.qml")
case "video":

View file

@ -190,7 +190,7 @@ PanelBackground {
left: parent.left
verticalCenter: parent.verticalCenter
}
height: parent
height: parent.height
source: "image://theme/icon-s-device-upload"
visible: controllingRemote
}

View file

@ -106,6 +106,9 @@ SilicaItem {
BusyIndicator {
anchors.centerIn: parent
running: realImage.status === Image.Loading
size: root.height <= Theme.fontSizeLarge
? BusyIndicatorSize.Small
: BusyIndicatorSize.Medium
}
HighlightImage {

View file

@ -114,6 +114,12 @@ Page {
apiClient: appWindow.apiClient
parentId: jellyfinId
}
Binding on loader {
when: model.collectionType == "livetv"
value: J.LiveTvChannelsLoader{
apiClient: appWindow.apiClient
}
}
Connections {
target: mediaLibraryLoader
onReady: loader.reload()

View file

@ -0,0 +1,23 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
import "../../components"
VideoPage {
title: itemData.currentProgram.name
subtitle: qsTr("%1 | %2 - %3")
.arg(itemData.name)
.arg(Qt.formatTime(itemData.currentProgram.startDate))
.arg(Qt.formatTime(itemData.currentProgram.endDate))
SectionHeader {
text: qsTr("Program info")
}
PlainLabel {
id: overviewText
text: itemData.currentProgram.overview || qsTr("No program info available")
font.pixelSize: Theme.fontSizeSmall
color: Theme.secondaryHighlightColor
}
}

View file

@ -0,0 +1,109 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
import nl.netsoj.chris.Jellyfin 1.0 as J
import "../../components"
import "../../"
BaseDetailPage {
J.ItemModel {
id: episodeModel
loader: J.LiveTvChannelsLoader{
apiClient: appWindow.apiClient
fields: [J.ItemFields.Overview]
autoReload: itemData.jellyfinId.length > 0
}
}
SilicaListView {
anchors.fill: parent
model: episodeModel
header: PageHeader {
title: itemData.name
description: itemData.seriesName
}
delegate: BackgroundItem {
height: content.height
onClicked: appWindow.navigateToItem(model.jellyfinId, model.mediaType, model.type, model.isFolder);
Column {
id: content
anchors {
left: parent.left
leftMargin: Theme.horizontalPageMargin
right: parent.right
rightMargin: Theme.horizontalPageMargin
}
spacing: Theme.paddingSmall
topPadding: Theme.paddingLarge
bottomPadding: Theme.paddingLarge
width: parent.width
Row {
anchors {
left: parent.left
right: parent.right
}
spacing: Theme.paddingMedium
RemoteImage {
id: channelLogo
width: Theme.fontSizeLarge
height: width
source: Utils.itemModelImageUrl(apiClient.baseUrl, model.jellyfinId, model.imageTags.Primary, "Primary", {"maxHeight": height})
blurhash: model.imageBlurHashes.Primary[model.imageTags.Primary]
fillMode: Image.PreserveAspectFit
fallbackColor: "transparent"
}
Label {
anchors.verticalCenter: channelLogo.verticalCenter
id: channelName
text: model.name
font.pixelSize: Theme.fontSizeLarge
}
}
Item {
anchors { left: parent.left; right: parent.right }
height: programName.height
Label {
id: programName
anchors {
left: parent.left
right: programTime.left
rightMargin: Theme.paddingLarge
}
text: currentProgramName
? currentProgramName
//: Shown in the channel list when the name of the current program is unknown
: qsTr("No program information available")
truncationMode: TruncationMode.Fade
}
Label {
id: programTime
anchors.right: parent.right
text: "%1 - %2"
.arg(Qt.formatTime(currentProgramStartDate))
.arg(Qt.formatTime(currentProgramEndDate))
color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor
}
}
Label {
anchors {
left: parent.left
right: parent.right
}
visible: text
text: currentProgramOverview
color: highlighted ? Theme.secondaryHighlightColor : Theme.secondaryColor
maximumLineCount: 2
wrapMode: Text.WordWrap
elide: "ElideRight"
}
}
}
VerticalScrollDecorator {}
}
}

View file

@ -55,7 +55,6 @@ BaseDetailPage {
fields: [J.ItemFields.ItemCounts, J.ItemFields.PrimaryImageAspectRatio]
parentId: itemData.jellyfinId
autoReload: itemData.jellyfinId.length > 0
//onParentIdChanged: if (parentId.length > 0) reload()
}
}
RowLayout {

View file

@ -37,11 +37,6 @@ BaseDetailPage {
}
}
Connections {
target: itemData
onReady: episodeModel.reload()
}
SilicaListView {
anchors.fill: parent
contentHeight: content.height
@ -149,19 +144,4 @@ BaseDetailPage {
VerticalScrollDecorator {}
}
Connections {
target: itemData
onStatusChanged: {
if (itemData.status == JellyfinItem.Ready) {
episodeModel.reload()
}
}
}
onStatusChanged: {
if (status == PageStatus.Active) {
//console.log(JSON.stringify(itemData))
//episodeModel.show = itemData.seriesId
//episodeModel.seasonId = itemData.jellyfinId
}
}
}

View file

@ -72,10 +72,6 @@ BaseDetailPage {
autoReload: itemData.jellyfinId.length > 0
}
}
Connections {
target: itemData
onReady: showSeasonsModel.reload()
}
SilicaListView {
model: showSeasonsModel
@ -96,17 +92,4 @@ BaseDetailPage {
}
}
/*onStatusChanged: {
if (status == PageStatus.Active) {
showSeasonsModel.reload()
}
}*/
Connections {
target: itemData
onJellyfinIdChanged: {
console.log("Item id changed")
//showSeasonsModel.show = itemData.jellyfinId
}
}
}

View file

@ -31,6 +31,7 @@ import "../.."
*/
BaseDetailPage {
id: detailPage
property alias title: pageHeader.title
property alias subtitle: pageHeader.description
default property alias _data: content.data
property real _playbackProsition: itemData.userData.playbackPositionTicks

View file

@ -265,6 +265,29 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelPage</name>
<message>
<source>%1 | %2 - %3</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Program info</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No program info available</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelsPage</name>
<message>
<source>No program information available</source>
<extracomment>Shown in the channel list when the name of the current program is unknown</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginDialog</name>
<message>

View file

@ -265,6 +265,29 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelPage</name>
<message>
<source>%1 | %2 - %3</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Program info</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No program info available</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelsPage</name>
<message>
<source>No program information available</source>
<extracomment>Shown in the channel list when the name of the current program is unknown</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginDialog</name>
<message>

View file

@ -265,6 +265,29 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelPage</name>
<message>
<source>%1 | %2 - %3</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Program info</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No program info available</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LiveTvChannelsPage</name>
<message>
<source>No program information available</source>
<extracomment>Shown in the channel list when the name of the current program is unknown</extracomment>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>LoginDialog</name>
<message>