Worked on multi account settings. The app is still not usable!

This commit is contained in:
Scharel Clemens 2018-11-14 22:13:47 +01:00
parent e03d822b94
commit 3f95d6ed1b
9 changed files with 329 additions and 173 deletions

View file

@ -31,7 +31,8 @@ DISTFILES += qml/harbour-nextcloudnotes.qml \
qml/pages/EditPage.qml \ qml/pages/EditPage.qml \
qml/pages/SettingsPage.qml \ qml/pages/SettingsPage.qml \
qml/pages/AboutPage.qml \ qml/pages/AboutPage.qml \
qml/pages/MarkdownPage.qml qml/pages/MarkdownPage.qml \
qml/pages/UnencryptedDialog.qml
SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172 SAILFISHAPP_ICONS = 86x86 108x108 128x128 172x172

View file

@ -10,6 +10,7 @@ CoverBackground {
CoverActionList { CoverActionList {
id: coverAction id: coverAction
enabled: appSettings.currentAccount >= 0
CoverAction { CoverAction {
iconSource: "image://theme/icon-cover-new" iconSource: "image://theme/icon-cover-new"

View file

@ -10,35 +10,50 @@ ApplicationWindow
ConfigurationGroup { ConfigurationGroup {
id: appSettings id: appSettings
path: "/apps/harbour-nextcloudnotes/settings" path: "/apps/harbour-nextcloudnotes/settings"
property var accounts: [ ] // FIXME
property int currentAccount: 0 // FIXME property int currentAccount: value("currentAccount", -1)
// For testing property var accountIDs: value("accountIDs", [])
Component.onCompleted: { //Component.onCompleted: clear()
//appSettings.clear() }
//accounts[0] = { server: "127.0.0.1", username: "fu", password: "bar", lastUpdate: new Date(0) } ConfigurationGroup {
//accounts[1] = { server: "127.0.0.2", username: "fu", password: "bar", lastUpdate: new Date(0) } id: accounts
//accounts[2] = { server: "127.0.0.3", username: "fu", password: "bar", lastUpdate: new Date(0) } path: "/apps/harbour-nextcloudnotes/accounts"
console.log("Configured accounts: " + accounts.length)
for(var i=0; i<accounts.length; i++) { ConfigurationGroup {
console.log("Account " + i + (i === currentAccount ? " (active):" : ":")) id: account
console.log("- Server: " + accounts[i].server) path: appSettings.accountIDs[appSettings.currentAccount]
console.log("- Username: " + accounts[i].username)
console.log("- Password: " + accounts[i].password) property string name
} property url server
if (typeof(accounts[currentAccount]) !== 'undefined') { property string username
notes.account = appSettings.accounts[appSettings.currentAccount] property string password
} property date update
else { property bool unsecureConnection: false
currentAccount = 0 property bool unencryptedConnection: false
notes.account = appSettings.accounts[0]
onPathChanged: {
console.log(scope.path + "/" + path + ": " + name)
console.log(name)
} }
} }
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function add() {
var uuid = uuidv4()
return uuid
}
//Component.onCompleted: clear()
} }
property var notes: NotesApi { NotesApi {
name: "notes" id: notes
//account: appSettings.accounts[appSettings.currentAccount]
saveFile: false
} }
initialPage: Component { NotesPage { } } initialPage: Component { NotesPage { } }

View file

@ -1,74 +1,147 @@
import QtQuick 2.0 import QtQuick 2.0
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
Dialog { Dialog {
id: loginDialog id: loginDialog
property var account property var accountID
ConfigurationGroup {
canAccept: (serverField.acceptableInput && usernameField.text.length > 0 && passwordField.text.length > 0) id: account
onAccepted: { path: "/apps/harbour-nextcloudnotes/accounts/" + accountID
account = {
server: serverField.text,
username: usernameField.text,
password: passwordField.text,
lastUpdate: new Date(0)
}
} }
Column { canAccept: (nameField.text.length > 0 && serverField.acceptableInput && usernameField.text.length > 0 && passwordField.text.length > 0)
width: parent.width onAccepted: {
account.setValue("name", nameField.text)
account.setValue("server", serverField.text)
account.setValue("username", usernameField.text)
account.setValue("password", passwordField.text)
//accounts.itemAt(iAccount).unsecureConnection = unsecureConnectionTextSwitch.checked
//accounts.itemAt(iAccount).unencryptedConnection = unencryptedConnectionTextSwitch.checked
account.setValue("valid", true)
account.sync()
}
DialogHeader { SilicaFlickable {
id: header anchors.fill: parent
//title: qsTr("Nextcloud Login") contentHeight: column.height
acceptText: qsTr("Login")
}
Image { Column {
anchors.horizontalCenter: parent.horizontalCenter id: column
height: Theme.itemSizeHuge
fillMode: Image.PreserveAspectFit
source: "../img/nextcloud-logo-transparent.png"
}
TextField {
id: serverField
focus: true
width: parent.width width: parent.width
text: (typeof(account) !== 'undefined' && account.server.toString().length > 0) ? account.server : "https://"
placeholderText: qsTr("Nextcloud server")
label: placeholderText + " " + qsTr("(starting with \"https://\")")
inputMethodHints: Qt.ImhUrlCharactersOnly
// regExp from https://stackoverflow.com/a/3809435 (EDIT: removed ? after https to force SSL)
validator: RegExpValidator { regExp: /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/ } // TODO disable unencrypted communication
EnterKey.enabled: acceptableInput
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: usernameField.focus = true
}
TextField { DialogHeader {
id: usernameField id: header
width: parent.width //title: qsTr("Nextcloud Login")
text: (typeof(account) !== 'undefined' && account.username.length > 0) ? account.username : "" acceptText: qsTr("Login")
placeholderText: qsTr("Username") }
label: placeholderText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
errorHighlight: text.length === 0 && focus === true
EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: passwordField.focus = true
}
PasswordField { Image {
id: passwordField anchors.horizontalCenter: parent.horizontalCenter
width: parent.width height: Theme.itemSizeHuge
text: (typeof(account) !== 'undefined' && account.password.length > 0) ? account.password : "" fillMode: Image.PreserveAspectFit
label: placeholderText source: "../img/nextcloud-logo-transparent.png"
errorHighlight: text.length === 0 && focus === true }
EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-accept" TextField {
EnterKey.onClicked: loginDialog.accept() id: nameField
focus: true
width: parent.width
text: account.value("name", qsTr("My Nextcloud account"), String)
placeholderText: qsTr("Account name")
label: placeholderText
errorHighlight: text.length === 0// && focus === true
EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: serverField.focus = true
}
TextField {
id: serverField
width: parent.width
text: account.value("server", "https://", String)
placeholderText: qsTr("Nextcloud server")
label: placeholderText + " " + qsTr("(starting with \"https://\")")
inputMethodHints: Qt.ImhUrlCharactersOnly
// regExp from https://stackoverflow.com/a/3809435 (EDIT: removed ? after https to force SSL)
validator: RegExpValidator { regExp: /https:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/ }
errorHighlight: !acceptableInput// && focus === true
EnterKey.enabled: acceptableInput
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: usernameField.focus = true
}
TextField {
id: usernameField
width: parent.width
text: account.value("username", "", String)
placeholderText: qsTr("Username")
label: placeholderText
inputMethodHints: Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
errorHighlight: text.length === 0// && focus === true
EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-next"
EnterKey.onClicked: passwordField.focus = true
}
PasswordField {
id: passwordField
width: parent.width
text: account.value("password", "", String)
label: placeholderText
errorHighlight: text.length === 0// && focus === true
EnterKey.enabled: text.length > 0
EnterKey.iconSource: "image://theme/icon-m-enter-accept"
EnterKey.onClicked: loginDialog.accept()
}
SectionHeader {
text: qsTr("Security")
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2*x
wrapMode: Text.Wrap
color: Theme.secondaryColor
text: qsTr("Please consider creating a dedicated app password! Open your Nextcloud in a browser and go to <i>Settings</i> → <i>Security</i>.")
}
/*TextSwitch {
id: unsecureConnectionTextSwitch
checked: appSettings.unsecureConnection
automaticCheck: true
text: qsTr("Do not check certificates")
description: qsTr("Enable this option to allow selfsigned certificates")
onCheckedChanged: {
if (checked) {
}
else {
unencryptedConnection.checked = false
}
}
}
TextSwitch {
id: unencryptedConnectionTextSwitch
enabled: unsecureConnectionTextSwitch.checked
checked: appSettings.unencryptedConnection
automaticCheck: false
text: qsTr("Allow unencrypted connection")
description: qsTr("")
onClicked: {
if (!checked) {
var dialog = pageStack.push(Qt.resolvedUrl("UnencryptedDialog.qml"))
dialog.accepted.connect(function() {
checked = true
})
dialog.rejected.connect(function() {
checked = false
})
}
else
checked = false
}
}*/
} }
} }
} }

View file

@ -2,12 +2,8 @@ import QtQuick 2.0
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
Item { Item {
property string name
property var account
property var model: ListModel { } property var model: ListModel { }
property string file: StandardPaths.data + "/" + account.name + ".json"
property string json
property string file: StandardPaths.data + "/" + name + ".json"
property bool saveFile: false property bool saveFile: false
property bool busy: false property bool busy: false
//property date lastUpdate: new Date(0) //property date lastUpdate: new Date(0)
@ -92,7 +88,7 @@ Item {
callApi("DELETE", { 'id': id } ) callApi("DELETE", { 'id': id } )
} }
onJsonChanged: refresh() //onJsonChanged: refresh()
function flush() { function flush() {
json = "" json = ""
@ -100,7 +96,7 @@ Item {
filePut.open("PUT", file) filePut.open("PUT", file)
filePut.send(json) filePut.send(json)
model.clear() model.clear()
account.lastUpdate = new Date(0) account.update = new Date(0)
status = 200 status = 200
} }
@ -127,7 +123,7 @@ Item {
function parseJson() { function parseJson() {
var elements = JSON.parse(json) var elements = JSON.parse(json)
if (elements === null) { if (elements === null) {
console.log("Error parsing " + name + "-JSON") console.log("Error parsing " + account.name + "-JSON")
elements = "" elements = ""
json = "" json = ""
return null return null
@ -140,7 +136,7 @@ Item {
/*Component.onCompleted: { /*Component.onCompleted: {
if (saveFile) { if (saveFile) {
if (name === "") { if (account.name === "") {
saveFile = false saveFile = false
} }
else { else {
@ -153,7 +149,7 @@ Item {
update() update()
} }
else { else {
console.log("Loaded " + name + " from local JSON file") console.log("Loaded " + account.name + " from local JSON file")
json = fileReq.responseText json = fileReq.responseText
busy = false busy = false
} }

View file

@ -12,28 +12,32 @@ Page {
PullDownMenu { PullDownMenu {
busy: notes.busy busy: notes.busy
MenuLabel {
visible: appSettings.accounts.length > 0
text: appSettings.accounts.length > 0 ?
(qsTr("Last update") + ": " +
(appSettings.accounts[appSettings.currentAccount].lastUpdate.value === 0 ?
appSettings.accounts[appSettings.currentAccount].lastUpdate.toLocaleString(Qt.locale(), Locale.ShortFormat) :
qsTr("never"))) : ""
}
MenuItem {
text: qsTr("Reload")
visible: appSettings.accounts.length > 0
onClicked: notes.getNotes()
}
MenuItem { MenuItem {
text: qsTr("Settings") text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml")) onClicked: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
} }
MenuItem { MenuItem {
text: qsTr("Add note") text: qsTr("Add note")
enabled: appSettings.accounts.length > 0 enabled: !notes.busy
visible: account.server.length > 0
onClicked: console.log("Add note") onClicked: console.log("Add note")
} }
MenuItem {
text: qsTr("Reload")
enabled: !notes.busy
visible: account.server.length > 0
onClicked: notes.getNotes()
}
MenuLabel {
visible: account.server.length > 0
text: qsTr("Last update") + ": " +
account.update.value !== 0 ?
new Date(account.update).toLocaleString(Qt.locale(), Locale.ShortFormat) :
qsTr("never")
//(new Date(appSettings.value("accountUpdates", [appSettings.currentAccount])).value === 0 ?
//new Date(appSettings.value("accountUpdates", [appSettings.currentAccount])).toLocaleString(Qt.locale(), Locale.ShortFormat) :
//qsTr("never"))
}
} }
header: SearchField { header: SearchField {
@ -43,12 +47,12 @@ Page {
EnterKey.iconSource: "image://theme/icon-m-enter-close" EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: focus = false EnterKey.onClicked: focus = false
enabled: false //notesList.count > 0 // TODO enabled: notesList.count > 0
} }
currentIndex: -1 currentIndex: -1
Component.onCompleted: { Component.onCompleted: {
if (appSettings.accounts.length > 0) { if (account.valid) {
notes.getNotes() notes.getNotes()
} }
} }
@ -141,33 +145,33 @@ Page {
running: visible running: visible
} }
ViewPlaceholder {
id: noLoginPlaceholder
enabled: (appSettings.accounts.length === 0)
text: qsTr("No accounts yet")
}
ViewPlaceholder { ViewPlaceholder {
enabled: notesList.count === 0 && !notes.busy && !noLoginPlaceholder.enabled enabled: notesList.count === 0 && !notes.busy && !noLoginPlaceholder.enabled
text: qsTr("No notes yet") text: qsTr("No notes yet")
hintText: qsTr("Pull down to add a note") hintText: qsTr("Pull down to add a note")
} }
ViewPlaceholder {
id: noLoginPlaceholder
enabled: appSettings.accountIDs.length <= 0
text: qsTr("No account yet")
hintText: qsTr("Got to the settings to add an account")
}
TouchInteractionHint {
id: addAccountHint
Component.onCompleted: if(!account.valid) restart()
interactionMode: TouchInteraction.Pull
direction: TouchInteraction.Down
}
InteractionHintLabel {
anchors.fill: parent
text: qsTr("Open the settings to configure your Nextcloud accounts")
opacity: addAccountHint.running ? 1.0 : 0.0
Behavior on opacity { FadeAnimation {} }
width: parent.width
}
VerticalScrollDecorator { flickable: notesList } VerticalScrollDecorator { flickable: notesList }
} }
TouchInteractionHint {
id: addAccountHint
Component.onCompleted: if (appSettings.accounts.length === 0) restart()
interactionMode: TouchInteraction.Pull
direction: TouchInteraction.Down
}
InteractionHintLabel {
anchors.fill: parent
text: qsTr("Open the settings to add a Nextcloud account")
opacity: addAccountHint.running ? 1.0 : 0.0
Behavior on opacity { FadeAnimation {} }
width: parent.width
}
} }

View file

@ -1,5 +1,6 @@
import QtQuick 2.0 import QtQuick 2.0
import Sailfish.Silica 1.0 import Sailfish.Silica 1.0
import Nemo.Configuration 1.0
Page { Page {
id: page id: page
@ -30,40 +31,51 @@ Page {
} }
Label { Label {
id: noAccountsLabel id: noAccountsLabel
visible: appSettings.accounts.length === 0 visible: appSettings.accountIDs.length <= 0
text: qsTr("No Nextcloud account yet") text: qsTr("No Nextcloud account yet")
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.secondaryHighlightColor color: Theme.secondaryHighlightColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
Repeater { Repeater {
model: appSettings.accounts model: appSettings.accountIDs.length
delegate: ListItem { delegate: ListItem {
id: listItem id: listItem
ConfigurationGroup {
id: account
path: "/apps/harbour-nextcloudnotes/accounts/" + appSettings.accountIDs[index]
}
TextSwitch { TextSwitch {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
automaticCheck: false automaticCheck: false
checked: index === appSettings.currentAccount checked: index === appSettings.currentAccount
text: appSettings.accounts[index].username + "@" + appSettings.accounts[index].server text: account.value("name", qsTr("Account") + " " + (index+1), String)
description: checked ? qsTr("Press and hold to edit") : qsTr("Click to choose as active account") //enabled: account.value("valid", false, Boolean)
onClicked: appSettings.currentAccount = index description: account.value("valid", false, Boolean) ? account.value("username", qsTr("user"), String) + "@" + account.value("server", qsTr("server"), String) : qsTr("Press and hold to configure")
onClicked: if (account.value("valid", false, Boolean)) appSettings.currentAccount = index
onPressAndHold: listItem.openMenu() onPressAndHold: listItem.openMenu()
} }
menu: ContextMenu { menu: ContextMenu {
MenuItem { MenuItem {
text: qsTr("Edit") text: qsTr("Configure")
onClicked: { onClicked: {
var login = pageStack.push(Qt.resolvedUrl("LoginDialog.qml"), { account: appSettings.accounts[index] } ) var login = pageStack.push(Qt.resolvedUrl("LoginDialog.qml"), { accountID: appSettings.accountIDs[index] })
login.accepted.connect(function() { login.accepted.connect(function() {
console.log(login.account.username + ":" + login.account.password + "@" + login.account.server.toString()) update()
appSettings.accounts[index] = login.account })
login.rejected.connect(function() {
}) })
} }
} }
MenuItem { /*MenuItem {
text: qsTr("Delete") text: qsTr("Delete")
visible: false // TODO onClicked: {
} accounts.itemAt(index).clear()
// TODO reorder items
}
}*/
} }
} }
} }
@ -71,23 +83,16 @@ Page {
text: qsTr("Add account") text: qsTr("Add account")
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
onClicked: { onClicked: {
var login = pageStack.push(Qt.resolvedUrl("LoginDialog.qml")) var login = pageStack.push(Qt.resolvedUrl("LoginDialog.qml"), { accountID: accounts.add() })
login.accepted.connect(function() { login.accepted.connect(function() {
var list = appSettings.accounts var tmpIDs = appSettings.accountIDs
list.push(login.account) tmpIDs.push(login.accountID)
appSettings.accounts = list appSettings.accountIDs = tmpIDs
appSettings.sync() })
appSettings.currentAccount = appSettings.accounts.length login.rejected.connect(function() {
appSettings.sync()
notes.account = appSettings.accounts[appSettings.currentAccount]
notes.getNotes()
}) })
} }
} }
SectionHeader {
text: qsTr("Security")
}
} }
VerticalScrollDecorator {} VerticalScrollDecorator {}

View file

@ -0,0 +1,34 @@
import QtQuick 2.0
import Sailfish.Silica 1.0
Dialog {
id: unencryptedDialog
canAccept: textSwitch.checked
SilicaFlickable {
anchors.fill: parent
contentHeight: column.height
Column {
id: column
width: parent.width
DialogHeader {
}
Label {
x: Theme.horizontalPageMargin
width: parent.width - 2*x
wrapMode: Text.Wrap
linkColor: Theme.highlightColor
text: qsTr("<strong>Your username and password will be transferred unencrypted over the network when you enable this option.<br>Do not accept unless you know exactly what you are doing!</strong ><br><a href=\"https://github.com/nextcloud/notes/wiki/API-0.2\">More information...</a>")
}
TextSwitch {
id: textSwitch
text: qsTr("I do understand")
}
}
}
}

View file

@ -48,6 +48,26 @@
<source>Nextcloud server</source> <source>Nextcloud server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Account name</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Security</source>
<translation type="unfinished"></translation>
</message>
<message>
<source></source>
<translation></translation>
</message>
<message>
<source>Please consider creating a dedicated app password! Open your Nextcloud in a browser and go to &lt;i&gt;Settings&lt;/i&gt; → &lt;i&gt;Security&lt;/i&gt;.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>My Nextcloud account</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>MarkdownPage</name> <name>MarkdownPage</name>
@ -105,10 +125,6 @@
<source>Reload</source> <source>Reload</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>No accounts yet</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>never</source> <source>never</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -117,6 +133,14 @@
<source>Open the settings to add a Nextcloud account</source> <source>Open the settings to add a Nextcloud account</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>No account yet</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Got to the settings to add an account</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>SettingsPage</name> <name>SettingsPage</name>
@ -128,36 +152,39 @@
<source>Accounts</source> <source>Accounts</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Add account</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Nextcloud account yet</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>About</source> <source>About</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Press and hold to edit</source> <source>user</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Click to choose as active account</source> <source>server</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Edit</source> <source>Account</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Delete</source> <source>Press and hold to configure</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Security</source> <source>Configure</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>UnencryptedDialog</name>
<message>
<source>I do understand</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&lt;strong&gt;Your username and password will be transferred unencrypted over the network when you enable this option.&lt;br&gt;Do not accept unless you know exactly what you are doing!&lt;/strong &gt;&lt;br&gt;&lt;a href=&quot;https://github.com/nextcloud/notes/wiki/API-0.2&quot;&gt;More information...&lt;/a&gt;</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>