From c9beb7ba18f03bdfdf8dadc22474f066c87dde60 Mon Sep 17 00:00:00 2001 From: Marius Meisenzahl Date: Wed, 11 Jan 2023 21:06:11 +0100 Subject: [PATCH 1/3] Add ui mock --- src/Dialogs/SettingsSync/GitHub.vala | 113 ++++++++++++++++ .../SettingsSync/SettingsSyncDialog.vala | 123 ++++++++++++++++++ .../SettingsSync/SettingsSyncLoginPage.vala | 76 +++++++++++ .../SettingsSync/SettingsSyncSavePage.vala | 88 +++++++++++++ src/MainView.vala | 16 +++ src/Plug.vala | 1 + src/Widgets/AccountMenuItem.vala | 30 ++++- src/meson.build | 4 + 8 files changed, 446 insertions(+), 5 deletions(-) create mode 100644 src/Dialogs/SettingsSync/GitHub.vala create mode 100644 src/Dialogs/SettingsSync/SettingsSyncDialog.vala create mode 100644 src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala create mode 100644 src/Dialogs/SettingsSync/SettingsSyncSavePage.vala diff --git a/src/Dialogs/SettingsSync/GitHub.vala b/src/Dialogs/SettingsSync/GitHub.vala new file mode 100644 index 000000000..6dbae8da2 --- /dev/null +++ b/src/Dialogs/SettingsSync/GitHub.vala @@ -0,0 +1,113 @@ +/* + * Copyright 2023 elementary, Inc. (https://elementary.io) + * + * 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 3 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, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + */ + +namespace GitHub { + public class DeviceAndUserVerificationCodesResponse : Object { + public string device_code { get; set; } + public int expires_in { get; set; } + public int interval { get; set; } + public string user_code { get; set; } + public string verification_uri { get; set;} + } + + public class UserAuthorizedDeviceResponse : Object { + public string access_token { get; set; } + public string token_type { get; set; } + public string scope { get; set;} + } + + public errordomain Error { + ERROR + } +} + +public class GitHub.Manager { + public async DeviceAndUserVerificationCodesResponse requestDeviceAndUserVerificationCodes () throws GitHub.Error { + Timeout.add (Random.int_range (3000, 10000), () => { + requestDeviceAndUserVerificationCodes.callback (); + return false; + }, Priority.DEFAULT); + + yield; + + var response = new DeviceAndUserVerificationCodesResponse () { + device_code = "asf7ahsf78ahsf7azgsf7ags7f6agsf67asgf7as", + user_code = "B00A-6EB8", + verification_uri = "https://github.com/login/device", + expires_in = 899, + interval = 5 + }; + + return response; + } + + public async UserAuthorizedDeviceResponse requestUserAuthorizedDevice(string device_code) throws GitHub.Error { + var response = new UserAuthorizedDeviceResponse () { + access_token = "asjfiajiufahjs89fahsf79ahf783h78aqfh783a", + token_type = "bearer", + scope = "gist" + }; + + return response; + } + + public async UserAuthorizedDeviceResponse pollUserAuthorizedDevice(string device_code, int expires_in, int interval) throws GitHub.Error { + var seconds_left = expires_in - interval; + + UserAuthorizedDeviceResponse? response = null; + Timeout.add_seconds_full (Priority.DEFAULT, interval, ()=> { + if (seconds_left <= 0 || response != null) { + pollUserAuthorizedDevice.callback (); + return false; + } + + if (seconds_left < 894) { + requestUserAuthorizedDevice.begin (device_code, (obj, res) => { + try { + response = requestUserAuthorizedDevice.end (res); + } catch (Error e) { + warning ("Could not request user authorized device: %s", e.message); + } + }); + } + + seconds_left -= interval; + + return true; + }); + + yield; + + if (response == null) { + throw new GitHub.Error.ERROR ("Could not get response"); + } + + return response; + } + + private static GLib.Once instance; + public static unowned Manager get_default () { + return instance.once (() => { + return new Manager (); + }); + } + + private Manager () {} +} diff --git a/src/Dialogs/SettingsSync/SettingsSyncDialog.vala b/src/Dialogs/SettingsSync/SettingsSyncDialog.vala new file mode 100644 index 000000000..3cb8c9b68 --- /dev/null +++ b/src/Dialogs/SettingsSync/SettingsSyncDialog.vala @@ -0,0 +1,123 @@ +/* + * Copyright 2023 elementary, Inc. (https://elementary.io) + * + * 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 3 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, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + */ + +public class OnlineAccounts.SettingsSyncDialog : Hdy.Window { + private Hdy.Deck deck; + private SettingsSyncLoginPage login_page; + private SettingsSyncSavePage save_page; + private Gtk.Spinner spinner; + + construct { + login_page = new SettingsSyncLoginPage (); + save_page = new SettingsSyncSavePage (); + + var header_label = new Granite.HeaderLabel (_("Synchronize your settings across devices")); + + var explanation_label = new Gtk.Label (_("Settings Sync syncs your settings to a private Git repository hosted on GitHub.")); + explanation_label.set_line_wrap (true); + + spinner = new Gtk.Spinner () { + margin = 12 + }; + + var authenticate_cancel_button = new Gtk.Button.with_label (_("Cancel")); + + var authenticate_button = new Gtk.Button.with_label (_("Authenticate")) { + can_default = true + }; + authenticate_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + var action_area = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL) { + layout_style = Gtk.ButtonBoxStyle.END, + margin_top = 24, + spacing = 6, + valign = Gtk.Align.END, + vexpand = true + }; + action_area.add (authenticate_cancel_button); + action_area.add (authenticate_button); + + var authenticate_page = new Gtk.Grid () { + orientation = Gtk.Orientation.VERTICAL, + margin = 12 + }; + authenticate_page.add (header_label); + authenticate_page.add (explanation_label); + authenticate_page.add (spinner); + authenticate_page.add (action_area); + + deck = new Hdy.Deck () { + can_swipe_back = true, + expand = true + }; + deck.add (authenticate_page); + deck.add (login_page); + deck.add (save_page); + + var window_handle = new Hdy.WindowHandle (); + window_handle.add (deck); + + default_height = 400; + default_width = 300; + window_position = Gtk.WindowPosition.CENTER_ON_PARENT; + modal = true; + type_hint = Gdk.WindowTypeHint.DIALOG; + add (window_handle); + + authenticate_button.has_default = true; + + authenticate_cancel_button.clicked.connect (() => { + destroy (); + }); + + authenticate_button.clicked.connect (() => { + authenticate_button.sensitive = false; + spinner.start (); + + authenticate.begin ((obj, res) => { + try { + authenticate.end (res); + } catch (Error e) { + save_page.show_error (e); + deck.visible_child = save_page; + } + }); + }); + + login_page.cancel.connect (destroy); + + save_page.close.connect (destroy); + } + + private async void authenticate () throws Error { + var github = GitHub.Manager.get_default (); + var device_response = yield github.requestDeviceAndUserVerificationCodes (); + + spinner.stop (); + login_page.update (device_response.user_code, device_response.verification_uri); + deck.visible_child = login_page; + + var token_response = yield github.pollUserAuthorizedDevice (device_response.device_code, device_response.expires_in, device_response.interval); + + // TODO: save token + save_page.show_success (); + deck.visible_child = save_page; + } +} diff --git a/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala new file mode 100644 index 000000000..b570185a1 --- /dev/null +++ b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala @@ -0,0 +1,76 @@ +/* + * Copyright 2023 elementary, Inc. (https://elementary.io) + * + * 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 3 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, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + */ + +public class OnlineAccounts.SettingsSyncLoginPage : Gtk.Grid { + public signal void cancel (); + + private Gtk.Label code_label; + private Gtk.LinkButton link_button; + + construct { + var header_label = new Granite.HeaderLabel (_("Your Code")); + + code_label = new Gtk.Label ("") { + selectable = true + }; + code_label.get_style_context ().add_class ("h3"); + + var spinner = new Gtk.Spinner () { + margin = 12 + }; + spinner.start (); + + var cancel_button = new Gtk.Button.with_label (_("Cancel")); + + link_button = new Gtk.LinkButton.with_label ("", _("Open in browser")); + link_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + var action_area = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL) { + layout_style = Gtk.ButtonBoxStyle.END, + margin_top = 24, + spacing = 6, + valign = Gtk.Align.END, + vexpand = true + }; + action_area.add (cancel_button); + action_area.add (link_button); + + margin = 12; + orientation = Gtk.Orientation.VERTICAL; + row_spacing = 6; + add (header_label); + add (code_label); + add (spinner); + add (action_area); + + cancel_button.clicked.connect (() => { + cancel (); + }); + + link_button.clicked.connect (() => { + link_button.sensitive = false; + }); + } + + public void update (string user_code, string verification_uri) { + code_label.label = user_code; + link_button.uri = verification_uri; + } +} diff --git a/src/Dialogs/SettingsSync/SettingsSyncSavePage.vala b/src/Dialogs/SettingsSync/SettingsSyncSavePage.vala new file mode 100644 index 000000000..53c23a557 --- /dev/null +++ b/src/Dialogs/SettingsSync/SettingsSyncSavePage.vala @@ -0,0 +1,88 @@ +/* + * Copyright 2023 elementary, Inc. (https://elementary.io) + * + * 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 3 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, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + */ + +public class OnlineAccounts.SettingsSyncSavePage : Gtk.Grid { + public signal void close (); + + private Gtk.Button close_button; + + private Gtk.Stack stack; + private Granite.Widgets.AlertView error_alert_view; + + construct { + error_alert_view = new Granite.Widgets.AlertView ( + _("Settings Sync could not be set up."), + "", + "process-error" + ); + error_alert_view.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + error_alert_view.show_all (); + + var success_alert_view = new Granite.Widgets.AlertView ( + _("Success"), + _("Settings Sync has been set up."), + "process-completed" + ); + success_alert_view.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + success_alert_view.show_all (); + + stack = new Gtk.Stack () { + expand = true, + homogeneous = false, + halign = Gtk.Align.CENTER, + valign = Gtk.Align.CENTER + }; + stack.add_named (error_alert_view, "error"); + stack.add_named (success_alert_view, "success"); + + close_button = new Gtk.Button.with_label (_("Close")) { + can_default = true + }; + close_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + var action_area = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL) { + layout_style = Gtk.ButtonBoxStyle.END, + margin_top = 24, + spacing = 6, + valign = Gtk.Align.END, + vexpand = true + }; + action_area.add (close_button); + + margin = 12; + orientation = Gtk.Orientation.VERTICAL; + row_spacing = 6; + add (stack); + add (action_area); + + close_button.clicked.connect (() => { + close (); + }); + } + + public void show_success () { + stack.set_visible_child_name ("success"); + } + + public void show_error (Error error) { + error_alert_view.description = error.message; + stack.set_visible_child_name ("error"); + } +} diff --git a/src/MainView.vala b/src/MainView.vala index 7c78d5131..6bfe92cc5 100644 --- a/src/MainView.vala +++ b/src/MainView.vala @@ -55,6 +55,13 @@ public class OnlineAccounts.MainView : Gtk.Grid { _("Mail") ); + var settings_sync_menuitem = new AccountMenuItem ( + "preferences-system", + _("Settings Sync"), + _("Synchronize your Settings"), + "emblem-synchronized" + ); + var add_acount_grid = new Gtk.Grid () { margin_top = 3, margin_bottom = 3, @@ -62,6 +69,7 @@ public class OnlineAccounts.MainView : Gtk.Grid { }; add_acount_grid.add (caldav_menuitem); add_acount_grid.add (imap_menuitem); + add_acount_grid.add (settings_sync_menuitem); add_acount_grid.show_all (); var add_account_popover = new Gtk.Popover (null); @@ -106,6 +114,14 @@ public class OnlineAccounts.MainView : Gtk.Grid { imap_dialog.show_all (); add_account_popover.popdown (); }); + + settings_sync_menuitem.clicked.connect (() => { + var settings_sync_dialog = new SettingsSyncDialog () { + transient_for = (Gtk.Window) get_toplevel () + }; + settings_sync_dialog.show_all (); + add_account_popover.popdown (); + }); } private Gtk.Widget create_account_row (GLib.Object object) { diff --git a/src/Plug.vala b/src/Plug.vala index d59d769d1..8e92bb6e2 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -62,6 +62,7 @@ public class OnlineAccounts.Plug : Switchboard.Plug { search_results.set ("%s → %s".printf (display_name, _("Calendars")), ""); search_results.set ("%s → %s".printf (display_name, _("IMAP")), ""); search_results.set ("%s → %s".printf (display_name, _("Mail")), ""); + search_results.set ("%s → %s".printf (display_name, _("Settings Sync")), ""); return search_results; } } diff --git a/src/Widgets/AccountMenuItem.vala b/src/Widgets/AccountMenuItem.vala index f76097724..e174fdd28 100644 --- a/src/Widgets/AccountMenuItem.vala +++ b/src/Widgets/AccountMenuItem.vala @@ -21,12 +21,14 @@ public class OnlineAccounts.AccountMenuItem : Gtk.Button { public string icon_name { get; construct; } public string primary_label { get; construct; } public string secondary_label { get; construct; } + public string? badge_icon_name { get; construct; } - public AccountMenuItem (string icon_name, string primary_label, string secondary_label) { + public AccountMenuItem (string icon_name, string primary_label, string secondary_label, string? badge_icon_name = null) { Object ( icon_name: icon_name, primary_label: primary_label, - secondary_label: secondary_label + secondary_label: secondary_label, + badge_icon_name: badge_icon_name ); } @@ -45,12 +47,30 @@ public class OnlineAccounts.AccountMenuItem : Gtk.Button { }; description.get_style_context ().add_class (Granite.STYLE_CLASS_SMALL_LABEL); - var image = new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.DND); - var grid = new Gtk.Grid () { column_spacing = 6 }; - grid.attach (image, 0, 0, 1, 2); + + var image = new Gtk.Image.from_icon_name (icon_name, Gtk.IconSize.DND); + + if (badge_icon_name == null) { + grid.attach (image, 0, 0, 1, 2); + } else { + var badge = new Gtk.Image.from_icon_name (badge_icon_name, Gtk.IconSize.SMALL_TOOLBAR) { + halign = Gtk.Align.END, + valign = Gtk.Align.END, + pixel_size = 16 + }; + + var overlay = new Gtk.Overlay () { + valign = Gtk.Align.START + }; + overlay.add (image); + overlay.add_overlay (badge); + + grid.attach (overlay, 0, 0, 1, 2); + } + grid.attach (label, 1, 0); grid.attach (description, 1, 1); diff --git a/src/meson.build b/src/meson.build index 602c67a23..a9c394f8d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,10 @@ plug_files = files( 'Dialogs/Imap/ImapDialog.vala', 'Dialogs/Imap/ImapSavePage.vala', 'Dialogs/Imap/ImapLoginPage.vala', + 'Dialogs/SettingsSync/SettingsSyncDialog.vala', + 'Dialogs/SettingsSync/SettingsSyncLoginPage.vala', + 'Dialogs/SettingsSync/SettingsSyncSavePage.vala', + 'Dialogs/SettingsSync/GitHub.vala', 'MainView.vala', 'Plug.vala', 'Widgets/AccountMenuItem.vala', From 51a787b2c2672fd5cdf415ad0d7a9d6c72584648 Mon Sep 17 00:00:00 2001 From: Marius Meisenzahl Date: Wed, 11 Jan 2023 21:12:51 +0100 Subject: [PATCH 2/3] Satisfy linter --- src/Dialogs/SettingsSync/GitHub.vala | 14 +++++++------- src/Dialogs/SettingsSync/SettingsSyncDialog.vala | 4 ++-- .../SettingsSync/SettingsSyncLoginPage.vala | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Dialogs/SettingsSync/GitHub.vala b/src/Dialogs/SettingsSync/GitHub.vala index 6dbae8da2..907f31242 100644 --- a/src/Dialogs/SettingsSync/GitHub.vala +++ b/src/Dialogs/SettingsSync/GitHub.vala @@ -39,9 +39,9 @@ namespace GitHub { } public class GitHub.Manager { - public async DeviceAndUserVerificationCodesResponse requestDeviceAndUserVerificationCodes () throws GitHub.Error { + public async DeviceAndUserVerificationCodesResponse request_device_and_user_verification_codes () throws GitHub.Error { Timeout.add (Random.int_range (3000, 10000), () => { - requestDeviceAndUserVerificationCodes.callback (); + request_device_and_user_verification_codes.callback (); return false; }, Priority.DEFAULT); @@ -58,7 +58,7 @@ public class GitHub.Manager { return response; } - public async UserAuthorizedDeviceResponse requestUserAuthorizedDevice(string device_code) throws GitHub.Error { + public async UserAuthorizedDeviceResponse request_user_authorized_device (string device_code) throws GitHub.Error { var response = new UserAuthorizedDeviceResponse () { access_token = "asjfiajiufahjs89fahsf79ahf783h78aqfh783a", token_type = "bearer", @@ -68,20 +68,20 @@ public class GitHub.Manager { return response; } - public async UserAuthorizedDeviceResponse pollUserAuthorizedDevice(string device_code, int expires_in, int interval) throws GitHub.Error { + public async UserAuthorizedDeviceResponse poll_user_authorized_device (string device_code, int expires_in, int interval) throws GitHub.Error { var seconds_left = expires_in - interval; UserAuthorizedDeviceResponse? response = null; Timeout.add_seconds_full (Priority.DEFAULT, interval, ()=> { if (seconds_left <= 0 || response != null) { - pollUserAuthorizedDevice.callback (); + poll_user_authorized_device.callback (); return false; } if (seconds_left < 894) { - requestUserAuthorizedDevice.begin (device_code, (obj, res) => { + request_user_authorized_device.begin (device_code, (obj, res) => { try { - response = requestUserAuthorizedDevice.end (res); + response = request_user_authorized_device.end (res); } catch (Error e) { warning ("Could not request user authorized device: %s", e.message); } diff --git a/src/Dialogs/SettingsSync/SettingsSyncDialog.vala b/src/Dialogs/SettingsSync/SettingsSyncDialog.vala index 3cb8c9b68..22dda4c23 100644 --- a/src/Dialogs/SettingsSync/SettingsSyncDialog.vala +++ b/src/Dialogs/SettingsSync/SettingsSyncDialog.vala @@ -108,13 +108,13 @@ public class OnlineAccounts.SettingsSyncDialog : Hdy.Window { private async void authenticate () throws Error { var github = GitHub.Manager.get_default (); - var device_response = yield github.requestDeviceAndUserVerificationCodes (); + var device_response = yield github.request_device_and_user_verification_codes (); spinner.stop (); login_page.update (device_response.user_code, device_response.verification_uri); deck.visible_child = login_page; - var token_response = yield github.pollUserAuthorizedDevice (device_response.device_code, device_response.expires_in, device_response.interval); + var token_response = yield github.poll_user_authorized_device (device_response.device_code, device_response.expires_in, device_response.interval); // TODO: save token save_page.show_success (); diff --git a/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala index b570185a1..82e50b2f0 100644 --- a/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala +++ b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala @@ -31,7 +31,7 @@ public class OnlineAccounts.SettingsSyncLoginPage : Gtk.Grid { selectable = true }; code_label.get_style_context ().add_class ("h3"); - + var spinner = new Gtk.Spinner () { margin = 12 }; From 163f8e56caffc29171a101ab7983e1887f752ac6 Mon Sep 17 00:00:00 2001 From: Marius Meisenzahl Date: Wed, 11 Jan 2023 21:17:52 +0100 Subject: [PATCH 3/3] Move LinkButton to grid --- src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala index 82e50b2f0..af4e1dcd7 100644 --- a/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala +++ b/src/Dialogs/SettingsSync/SettingsSyncLoginPage.vala @@ -37,10 +37,9 @@ public class OnlineAccounts.SettingsSyncLoginPage : Gtk.Grid { }; spinner.start (); - var cancel_button = new Gtk.Button.with_label (_("Cancel")); - link_button = new Gtk.LinkButton.with_label ("", _("Open in browser")); - link_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + var cancel_button = new Gtk.Button.with_label (_("Cancel")); var action_area = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL) { layout_style = Gtk.ButtonBoxStyle.END, @@ -50,7 +49,6 @@ public class OnlineAccounts.SettingsSyncLoginPage : Gtk.Grid { vexpand = true }; action_area.add (cancel_button); - action_area.add (link_button); margin = 12; orientation = Gtk.Orientation.VERTICAL; @@ -58,15 +56,12 @@ public class OnlineAccounts.SettingsSyncLoginPage : Gtk.Grid { add (header_label); add (code_label); add (spinner); + add (link_button); add (action_area); cancel_button.clicked.connect (() => { cancel (); }); - - link_button.clicked.connect (() => { - link_button.sensitive = false; - }); } public void update (string user_code, string verification_uri) {