mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-25 02:15:17 +00:00
Add BlurHash placeholders at several places
* [ui] Added: Blurry previews of images before they are loaded Also fixes a bug where ApiModel would remove items that already started with a lowecase letter or didn't start with a letter at all.
This commit is contained in:
parent
bb2f6f3a3e
commit
79d378c9ed
|
@ -362,6 +362,8 @@ public:
|
||||||
// a QHash at the moment.
|
// a QHash at the moment.
|
||||||
Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged)
|
Q_PROPERTY(QJsonObject imageTags MEMBER m_imageTags NOTIFY imageTagsChanged)
|
||||||
Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged)
|
Q_PROPERTY(QJsonObject imageBlurHashes MEMBER m_imageBlurHashes NOTIFY imageBlurHashesChanged)
|
||||||
|
Q_PROPERTY(int width MEMBER m_width NOTIFY widthChanged)
|
||||||
|
Q_PROPERTY(int height MEMBER m_height NOTIFY heightChanged)
|
||||||
|
|
||||||
QString jellyfinId() const { return m_id; }
|
QString jellyfinId() const { return m_id; }
|
||||||
void setJellyfinId(QString newId);
|
void setJellyfinId(QString newId);
|
||||||
|
@ -450,6 +452,8 @@ signals:
|
||||||
void artistsChanged(const QStringList &newArtists);
|
void artistsChanged(const QStringList &newArtists);
|
||||||
void imageTagsChanged();
|
void imageTagsChanged();
|
||||||
void imageBlurHashesChanged();
|
void imageBlurHashesChanged();
|
||||||
|
void widthChanged(int newWidth);
|
||||||
|
void heightChanged(int newHeight);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
||||||
|
@ -505,6 +509,8 @@ protected:
|
||||||
QStringList m_artists;
|
QStringList m_artists;
|
||||||
QJsonObject m_imageTags;
|
QJsonObject m_imageTags;
|
||||||
QJsonObject m_imageBlurHashes;
|
QJsonObject m_imageBlurHashes;
|
||||||
|
int m_width;
|
||||||
|
int m_height;
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) {
|
QQmlListProperty<T> toReadOnlyQmlListProperty(QList<T *> &list) {
|
||||||
|
|
|
@ -183,6 +183,7 @@ QVariant ApiModel::data(const QModelIndex &index, int role) const {
|
||||||
QJsonObject obj = m_array.at(index.row()).toObject();
|
QJsonObject obj = m_array.at(index.row()).toObject();
|
||||||
|
|
||||||
const QString &key = m_roles[role];
|
const QString &key = m_roles[role];
|
||||||
|
|
||||||
if (obj.contains(key)) {
|
if (obj.contains(key)) {
|
||||||
return obj[key].toVariant();
|
return obj[key].toVariant();
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,9 +29,14 @@ void convertToCamelCase(QJsonValueRef val) {
|
||||||
for(const QString &key: obj.keys()) {
|
for(const QString &key: obj.keys()) {
|
||||||
QJsonValueRef ref = obj[key];
|
QJsonValueRef ref = obj[key];
|
||||||
convertToCamelCase(ref);
|
convertToCamelCase(ref);
|
||||||
|
obj[convertToCamelCaseHelper(key)] = ref;
|
||||||
|
if (key[0].isLower() || !key[0].isLetter()) {
|
||||||
|
obj[key] = ref;
|
||||||
|
} else {
|
||||||
obj[convertToCamelCaseHelper(key)] = ref;
|
obj[convertToCamelCaseHelper(key)] = ref;
|
||||||
obj.remove(key);
|
obj.remove(key);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val = obj;
|
val = obj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ set(sailfin_QML_SOURCES
|
||||||
qml/pages/itemdetails/EpisodePage.qml
|
qml/pages/itemdetails/EpisodePage.qml
|
||||||
qml/pages/itemdetails/FilmPage.qml
|
qml/pages/itemdetails/FilmPage.qml
|
||||||
qml/pages/itemdetails/MusicAlbumPage.qml
|
qml/pages/itemdetails/MusicAlbumPage.qml
|
||||||
|
qml/pages/itemdetails/PhotoPage.qml
|
||||||
qml/pages/itemdetails/SeasonPage.qml
|
qml/pages/itemdetails/SeasonPage.qml
|
||||||
qml/pages/itemdetails/SeriesPage.qml
|
qml/pages/itemdetails/SeriesPage.qml
|
||||||
qml/pages/itemdetails/UnsupportedPage.qml
|
qml/pages/itemdetails/UnsupportedPage.qml
|
||||||
|
|
|
@ -28,6 +28,7 @@ BackgroundItem {
|
||||||
id: root
|
id: root
|
||||||
property alias poster: posterImage.source
|
property alias poster: posterImage.source
|
||||||
property alias title: titleText.text
|
property alias title: titleText.text
|
||||||
|
property alias blurhash: posterImage.blurhash
|
||||||
property bool landscape: false
|
property bool landscape: false
|
||||||
property real progress: 0.0
|
property real progress: 0.0
|
||||||
|
|
||||||
|
|
|
@ -20,17 +20,34 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
import QtQuick 2.6
|
import QtQuick 2.6
|
||||||
import Sailfish.Silica 1.0
|
import Sailfish.Silica 1.0
|
||||||
|
|
||||||
|
import nl.netsoj.chris.blurhash 1.0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An image for "remote" images (loaded over e.g. http), with a spinner and a fallback image
|
* An image for "remote" images (loaded over e.g. http), with a spinner and a fallback image
|
||||||
*/
|
*/
|
||||||
SilicaItem {
|
SilicaItem {
|
||||||
|
id: root
|
||||||
property string fallbackImage
|
property string fallbackImage
|
||||||
property bool usingFallbackImage
|
property bool usingFallbackImage
|
||||||
property color fallbackColor: Theme.highlightColor
|
property color fallbackColor: Theme.highlightColor
|
||||||
|
|
||||||
property alias source: realImage.source
|
property var __parentPage : null
|
||||||
|
property bool alreadyLoaded: false
|
||||||
|
|
||||||
|
onSourceChanged: alreadyLoaded = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BlurHash that is used as placeholder
|
||||||
|
*/
|
||||||
|
property string blurhash: ""
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
property real aspectRatio: 1.0
|
||||||
|
property string source: ""
|
||||||
property alias sourceSize: realImage.sourceSize
|
property alias sourceSize: realImage.sourceSize
|
||||||
property alias fillMode: realImage.fillMode
|
property var fillMode: Image.Stretch
|
||||||
|
property alias status: realImage.status
|
||||||
implicitHeight: realImage.implicitHeight
|
implicitHeight: realImage.implicitHeight
|
||||||
implicitWidth: realImage.implicitWidth
|
implicitWidth: realImage.implicitWidth
|
||||||
|
|
||||||
|
@ -38,6 +55,14 @@ SilicaItem {
|
||||||
id: realImage
|
id: realImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
fillMode: root.fillMode
|
||||||
|
opacity: 1
|
||||||
|
source: alreadyLoaded || [PageStatus.Active, PageStatus.Deactivating].indexOf(__parentPage.status) >= 0 ? root.source : ""
|
||||||
|
onStatusChanged: {
|
||||||
|
if (status == Image.Ready) {
|
||||||
|
alreadyLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -47,7 +72,17 @@ SilicaItem {
|
||||||
GradientStop { position: 0.0; color: fallbackColor; }
|
GradientStop { position: 0.0; color: fallbackColor; }
|
||||||
GradientStop { position: 1.0; color: Theme.highlightDimmerFromColor(fallbackColor, Theme.colorScheme); }
|
GradientStop { position: 1.0; color: Theme.highlightDimmerFromColor(fallbackColor, Theme.colorScheme); }
|
||||||
}
|
}
|
||||||
visible: realImage.status === Image.Error || realImage.status === Image.Null || realImage.status === Image.Loading
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: blurhashImage
|
||||||
|
anchors.fill: parent
|
||||||
|
fillMode: root.fillMode
|
||||||
|
sourceSize.height: 32
|
||||||
|
sourceSize.width: 32 * aspectRatio
|
||||||
|
source: "image://blurhash/" + encodeURIComponent(blurhash)
|
||||||
|
opacity: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
@ -65,7 +100,60 @@ SilicaItem {
|
||||||
HighlightImage {
|
HighlightImage {
|
||||||
id: fallbackImageItem
|
id: fallbackImageItem
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
visible: realImage.status === Image.Error || realImage.status === Image.Null
|
visible: realImage.status === Image.Error || (realImage.status === Image.Null && blurhash.length === 0)
|
||||||
source: fallbackImage ? fallbackImage : "image://theme/icon-m-question"
|
source: fallbackImage ? fallbackImage : "image://theme/icon-m-question"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: name
|
||||||
|
text: state
|
||||||
|
color: Qt.red
|
||||||
|
}
|
||||||
|
onStateChanged: console.log("New state: " + state + ", blurhash: '" + blurhash + "'")
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "fallback"
|
||||||
|
when: (blurhash.length === 0) && (realImage.status === Image.Error || realImage.status === Image.Null || realImage.status === Image.Loading)
|
||||||
|
PropertyChanges {
|
||||||
|
target: fallbackBackground
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "blurhash"
|
||||||
|
when: blurhash.length > 0 && (realImage.status === Image.Error || realImage.status === Image.Null || realImage.status === Image.Loading)
|
||||||
|
PropertyChanges {
|
||||||
|
target: blurhashImage
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "loaded"
|
||||||
|
when: realImage.status === Image.Ready
|
||||||
|
PropertyChanges {
|
||||||
|
target: realImage
|
||||||
|
//opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: "*"
|
||||||
|
to: "*"
|
||||||
|
FadeAnimation {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
var item = parent;
|
||||||
|
while (item != null) {
|
||||||
|
if ("__silica_page" in item) {
|
||||||
|
__parentPage = item
|
||||||
|
console.log("Found parent " + item)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
item = item.parent
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ Item {
|
||||||
property real duration
|
property real duration
|
||||||
property int songCount
|
property int songCount
|
||||||
property string name
|
property string name
|
||||||
|
property alias blurhash : albumArt.blurhash
|
||||||
|
|
||||||
|
|
||||||
property string stateIfArt: "largeArt"
|
property string stateIfArt: "largeArt"
|
||||||
|
|
|
@ -35,6 +35,7 @@ Column {
|
||||||
property real duration
|
property real duration
|
||||||
property int songCount
|
property int songCount
|
||||||
property string name
|
property string name
|
||||||
|
property alias blurhash : albumArt.blurhash
|
||||||
|
|
||||||
Item { width:1; height: Theme.paddingLarge }
|
Item { width:1; height: Theme.paddingLarge }
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import "../Utils.js" as Utils
|
||||||
* Main page, which simply shows some content of every library, as well as next items.
|
* Main page, which simply shows some content of every library, as well as next items.
|
||||||
*/
|
*/
|
||||||
Page {
|
Page {
|
||||||
|
/// True if the models on this page already have been loaded and don't necessarily need a refresh
|
||||||
property bool _modelsLoaded: false
|
property bool _modelsLoaded: false
|
||||||
|
|
||||||
id: page
|
id: page
|
||||||
|
@ -192,12 +193,6 @@ Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
|
||||||
text: Screen.sizeCategory
|
|
||||||
x: 100
|
|
||||||
y: 100
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: carrouselView
|
id: carrouselView
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
|
@ -227,9 +222,7 @@ Page {
|
||||||
property string id: model.id
|
property string id: model.id
|
||||||
title: model.name
|
title: model.name
|
||||||
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["primary"], "Primary", {"maxHeight": height})
|
poster: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags["primary"], "Primary", {"maxHeight": height})
|
||||||
/*model.imageTags["Primary"] ? ApiClient.baseUrl + "/Items/" + model.id
|
blurhash: model.imageBlurHashes["primary"][model.imageTags["primary"]]
|
||||||
+ "/Images/Primary?maxHeight=" + height + "&tag=" + model.imageTags["Primary"]
|
|
||||||
: ""*/
|
|
||||||
landscape: !Utils.usePortraitCover(collectionType)
|
landscape: !Utils.usePortraitCover(collectionType)
|
||||||
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
|
progress: (typeof model.userData !== "undefined") ? model.userData.playedPercentage / 100 : 0.0
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ Page {
|
||||||
id: loggedInUser
|
id: loggedInUser
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
}
|
}
|
||||||
Image {
|
RemoteImage {
|
||||||
id: userIcon
|
id: userIcon
|
||||||
width: height
|
width: height
|
||||||
anchors {
|
anchors {
|
||||||
|
|
|
@ -61,6 +61,7 @@ BaseDetailPage {
|
||||||
id: itemImage
|
id: itemImage
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxWidth": width})
|
source: Utils.itemModelImageUrl(ApiClient.baseUrl, model.id, model.imageTags.primary, "Primary", {"maxWidth": width})
|
||||||
|
blurhash: model.imageBlurHashes.primary[model.imageTags.primary]
|
||||||
fallbackColor: Utils.colorFromString(model.name)
|
fallbackColor: Utils.colorFromString(model.name)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
clip: true
|
clip: true
|
||||||
|
|
|
@ -106,5 +106,6 @@ BaseDetailPage {
|
||||||
item.duration = Qt.binding(function() { return itemData.runTimeTicks})
|
item.duration = Qt.binding(function() { return itemData.runTimeTicks})
|
||||||
item.songCount = Qt.binding(function() { return itemData.childCount})
|
item.songCount = Qt.binding(function() { return itemData.childCount})
|
||||||
item.listview = Qt.binding(function() { return list})
|
item.listview = Qt.binding(function() { return list})
|
||||||
|
item.blurhash = Qt.binding(function() { return itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]]; })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import Sailfish.Silica 1.0
|
||||||
|
|
||||||
import nl.netsoj.chris.Jellyfin 1.0
|
import nl.netsoj.chris.Jellyfin 1.0
|
||||||
|
|
||||||
|
import "../../components"
|
||||||
|
|
||||||
BaseDetailPage {
|
BaseDetailPage {
|
||||||
id: pageRoot
|
id: pageRoot
|
||||||
navigationStyle: PageNavigation.Vertical
|
navigationStyle: PageNavigation.Vertical
|
||||||
|
@ -17,19 +19,13 @@ BaseDetailPage {
|
||||||
titleColor: Theme.primaryColor
|
titleColor: Theme.primaryColor
|
||||||
}
|
}
|
||||||
|
|
||||||
Image {
|
RemoteImage {
|
||||||
id: image
|
id: image
|
||||||
source: ApiClient.downloadUrl(itemId)
|
source: ApiClient.downloadUrl(itemId)
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
opacity: status == Image.Ready ? 1.0 : 0.0
|
blurhash: itemData.imageBlurHashes["Primary"][itemData.imageTags["Primary"]]
|
||||||
Behavior on opacity { FadeAnimator {}}
|
aspectRatio: itemData.width / itemData.height
|
||||||
}
|
|
||||||
|
|
||||||
BusyIndicator {
|
|
||||||
running: image.status == Image.Loading
|
|
||||||
size: BusyIndicatorSize.Large
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue