Warning, /plasma-mobile/spacebar/src/contents/ui/ContactsList.qml is written in an unsupported language. File is not indexed.

0001 // SPDX-FileCopyrightText: 2022 Michael Lang <criticaltemp@protonmail.com>
0002 //
0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 
0005 import QtQuick
0006 import QtQuick.Layouts
0007 import QtQuick.Controls as Controls
0008 
0009 import org.kde.kirigami as Kirigami
0010 import org.kde.kirigamiaddons.components as Components
0011 import org.kde.kirigamiaddons.delegates as Delegates
0012 import org.kde.people as KPeople
0013 
0014 import org.kde.spacebar
0015 
0016 ListView {
0017     id: contactsList
0018 
0019     property var selected: []
0020     property bool multiSelect: false
0021     property bool showAll: true
0022     property bool showNumber: false
0023     property bool showSections: false
0024     property string searchText
0025 
0026     signal clicked(var numbers)
0027     signal select(var number)
0028 
0029     Component {
0030         id: numberPopup
0031 
0032         PhoneNumberDialog {}
0033     }
0034 
0035     function formatNumber(number) {
0036         return Utils.phoneNumberToInternationalString(Utils.phoneNumber(number))
0037     }
0038 
0039     function selectNumber(personUri, name) {
0040         const phoneNumbers = Utils.phoneNumbers(personUri)
0041 
0042         if (phoneNumbers.length === 1) {
0043             modifySelection(formatNumber(phoneNumbers[0].normalizedNumber), name)
0044         } else {
0045             const pop = numberPopup.createObject(parent, {
0046                 numbers: phoneNumbers,
0047                 title: i18n("Select a number"),
0048                 selected: selected
0049             })
0050             pop.onNumberSelected.connect(number => modifySelection(formatNumber(number), name))
0051             pop.open()
0052         }
0053     }
0054 
0055     function modifySelection(number, name) {
0056         contactsList.select(number)
0057         const index = selected.findIndex(o => o.phoneNumber == number)
0058         if (index == -1) {
0059             selected.push({name: name, phoneNumber: number})
0060         } else {
0061             selected.splice(index, 1)
0062         }
0063         selected = selected
0064     }
0065 
0066     function isSelected(personUri) {
0067         return Utils.phoneNumbers(personUri).find(number => {
0068             const normalized = formatNumber(number.normalizedNumber)
0069             return selected.findIndex(o => o.phoneNumber == normalized) != -1
0070         }) ? true : false
0071     }
0072 
0073     function alphaToNumeric(text) {
0074         const chars = text.split("")
0075         for (let i = 0; i < chars.length; i++) {
0076             chars[i] = chars[i].toUpperCase()
0077             switch (chars[i]) {
0078                 case "A":
0079                 case "B":
0080                 case "C":
0081                     chars[i] = 2
0082                     break;
0083                 case "D":
0084                 case "E":
0085                 case "F":
0086                     chars[i] = 3
0087                     break;
0088                 case "G":
0089                 case "H":
0090                 case "I":
0091                     chars[i] = 4
0092                     break;
0093                 case "J":
0094                 case "K":
0095                 case "L":
0096                     chars[i] = 5
0097                     break;
0098                 case "M":
0099                 case "N":
0100                 case "O":
0101                     chars[i] = 6
0102                     break;
0103                 case "P":
0104                 case "Q":
0105                 case "R":
0106                 case "S":
0107                     chars[i] = 7
0108                     break;
0109                 case "T":
0110                 case "U":
0111                 case "V":
0112                     chars[i] = 8
0113                     break;
0114                 case "W":
0115                 case "X":
0116                 case "Y":
0117                 case "Z":
0118                     chars[i] = 9
0119                     break;
0120                 default:
0121                     chars[i] = 0
0122             }
0123         }
0124 
0125         return chars.join("")
0126     }
0127 
0128     function quickScroll(index) {
0129         let i
0130         for (i = index; i < az.count; i++) {
0131             const index = contactsProxyModel.match(contactsProxyModel.index(0,0), 0, az.itemAt(i).contentItem.text, 1, Qt.MatchStartsWith)[0]
0132 
0133             if (index) {
0134                 contactsList.positionViewAtIndex(index.row, ListView.Beginning)
0135                 break
0136             }
0137         }
0138         if (i === az.count) {
0139             contactsList.positionViewAtEnd()
0140         }
0141     }
0142 
0143     MouseArea {
0144         anchors.fill: contactsList.contentItem
0145         onPressed: mouse => mouse.accepted = false
0146     }
0147 
0148     pressDelay: Kirigami.Settings.isMobile ? 200 : 0
0149 
0150     headerPositioning: ListView.OverlayHeader
0151     header: Rectangle {
0152         Kirigami.Theme.inherit: false
0153         Kirigami.Theme.colorSet: Kirigami.Theme.View
0154 
0155         width: parent.width
0156         height: headerColumn.height
0157         z: 3
0158         color: Kirigami.Theme.backgroundColor
0159 
0160         ColumnLayout {
0161             id: headerColumn
0162             width: parent.width
0163 
0164             RowLayout {
0165                 visible: multiSelect
0166                 Layout.fillWidth: true
0167                 height: compose.height
0168 
0169                 Kirigami.Heading {
0170                     padding: Kirigami.Units.largeSpacing
0171                     level: 4
0172                     type: Kirigami.Heading.Type.Normal
0173                     text: i18nc("Number of items selected", "%1 Selected", selected.length)
0174                     color: Kirigami.Theme.disabledTextColor
0175                 }
0176 
0177                 Row {
0178                     Layout.fillWidth: true
0179                     layoutDirection: Qt.RightToLeft
0180                     padding: Kirigami.Units.smallSpacing
0181                     Controls.Button {
0182                         id: compose
0183                         text: i18nc("Open chat conversation window", "Next")
0184                         onClicked: contactsList.clicked(selected.map(o => o.phoneNumber))
0185                     }
0186                 }
0187             }
0188 
0189             Controls.Control {
0190                 Layout.fillWidth: true
0191                 padding: Kirigami.Units.largeSpacing
0192                 topPadding: 0
0193 
0194                 contentItem: Kirigami.ActionTextField {
0195                     background: Rectangle {
0196                         anchors.fill: parent
0197                         color: Kirigami.Theme.alternateBackgroundColor
0198                     }
0199 
0200                     id: searchField
0201                     onTextChanged: {
0202                         contactsProxyModel.setFilterFixedString(text)
0203                         searchText = text
0204                     }
0205                     inputMethodHints: Qt.ImhNoPredictiveText
0206                     placeholderText: i18n("Search or enter number…")
0207                     focusSequence: "Ctrl+F"
0208                     rightActions: [
0209                         Kirigami.Action {
0210                             icon.name: "edit-delete-remove"
0211                             visible: searchField.text.length > 0
0212                             onTriggered: {
0213                                 searchField.text = ""
0214                                 searchField.accepted()
0215                             }
0216                         }
0217                     ]
0218                 }
0219             }
0220 
0221             Delegates.RoundedItemDelegate {
0222                 id: delegateItem
0223                 visible: searchField.text.length > 0
0224                 Layout.fillWidth: true
0225                 implicitHeight: Kirigami.Units.iconSizes.medium + Kirigami.Units.largeSpacing * 2
0226                 verticalPadding: 0
0227                 contentItem: RowLayout {
0228                     spacing: Kirigami.Units.largeSpacing
0229 
0230                     Rectangle {
0231                         Layout.preferredWidth: Kirigami.Units.iconSizes.medium
0232                         Layout.preferredHeight: Kirigami.Units.iconSizes.medium
0233                         radius: width / 2
0234                         border.color: Kirigami.Theme.linkColor
0235                         border.width: 2
0236                         color: "transparent"
0237 
0238                         Text {
0239                             anchors.centerIn: parent
0240                             text: "+"
0241                             color: Kirigami.Theme.linkColor
0242                             font.bold: true
0243                             font.pixelSize: parent.height / 1.5
0244                         }
0245                     }
0246 
0247                     ColumnLayout {
0248                         Layout.fillWidth: true
0249                         spacing: 0
0250 
0251                         Controls.Label {
0252                             id: labelItem
0253                             Layout.fillWidth: true
0254                             Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
0255                             text: searchField.text
0256                             elide: Text.ElideRight
0257                             color: Kirigami.Theme.textColor
0258                         }
0259                         Controls.Label {
0260                             id: subtitleItem
0261                             visible: text
0262                             Layout.fillWidth: true
0263                             Layout.alignment: Qt.AlignLeft | Qt.AlignTop
0264                             text: isNaN(searchField.text) ? alphaToNumeric(searchField.text) : ""
0265                             elide: Text.ElideRight
0266                             color: Kirigami.Theme.textColor
0267                             opacity: 0.7
0268                             font: Kirigami.Theme.smallFont
0269                         }
0270                     }
0271 
0272                 }
0273                 onClicked: {
0274                     const text = isNaN(searchField.text) ? alphaToNumeric(searchField.text) : searchField.text
0275                     modifySelection(text, text)
0276                     searchField.text = ""
0277                 }
0278             }
0279         }
0280     }
0281 
0282     section.property: showSections && searchText === "" ? "display" : ""
0283     section.criteria: ViewSection.FirstCharacter
0284     section.delegate: Kirigami.ListSectionHeader {
0285         width: contactsList.width - Kirigami.Units.smallSpacing
0286         text: section.toUpperCase()
0287     }
0288     clip: true
0289     reuseItems: false
0290     currentIndex: -1
0291 
0292     model: KPeople.PersonsSortFilterProxyModel {
0293         id: contactsProxyModel
0294         sourceModel: KPeople.PersonsModel {
0295             id: contactsModel
0296         }
0297         requiredProperties: "phoneNumber"
0298         filterRole: Qt.DisplayRole
0299         sortRole: Qt.DisplayRole
0300         filterCaseSensitivity: Qt.CaseInsensitive
0301         Component.onCompleted: sort(0)
0302     }
0303 
0304     interactive: showAll || searchText.length > 0
0305 
0306     delegate: Delegates.RoundedItemDelegate {
0307         property bool selected: isSelected(model.personUri)
0308 
0309         id: delegateItem
0310         visible: showAll || searchText.length > 0
0311         width: contactsList.width
0312         implicitHeight: Kirigami.Units.iconSizes.medium + Kirigami.Units.largeSpacing * 2
0313         verticalPadding: 0
0314         contentItem: RowLayout {
0315             spacing: Kirigami.Units.largeSpacing
0316 
0317             Components.Avatar {
0318                 Layout.preferredWidth: Kirigami.Units.iconSizes.medium
0319                 Layout.preferredHeight: Kirigami.Units.iconSizes.medium
0320                 source: model.phoneNumber ? "image://avatar/" + model.phoneNumber : ""
0321                 name: model.display
0322                 imageMode: Components.Avatar.ImageMode.AdaptiveImageOrInitals
0323 
0324                 Rectangle {
0325                     anchors.fill: parent
0326                     radius: width * 0.5
0327                     color: Kirigami.Theme.highlightColor
0328                     visible: selected
0329 
0330                     Kirigami.Icon {
0331                         anchors.fill: parent
0332                         source: "checkbox"
0333                         color: Kirigami.Theme.highlightedTextColor
0334                     }
0335                 }
0336             }
0337 
0338             ColumnLayout {
0339                 Layout.fillWidth: true
0340                 spacing: 0
0341 
0342                 Controls.Label {
0343                     id: labelItem
0344                     Layout.fillWidth: true
0345                     Layout.alignment: subtitleItem.visible ? Qt.AlignLeft | Qt.AlignBottom : Qt.AlignLeft | Qt.AlignVCenter
0346                     text: model.display
0347                     elide: Text.ElideRight
0348                     color: Kirigami.Theme.textColor
0349                 }
0350                 Controls.Label {
0351                     id: subtitleItem
0352                     visible: text
0353                     Layout.fillWidth: true
0354                     Layout.alignment: Qt.AlignLeft | Qt.AlignTop
0355                     text: showNumber ? Utils.phoneNumberToInternationalString(Utils.phoneNumber(model.phoneNumber)) : ""
0356                     elide: Text.ElideRight
0357                     color: Kirigami.Theme.textColor
0358                     opacity: 0.7
0359                     font: Kirigami.Theme.smallFont
0360                 }
0361             }
0362 
0363         }
0364         onReleased: selectNumber(model.personUri, model.name)
0365     }
0366 
0367     Kirigami.PlaceholderMessage {
0368         id: noContacts
0369         anchors.centerIn: parent
0370         text: i18n("No contacts with phone numbers yet")
0371         visible: contactsProxyModel.rowCount() === 0
0372         helpfulAction: Kirigami.Action {
0373             text: i18n("Open contacts app")
0374             onTriggered: Utils.launchPhonebook()
0375         }
0376     }
0377 
0378     Kirigami.PlaceholderMessage {
0379         anchors.centerIn: parent
0380         text: i18n("No results found")
0381         visible: contactsList.count === 0 && !noContacts.visible
0382     }
0383 
0384     Rectangle {
0385         visible: showAll && searchText === "" && contactsList.count > 0 && !noContacts.visible
0386         anchors.right: parent.right
0387         anchors.rightMargin: Kirigami.Units.smallSpacing
0388         anchors.verticalCenter: parent.verticalCenter
0389         anchors.verticalCenterOffset: contactsList.headerItem.height / 2
0390         height: contactsList.height - contactsList.headerItem.height - Kirigami.Units.largeSpacing
0391         width: Kirigami.Units.gridUnit * 1.5
0392         color: Kirigami.Theme.backgroundColor
0393         border.width: 1
0394         border.color: Kirigami.Theme.disabledTextColor
0395         radius: width / 2
0396 
0397         ColumnLayout {
0398             spacing: 0
0399             anchors.fill: parent
0400 
0401             Repeater {
0402                 id: az
0403                 model: parent.height < 320 ? [
0404                 "A","C","E","G","I","K","M","O","Q","S","U","W","Z"] : [
0405                 "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
0406 
0407                 Controls.Button {
0408                     Layout.fillWidth: true
0409                     Layout.fillHeight: true
0410                     flat: true
0411                     contentItem: Text {
0412                         text: modelData
0413                         font: Kirigami.Theme.smallFont
0414                         color: Kirigami.Theme.disabledTextColor
0415                         horizontalAlignment: Text.AlignHCenter
0416                         verticalAlignment: Text.AlignVCenter
0417                     }
0418                     onPressed: quickScroll(index)
0419                 }
0420             }
0421         }
0422     }
0423 }