2017-08-17 15:02:21 -04:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
|
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
|
|
* version 3 of the License.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "DetailsWidget.h"
|
|
|
|
#include "ui_DetailsWidget.h"
|
|
|
|
|
|
|
|
#include <QDebug>
|
2017-12-13 15:22:31 -05:00
|
|
|
#include <QDir>
|
|
|
|
#include <QDesktopServices>
|
2017-08-17 15:02:21 -04:00
|
|
|
|
|
|
|
#include "core/Config.h"
|
|
|
|
#include "core/FilePath.h"
|
2017-08-30 18:52:05 -04:00
|
|
|
#include "gui/Clipboard.h"
|
2017-12-13 15:22:31 -05:00
|
|
|
#include "entry/EntryAttachmentsModel.h"
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
namespace {
|
|
|
|
constexpr int GeneralTabIndex = 0;
|
|
|
|
}
|
|
|
|
|
2017-08-17 15:02:21 -04:00
|
|
|
DetailsWidget::DetailsWidget(QWidget* parent)
|
|
|
|
: QWidget(parent)
|
|
|
|
, m_ui(new Ui::DetailsWidget())
|
2017-09-27 13:18:13 -04:00
|
|
|
, m_locked(false)
|
2017-08-17 15:02:21 -04:00
|
|
|
, m_currentEntry(nullptr)
|
|
|
|
, m_currentGroup(nullptr)
|
2017-12-25 08:53:22 -05:00
|
|
|
, m_step(0)
|
|
|
|
, m_totpTimer(nullptr)
|
2017-09-29 16:02:07 -04:00
|
|
|
, m_selectedTabEntry(0)
|
|
|
|
, m_selectedTabGroup(0)
|
2017-08-17 15:02:21 -04:00
|
|
|
{
|
|
|
|
m_ui->setupUi(this);
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
// Entry
|
|
|
|
m_ui->entryTotpButton->setIcon(filePath()->icon("actions", "chronometer"));
|
|
|
|
m_ui->entryCloseButton->setIcon(filePath()->icon("actions", "dialog-close"));
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryAttachmentsWidget->setReadOnly(true);
|
|
|
|
m_ui->entryAttachmentsWidget->setButtonsVisible(false);
|
2017-12-13 15:22:31 -05:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpWidget, SLOT(setVisible(bool)));
|
|
|
|
connect(m_ui->entryCloseButton, SIGNAL(toggled(bool)), SLOT(hide()));
|
|
|
|
connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
2017-12-13 15:22:31 -05:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
// Group
|
|
|
|
m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close"));
|
|
|
|
connect(m_ui->groupCloseButton, SIGNAL(toggled(bool)), SLOT(hide()));
|
|
|
|
connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
DetailsWidget::~DetailsWidget()
|
|
|
|
{
|
2017-12-25 08:53:22 -05:00
|
|
|
deleteTotpTimer();
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::setEntry(Entry* selectedEntry)
|
2017-08-17 15:02:21 -04:00
|
|
|
{
|
2017-10-28 14:57:11 -04:00
|
|
|
if (!selectedEntry) {
|
2017-12-25 08:53:22 -05:00
|
|
|
hide();
|
2017-10-28 14:57:11 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-08-17 15:02:21 -04:00
|
|
|
m_currentEntry = selectedEntry;
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
updateEntryHeaderLine();
|
|
|
|
updateEntryTotp();
|
|
|
|
updateEntryGeneralTab();
|
|
|
|
updateEntryNotesTab();
|
|
|
|
updateEntryAttributesTab();
|
|
|
|
updateEntryAttachmentsTab();
|
|
|
|
updateEntryAutotypeTab();
|
|
|
|
|
|
|
|
setVisible(!config()->get("GUI/HideDetailsView").toBool());
|
|
|
|
|
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry);
|
|
|
|
const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry
|
|
|
|
: GeneralTabIndex;
|
|
|
|
Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex));
|
|
|
|
m_ui->entryTabWidget->setCurrentIndex(tabIndex);
|
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::setGroup(Group* selectedGroup)
|
|
|
|
{
|
|
|
|
if (!selectedGroup) {
|
|
|
|
hide();
|
|
|
|
return;
|
2017-09-29 14:37:37 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
m_currentGroup = selectedGroup;
|
|
|
|
updateGroupHeaderLine();
|
|
|
|
updateGroupGeneralTab();
|
|
|
|
updateGroupNotesTab();
|
2017-09-27 13:18:13 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
setVisible(!config()->get("GUI/HideDetailsView").toBool());
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup);
|
|
|
|
const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup
|
|
|
|
: GeneralTabIndex;
|
|
|
|
Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex));
|
|
|
|
m_ui->groupTabWidget->setCurrentIndex(tabIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
void DetailsWidget::setDatabaseMode(DatabaseWidget::Mode mode)
|
|
|
|
{
|
|
|
|
m_locked = mode == DatabaseWidget::LockedMode;
|
|
|
|
if (m_locked) {
|
|
|
|
return;
|
2017-09-28 14:48:49 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
|
|
|
|
if (mode == DatabaseWidget::ViewMode) {
|
|
|
|
if (m_ui->stackedWidget->currentWidget() == m_ui->pageGroup) {
|
|
|
|
setGroup(m_currentGroup);
|
|
|
|
} else {
|
|
|
|
setEntry(m_currentEntry);
|
2017-08-30 18:52:05 -04:00
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
void DetailsWidget::updateEntryHeaderLine()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title());
|
2018-02-24 20:00:31 -05:00
|
|
|
m_ui->entryTitleLabel->setRawText(hierarchy(m_currentEntry->group(), title));
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryIcon->setPixmap(preparePixmap(m_currentEntry->iconPixmap(), 16));
|
|
|
|
}
|
|
|
|
|
|
|
|
void DetailsWidget::updateEntryTotp()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
const bool hasTotp = m_currentEntry->hasTotp();
|
|
|
|
m_ui->entryTotpButton->setVisible(hasTotp);
|
|
|
|
m_ui->entryTotpWidget->hide();
|
|
|
|
m_ui->entryTotpButton->setChecked(false);
|
|
|
|
|
|
|
|
if (hasTotp) {
|
|
|
|
deleteTotpTimer();
|
|
|
|
m_totpTimer = new QTimer(m_currentEntry);
|
|
|
|
connect(m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel()));
|
|
|
|
m_totpTimer->start(1000);
|
|
|
|
|
|
|
|
m_step = m_currentEntry->totpStep();
|
|
|
|
updateTotpLabel();
|
|
|
|
} else {
|
|
|
|
m_ui->entryTotpLabel->clear();
|
|
|
|
stopTotpTimer();
|
|
|
|
}
|
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateEntryGeneralTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
m_ui->entryUsernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username()));
|
2017-08-17 15:02:21 -04:00
|
|
|
|
|
|
|
if (!config()->get("security/hidepassworddetails").toBool()) {
|
2017-12-25 08:53:22 -05:00
|
|
|
const QString password = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password());
|
2017-12-29 03:46:16 -05:00
|
|
|
m_ui->entryPasswordLabel->setRawText(password);
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryPasswordLabel->setToolTip(password);
|
2017-08-17 15:02:21 -04:00
|
|
|
} else {
|
2017-12-29 03:46:16 -05:00
|
|
|
m_ui->entryPasswordLabel->setRawText(QString("\u25cf").repeated(6));
|
|
|
|
m_ui->entryPasswordLabel->setToolTip({});
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-29 03:46:16 -05:00
|
|
|
const QString url = m_currentEntry->webUrl();
|
2017-10-25 00:01:29 -04:00
|
|
|
if (!url.isEmpty()) {
|
|
|
|
// URL is well formed and can be opened in a browser
|
|
|
|
// create a new display url that masks password placeholders
|
|
|
|
// the actual link will use the password
|
2017-12-29 03:46:16 -05:00
|
|
|
m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl());
|
|
|
|
m_ui->entryUrlLabel->setUrl(url);
|
2017-08-17 15:02:21 -04:00
|
|
|
} else {
|
2017-10-25 00:01:29 -04:00
|
|
|
// Fallback to the raw url string
|
2017-12-29 03:46:16 -05:00
|
|
|
m_ui->entryUrlLabel->setRawText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->url()));
|
|
|
|
m_ui->entryUrlLabel->setUrl({});
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
const TimeInfo entryTime = m_currentEntry->timeInfo();
|
|
|
|
const QString expires = entryTime.expires() ? entryTime.expiryTime().toString(Qt::DefaultLocaleShortDate)
|
|
|
|
: tr("Never");
|
|
|
|
m_ui->entryExpirationLabel->setText(expires);
|
|
|
|
}
|
2017-09-27 13:18:13 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateEntryNotesTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
const QString notes = m_currentEntry->notes();
|
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryNotesTab, !notes.isEmpty());
|
2018-05-05 17:58:37 -04:00
|
|
|
m_ui->entryNotesEdit->setText(notes);
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
2017-09-27 13:18:13 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateEntryAttributesTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
m_ui->entryAttributesEdit->clear();
|
|
|
|
const EntryAttributes* attributes = m_currentEntry->attributes();
|
|
|
|
const QStringList customAttributes = attributes->customKeys();
|
|
|
|
const bool haveAttributes = customAttributes.size() > 0;
|
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAttributesTab, haveAttributes);
|
|
|
|
if (haveAttributes) {
|
|
|
|
QString attributesText;
|
2017-09-27 13:18:13 -04:00
|
|
|
for (const QString& key : customAttributes) {
|
|
|
|
QString value = m_currentEntry->attributes()->value(key);
|
|
|
|
if (m_currentEntry->attributes()->isProtected(key)) {
|
2017-10-24 08:08:22 -04:00
|
|
|
value = "<i>" + tr("[PROTECTED]") + "</i>";
|
2017-09-27 13:18:13 -04:00
|
|
|
}
|
|
|
|
attributesText.append(QString("<b>%1</b>: %2<br/>").arg(key, value));
|
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryAttributesEdit->setText(attributesText);
|
2017-09-27 13:18:13 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
2017-09-29 14:37:37 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateEntryAttachmentsTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
2017-12-13 15:22:31 -05:00
|
|
|
const bool hasAttachments = !m_currentEntry->attachments()->isEmpty();
|
2017-12-25 08:53:22 -05:00
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAttachmentsTab, hasAttachments);
|
|
|
|
m_ui->entryAttachmentsWidget->setEntryAttachments(m_currentEntry->attachments());
|
|
|
|
}
|
2017-12-13 15:22:31 -05:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateEntryAutotypeTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentEntry);
|
|
|
|
m_ui->entryAutotypeTree->clear();
|
2017-10-24 08:08:22 -04:00
|
|
|
QList<QTreeWidgetItem*> items;
|
2017-12-25 08:53:22 -05:00
|
|
|
const AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations();
|
|
|
|
const auto associations = autotypeAssociations->getAll();
|
|
|
|
for (const auto& assoc : associations) {
|
|
|
|
const QString sequence = assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence()
|
|
|
|
: assoc.sequence;
|
|
|
|
items.append(new QTreeWidgetItem(m_ui->entryAutotypeTree, {assoc.window, sequence}));
|
2017-09-29 14:37:37 -04:00
|
|
|
}
|
2017-09-29 16:02:07 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryAutotypeTree->addTopLevelItems(items);
|
|
|
|
setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, !items.isEmpty());
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateGroupHeaderLine()
|
2017-08-17 15:02:21 -04:00
|
|
|
{
|
2017-12-25 08:53:22 -05:00
|
|
|
Q_ASSERT(m_currentGroup);
|
2018-02-24 20:00:31 -05:00
|
|
|
m_ui->groupTitleLabel->setRawText(hierarchy(m_currentGroup, {}));
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->groupIcon->setPixmap(preparePixmap(m_currentGroup->iconPixmap(), 32));
|
|
|
|
}
|
2017-09-29 14:37:37 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateGroupGeneralTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentGroup);
|
|
|
|
const QString searchingText = m_currentGroup->resolveSearchingEnabled() ? tr("Enabled") : tr("Disabled");
|
|
|
|
m_ui->groupSearchingLabel->setText(searchingText);
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
const QString autotypeText = m_currentGroup->resolveAutoTypeEnabled() ? tr("Enabled") : tr("Disabled");
|
|
|
|
m_ui->groupAutotypeLabel->setText(autotypeText);
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
const TimeInfo groupTime = m_currentGroup->timeInfo();
|
|
|
|
const QString expiresText = groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate)
|
|
|
|
: tr("Never");
|
|
|
|
m_ui->groupExpirationLabel->setText(expiresText);
|
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateGroupNotesTab()
|
|
|
|
{
|
|
|
|
Q_ASSERT(m_currentGroup);
|
|
|
|
const QString notes = m_currentGroup->notes();
|
|
|
|
setTabEnabled(m_ui->groupTabWidget, m_ui->groupNotesTab, !notes.isEmpty());
|
|
|
|
m_ui->groupNotesEdit->setText(notes);
|
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::stopTotpTimer()
|
|
|
|
{
|
|
|
|
if (m_totpTimer) {
|
|
|
|
m_totpTimer->stop();
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
2017-08-17 15:02:21 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::deleteTotpTimer()
|
|
|
|
{
|
|
|
|
if (m_totpTimer) {
|
|
|
|
delete m_totpTimer;
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
2017-11-11 17:25:20 -05:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateTotpLabel()
|
|
|
|
{
|
|
|
|
if (!m_locked && m_currentEntry) {
|
|
|
|
const QString totpCode = m_currentEntry->totp();
|
|
|
|
const QString firstHalf = totpCode.left(totpCode.size() / 2);
|
|
|
|
const QString secondHalf = totpCode.mid(totpCode.size() / 2);
|
|
|
|
m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf);
|
2017-08-17 15:02:21 -04:00
|
|
|
} else {
|
2017-12-25 08:53:22 -05:00
|
|
|
m_ui->entryTotpLabel->clear();
|
|
|
|
stopTotpTimer();
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
}
|
2017-09-29 16:02:07 -04:00
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::updateTabIndexes()
|
|
|
|
{
|
|
|
|
m_selectedTabEntry = m_ui->entryTabWidget->currentIndex();
|
|
|
|
m_selectedTabGroup = m_ui->groupTabWidget->currentIndex();
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
void DetailsWidget::setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled)
|
2017-08-17 15:02:21 -04:00
|
|
|
{
|
2017-12-25 08:53:22 -05:00
|
|
|
const int tabIndex = tabWidget->indexOf(widget);
|
|
|
|
Q_ASSERT(tabIndex != -1);
|
|
|
|
tabWidget->setTabEnabled(tabIndex, enabled);
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
QPixmap DetailsWidget::preparePixmap(const QPixmap& pixmap, int size)
|
2017-11-11 17:25:20 -05:00
|
|
|
{
|
2017-12-25 08:53:22 -05:00
|
|
|
if (pixmap.width() > size || pixmap.height() > size) {
|
|
|
|
return pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
2017-11-11 17:25:20 -05:00
|
|
|
}
|
2017-12-25 08:53:22 -05:00
|
|
|
return pixmap;
|
2017-08-17 15:02:21 -04:00
|
|
|
}
|
|
|
|
|
2017-12-25 08:53:22 -05:00
|
|
|
QString DetailsWidget::hierarchy(const Group* group, const QString& title)
|
2017-11-11 17:25:20 -05:00
|
|
|
{
|
2017-12-29 03:46:16 -05:00
|
|
|
const QString separator(" / ");
|
2017-12-25 08:53:22 -05:00
|
|
|
QStringList hierarchy = group->hierarchy();
|
|
|
|
hierarchy.removeFirst();
|
|
|
|
hierarchy.append(title);
|
2017-12-29 03:46:16 -05:00
|
|
|
return QString("%1%2").arg(separator, hierarchy.join(separator));
|
2017-10-25 23:29:01 -04:00
|
|
|
}
|