summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHendrik Müller <henne90gen@gmail.com>2022-11-18 20:13:04 +0100
committerNiels De Graef <nielsdegraef@gmail.com>2023-02-11 06:34:02 +0000
commit19f37fbe3e5be87fefe79d1fe6f3c66319acb69e (patch)
treee5bce185478678ea4758c560b26c227043fc524e
parentac886150ac4533035e8451a2035f05ce412ef2cf (diff)
downloadgnome-contacts-19f37fbe3e5be87fefe79d1fe6f3c66319acb69e.tar.gz
contact: Add QR code to share individual contacts
Sharing contacts in an easy and offline way is currently not possible. Most mobile phones have a camera and are capable of scanning QR codes. The vCard format is widely used to easily exchange contact information. A contact can be saved in vCard format into a QR code. A button with a QR code icon is added next to the "favourite" and "edit" buttons. When the user presses this button, a dialog opens up, which shows a QR code containing the current contacts data in vCard format.
-rw-r--r--data/contacts.gresource.xml2
-rw-r--r--data/icons/scalable/actions/qr-code-symbolic.svg2
-rw-r--r--data/ui/contacts-main-window.ui36
-rw-r--r--data/ui/contacts-qr-code-dialog.ui60
-rw-r--r--data/ui/style.css10
-rw-r--r--meson.build1
-rw-r--r--po/POTFILES.in1
-rw-r--r--src/contacts-main-window.vala34
-rw-r--r--src/contacts-qr-code-dialog.vala109
-rw-r--r--src/meson.build2
-rw-r--r--vapi/custom.vapi1
-rw-r--r--vapi/libqrencode.vapi63
12 files changed, 300 insertions, 21 deletions
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 89c8052..3daf6d6 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -11,6 +11,7 @@
<file preprocess="xml-stripblanks">icons/scalable/actions/map-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/note-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/photo-camera-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/scalable/actions/qr-code-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/website-symbolic.svg</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
@@ -22,6 +23,7 @@
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-linked-personas-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-main-window.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-preferences-window.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks">ui/contacts-qr-code-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks">ui/contacts-setup-window.ui</file>
</gresource>
</gresources>
diff --git a/data/icons/scalable/actions/qr-code-symbolic.svg b/data/icons/scalable/actions/qr-code-symbolic.svg
new file mode 100644
index 0000000..8096300
--- /dev/null
+++ b/data/icons/scalable/actions/qr-code-symbolic.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" height="16px" viewBox="0 0 16 16" width="16px"><path d="m 1 1 v 5 h 5 v -5 z m 7 0 v 1 h 1 v -1 z m 0 1 h -1 v 1 h 1 z m 0 1 v 1 h -1 v 2 h 2 v -3 z m 2 -2 v 5 h 5 v -5 z m -7 2 h 1 v 1 h -1 z m 9 0 h 1 v 1 h -1 z m -11 4 v 1 h 1 v -1 z m 1 1 v 1 h 1 v -1 z m 1 0 h 1 v 1 h 2 v -2 h -3 z m 4 -1 v 4 h 1 v -1 h 2 v -3 z m 3 3 v 1 h 1 v -1 z m 1 0 h 1 v -1 h -1 z m 1 -1 h 1 v 1 h 1 v -2 h 1 v -1 h -3 z m 1 1 h -1 v 1 h 1 z m -3 1 h -1 v 1 h 1 z m 0 1 v 1 h 1 v -1 z m -1 0 h -2 v 3 h 1 v -1 h 1 z m 0 2 v 1 h 1 v -1 z m -1 -6 h 1 v 1 h -1 z m -7 2 v 5 h 5 v -5 z m 13 1 v 1 h -2 v 3 h 1 v -1 h 1 v -1 h 1 v -2 z m 0 3 v 1 h 1 v -1 z m -11 -2 h 1 v 1 h -1 z m 0 0" fill="#222222"/></svg>
diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui
index 1b2cd6b..7682ebf 100644
--- a/data/ui/contacts-main-window.ui
+++ b/data/ui/contacts-main-window.ui
@@ -40,6 +40,25 @@
</section>
</menu>
+ <menu id="contact_hamburger_menu_popover">
+ <section>
+ <item>
+ <attribute name="action">win.toggle-favorite</attribute>
+ <attribute name="custom">favorite-toggle</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Share as QR Code</attribute>
+ <attribute name="action">win.show-contact-qr-code</attribute>
+ </item>
+ </section>
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Delete Contact</attribute>
+ <attribute name="action">win.delete-contact</attribute>
+ </item>
+ </section>
+ </menu>
+
<template class="ContactsMainWindow" parent="AdwApplicationWindow">
<property name="default_width">800</property>
<property name="default_height">600</property>
@@ -247,23 +266,18 @@
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
- <object class="GtkToggleButton" id="favorite_button">
- <property name="icon-name">starred-symbolic</property>
- <signal name="toggled" handler="on_favorite_button_toggled"/>
- </object>
- </child>
- <child>
<object class="GtkButton" id="edit_contact_button">
<property name="icon-name">document-edit-symbolic</property>
<property name="action-name">win.edit-contact</property>
<property name="tooltip-text" translatable="yes">Edit Contact</property>
</object>
</child>
- <child>
- <object class="GtkButton" id="delete_contact_button">
- <property name="icon-name">user-trash-symbolic</property>
- <property name="action-name">win.delete-contact</property>
- <property name="tooltip-text" translatable="yes">Delete Contact</property>
+ <child type="end">
+ <object class="GtkMenuButton" id="contact_hamburger_menu_button">
+ <property name="menu-model">contact_hamburger_menu_popover</property>
+ <property name="primary">True</property>
+ <property name="tooltip_text" translatable="yes">More Contact Actions</property>
+ <property name="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
diff --git a/data/ui/contacts-qr-code-dialog.ui b/data/ui/contacts-qr-code-dialog.ui
new file mode 100644
index 0000000..51d4929
--- /dev/null
+++ b/data/ui/contacts-qr-code-dialog.ui
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContactsQrCodeDialog" parent="AdwPreferencesWindow">
+ <property name="modal">True</property>
+ <property name="default-width">400</property>
+ <property name="default-height">600</property>
+ <property name="height-request">600</property>
+ <property name="destroy-with-parent">True</property>
+ <property name="search-enabled">False</property>
+ <property name="can-navigate-back">False</property>
+ <property name="title" translatable="yes">Share Contact</property>
+
+ <child>
+ <object class="AdwPreferencesPage">
+ <property name="margin-top">18</property>
+ <property name="margin-bottom">18</property>
+ <property name="margin-start">18</property>
+ <property name="margin-end">18</property>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkPicture" id="qr_image">
+ <property name="alternative-text">QR Code</property>
+ <property name="halign">center</property>
+ <property name="width-request">300</property>
+ <property name="height-request">300</property>
+ <style>
+ <class name="frame"/>
+ <class name="contacts-qr-code-dialog-qr-image"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel" id="qr_title">
+ <property name="label" translatable="yes">Scan to Save</property>
+ <property name="css-classes">title-1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwPreferencesGroup">
+ <child>
+ <object class="GtkLabel" id="qr_subtitle">
+ <property name="halign">center</property>
+ <property name="wrap">true</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/ui/style.css b/data/ui/style.css
index 63be3c8..f33ee8d 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -88,3 +88,13 @@ flowboxchild.circular {
.contacts-editor-birthday {
margin: 12px;
}
+
+.contacts-qr-code-dialog-qr-image {
+ background: #fff;
+ padding: 12px;
+}
+
+.favorite-button {
+ font-weight: normal;
+}
+
diff --git a/meson.build b/meson.build
index 8ea040f..339e5d2 100644
--- a/meson.build
+++ b/meson.build
@@ -59,6 +59,7 @@ libportal_dep = dependency('libportal-gtk4', version: '>= 0.6')
# Cheese
# cheese_dep = dependency('cheese', required: get_option('cheese'))
# cheese_gtk_dep = dependency('cheese-gtk', version: '>= 3.3.91', required: get_option('cheese'))
+libqrencode_dep = dependency('libqrencode', version: '>=4.1.1')
# gnome-online-accounts
if get_option('goa')
goa_dep = dependency('goa-1.0')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index ead6b22..5a8c2a2 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -11,6 +11,7 @@ data/ui/contacts-linked-personas-dialog.ui
data/ui/contacts-link-suggestion-grid.ui
data/ui/contacts-main-window.ui
data/ui/contacts-preferences-window.ui
+data/ui/contacts-qr-code-dialog.ui
data/ui/contacts-setup-window.ui
src/contacts-accounts-list.vala
src/contacts-app.vala
diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala
index 0d62184..ca1181f 100644
--- a/src/contacts-main-window.vala
+++ b/src/contacts-main-window.vala
@@ -25,10 +25,11 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
{ "new-contact", new_contact },
{ "edit-contact", edit_contact },
{ "stop-editing-contact", stop_editing_contact, "b" },
+ { "toggle-favorite", toggle_favorite },
{ "link-marked-contacts", link_marked_contacts },
{ "delete-marked-contacts", delete_marked_contacts },
{ "export-marked-contacts", export_marked_contacts },
- // { "share-contact", share_contact },
+ { "show-contact-qr-code", show_contact_qr_code },
{ "unlink-contact", unlink_contact },
{ "delete-contact", delete_contact },
{ "sort-on", null, "s", "'surname'", sort_on_changed },
@@ -68,9 +69,11 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
[GtkChild]
private unowned Gtk.MenuButton primary_menu_button;
[GtkChild]
- private unowned Gtk.Box contact_sheet_buttons;
+ private unowned Gtk.MenuButton contact_hamburger_menu_button;
+ private unowned Gtk.PopoverMenu contact_hamburger_popover_menu;
+ private Gtk.Button favorite_button;
[GtkChild]
- private unowned Gtk.ToggleButton favorite_button;
+ private unowned Gtk.Box contact_sheet_buttons;
private bool ignore_favorite_button_toggled;
[GtkChild]
private unowned Gtk.Button add_button;
@@ -135,6 +138,12 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
unowned var sort_key = this.settings.sort_on_surname? "surname" : "firstname";
var sort_action = (SimpleAction) this.lookup_action ("sort-on");
sort_action.set_state (new Variant.string (sort_key));
+
+ contact_hamburger_popover_menu = (Gtk.PopoverMenu) contact_hamburger_menu_button.get_popover ();
+ favorite_button = new Gtk.Button.with_label (_("Mark as Favorite"));
+ favorite_button.set_action_name ("win.toggle-favorite");
+ favorite_button.set_css_classes ({"flat", "favorite-button"});
+ contact_hamburger_popover_menu.add_child (favorite_button, "favorite-toggle");
}
private void restore_window_state () {
@@ -253,8 +262,13 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
this.contact_pane.edit_contact ();
}
- [GtkCallback]
- private void on_favorite_button_toggled (Gtk.ToggleButton button) {
+ private void show_contact_qr_code (GLib.SimpleAction action, GLib.Variant? parameter) {
+ unowned var selected = this.store.get_selected_contact ();
+ var dialog = new QrCodeDialog.for_contact (selected, get_root () as Gtk.Window);
+ dialog.show ();
+ }
+
+ private void toggle_favorite (GLib.SimpleAction action, GLib.Variant? parameter) {
// Don't change the contact being favorite while switching between the two of them
if (this.ignore_favorite_button_toggled)
return;
@@ -263,6 +277,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
return_if_fail (selected != null);
selected.is_favourite = !selected.is_favourite;
+
+ this.state = UiState.NORMAL;
+ this.contact_hamburger_popover_menu.popdown ();
}
[GtkCallback]
@@ -445,13 +462,10 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
// clearing right_header
this.right_header.title_widget = new Adw.WindowTitle ("", "");
if (selected != null) {
- this.ignore_favorite_button_toggled = true;
- this.favorite_button.active = selected.is_favourite;
- this.ignore_favorite_button_toggled = false;
if (selected.is_favourite)
- this.favorite_button.tooltip_text = _("Unmark as favorite");
+ this.favorite_button.set_label (_("Unmark as Favorite"));
else
- this.favorite_button.tooltip_text = _("Mark as favorite");
+ this.favorite_button.set_label (_("Mark as Favorite"));
}
this.state = UiState.SHOWING;
}
diff --git a/src/contacts-qr-code-dialog.vala b/src/contacts-qr-code-dialog.vala
new file mode 100644
index 0000000..02c0539
--- /dev/null
+++ b/src/contacts-qr-code-dialog.vala
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2018 Elias Entrup <elias-git@flump.de>
+ *
+ * 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 of the License, or
+ * (at your option) any later version.
+ *
+ * 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/>.
+ */
+
+using Folks;
+using GLib;
+
+[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-qr-code-dialog.ui")]
+public class Contacts.QrCodeDialog : Adw.PreferencesWindow {
+
+ [GtkChild]
+ private unowned Gtk.Picture qr_image;
+
+ [GtkChild]
+ private unowned Gtk.Label qr_subtitle;
+
+ public QrCodeDialog.for_contact (Individual individual, Gtk.Window? parent = null) {
+ Object (transient_for: parent);
+
+ var subtitle = GLib.Markup.printf_escaped (_("Scan the QR code to save the contact <b>%s</b>."),
+ individual.display_name);
+ this.qr_subtitle.set_markup (subtitle);
+
+ var individuals = new Gee.ArrayList<Individual> ();
+ individuals.add (individual);
+
+ var stringstream = new GLib.MemoryOutputStream.resizable ();
+ var op = new Io.VCardExportOperation (individuals, stringstream);
+ op.execute.begin ((obj, res) => {
+ try {
+ op.execute.end (res);
+ uint8[] chars = {0};
+ stringstream.write (chars);
+ stringstream.close ();
+ } catch (Error e) {
+ warning ("ERROR: %s", e.message);
+ }
+
+ var content = (string) stringstream.steal_data ();
+ int QR_IMAGE_SIZE = 300;
+ var scale = this.qr_image.get_scale_factor ();
+ create_qr_code (content, QR_IMAGE_SIZE * scale);
+ });
+ }
+
+ private void create_qr_code (string content, int size) {
+ if (content == "") {
+ warning ("Failed to create QR code: no content");
+ return;
+ }
+
+ var result = new QRencode.QRcode.encodeString (content,
+ 0,
+ QRencode.EcLevel.M,
+ QRencode.Mode.B8,
+ 1);
+ if (result == null) {
+ warning ("Failed to create QR code: libqrencode error");
+ return;
+ }
+
+ var qr_size = result.width;
+ var pixel_size = (int) double.max (1, size / qr_size);
+ var total_size = qr_size * pixel_size;
+ var BYTES_PER_R8G8B8 = 3;
+ var qr_matrix = new GLib.ByteArray.sized ((uint)(total_size * total_size * pixel_size * BYTES_PER_R8G8B8));
+
+ for (var column = 0; column < total_size; column++) {
+ for (var i = 0; i < pixel_size; i++) {
+ for (var row = 0; row < total_size / pixel_size; row++) {
+ if ((result.data[qr_size*row + column] & 0x01) > 0) {
+ fill_pixel (qr_matrix, 0x00, pixel_size);
+ } else {
+ fill_pixel (qr_matrix, 0xff, pixel_size);
+ }
+ }
+ }
+ }
+
+ var bytes = ByteArray.free_to_bytes (qr_matrix);
+ var paintable = new Gdk.MemoryTexture (total_size, total_size,
+ Gdk.MemoryFormat.R8G8B8,
+ bytes,
+ total_size * BYTES_PER_R8G8B8);
+ this.qr_image.set_paintable (paintable);
+ }
+
+ private void fill_pixel (GLib.ByteArray array, uint8 val, int pixel_size) {
+ for (uint i = 0; i < pixel_size; i++) {
+ array.append ({val}); // R
+ array.append ({val}); // G
+ array.append ({val}); // B
+ }
+ }
+}
+
diff --git a/src/meson.build b/src/meson.build
index 87a9fd0..6f680e8 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -59,6 +59,7 @@ contacts_deps = [
# libedataserverui,
libportal_dep,
math,
+ libqrencode_dep,
]
if get_option('goa')
@@ -94,6 +95,7 @@ contacts_vala_sources = files(
'contacts-link-suggestion-grid.vala',
'contacts-linked-personas-dialog.vala',
'contacts-main-window.vala',
+ 'contacts-qr-code-dialog.vala',
'contacts-preferences-window.vala',
'contacts-settings.vala',
'contacts-setup-window.vala',
diff --git a/vapi/custom.vapi b/vapi/custom.vapi
index 5d1484d..9aee3db 100644
--- a/vapi/custom.vapi
+++ b/vapi/custom.vapi
@@ -9,3 +9,4 @@ namespace Cc {
public Gdk.Pixbuf create_pixbuf ();
}
}
+
diff --git a/vapi/libqrencode.vapi b/vapi/libqrencode.vapi
new file mode 100644
index 0000000..2b8f104
--- /dev/null
+++ b/vapi/libqrencode.vapi
@@ -0,0 +1,63 @@
+/* qrencode.vapi
+ *
+ * Copyright (C) 2015 Ignacio Casal Quinteiro
+ *
+ * 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 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301 USA
+ *
+ * As a special exception, if you use inline functions from this file,
+ * this file does not by itself cause the resulting executable to be
+ * covered by the GNU Lesser General Public License.
+ */
+namespace QRencode {
+ [CCode (cheader_filename = "qrencode.h", cname = "QRcode", unref_function = "QRcode_free")]
+ public class QRcode {
+ [CCode (cname = "QRcode_encodeString")]
+ public QRcode.encodeString(string digits, int version, EcLevel level, Mode hint, int casesensitive);
+
+ public int version;
+ public int width;
+ [CCode (array_length = false)]
+ public uint8[] data;
+ }
+
+ [CCode (cheader_filename = "qrencode.h", cname="QRencLevel")]
+ public enum EcLevel {
+ [CCode (cname="QR_ECLEVEL_L")]
+ L,
+ [CCode (cname="QR_ECLEVEL_M")]
+ M,
+ [CCode (cname="QR_ECLEVEL_Q")]
+ Q,
+ [CCode (cname="QR_ECLEVEL_H")]
+ H
+ }
+
+ [CCode (cheader_filename = "qrencode.h", cname="QRencodeMode")]
+ public enum Mode {
+ [CCode (cname="QR_MODE_NUL")]
+ NUL,
+ [CCode (cname="QR_MODE_NUM")]
+ NUM,
+ [CCode (cname="QR_MODE_AN")]
+ AN,
+ [CCode (cname="QR_MODE_8")]
+ B8,
+ [CCode (cname="QR_MODE_KANJI")]
+ KANJI,
+ [CCode (cname="QR_MODE_STRUCTURE")]
+ STRUCTURE
+ }
+}