mirror of
https://github.com/HenkKalkwater/harbour-sailfin.git
synced 2024-11-22 09:15:18 +00:00
Added user details and somewhat imporved error handling
* [UI] Improved: error handling should be slightly better * [UI] Improved: settings now show the user name and picture instead of the user id if network is available.
This commit is contained in:
parent
8a683df2a2
commit
d3a7c17586
|
@ -126,9 +126,39 @@ signals:
|
||||||
void apiClientChanged(ApiClient *newApiClient);
|
void apiClientChanged(ApiClient *newApiClient);
|
||||||
void errorChanged(QNetworkReply::NetworkError newError);
|
void errorChanged(QNetworkReply::NetworkError newError);
|
||||||
void errorStringChanged(QString newErrorString);
|
void errorStringChanged(QString newErrorString);
|
||||||
|
/**
|
||||||
|
* @brief Convenience signal for status == RemoteData.Ready.
|
||||||
|
*/
|
||||||
|
void ready();
|
||||||
public slots:
|
public slots:
|
||||||
virtual void reload() = 0;
|
|
||||||
|
/**
|
||||||
|
* @brief Overload this method to reimplement the fetching mechanism to
|
||||||
|
* populate the RemoteData with data from the server.
|
||||||
|
*
|
||||||
|
* The default implementation makes a GET request to getDataUrl() and parses the resulting JSON,
|
||||||
|
* which should be enough for most cases. Consider overriding getDataUrl() and
|
||||||
|
* canRelaod() if possible. Manual overrides need to make sure that
|
||||||
|
* they're calling setStatus(Status), setError(QNetworkReply::NetworkError) and
|
||||||
|
* setErrorString() to let the QML side know what this thing is up to.
|
||||||
|
*/
|
||||||
|
virtual void reload();
|
||||||
protected:
|
protected:
|
||||||
|
/**
|
||||||
|
* @brief Subclasses should implement this to determine if they can
|
||||||
|
* load data from the server.
|
||||||
|
*
|
||||||
|
* Usage cases include checking if the
|
||||||
|
* required properties, such as the item id are set.
|
||||||
|
*/
|
||||||
|
virtual bool canReload() const = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct the URL to fetch the data from.
|
||||||
|
* @return The URL to load data from.
|
||||||
|
*/
|
||||||
|
virtual QString getDataUrl() const = 0;
|
||||||
|
|
||||||
void setStatus(Status newStatus);
|
void setStatus(Status newStatus);
|
||||||
void setError(QNetworkReply::NetworkError error);
|
void setError(QNetworkReply::NetworkError error);
|
||||||
void setErrorString(const QString &newErrorString);
|
void setErrorString(const QString &newErrorString);
|
||||||
|
@ -139,6 +169,33 @@ private:
|
||||||
QString m_errorString;
|
QString m_errorString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class User : public RemoteData {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
Q_INVOKABLE User(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
Q_PROPERTY(QString userId MEMBER m_userId WRITE setUserId NOTIFY userIdChanged)
|
||||||
|
Q_PROPERTY(QString name MEMBER m_name NOTIFY nameChanged)
|
||||||
|
Q_PROPERTY(QString primaryImageTag MEMBER m_primaryImageTag NOTIFY primaryImageTagChanged)
|
||||||
|
|
||||||
|
void setUserId(const QString &newUserId) {
|
||||||
|
this->m_userId = newUserId;
|
||||||
|
emit userIdChanged(newUserId);
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
signals:
|
||||||
|
void userIdChanged(const QString &newUserId);
|
||||||
|
void nameChanged(const QString &newName);
|
||||||
|
void primaryImageTagChanged(const QString &newPrimaryImageTag);
|
||||||
|
protected:
|
||||||
|
QString getDataUrl() const override;
|
||||||
|
bool canReload() const override;
|
||||||
|
private:
|
||||||
|
QString m_userId;
|
||||||
|
QString m_name;
|
||||||
|
QString m_primaryImageTag;
|
||||||
|
};
|
||||||
|
|
||||||
class MediaStream : public JsonSerializable {
|
class MediaStream : public JsonSerializable {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
|
@ -365,12 +422,12 @@ signals:
|
||||||
void imageBlurHashesChanged();
|
void imageBlurHashesChanged();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
/**
|
|
||||||
* @brief (Re)loads the item from the Jellyfin server.
|
|
||||||
*/
|
|
||||||
void reload() override;
|
|
||||||
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
void onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData);
|
||||||
protected:
|
protected:
|
||||||
|
// Overrides
|
||||||
|
QString getDataUrl() const override;
|
||||||
|
bool canReload() const override;
|
||||||
|
|
||||||
QString m_id;
|
QString m_id;
|
||||||
QString m_name;
|
QString m_name;
|
||||||
QString m_originalTitle;
|
QString m_originalTitle;
|
||||||
|
|
|
@ -198,6 +198,7 @@ RemoteData::RemoteData(QObject *parent) : JsonSerializable (parent) {}
|
||||||
void RemoteData::setStatus(Status newStatus) {
|
void RemoteData::setStatus(Status newStatus) {
|
||||||
m_status = newStatus;
|
m_status = newStatus;
|
||||||
emit statusChanged(newStatus);
|
emit statusChanged(newStatus);
|
||||||
|
if (newStatus == Ready) emit ready();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoteData::setError(QNetworkReply::NetworkError error) {
|
void RemoteData::setError(QNetworkReply::NetworkError error) {
|
||||||
|
@ -216,6 +217,54 @@ void RemoteData::setApiClient(ApiClient *newApiClient) {
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RemoteData::reload() {
|
||||||
|
if (!canReload() || m_apiClient == nullptr) {
|
||||||
|
setStatus(Uninitialised);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setStatus(Loading);
|
||||||
|
}
|
||||||
|
QNetworkReply *rep = m_apiClient->get(getDataUrl());
|
||||||
|
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
||||||
|
rep->deleteLater();
|
||||||
|
|
||||||
|
QJsonParseError error;
|
||||||
|
QString data(rep->readAll());
|
||||||
|
data = data.normalized(QString::NormalizationForm_D);
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &error);
|
||||||
|
if (doc.isNull()) {
|
||||||
|
this->setError(QNetworkReply::ProtocolFailure);
|
||||||
|
this->setErrorString(error.errorString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
this->setError(QNetworkReply::ProtocolFailure);
|
||||||
|
this->setErrorString(tr("Invalid response from the server: root element is not an object."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->deserialize(doc.object());
|
||||||
|
this->setStatus(Ready);
|
||||||
|
});
|
||||||
|
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
||||||
|
this, [this, rep](QNetworkReply::NetworkError error) {
|
||||||
|
this->setError(error);
|
||||||
|
this->setErrorString(rep->errorString());
|
||||||
|
this->setStatus(Error);
|
||||||
|
rep->deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// User
|
||||||
|
User::User(QObject *parent) : RemoteData (parent) {}
|
||||||
|
|
||||||
|
QString User::getDataUrl() const {
|
||||||
|
return QString("/Users/") + m_apiClient->userId();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool User::canReload() const {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// MediaStream
|
// MediaStream
|
||||||
MediaStream::MediaStream(QObject *parent) : JsonSerializable (parent) {}
|
MediaStream::MediaStream(QObject *parent) : JsonSerializable (parent) {}
|
||||||
MediaStream::MediaStream(const MediaStream &other)
|
MediaStream::MediaStream(const MediaStream &other)
|
||||||
|
@ -277,6 +326,13 @@ Item::Item(QObject *parent) : RemoteData(parent) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Item::getDataUrl() const {
|
||||||
|
return QString("/Users/") + m_apiClient->userId() + "/Items/" + m_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Item::canReload() const {
|
||||||
|
return !m_id.isNull();
|
||||||
|
}
|
||||||
|
|
||||||
void Item::setJellyfinId(QString newId) {
|
void Item::setJellyfinId(QString newId) {
|
||||||
m_id = newId.trimmed();
|
m_id = newId.trimmed();
|
||||||
|
@ -286,42 +342,7 @@ void Item::setJellyfinId(QString newId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Item::reload() {
|
|
||||||
if (m_id.isEmpty() || m_apiClient == nullptr) {
|
|
||||||
setStatus(Uninitialised);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
setStatus(Loading);
|
|
||||||
}
|
|
||||||
QNetworkReply *rep = m_apiClient->get("/Users/" + m_apiClient->userId() + "/Items/" + m_id);
|
|
||||||
connect(rep, &QNetworkReply::finished, this, [this, rep]() {
|
|
||||||
rep->deleteLater();
|
|
||||||
|
|
||||||
QJsonParseError error;
|
|
||||||
QString data(rep->readAll());
|
|
||||||
data = data.normalized(QString::NormalizationForm_D);
|
|
||||||
QJsonDocument doc = QJsonDocument::fromJson(data.toUtf8(), &error);
|
|
||||||
if (doc.isNull()) {
|
|
||||||
this->setError(QNetworkReply::ProtocolFailure);
|
|
||||||
this->setErrorString(error.errorString());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!doc.isObject()) {
|
|
||||||
this->setError(QNetworkReply::ProtocolFailure);
|
|
||||||
this->setErrorString(tr("Invalid response from the server: root element is not an object."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this->deserialize(doc.object());
|
|
||||||
this->setStatus(Ready);
|
|
||||||
});
|
|
||||||
connect(rep, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error),
|
|
||||||
this, [this, rep](QNetworkReply::NetworkError error) {
|
|
||||||
rep->deleteLater();
|
|
||||||
this->setError(error);
|
|
||||||
this->setErrorString(rep->errorString());
|
|
||||||
this->setStatus(Error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
void Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> userData) {
|
||||||
if (itemId != m_id || m_userData == nullptr) return;
|
if (itemId != m_id || m_userData == nullptr) return;
|
||||||
|
@ -330,6 +351,7 @@ void Item::onUserDataChanged(const QString &itemId, QSharedPointer<UserData> use
|
||||||
|
|
||||||
void registerSerializableJsonTypes(const char* URI) {
|
void registerSerializableJsonTypes(const char* URI) {
|
||||||
qmlRegisterType<MediaStream>(URI, 1, 0, "MediaStream");
|
qmlRegisterType<MediaStream>(URI, 1, 0, "MediaStream");
|
||||||
|
qmlRegisterType<User>(URI, 1, 0, "User");
|
||||||
qmlRegisterType<UserData>(URI, 1, 0, "UserData");
|
qmlRegisterType<UserData>(URI, 1, 0, "UserData");
|
||||||
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
|
qmlRegisterType<Item>(URI, 1, 0, "JellyfinItem");
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,12 +51,13 @@ Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tell SilicaFlickable the height of its content.
|
// Tell SilicaFlickable the height of its content.
|
||||||
contentHeight: column.height
|
contentHeight: column.visible ? column.height : errorColumn.height
|
||||||
|
|
||||||
// Place our content in a Column. The PageHeader is always placed at the top
|
// Place our content in a Column. The PageHeader is always placed at the top
|
||||||
// of the page, followed by our content.
|
// of the page, followed by our content.
|
||||||
Column {
|
Column {
|
||||||
id: column
|
id: column
|
||||||
|
visible: mediaLibraryModel.status != ApiModel.Error
|
||||||
|
|
||||||
width: page.width
|
width: page.width
|
||||||
//spacing: Theme.paddingLarge
|
//spacing: Theme.paddingLarge
|
||||||
|
@ -123,25 +124,31 @@ Page {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column {
|
}
|
||||||
width: parent.width
|
|
||||||
visible: mediaLibraryModel.status == ApiModel.Error
|
|
||||||
PageHeader {
|
|
||||||
title: qsTr("Network error")
|
|
||||||
//clickable: false
|
|
||||||
}
|
|
||||||
|
|
||||||
PlainLabel {
|
Column {
|
||||||
text: qsTr("An error has occurred. Please try again.")
|
id: errorColumn
|
||||||
}
|
width: parent.width
|
||||||
Item { width: 1; height: Theme.paddingLarge }
|
visible: mediaLibraryModel.status == ApiModel.Error
|
||||||
Button {
|
PageHeader {
|
||||||
text: qsTr("Retry")
|
title: qsTr("Network error")
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
onClicked: loadModels(true)
|
|
||||||
}
|
|
||||||
Item { width: 1; height: Theme.paddingLarge }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: page.height / 3
|
||||||
|
}
|
||||||
|
|
||||||
|
PlainLabel {
|
||||||
|
text: qsTr("An error has occurred. Please try again.")
|
||||||
|
}
|
||||||
|
Item { width: 1; height: Theme.paddingLarge }
|
||||||
|
Button {
|
||||||
|
text: qsTr("Retry")
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
onClicked: loadModels(true)
|
||||||
|
}
|
||||||
|
Item { width: 1; height: Theme.paddingLarge }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,24 +48,54 @@ Page {
|
||||||
text: qsTr("Session")
|
text: qsTr("Session")
|
||||||
}
|
}
|
||||||
|
|
||||||
PlainLabel {
|
Item {
|
||||||
text: qsTr("Server")
|
anchors {
|
||||||
}
|
left: parent.left
|
||||||
|
leftMargin: Theme.horizontalPageMargin
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: Theme.horizontalPageMargin
|
||||||
|
}
|
||||||
|
height: user.implicitHeight + server.implicitHeight + Theme.paddingMedium
|
||||||
|
User {
|
||||||
|
id: loggedInUser
|
||||||
|
apiClient: ApiClient
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
id: userIcon
|
||||||
|
width: height
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
source: ApiClient.baseUrl + "/Users/" + ApiClient.userId + "/Images/Primary?tag=" + loggedInUser.primaryImageTag
|
||||||
|
}
|
||||||
|
|
||||||
PlainLabel {
|
Label {
|
||||||
text: ApiClient.baseUrl
|
id: user
|
||||||
color: Theme.secondaryHighlightColor
|
anchors {
|
||||||
}
|
left: userIcon.right
|
||||||
|
leftMargin: Theme.paddingLarge
|
||||||
|
bottom: parent.verticalCenter
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
text: loggedInUser.status == User.Ready ? loggedInUser.name : ApiClient.userId
|
||||||
|
color: Theme.highlightColor
|
||||||
|
}
|
||||||
|
|
||||||
Item { width: 1; height: Theme.paddingMedium; }
|
Label {
|
||||||
|
id: server
|
||||||
|
anchors {
|
||||||
|
left: userIcon.right
|
||||||
|
leftMargin: Theme.paddingLarge
|
||||||
|
top: parent.verticalCenter
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
text: ApiClient.baseUrl
|
||||||
|
color: Theme.secondaryHighlightColor
|
||||||
|
}
|
||||||
|
|
||||||
PlainLabel {
|
|
||||||
text: qsTr("User id")
|
|
||||||
}
|
|
||||||
|
|
||||||
PlainLabel {
|
|
||||||
text: ApiClient.userId
|
|
||||||
color: Theme.secondaryHighlightColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Item { width: 1; height: Theme.paddingLarge; }
|
Item { width: 1; height: Theme.paddingLarge; }
|
||||||
|
|
|
@ -68,6 +68,31 @@ Page {
|
||||||
running: pageRoot._loading
|
running: pageRoot._loading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SilicaFlickable {
|
||||||
|
anchors.fill: parent
|
||||||
|
contentHeight: errorContent.height
|
||||||
|
visible: jItem.status == JellyfinItem.Error
|
||||||
|
|
||||||
|
PullDownMenu {
|
||||||
|
busy: jItem.status == JellyfinItem.Loading
|
||||||
|
MenuItem {
|
||||||
|
text: qsTr("Retry")
|
||||||
|
onClicked: jItem.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: errorContent
|
||||||
|
width: parent.width
|
||||||
|
|
||||||
|
ViewPlaceholder {
|
||||||
|
enabled: true
|
||||||
|
text: qsTr("An error has occured")
|
||||||
|
hintText: jItem.errorString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JellyfinItem {
|
JellyfinItem {
|
||||||
id: jItem
|
id: jItem
|
||||||
apiClient: ApiClient
|
apiClient: ApiClient
|
||||||
|
|
|
@ -42,6 +42,8 @@ BaseDetailPage {
|
||||||
cellWidth: Constants.libraryDelegateWidth
|
cellWidth: Constants.libraryDelegateWidth
|
||||||
cellHeight: Utils.usePortraitCover(itemData.CollectionType) ? Constants.libraryDelegatePosterHeight
|
cellHeight: Utils.usePortraitCover(itemData.CollectionType) ? Constants.libraryDelegatePosterHeight
|
||||||
: Constants.libraryDelegateHeight
|
: Constants.libraryDelegateHeight
|
||||||
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
|
|
||||||
header: PageHeader {
|
header: PageHeader {
|
||||||
title: itemData.name || qsTr("Loading")
|
title: itemData.name || qsTr("Loading")
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,16 @@ BaseDetailPage {
|
||||||
fields: ["Overview"]
|
fields: ["Overview"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: itemData
|
||||||
|
onReady: episodeModel.reload()
|
||||||
|
}
|
||||||
|
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height
|
contentHeight: content.height
|
||||||
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
|
|
||||||
header: PageHeader {
|
header: PageHeader {
|
||||||
title: itemData.name
|
title: itemData.name
|
||||||
description: itemData.seriesName
|
description: itemData.seriesName
|
||||||
|
|
|
@ -28,6 +28,7 @@ BaseDetailPage {
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height
|
contentHeight: content.height
|
||||||
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: content
|
id: content
|
||||||
|
@ -68,6 +69,10 @@ BaseDetailPage {
|
||||||
show: itemData.jellyfinId
|
show: itemData.jellyfinId
|
||||||
onShowChanged: reload()
|
onShowChanged: reload()
|
||||||
}
|
}
|
||||||
|
Connections {
|
||||||
|
target: itemData
|
||||||
|
onReady: showSeasonsModel.reload()
|
||||||
|
}
|
||||||
|
|
||||||
SilicaListView {
|
SilicaListView {
|
||||||
model: showSeasonsModel
|
model: showSeasonsModel
|
||||||
|
|
|
@ -23,6 +23,7 @@ import Sailfish.Silica 1.0
|
||||||
BaseDetailPage {
|
BaseDetailPage {
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
PageHeader {
|
PageHeader {
|
||||||
title: itemData.name
|
title: itemData.name
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ BaseDetailPage {
|
||||||
SilicaFlickable {
|
SilicaFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
contentHeight: content.height + Theme.paddingLarge
|
contentHeight: content.height + Theme.paddingLarge
|
||||||
|
visible: itemData.status !== JellyfinItem.Error
|
||||||
|
|
||||||
VerticalScrollDecorator {}
|
VerticalScrollDecorator {}
|
||||||
|
|
||||||
|
|
|
@ -62,6 +62,17 @@
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
</context>
|
</context>
|
||||||
|
<context>
|
||||||
|
<name>BaseDetailPage</name>
|
||||||
|
<message>
|
||||||
|
<source>Retry</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
<message>
|
||||||
|
<source>An error has occured</source>
|
||||||
|
<translation type="unfinished"></translation>
|
||||||
|
</message>
|
||||||
|
</context>
|
||||||
<context>
|
<context>
|
||||||
<name>CollectionPage</name>
|
<name>CollectionPage</name>
|
||||||
<message>
|
<message>
|
||||||
|
@ -259,14 +270,6 @@
|
||||||
<source>Session</source>
|
<source>Session</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
</message>
|
</message>
|
||||||
<message>
|
|
||||||
<source>Server</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
|
||||||
<source>User id</source>
|
|
||||||
<translation type="unfinished"></translation>
|
|
||||||
</message>
|
|
||||||
<message>
|
<message>
|
||||||
<source>Log out</source>
|
<source>Log out</source>
|
||||||
<translation type="unfinished"></translation>
|
<translation type="unfinished"></translation>
|
||||||
|
|
Loading…
Reference in a new issue