Commit 27fb3111 authored by Carl Schwan's avatar Carl Schwan 🚴
Browse files

Add contact applet



Signed-off-by: Carl Schwan's avatarCarl Schwan <carl@carlschwan.eu>
parent 74fdca97
Pipeline #177124 passed with stage
in 7 minutes and 19 seconds
......@@ -6,7 +6,7 @@ Files: logo.png org.kde.kalendar.svg
Copyright: 2021 Áron Kovács <aronkvh@gmail.com>
License: LGPL-2.0-or-later
Files: src/contacts/resources/fallbackBackground.png
Files: src/contacts/resources/fallbackBackground.png src/contacts/applet/package/contents/resources/fallbackBackground.png
Copyright: Jonah Brüchert <jbb.prv@gmx.de>
License: GPL-3.0-or-later
......
This diff is collapsed.
This diff is collapsed.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the license or (at your option) any later version
that is accepted by the membership of KDE e.V. (or its successor
approved by the membership of KDE e.V.), which shall act as a
proxy as defined in Section 6 of 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.
......@@ -33,7 +33,7 @@ ecm_qt_declare_logging_category(incidenceOccurrenceModelTest_SRCS HEADER kalenda
# the tests need the ical resource, which we might not have at this point (e.g. on the CI)
find_program(AKONADI_ICAL_RESOURCE NAMES akonadi_ical_resource)
# if (AKONADI_ICAL_RESOURCE)
if (UNIX)
add_akonadi_isolated_test_advanced(incidenceoccurrencemodeltest.cpp "${incidenceOccurrenceModelTest_SRCS}" "${commonAkonadiTest_LIBS}")
add_akonadi_isolated_test_advanced(todosortfilterproxymodeltest.cpp "${todoSortFilterProxyModelTest_SRCS}" "${commonAkonadiTest_LIBS}")
# endif()
endif()
......@@ -157,7 +157,6 @@ void AgentConfiguration::processInstanceProgressChanged(const Akonadi::AgentInst
Q_EMIT agentProgressChanged(instanceData);
}
KalendarApplication::Mode AgentConfiguration::mode() const
{
return m_mode;
......@@ -171,7 +170,6 @@ void AgentConfiguration::setMode(KalendarApplication::Mode mode)
m_mode = mode;
Q_EMIT modeChanged();
if (m_runningAgents) {
delete m_runningAgents;
m_runningAgents = nullptr;
......
......@@ -3,11 +3,11 @@
#pragma once
#include "kalendarapplication.h"
#include <Akonadi/AgentFilterProxyModel>
#include <Akonadi/AgentInstance>
#include <QObject>
#include <QTimer>
#include "kalendarapplication.h"
class AgentConfiguration : public QObject
{
......
......@@ -55,6 +55,8 @@ if (BUILD_TESTING)
add_subdirectory(autotests)
endif()
add_subdirectory(applet)
ecm_finalize_qml_module(kalendar_contact_plugin
DESTINATION ${KDE_INSTALL_QMLDIR}
BUILD_SHARED_LIBS OFF)
\ No newline at end of file
......@@ -5,6 +5,7 @@
#include "kalendar_contact_debug.h"
#include <Akonadi/ItemMonitor>
#include <KContacts/Addressee>
#include <KContacts/VCardConverter>
#include <KLocalizedString>
#include <QBitArray>
#include <QJSValue>
......@@ -148,3 +149,12 @@ EmailModel *AddresseeWrapper::emailModel() const
{
return m_emailModel;
}
QString AddresseeWrapper::qrCodeData() const
{
KContacts::VCardConverter converter;
KContacts::Addressee addr(m_addressee);
addr.setPhoto(KContacts::Picture());
addr.setLogo(KContacts::Picture());
return QString::fromUtf8(converter.createVCard(addr));
}
......@@ -53,6 +53,10 @@ public:
void setAddressee(const KContacts::Addressee &addressee);
void notifyDataChanged();
// Invokable since we don't want expensive data bindings when any of the
// fields change, instead generate it on demand
Q_INVOKABLE QString qrCodeData() const;
Q_SIGNALS:
void addresseeItemChanged();
void collectionIdChanged();
......
......@@ -2,8 +2,7 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "addressmodel.h"
#include <QDebug>
#include <qabstractitemmodel.h>
#include <QAbstractItemModel>
AddressModel::AddressModel(QObject *parent)
: QAbstractListModel(parent)
......@@ -57,21 +56,23 @@ QVariant AddressModel::data(const QModelIndex &idx, int role) const
QHash<int, QByteArray> AddressModel::roleNames() const
{
return {{CountryRole, QByteArrayLiteral("country")},
{ExtendedRole, QByteArrayLiteral("extended")},
{FormattedAddressRole, QByteArrayLiteral("formattedAddress")},
{HasGeoRole, QByteArrayLiteral("hasGeo")},
{LatitudeRole, QByteArrayLiteral("latitude")},
{LongitudeRole, QByteArrayLiteral("longitude")},
{IdRole, QByteArrayLiteral("id")},
{IsEmptyRole, QByteArrayLiteral("isEmpty")},
{LabelRole, QByteArrayLiteral("label")},
{PostalCodeRole, QByteArrayLiteral("postalCode")},
{PostOfficeBoxRole, QByteArrayLiteral("postOfficeBox")},
{RegionRole, QByteArrayLiteral("region")},
{StreetRole, QByteArrayLiteral("street")},
{TypeRole, QByteArrayLiteral("type")},
{TypeLabelRole, QByteArrayLiteral("typeLabel")}};
return {
{CountryRole, QByteArrayLiteral("country")},
{ExtendedRole, QByteArrayLiteral("extended")},
{FormattedAddressRole, QByteArrayLiteral("formattedAddress")},
{HasGeoRole, QByteArrayLiteral("hasGeo")},
{LatitudeRole, QByteArrayLiteral("latitude")},
{LongitudeRole, QByteArrayLiteral("longitude")},
{IdRole, QByteArrayLiteral("id")},
{IsEmptyRole, QByteArrayLiteral("isEmpty")},
{LabelRole, QByteArrayLiteral("label")},
{PostalCodeRole, QByteArrayLiteral("postalCode")},
{PostOfficeBoxRole, QByteArrayLiteral("postOfficeBox")},
{RegionRole, QByteArrayLiteral("region")},
{StreetRole, QByteArrayLiteral("street")},
{TypeRole, QByteArrayLiteral("type")},
{TypeLabelRole, QByteArrayLiteral("typeLabel")},
};
}
void AddressModel::setAddresses(const KContacts::Address::List &addresses)
......
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
find_package(KF5 ${KF5_MIN_VERSION} OPTIONAL_COMPONENTS Plasma)
if (KF5Plasma_FOUND)
plasma_install_package(package org.kde.kalendar.contact)
endif()
#!/usr/bin/env bash
# SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
# SPDX-License-Identifier: BSD-2-Clause
$XGETTEXT `find . -name '*.js' -o -name '*.qml'` -o $podir/plasma_applet_org.kde.kalendar.contact.pot
<?xml version="1.0" encoding="UTF-8"?>
<kcfg xmlns="http://www.kde.org/standards/kcfg/1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.kde.org/standards/kcfg/1.0
http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" >
<!--
SPDX-FileCopyrightText: 2022 Carl Schwan <carl@carlschwan.eu>
SPDX-License-Identifier: GPL-3.0-or-later
-->
<kcfgfile name=""/>
<group name="General">
<entry name="barcodeType" type="Int">
<default>1</default><!-- Prison::BarcodeType::QRCode -->
</entry>
</group>
</kcfg>
/*
SPDX-FileCopyrightText: 2014-2015 David Rosca <nowrep@gmail.com>
SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
import QtQuick 2.2
import QtQuick.Layouts 1.1
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents3
MouseArea {
readonly property bool inPanel: (plasmoid.location === PlasmaCore.Types.TopEdge
|| plasmoid.location === PlasmaCore.Types.RightEdge
|| plasmoid.location === PlasmaCore.Types.BottomEdge
|| plasmoid.location === PlasmaCore.Types.LeftEdge)
Layout.maximumWidth: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1
Layout.maximumHeight: inPanel ? PlasmaCore.Units.iconSizeHints.panel : -1
acceptedButtons: Qt.LeftButton
onClicked: plasmoid.expanded = !plasmoid.expanded;
hoverEnabled: true
}
/*
* SPDX-FileCopyrightText: 2017-2019 Kaidan Developers and Contributors
* SPDX-FileCopyrightText: 2019 Jonah Brüchert <jbb@kaidan.im>
* SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.3
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.0 as Controls
import org.kde.kirigami 2.14 as Kirigami
import org.kde.kalendar.contact 1.0
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.extras 2.0 as PlasmaExtras
PlasmaComponents3.ItemDelegate {
id: listItem
property string name
property var avatarIcon
contentItem: RowLayout {
Kirigami.Avatar {
id: avatar
Layout.maximumHeight: parent.height
Layout.maximumWidth: parent.height
source: ContactManager.decorationToUrl(avatarIcon)
name: name
}
PlasmaExtras.Heading {
text: name
textFormat: Text.PlainText
elide: Text.ElideRight
maximumLineCount: 1
level: 5
Layout.fillWidth: true
}
}
}
// SPDX-FileCopyrightText: 2022 Carl Schwan <car@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick 2.15
import QtQml 2.15
import QtQuick.Layouts 1.15
import org.kde.kirigami 2.19 as Kirigami
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.plasma.components 2.0 as PlasmaComponents
import org.kde.kalendar.contact 1.0
PlasmaComponents3.ScrollView {
id: root
property int itemId
topPadding: 0
property string title: addressee.name
property AddresseeWrapper addressee: AddresseeWrapper {
addresseeItem: ContactManager.getItem(root.itemId)
}
PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff
function callNumber(number) {
Qt.openUrlExternally("tel:" + number)
}
function sendSms(number) {
Qt.openUrlExternally("sms:" + number)
}
Keys.onPressed: if (event.key === Qt.Key_Escape) {
stack.pop()
event.accepted = true;
}
property var header: PlasmaExtras.PlasmoidHeading {
Component {
id: menuItemComponent
PlasmaComponents.MenuItem { }
}
RowLayout {
width: parent.width
PlasmaComponents3.Button {
icon.name: "go-previous-view"
text: i18n("Return to Contact List")
onClicked: stack.pop()
Layout.fillWidth: true
}
PlasmaComponents3.ToolButton {
id: configureButtonCall
Accessible.name: i18n("Call")
PlasmaComponents3.ToolTip { text: i18n("Call") }
icon.name: "call-start"
visible: addressee.phoneNumbers.length > 0
onClicked: {
const model = addressee.phoneNumbers;
if (model.length === 1) {
page.callNumber(model[0].normalizedNumber);
} else {
model.forEach((item) => {
let menuItem = menuItemComponent.createObject(menuCall, {
text: item.number,
});
menuItem.clicked.connect(() => {
callNumber(item.number);
});
menuCall.addMenuItem(menuItem);
});
menuCall.openRelative();
}
}
PlasmaComponents.ContextMenu {
id: menuCall
visualParent: configureButtonCall
placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup
onStatusChanged: {
if (status === PlasmaComponents.DialogStatus.Closed) {
configureButtonCall.checked = false;
}
}
}
}
PlasmaComponents3.ToolButton {
id: configureButtonSms
Accessible.name: i18n("Send SMS")
PlasmaComponents3.ToolTip { text: i18n("Send SMS") }
icon.name: "mail-message"
visible: addressee.phoneNumbers.length > 0
onClicked: {
const model = addressee.phoneNumbers;
if (addressee.phoneNumbers.length === 1) {
sendSms(model[0].normalizedNumber);
} else {
model.forEach((item) => {
let menuItem = menuItemComponent.createObject(menuSms, {
text: item.number,
});
menuItem.clicked.connect(() => {
sendSms(item.number);
});
menuSms.addMenuItem(menuItem);
});
menuSms.openRelative();
}
}
PlasmaComponents.ContextMenu {
id: menuSms
visualParent: configureButtonSms
placement: PlasmaCore.Types.BottomPosedLeftAlignedPopup
onStatusChanged: {
if (status === PlasmaComponents.DialogStatus.Closed) {
configureButtonSms.checked = false;
}
}
}
}
PlasmaComponents3.ToolButton {
Accessible.name: i18n("Send Email")
PlasmaComponents3.ToolTip { text: i18n("Send Email") }
icon.name: "mail-message"
visible: addressee.preferredEmail.length > 0
onClicked: Qt.openUrlExternally(`mailto:${addressee.preferredEmail}`)
}
PlasmaComponents3.ToolButton {
icon.name: 'view-barcode-qr'
Accessible.name: i18n("Show QR Code")
PlasmaComponents3.ToolTip { text: i18n("Show QR Code") }
onClicked: stack.push(Qt.resolvedUrl('./QrCodePage.qml'), {
qrCodeData: addressee.qrCodeData(),
})
}
}
}
contentItem: Flickable {
contentHeight: layout.implicitHeight
ColumnLayout {
id: layout
width: parent.width
spacing: 0
Header {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 8
source: addressee.photo.isIntern ? addressee.photo.data : addressee.photo.url
backgroundSource: Qt.resolvedUrl("../resources/fallbackBackground.png")
contentItems: [
PlasmaExtras.Heading {
text: addressee.name
color: "#fcfcfc"
level: 2
},
Repeater {
model: addressee.phoneNumbers
PlasmaExtras.Heading {
text: modelData.normalizedNumber
color: "#fcfcfc"
level: 3
}
}
]
}
PlasmaComponents3.Label {
Layout.leftMargin: PlasmaCore.Units.smallSpacing
Layout.rightMargin: PlasmaCore.Units.smallSpacing
Layout.topMargin: PlasmaCore.Units.smallSpacing
visible: text !== i18n('Birthday:') + ' '
text: if (addressee.birthday.getFullYear() === 0) {
return Qt.formatDate(addressee.birthday, i18nc('Day month format', 'dd.MM.'))
} else {
return i18n('Birthday:') + ' ' + addressee.birthday.toLocaleDateString()
}
}
PlasmaExtras.Heading {
Layout.leftMargin: PlasmaCore.Units.smallSpacing
Layout.rightMargin: PlasmaCore.Units.smallSpacing
Layout.topMargin: PlasmaCore.Units.smallSpacing
visible: addressesRepeater.count > 0
text: i18np("Address", "Addresses", addressesRepeater.count)
level: 4
}
Repeater {
id: addressesRepeater
model: addressee.addressesModel
PlasmaComponents3.Label {
Layout.leftMargin: PlasmaCore.Units.smallSpacing
Layout.rightMargin: PlasmaCore.Units.smallSpacing
visible: text.lenght !== 0
text: (model.typeLabel ? i18nc("%1 is the type of the address, e.g. home, work, ...", "%1:", model.typeLabel) : '') + ' ' + model.formattedAddress
}
}
PlasmaExtras.Heading {
Layout.topMargin: PlasmaCore.Units.smallSpacing
Layout.leftMargin: PlasmaCore.Units.smallSpacing
Layout.rightMargin: PlasmaCore.Units.smallSpacing
visible: emailRepeater.count > 0
text: i18np("Email Address", "Email Addresses", emailRepeater.count)
level: 4
}
Repeater {
id: emailRepeater
model: addressee.emailModel
PlasmaComponents3.Label {
visible: text !== ""
text: `${model.type} <a href="mailto:${model.display}">${model.display}</a>`
Layout.leftMargin: PlasmaCore.Units.smallSpacing
Layout.rightMargin: PlasmaCore.Units.smallSpacing
PlasmaComponents3.ToolTip { text: i18n("Send Email") }
onLinkActivated: Qt.openUrlExternally(link)
MouseArea {
id: area
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Qt.openUrlExternally(`mailto:${model.display}`)
onPressed: Qt.openUrlExternally(`mailto:${model.display}`)
}
}
}
}
}
}
// SPDX-FileCopyrightText: 2022 Carl Schwan <car@carlschwan.eu>
// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
import QtQuick 2.15
import QtQml 2.15
import QtQuick.Layouts 1.15
import org.kde.plasma.core 2.0 as PlasmaCore
import org.kde.plasma.extras 2.0 as PlasmaExtras
import org.kde.plasma.components 3.0 as PlasmaComponents3
import org.kde.kalendar.contact 1.0
PlasmaComponents3.ScrollView {
id: scrollView
anchors.fill: parent
property string title: i18n("Contacts")
property var header: PlasmaExtras.PlasmoidHeading {
focus: true
RowLayout {
width: parent.width
PlasmaExtras.SearchField {
id: searchField
Layout.fillWidth: true
onTextChanged: ContactManager.filteredContacts.setFilterFixedString(text)
}
}
}
Keys.onPressed: {
function goToCurrent() {
contactsList.positionViewAtIndex(contactsList.currentIndex, ListView.Contain);
if (contactsList.currentIndex != -1) {
contactsList.currentItem.forceActiveFocus();
}
}
if (event.modifiers & Qt.ControlModifier && event.key == Qt.Key_F) {
toolbar.searchField.forceActiveFocus();
toolbar.searchField.selectAll();
event.accepted = true;
} else if (event.key == Qt.Key_Down) {
contactsList.incrementCurrentIndex();
goToCurrent()
event.accepted = true;
} else if (event.key == Qt.Key_Up) {
if (contactsList.currentIndex == 0) {
contactsList.currentIndex = -1;
searchField.forceActiveFocus();
searchField.selectAll();
} else {
contactsList.decrementCurrentIndex();
goToCurrent();
}
event.accepted = true;
}
}
// HACK: workaround for https://bugreports.qt.io/browse/QTBUG-83890
PlasmaComponents3.ScrollBar.horizontal.policy: PlasmaComponents3.ScrollBar.AlwaysOff
contentWidth: availableWidth - contentItem.leftMargin - contentItem.rightMargin
contentItem: ListView {
id: contactsList
model: ContactManager.filteredContacts
boundsBehavior: Flickable.StopAtBounds
topMargin: PlasmaCore.Units.smallSpacing * 2
bottomMargin: PlasmaCore.Units.smallSpacing * 2
leftMargin: PlasmaCore.Units.smallSpacing * 2
rightMargin: PlasmaCore.Units.smallSpacing * 2
spacing: PlasmaCore.Units.smallSpacing
activeFocusOnTab: true
section.property: "display"
section.criteria: ViewSection.FirstCharacter
section.delegate: PlasmaExtras.Heading {level: 4; text: section}
highlight: PlasmaExtras.Highlight { }
highlightMoveDuration: 0
highlightResizeDuration: 0
focus: true