diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7257b07..db0178ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y libgranite-7-dev libgtk-4-dev libpantheon-wayland-1-dev meson valac + apt install -y libaccountsservice-dev libadwaita-1-dev libgranite-7-dev libgtk-4-dev libpantheon-wayland-1-dev meson valac - name: Build run: | meson build diff --git a/data/Application.css b/data/Application.css index 0543b2c9..cbb92291 100644 --- a/data/Application.css +++ b/data/Application.css @@ -14,3 +14,8 @@ .dialog toolbox .bottom button { min-width: 5em; } + +.flat avatar { + border: none; + box-shadow: none; +} diff --git a/data/gresource.xml b/data/gresource.xml index 3baf25c1..aa209733 100644 --- a/data/gresource.xml +++ b/data/gresource.xml @@ -4,6 +4,9 @@ Application.css + icons/account.svg + icons/account.svg + icons/screencast.svg icons/screencast.svg diff --git a/data/icons/account.svg b/data/icons/account.svg new file mode 100644 index 00000000..2cbf827c --- /dev/null +++ b/data/icons/account.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/data/pantheon.portal b/data/pantheon.portal index b9de94e3..dab25b77 100644 --- a/data/pantheon.portal +++ b/data/pantheon.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.pantheon -Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast +Interfaces=org.freedesktop.impl.portal.Access;org.freedesktop.impl.portal.Account;org.freedesktop.impl.portal.AppChooser;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.ScreenCast UseIn=pantheon diff --git a/meson.build b/meson.build index 1fe653ab..83d8b459 100644 --- a/meson.build +++ b/meson.build @@ -10,6 +10,8 @@ localedir = prefix / get_option('localedir') systemd_dep = dependency('systemd') +act_dep = dependency('accountsservice') +adw_dep = dependency('libadwaita-1') glib_dep = dependency('glib-2.0') gobject_dep = dependency('gobject-2.0') gio_dep = dependency('gio-2.0') diff --git a/po/POTFILES b/po/POTFILES index e43f71dc..3691af5a 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -2,6 +2,8 @@ src/XdgDesktopPortalPantheon.vala src/ExternalWindow.vala src/Access/Portal.vala src/Access/Dialog.vala +src/Account/Dialog.vala +src/Account/Portal.vala src/AppChooser/Portal.vala src/AppChooser/Dialog.vala src/AppChooser/AppButton.vala diff --git a/src/Access/Portal.vala b/src/Access/Portal.vala index e000bb93..7a0bd0bc 100644 --- a/src/Access/Portal.vala +++ b/src/Access/Portal.vala @@ -69,10 +69,10 @@ public class Access.Portal : Object { } var _results = new HashTable (str_hash, str_equal); - var _response = 2; + uint32 _response = 2; - dialog.response.connect ((id) => { - switch (id) { + dialog.response.connect ((response) => { + switch (response) { case ALLOW: var choices_builder = new VariantBuilder (new VariantType ("a(ss)")); @@ -81,18 +81,13 @@ public class Access.Portal : Object { }); _results["choices"] = choices_builder.end (); - _response = 0; break; - - case CANCEL: - _response = 1; - break; - - case DELETE_EVENT: - _response = 2; + default: break; } + _response = response.to_id (); + access_dialog.callback (); }); diff --git a/src/Account/Dialog.vala b/src/Account/Dialog.vala new file mode 100644 index 00000000..157f01cd --- /dev/null +++ b/src/Account/Dialog.vala @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrigthText: 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +[DBus (name = "org.freedesktop.impl.portal.Request")] +public class Account.Dialog : PortalDialog { + // The ID used to register this dialog on the DBusConnection + public uint register_id { get; set; default = 0; } + + // The ID of the app sending the request + public string app_id { get; construct; } + + public string user_name; + public string image_filename; + public string real_name; + + private Adw.Avatar avatar; + + public string reason { + set { + secondary_text = _("It provided the following reason, ā€œ%sā€").printf (value); + } + } + + public Dialog (string app_id) { + Object (app_id: app_id); + } + + construct { + default_height = -1; + title = _("An application wants to access your name and photo"); + secondary_icon = new ThemedIcon ("emblem-portal-account"); + secondary_text = _("It did not provide a reason for this request."); + + if (app_id != "") { + var app_info = new DesktopAppInfo (app_id + ".desktop"); + if (app_info != null) { + primary_icon = app_info.get_icon (); + title = _("ā€œ%sā€ wants to access your name and photo").printf (app_info.get_display_name ()); + } + } + + user_name = Environment.get_user_name (); + real_name = Environment.get_real_name (); + + avatar = new Adw.Avatar (32, null, false); + avatar.add_css_class (Granite.STYLE_CLASS_FLAT); + + var username_label = new Gtk.Label (user_name) { + halign = START + }; + username_label.add_css_class (Granite.CssClass.DIM); + username_label.add_css_class (Granite.CssClass.SMALL); + + var name_box = new Granite.Box (VERTICAL, NONE) { + valign = CENTER + }; + name_box.append (new Gtk.Label (real_name)); + name_box.append (username_label); + + var box = new Granite.Box (HORIZONTAL, SINGLE); + box.append (avatar); + box.append (name_box); + + var listitem = new Granite.ListItem () { + child = box, + margin_top = 12, + margin_end = 12, + margin_bottom = 12, + margin_start = 12 + }; + listitem.add_css_class (Granite.CssClass.CARD); + + content = listitem; + + var usermanager = Act.UserManager.get_default (); + if (usermanager.is_loaded) { + on_usermanager_loaded (usermanager); + } else { + usermanager.notify["is-loaded"].connect (() => on_usermanager_loaded (usermanager)); + } + } + + private void on_usermanager_loaded (Act.UserManager usermanager) { + var user = usermanager.get_user (user_name); + if (user.is_loaded) { + on_user_loaded (user); + } else { + user.notify["is-loaded"].connect (() => on_user_loaded (user)); + } + + } + + private void on_user_loaded (Act.User user) { + image_filename = user.get_icon_file (); + try { + avatar.custom_image = Gdk.Texture.from_filename (image_filename); + } catch { + debug ("unable to set avatar"); + } + } + + [DBus (name = "Close")] + public void on_close () throws DBusError, IOError { + response (DELETE_EVENT); + } +} diff --git a/src/Account/Portal.vala b/src/Account/Portal.vala new file mode 100644 index 00000000..45bd2aff --- /dev/null +++ b/src/Account/Portal.vala @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrigthText: 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +[DBus (name = "org.freedesktop.impl.portal.Account")] +public class Account.Portal : Object { + private DBusConnection connection; + + public Portal (DBusConnection connection) { + this.connection = connection; + } + + [DBus (name = "GetUserInformation")] + public async void get_user_information ( + ObjectPath handle, + string app_id, + string parent_window, + HashTable options, + out uint response, + out HashTable results + ) throws DBusError, IOError { + var dialog = new Dialog (app_id) { + parent_handle = parent_window + }; + + if ("reason" in options) { + dialog.reason = options["reason"].get_string (); + } + + try { + dialog.register_id = connection.register_object (handle, dialog); + } catch (Error e) { + critical (e.message); + } + + var _results = new HashTable (str_hash, str_equal); + uint _response = 2; + + dialog.response.connect ((response) => { + switch (response) { + case ALLOW: + _results["id"] = dialog.user_name; + _results["name"] = dialog.real_name; + _results["image"] = Filename.to_uri (dialog.image_filename); + break; + default: + break; + } + + _response = response.to_id (); + + get_user_information.callback (); + }); + + dialog.present (); + yield; + + connection.unregister_object (dialog.register_id); + dialog.destroy (); + + results = _results; + response = _response; + } +} diff --git a/src/PortalDialog.vala b/src/PortalDialog.vala index 9dc7bdc4..bbb811fe 100644 --- a/src/PortalDialog.vala +++ b/src/PortalDialog.vala @@ -54,7 +54,20 @@ public class PortalDialog : Gtk.Window, PantheonWayland.ExtendedBehavior { public enum ResponseType { ALLOW, CANCEL, - DELETE_EVENT + DELETE_EVENT; + + // Convert to response ID expected by portal backends + public uint32 to_id () { + switch (this) { + case ALLOW: + return 0; + case CANCEL: + return 1; + default: + case DELETE_EVENT: + return 2; + } + } } /** diff --git a/src/XdgDesktopPortalPantheon.vala b/src/XdgDesktopPortalPantheon.vala index 1569817e..b83a1ac6 100644 --- a/src/XdgDesktopPortalPantheon.vala +++ b/src/XdgDesktopPortalPantheon.vala @@ -35,6 +35,9 @@ private void on_bus_acquired (DBusConnection connection, string name) { connection.register_object ("/org/freedesktop/portal/desktop", new Access.Portal (connection)); debug ("Access Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new Account.Portal (connection)); + debug ("Account Portal registered!"); + connection.register_object ("/org/freedesktop/portal/desktop", new AppChooser.Portal (connection)); debug ("AppChooser Portal registered!"); diff --git a/src/meson.build b/src/meson.build index 843990a7..82962862 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,6 +3,8 @@ executable( 'Access/Choice.vala', 'Access/Dialog.vala', 'Access/Portal.vala', + 'Account' / 'Dialog.vala', + 'Account' / 'Portal.vala', 'AppChooser/AppButton.vala', 'AppChooser/Dialog.vala', 'AppChooser/Portal.vala', @@ -26,6 +28,8 @@ executable( 'XdgDesktopPortalPantheon.vala', icon_res, dependencies: [ + act_dep, + adw_dep, glib_dep, gobject_dep, gio_dep,