diff --git a/.gitignore b/.gitignore index 4fffb2f..cebbfcc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +.dir-locals.el diff --git a/simplelogin-gui/.gitignore b/simplelogin-gui/.gitignore new file mode 100644 index 0000000..acba102 --- /dev/null +++ b/simplelogin-gui/.gitignore @@ -0,0 +1 @@ +/res/gschemas.compiled \ No newline at end of file diff --git a/simplelogin-gui/Cargo.toml b/simplelogin-gui/Cargo.toml index d6ad723..396c343 100644 --- a/simplelogin-gui/Cargo.toml +++ b/simplelogin-gui/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +gtk4 = { version = "0.7.3", features = ["blueprint"] } +simplelogin = { version = "0.1.0", path = "../simplelogin" } +tokio = { version = "1.33.0", features = ["full"] } diff --git a/simplelogin-gui/build.rs b/simplelogin-gui/build.rs new file mode 100644 index 0000000..5f3d2d6 --- /dev/null +++ b/simplelogin-gui/build.rs @@ -0,0 +1,29 @@ +use std::{env, io, fs, path::Path, process::Command}; + +const SCHEMA_NAME: &'static str = "im.zander.SimpleLogin.gschema.xml"; + +fn main() -> io::Result<()> { + println!("cargo:rerun-if-changed=data/{}", SCHEMA_NAME); + #[cfg(debug_assertions)] { + let out_dir = env::var("OUT_DIR").unwrap(); + let source_path = Path::new("data/").join(SCHEMA_NAME); + let compile_dir = Path::new(&out_dir).join("schemas"); + let compile_path = compile_dir.join(SCHEMA_NAME); + eprintln!("Creating \"{}\"...", compile_dir.display()); + fs::create_dir_all(&compile_dir)?; + eprintln!( + "Copping \"{}\" to \"{}\"...", + source_path.display(), + compile_path.display() + ); + fs::copy(source_path, &compile_path)?; + eprintln!("Compiling schemas..."); + let status = Command::new("glib-compile-schemas") + .args(["--strict", compile_dir.to_str().unwrap()]) + .status()?; + if !status.success() { + panic!("failed to compile schemas"); + } + } + Ok(()) +} diff --git a/simplelogin-gui/data/im.zander.SimpleLogin.gschema.xml b/simplelogin-gui/data/im.zander.SimpleLogin.gschema.xml new file mode 100644 index 0000000..bf42b08 --- /dev/null +++ b/simplelogin-gui/data/im.zander.SimpleLogin.gschema.xml @@ -0,0 +1,13 @@ + + + + + "" + Api key + The api key for the current user + + + (-1,-1) + + + diff --git a/simplelogin-gui/data/login_window.blp b/simplelogin-gui/data/login_window.blp new file mode 100644 index 0000000..fe14482 --- /dev/null +++ b/simplelogin-gui/data/login_window.blp @@ -0,0 +1,240 @@ +using Gtk 4.0; + +Popover device_entry_popover { + child: Label { + accessible-role: tooltip; + label: "This is the human readable name that will show up in the API key section of the SimpleLogin website."; + max-width-chars: 30; + wrap: true; + } + + ; +} + +template $LoginWindow : ApplicationWindow { + title: "SimpleLogin"; + + Box { + orientation: vertical; + + Overlay forms_overlay { + child: forms_wrapper; + valign: end; + vexpand: true; + + Box forms_wrapper { + valign: end; + vexpand: true; + + Grid { + halign: end; + hexpand: true; + margin-bottom: 10; + margin-end: 10; + margin-start: 10; + margin-top: 10; + valign: center; + + Label { + hexpand: true; + label: "Login:"; + margin-bottom: 5; + vexpand: true; + + styles [ + "login-title-label", + ] + + layout { + column: "0"; + column-span: "2"; + row: "0"; + } + } + + Label error_label { + label: "ERROR"; + margin-top: 5; + + styles [ + "error-message-label", + ] + + layout { + column: "0"; + column-span: "2"; + row: "4"; + } + } + + Entry device_entry { + margin-start: 6; + placeholder-text: "My Awesome Computer"; + secondary-icon-name: "dialog-question-symbolic"; + truncate-multiline: true; + width-chars: 18; + + layout { + column: "1"; + row: "3"; + } + } + + Label { + label: "Device:"; + xalign: 0.0; + + layout { + column: "0"; + row: "3"; + } + } + + Label { + label: "Email:"; + xalign: 0.0; + + layout { + column: "0"; + row: "1"; + } + } + + Label { + label: "Password:"; + xalign: 0.0; + + layout { + column: "0"; + row: "2"; + } + } + + Entry email_entry { + hexpand: true; + input-purpose: email; + margin-start: 6; + receives-default: true; + truncate-multiline: true; + + styles [ + ] + + layout { + column: "1"; + row: "1"; + } + } + + PasswordEntry password_entry { + hexpand: true; + margin-start: 6; + show-peek-icon: true; + + layout { + column: "1"; + row: "2"; + } + } + + Button login_button { + halign: center; + label: "Login"; + margin-top: 5; + sensitive: false; + valign: center; + + layout { + column: "0"; + column-span: "2"; + row: "5"; + } + } + + styles [ + "login-form-warpper", + ] + } + + Label { + halign: center; + justify: center; + label: "- or -"; + } + + Box { + halign: start; + hexpand: true; + margin-bottom: 10; + margin-end: 10; + margin-start: 10; + margin-top: 10; + orientation: vertical; + valign: center; + + Label { + hexpand: true; + label: "Set API Key:"; + margin-bottom: 5; + + styles [ + "login-title-label", + ] + } + + PasswordEntry api_key_entry { + hexpand: true; + margin-end: 3; + margin-start: 8; + show-peek-icon: true; + width-chars: 27; + } + + Button api_key_button { + halign: center; + label: "Accept"; + margin-top: 5; + sensitive: false; + } + } + } + } + + Label { + label: "Don\'t have an account? You can register here."; + margin-bottom: 10; + use-markup: true; + valign: start; + vexpand: true; + yalign: 0.0; + } + } +} + +Box loading_overlay { + orientation: vertical; + + Spinner { + spinning: true; + valign: end; + vexpand: true; + + styles [ + "loading-label", + ] + } + + Label { + label: "Loading..."; + valign: start; + vexpand: true; + + styles [ + "loading-label", + ] + } + + styles [ + "loading-label", + ] +} diff --git a/simplelogin-gui/data/totp_window.blp b/simplelogin-gui/data/totp_window.blp new file mode 100644 index 0000000..a2ca672 --- /dev/null +++ b/simplelogin-gui/data/totp_window.blp @@ -0,0 +1,121 @@ +using Gtk 4.0; + +template $TotpWindow : Window { + modal: true; + title: "TOTP Entry"; + + Overlay overlay { + child: form_wrapper; + halign: center; + hexpand: true; + valign: center; + vexpand: true; + + Grid form_wrapper { + column-spacing: 5; + halign: center; + hexpand: true; + margin-bottom: 10; + margin-end: 10; + margin-start: 10; + margin-top: 10; + row-spacing: 5; + valign: center; + vexpand: true; + + Box { + halign: center; + hexpand: true; + + Image { + halign: center; + icon-name: "dialog-password-symbolic"; + } + + Label { + label: "TOTP"; + + styles [ + "login-title-label", + ] + } + + layout { + column: "0"; + column-span: "2"; + row: "0"; + } + } + + Button accept_button { + label: "Accept"; + + layout { + column: "1"; + row: "3"; + } + } + + Button cancel_button { + label: "Cancel"; + + layout { + column: "0"; + row: "3"; + } + } + + Entry code_entry { + placeholder-text: "TOTP Code"; + + layout { + column: "0"; + column-span: "2"; + row: "1"; + } + } + + Label error_label { + label: "ERROR"; + + styles [ + "error-message-label", + ] + + layout { + column: "0"; + column-span: "2"; + row: "2"; + } + } + } + } +} + +Box loading_overlay { + orientation: vertical; + + Spinner { + spinning: true; + valign: end; + vexpand: true; + + styles [ + "loading-label", + ] + } + + Label { + label: "Loading..."; + valign: start; + vexpand: true; + + styles [ + "loading-label", + ] + } + + styles [ + "loading-label", + ] +} diff --git a/simplelogin-gui/res.old/global.css b/simplelogin-gui/res.old/global.css new file mode 100644 index 0000000..f5c5b89 --- /dev/null +++ b/simplelogin-gui/res.old/global.css @@ -0,0 +1,15 @@ +.login-title-label { + font-size: x-large; +} + +.error-message-label { + color: red; +} + +.loading-label { + font-size: xx-large; +} + +.bold { + font-weight: bold; +} \ No newline at end of file diff --git a/simplelogin-gui/res.old/main-panel.ui b/simplelogin-gui/res.old/main-panel.ui new file mode 100644 index 0000000..873787e --- /dev/null +++ b/simplelogin-gui/res.old/main-panel.ui @@ -0,0 +1,188 @@ + + + + + + + + vertical + + + True + end + True + + + + + + Loading... + start + True + + + + + + + True + No more results + True + + + + True + vertical + + + center + end + + + center + view-refresh-symbolic + end + True + + + + + + center + window-close-symbolic + end + + + + + + + + ERROR + start + True + + + + + + diff --git a/simplelogin-gui/res.old/settings-panel.ui b/simplelogin-gui/res.old/settings-panel.ui new file mode 100644 index 0000000..3996bee --- /dev/null +++ b/simplelogin-gui/res.old/settings-panel.ui @@ -0,0 +1,263 @@ + + + + + + + True + vertical + + + True + end + True + + + + + + Loading... + start + True + + + + + + + True + vertical + + + center + end + + + center + view-refresh-symbolic + end + True + + + + + + center + window-close-symbolic + end + + + + + + + + ERROR + start + True + + + + + diff --git a/simplelogin-gui/res.old/simplelogin.cmb b/simplelogin-gui/res.old/simplelogin.cmb new file mode 100644 index 0000000..c4deb09 --- /dev/null +++ b/simplelogin-gui/res.old/simplelogin.cmb @@ -0,0 +1,531 @@ + + + + + (1,1,"main-window.ui","main-panel.ui",None,None,None,None,None,None,""), + (3,18,None,"settings-panel.ui",None,None,None,None,None,None,None), + (4,13,None,"login-window.ui",None,None,None,None,None,None,None), + (5,1,None,"totp-dialog.ui",None,None,None,None,None,None,None) + + + (1,"global.css",None,1) + + + (1,1), + (1,3), + (1,4), + (1,5) + + + (1,1,"GtkBox","SLMainPanel",None,None,None,None,None,None), + (1,2,"GtkOverlay","overlay",1,None,None,None,None,None), + (1,3,"GtkBox","content_wrapper",2,None,None,None,None,None), + (1,4,"GtkBox",None,3,None,None,None,None,None), + (1,5,"GtkSearchEntry","search_box",4,None,None,None,None,None), + (1,8,"GtkDropDown","view_dropdown",4,None,None,None,1,None), + (1,9,"GtkStringList",None,8,None,None,None,None,None), + (1,10,"GtkButton","search_button",4,None,None,None,2,None), + (1,11,"GtkBox",None,3,None,None,None,1,None), + (1,12,"GtkButton","new_button",11,None,None,None,None,None), + (1,13,"GtkButton","edit_button",11,None,None,None,1,None), + (1,14,"GtkButton","delete_button",11,None,None,None,2,None), + (1,22,"GtkBox","loading_overlay",None,None,None,None,None,None), + (1,23,"GtkSpinner",None,22,None,None,None,None,None), + (1,24,"GtkLabel",None,22,None,None,None,1,None), + (1,28,"GtkButton","prev_page_button",11,None,None,None,3,None), + (1,29,"GtkButton","next_page_button",11,None,None,None,5,None), + (1,30,"GtkEntry","page_label",11,None,None,None,4,None), + (1,31,"GtkOverlay","scroll_overlay",3,None,None,None,2,None), + (1,32,"GtkScrolledWindow",None,31,None,None,None,None,None), + (1,33,"GtkListView","alias_list_view",32,None,None,None,None,None), + (1,34,"GtkSignalListItemFactory","alias_item_factory",33,None,None,None,None,None), + (1,35,"GtkMultiSelection","alias_selection_model",33,None,None,None,None,None), + (1,36,"GtkLabel","no_content_label",None,None,None,None,None,None), + (1,37,"GtkBox","error_overlay",None,None,None,None,None,None), + (1,38,"GtkBox",None,37,None,None,None,None,None), + (1,39,"GtkButton","error_refresh_button",38,None,None,None,None,None), + (1,40,"GtkButton","error_close_button",38,None,None,None,1,None), + (1,41,"GtkLabel","error_label",37,None,None,None,1,None), + (3,18,"GtkBox","SLSettingsPanel",None,None,None,None,None,None), + (3,19,"GtkOverlay","overlay",18,None,None,None,None,None), + (3,30,"GtkBox","loading_overlay",None,None,None,None,None,None), + (3,31,"GtkSpinner",None,30,None,None,None,None,None), + (3,32,"GtkLabel",None,30,None,None,None,1,None), + (3,33,"GtkBox","error_overlay",None,None,None,None,None,None), + (3,35,"GtkLabel","error_label",33,None,None,None,1,None), + (3,45,"GtkBox",None,33,None,None,None,None,None), + (3,46,"GtkButton","error_refresh_button",45,None,None,None,None,None), + (3,47,"GtkButton","error_close_button",45,None,None,None,1,None), + (3,48,"GtkBox","content_wrapper",19,None,None,None,-1,None), + (3,49,"GtkGrid","content",48,None,None,None,None,None), + (3,51,"GtkLabel","stats_label",49,None,None,None,1,None), + (3,52,"GtkLabel","email_label",49,None,None,None,2,None), + (3,53,"GtkButton","logout_button",49,None,None,None,3,None), + (3,55,"GtkLabel",None,49,None,None,None,5,None), + (3,57,"GtkLabel",None,49,None,None,None,7,None), + (3,58,"GtkPasswordEntry","api_key_field",49,None,None,None,8,None), + (3,59,"GtkLabel","status_label",49,None,None,None,9,None), + (3,60,"GtkButton","api_key_clear_button",49,None,None,None,10,None), + (3,63,"GtkButton","profile_image_button",49,None,None,None,None,""), + (3,64,"GtkImage","profile_image",63,None,None,None,None,None), + (3,65,"GtkBox",None,49,None,None,None,None,None), + (3,66,"GtkButton","remove_picture_button",65,None,None,None,None,None), + (3,67,"GtkEntry","name_label",65,None,None,None,1,None), + (3,68,"GtkBox",None,49,None,None,None,None,None), + (3,69,"GtkButton","edit_button",68,None,None,None,None,None), + (3,70,"GtkButton","cancel_edit_button",68,None,None,None,1,None), + (4,11,"GtkPopover","device_entry_popover",None,None,None,None,None,None), + (4,12,"GtkLabel",None,11,None,None,None,None,None), + (4,13,"GtkApplicationWindow","LoginWindow",None,None,None,None,None,None), + (4,14,"GtkBox",None,13,None,None,None,-1,None), + (4,30,"GtkLabel",None,14,None,None,None,1,None), + (4,31,"GtkOverlay","forms_overlay",14,None,None,None,None,None), + (4,32,"GtkBox","forms_wrapper",31,None,None,None,None,None), + (4,33,"GtkGrid",None,32,None,None,None,None,None), + (4,34,"GtkLabel",None,33,None,None,None,None,None), + (4,35,"GtkLabel","error_label",33,None,None,None,1,None), + (4,36,"GtkEntry","device_entry",33,None,None,None,2,None), + (4,37,"GtkLabel",None,33,None,None,None,3,None), + (4,38,"GtkLabel",None,33,None,None,None,4,None), + (4,39,"GtkLabel",None,33,None,None,None,5,None), + (4,40,"GtkEntry","email_entry",33,None,None,None,6,None), + (4,41,"GtkPasswordEntry","password_entry",33,None,None,None,7,None), + (4,42,"GtkButton","login_button",33,None,None,None,8,None), + (4,43,"GtkLabel",None,32,None,None,None,1,None), + (4,44,"GtkBox",None,32,None,None,None,2,None), + (4,45,"GtkLabel",None,44,None,None,None,None,None), + (4,46,"GtkPasswordEntry","api_key_entry",44,None,None,None,1,None), + (4,47,"GtkButton","api_key_button",44,None,None,None,2,None), + (4,48,"GtkBox","loading_overlay",None,None,None,None,None,None), + (4,49,"GtkSpinner",None,48,None,None,None,None,None), + (4,50,"GtkLabel",None,48,None,None,None,1,None), + (5,1,"GtkWindow","SLTotpDialog",None,None,None,None,None,None), + (5,2,"GtkOverlay","overlay",1,None,None,None,-1,None), + (5,11,"GtkBox","loading_overlay",None,None,None,None,None,None), + (5,12,"GtkSpinner",None,11,None,None,None,None,None), + (5,13,"GtkLabel",None,11,None,None,None,1,None), + (5,14,"GtkGrid","form_wrapper",2,None,None,None,None,None), + (5,15,"GtkBox",None,14,None,None,None,None,None), + (5,16,"GtkImage",None,15,None,None,None,None,None), + (5,17,"GtkLabel",None,15,None,None,None,1,None), + (5,19,"GtkButton","accept_button",14,None,None,None,2,None), + (5,20,"GtkButton","cancel_button",14,None,None,None,3,None), + (5,21,"GtkEntry","code_entry",14,None,None,None,4,None), + (5,23,"GtkLabel","error_label",14,None,None,None,4,None) + + + (1,3,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (1,3,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,5,"GtkSearchEntry","placeholder-text","Filter by email, description, etc.",None,None,None,None,None,None,None,None,None), + (1,5,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (1,8,"GtkDropDown","model",None,None,None,None,None,9,None,None,None,None), + (1,8,"GtkDropDown","selected","0",None,None,None,None,None,None,None,None,None), + (1,8,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (1,10,"GtkButton","label","Search",None,None,None,None,None,None,None,None,None), + (1,12,"GtkButton","label","New",None,None,None,None,None,None,None,None,None), + (1,13,"GtkButton","label","Edit",None,None,None,None,None,None,None,None,None), + (1,14,"GtkButton","label","Delete",None,None,None,None,None,None,None,None,None), + (1,22,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (1,23,"GtkSpinner","spinning","True",None,None,None,None,None,None,None,None,None), + (1,23,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (1,23,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,24,"GtkLabel","label","Loading...",None,None,None,None,None,None,None,None,None), + (1,24,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (1,24,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,28,"GtkButton","icon-name","go-previous-symbolic",None,None,None,None,None,None,None,None,None), + (1,28,"GtkWidget","halign","end",None,None,None,None,None,None,None,None,None), + (1,28,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (1,29,"GtkButton","icon-name","go-next-symbolic",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEditable","max-width-chars","0",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEditable","text","1",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEditable","width-chars","5",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEditable","xalign","0.5",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEntry","has-frame","False",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEntry","input-hints","no-emoji | no-spellcheck",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEntry","input-purpose","digits",None,None,None,None,None,None,None,None,None), + (1,30,"GtkEntry","truncate-multiline","True",None,None,None,None,None,None,None,None,None), + (1,32,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,33,"GtkListView","factory",None,None,None,None,None,34,None,None,None,None), + (1,33,"GtkListView","model",None,None,None,None,None,35,None,None,None,None), + (1,36,"GtkLabel","label","No more results",None,None,None,None,None,None,None,None,None), + (1,36,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (1,36,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,37,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (1,37,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (1,38,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (1,38,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (1,39,"GtkButton","icon-name","view-refresh-symbolic",None,None,None,None,None,None,None,None,None), + (1,39,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (1,39,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (1,39,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (1,40,"GtkButton","icon-name","window-close-symbolic",None,None,None,None,None,None,None,None,None), + (1,40,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (1,40,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (1,41,"GtkLabel","label","ERROR",None,None,None,None,None,None,None,None,None), + (1,41,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (1,41,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,19,"GtkOverlay","child","48",None,None,None,None,None,None,None,None,None), + (3,19,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,30,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (3,30,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,31,"GtkSpinner","spinning","True",None,None,None,None,None,None,None,None,None), + (3,31,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (3,31,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,32,"GtkLabel","label","Loading...",None,None,None,None,None,None,None,None,None), + (3,32,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (3,32,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,33,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (3,33,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,35,"GtkLabel","label","ERROR",None,None,None,None,None,None,None,None,None), + (3,35,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (3,35,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,45,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (3,45,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (3,46,"GtkButton","icon-name","view-refresh-symbolic",None,None,None,None,None,None,None,None,None), + (3,46,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (3,46,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (3,46,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,47,"GtkButton","icon-name","window-close-symbolic",None,None,None,None,None,None,None,None,None), + (3,47,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (3,47,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (3,48,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,48,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,49,"GtkGrid","row-homogeneous","True",None,None,None,None,None,None,None,None,None), + (3,49,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,49,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (3,51,"GtkLabel","label","Stats go here",None,None,None,None,None,None,None,None,None), + (3,51,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (3,51,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,52,"GtkLabel","label","mctesterface@test.tld",None,None,None,None,None,None,None,None,None), + (3,52,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None), + (3,52,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,53,"GtkButton","label","Logout",None,None,None,None,None,None,None,None,None), + (3,53,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (3,53,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (3,55,"GtkLabel","label","Account Status:",None,None,None,None,None,None,None,None,None), + (3,55,"GtkLabel","xalign","1.0",None,None,None,None,None,None,None,None,None), + (3,55,"GtkWidget","margin-end","5",None,None,None,None,None,None,None,None,None), + (3,57,"GtkLabel","label","Api Key:",None,None,None,None,None,None,None,None,None), + (3,57,"GtkLabel","xalign","1.0",None,None,None,None,None,None,None,None,None), + (3,57,"GtkWidget","margin-end","5",None,None,None,None,None,None,None,None,None), + (3,58,"GtkEditable","editable","False",None,None,None,None,None,None,None,None,None), + (3,58,"GtkEditable","enable-undo","False",None,None,None,None,None,None,None,None,None), + (3,58,"GtkPasswordEntry","show-peek-icon","True",None,None,None,None,None,None,None,None,None), + (3,58,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,58,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (3,59,"GtkLabel","label","Premium",None,None,None,None,None,None,None,None,None), + (3,59,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None), + (3,59,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,60,"GtkButton","label","Clear",None,None,None,None,None,None,None,None,None), + (3,60,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (3,60,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (3,63,"GtkButton","has-frame","False",None,None,None,None,None,None,None,None,None), + (3,64,"GtkImage","icon-name","user-info-symbolic",None,None,None,None,None,None,None,None,None), + (3,64,"GtkImage","icon-size","large",None,None,None,None,None,None,None,None,None), + (3,64,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (3,64,"GtkWidget","margin-end","5",None,None,None,None,None,None,None,None,None), + (3,64,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (3,64,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (3,66,"GtkButton","icon-name","window-close-symbolic",None,None,None,None,None,None,None,None,None), + (3,67,"GtkEditable","editable","False",None,None,None,None,None,None,None,None,None), + (3,67,"GtkEditable","text","Testy McTesterface",None,None,None,None,None,None,None,None,None), + (3,67,"GtkEntry","has-frame","False",None,None,None,None,None,None,None,None,None), + (3,67,"GtkEntry","input-hints","no-emoji | no-spellcheck",None,None,None,None,None,None,None,None,None), + (3,67,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (3,69,"GtkButton","label","Edit",None,None,None,None,None,None,None,None,None), + (3,69,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (3,69,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (3,70,"GtkButton","label","Cancel",None,None,None,None,None,None,None,None,None), + (4,11,"GtkPopover","child",None,None,None,None,None,12,None,None,None,None), + (4,12,"GtkAccessible","accessible-role","tooltip",None,None,None,None,None,None,None,None,None), + (4,12,"GtkLabel","label","This is the human readable name that will show up in the API key section of the SimpleLogin website.",None,None,None,None,None,None,None,None,None), + (4,12,"GtkLabel","max-width-chars","30",None,None,None,None,None,None,None,None,None), + (4,12,"GtkLabel","wrap","True",None,None,None,None,None,None,None,None,None), + (4,13,"GtkWindow","title","SimpleLogin",None,None,None,None,None,None,None,None,None), + (4,14,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (4,30,"GtkLabel","label","Don't have an account? You can register <a href=\"https://app.simplelogin.io/auth/register\">here</a>.",None,None,None,None,None,None,None,None,None), + (4,30,"GtkLabel","use-markup","True",None,None,None,None,None,None,None,None,None), + (4,30,"GtkLabel","yalign","0.0",None,None,None,None,None,None,None,None,None), + (4,30,"GtkWidget","margin-bottom","10",None,None,None,None,None,None,None,None,None), + (4,30,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (4,30,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (4,31,"GtkOverlay","child","32",None,None,None,None,None,None,None,None,None), + (4,31,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (4,31,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (4,32,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (4,32,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","halign","end",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","margin-bottom","10",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","margin-end","10",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","margin-start","10",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","margin-top","10",None,None,None,None,None,None,None,None,None), + (4,33,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (4,34,"GtkLabel","label","Login:",None,None,None,None,None,None,None,None,None), + (4,34,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,34,"GtkWidget","margin-bottom","5",None,None,None,None,None,None,None,None,None), + (4,34,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (4,35,"GtkLabel","label","ERROR",None,None,None,None,None,None,None,None,None), + (4,35,"GtkWidget","margin-top","5",None,None,None,None,None,None,None,None,None), + (4,36,"GtkEditable","width-chars","18",None,None,None,None,None,None,None,None,None), + (4,36,"GtkEntry","placeholder-text","My Awesome Computer",None,None,None,None,None,None,None,None,None), + (4,36,"GtkEntry","secondary-icon-name","dialog-question-symbolic",None,None,None,None,None,None,None,None,None), + (4,36,"GtkEntry","truncate-multiline","True",None,None,None,None,None,None,None,None,None), + (4,36,"GtkWidget","margin-start","6",None,None,None,None,None,None,None,None,None), + (4,37,"GtkLabel","label","Device:",None,None,None,None,None,None,None,None,None), + (4,37,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None), + (4,38,"GtkLabel","label","Email:",None,None,None,None,None,None,None,None,None), + (4,38,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None), + (4,39,"GtkLabel","label","Password:",None,None,None,None,None,None,None,None,None), + (4,39,"GtkLabel","xalign","0.0",None,None,None,None,None,None,None,None,None), + (4,40,"GtkEntry","input-purpose","email",None,None,None,None,None,None,None,None,None), + (4,40,"GtkEntry","truncate-multiline","True",None,None,None,None,None,None,None,None,None), + (4,40,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,40,"GtkWidget","margin-start","6",None,None,None,None,None,None,None,None,None), + (4,40,"GtkWidget","receives-default","True",None,None,None,None,None,None,None,None,None), + (4,41,"GtkPasswordEntry","show-peek-icon","True",None,None,None,None,None,None,None,None,None), + (4,41,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,41,"GtkWidget","margin-start","6",None,None,None,None,None,None,None,None,None), + (4,42,"GtkButton","label","Login",None,None,None,None,None,None,None,None,None), + (4,42,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (4,42,"GtkWidget","margin-top","5",None,None,None,None,None,None,None,None,None), + (4,42,"GtkWidget","sensitive","False",None,None,None,None,None,None,None,None,None), + (4,42,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (4,43,"GtkLabel","justify","center",None,None,None,None,None,None,None,None,None), + (4,43,"GtkLabel","label","- or -",None,None,None,None,None,None,None,None,None), + (4,43,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (4,44,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","halign","start",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","margin-bottom","10",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","margin-end","10",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","margin-start","10",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","margin-top","10",None,None,None,None,None,None,None,None,None), + (4,44,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (4,45,"GtkLabel","label","Set API Key:",None,None,None,None,None,None,None,None,None), + (4,45,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,45,"GtkWidget","margin-bottom","5",None,None,None,None,None,None,None,None,None), + (4,46,"GtkEditable","width-chars","27",None,None,None,None,None,None,None,None,None), + (4,46,"GtkPasswordEntry","show-peek-icon","True",None,None,None,None,None,None,None,None,None), + (4,46,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (4,46,"GtkWidget","margin-end","3",None,None,None,None,None,None,None,None,None), + (4,46,"GtkWidget","margin-start","8",None,None,None,None,None,None,None,None,None), + (4,47,"GtkButton","label","Accept",None,None,None,None,None,None,None,None,None), + (4,47,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (4,47,"GtkWidget","margin-top","5",None,None,None,None,None,None,None,None,None), + (4,47,"GtkWidget","sensitive","False",None,None,None,None,None,None,None,None,None), + (4,48,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (4,49,"GtkSpinner","spinning","True",None,None,None,None,None,None,None,None,None), + (4,49,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (4,49,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (4,50,"GtkLabel","label","Loading...",None,None,None,None,None,None,None,None,None), + (4,50,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (4,50,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (5,1,"GtkWindow","modal","True",None,None,None,None,None,None,None,None,None), + (5,1,"GtkWindow","title","TOTP Entry",None,None,None,None,None,None,None,None,None), + (5,2,"GtkOverlay","child","14",None,None,None,None,None,None,None,None,None), + (5,2,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (5,2,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (5,2,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (5,2,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (5,11,"GtkOrientable","orientation","vertical",None,None,None,None,None,None,None,None,None), + (5,12,"GtkSpinner","spinning","True",None,None,None,None,None,None,None,None,None), + (5,12,"GtkWidget","valign","end",None,None,None,None,None,None,None,None,None), + (5,12,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (5,13,"GtkLabel","label","Loading...",None,None,None,None,None,None,None,None,None), + (5,13,"GtkWidget","valign","start",None,None,None,None,None,None,None,None,None), + (5,13,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (5,14,"GtkGrid","column-spacing","5",None,None,None,None,None,None,None,None,None), + (5,14,"GtkGrid","row-spacing","5",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","margin-bottom","10",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","margin-end","10",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","margin-start","10",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","margin-top","10",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","valign","center",None,None,None,None,None,None,None,None,None), + (5,14,"GtkWidget","vexpand","True",None,None,None,None,None,None,None,None,None), + (5,15,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (5,15,"GtkWidget","hexpand","True",None,None,None,None,None,None,None,None,None), + (5,16,"GtkImage","icon-name","dialog-password-symbolic",None,None,None,None,None,None,None,None,None), + (5,16,"GtkWidget","halign","center",None,None,None,None,None,None,None,None,None), + (5,17,"GtkLabel","label","TOTP",None,None,None,None,None,None,None,None,None), + (5,19,"GtkButton","label","Accept",None,None,None,None,None,None,None,None,None), + (5,20,"GtkButton","label","Cancel",None,None,None,None,None,None,None,None,None), + (5,21,"GtkEntry","placeholder-text","TOTP Code",None,None,None,None,None,None,None,None,None), + (5,23,"GtkLabel","label","ERROR",None,None,None,None,None,None,None,None,None) + + + (3,49,51,"GtkGridLayoutChild","column","0",None,None,None,None), + (3,49,51,"GtkGridLayoutChild","column-span","3",None,None,None,None), + (3,49,51,"GtkGridLayoutChild","row","2",None,None,None,None), + (3,49,51,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (3,49,52,"GtkGridLayoutChild","column","1",None,None,None,None), + (3,49,52,"GtkGridLayoutChild","row","1",None,None,None,None), + (3,49,53,"GtkGridLayoutChild","column","2",None,None,None,None), + (3,49,53,"GtkGridLayoutChild","row","1",None,None,None,None), + (3,49,55,"GtkGridLayoutChild","column","0",None,None,None,None), + (3,49,55,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (3,49,55,"GtkGridLayoutChild","row","3",None,None,None,None), + (3,49,55,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (3,49,57,"GtkGridLayoutChild","column","0",None,None,None,None), + (3,49,57,"GtkGridLayoutChild","row","4",None,None,None,None), + (3,49,58,"GtkGridLayoutChild","column","1",None,None,None,None), + (3,49,58,"GtkGridLayoutChild","row","4",None,None,None,None), + (3,49,59,"GtkGridLayoutChild","column","1",None,None,None,None), + (3,49,59,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (3,49,59,"GtkGridLayoutChild","row","3",None,None,None,None), + (3,49,59,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (3,49,60,"GtkGridLayoutChild","column","2",None,None,None,None), + (3,49,60,"GtkGridLayoutChild","row","4",None,None,None,None), + (3,49,63,"GtkGridLayoutChild","column","0",None,None,None,None), + (3,49,63,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (3,49,63,"GtkGridLayoutChild","row-span","2",None,None,None,None), + (3,49,65,"GtkGridLayoutChild","column","1",None,None,None,None), + (3,49,65,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (3,49,65,"GtkGridLayoutChild","row","0",None,None,None,None), + (3,49,65,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (3,49,68,"GtkGridLayoutChild","column","2",None,None,None,None), + (3,49,68,"GtkGridLayoutChild","column-span","1",None,None,None,None), + (3,49,68,"GtkGridLayoutChild","row","0",None,None,None,None), + (3,49,68,"GtkGridLayoutChild","row-span","1",None,None,None,None), + (4,33,34,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,34,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (4,33,34,"GtkGridLayoutChild","row","0",None,None,None,None), + (4,33,35,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,35,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (4,33,35,"GtkGridLayoutChild","row","4",None,None,None,None), + (4,33,36,"GtkGridLayoutChild","column","1",None,None,None,None), + (4,33,36,"GtkGridLayoutChild","row","3",None,None,None,None), + (4,33,37,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,37,"GtkGridLayoutChild","row","3",None,None,None,None), + (4,33,38,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,38,"GtkGridLayoutChild","row","1",None,None,None,None), + (4,33,39,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,39,"GtkGridLayoutChild","row","2",None,None,None,None), + (4,33,40,"GtkGridLayoutChild","column","1",None,None,None,None), + (4,33,40,"GtkGridLayoutChild","row","1",None,None,None,None), + (4,33,41,"GtkGridLayoutChild","column","1",None,None,None,None), + (4,33,41,"GtkGridLayoutChild","row","2",None,None,None,None), + (4,33,42,"GtkGridLayoutChild","column","0",None,None,None,None), + (4,33,42,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (4,33,42,"GtkGridLayoutChild","row","5",None,None,None,None), + (5,14,15,"GtkGridLayoutChild","column","0",None,None,None,None), + (5,14,15,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (5,14,15,"GtkGridLayoutChild","row","0",None,None,None,None), + (5,14,19,"GtkGridLayoutChild","column","1",None,None,None,None), + (5,14,19,"GtkGridLayoutChild","row","3",None,None,None,None), + (5,14,20,"GtkGridLayoutChild","column","0",None,None,None,None), + (5,14,20,"GtkGridLayoutChild","row","3",None,None,None,None), + (5,14,21,"GtkGridLayoutChild","column","0",None,None,None,None), + (5,14,21,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (5,14,21,"GtkGridLayoutChild","row","1",None,None,None,None), + (5,14,23,"GtkGridLayoutChild","column-span","2",None,None,None,None), + (5,14,23,"GtkGridLayoutChild","row","2",None,None,None,None) + + + (4,34,"GtkWidget",1,1,None,None,None,None,None,None), + (4,34,"GtkWidget",2,2,None,1,None,None,None,None), + (4,35,"GtkWidget",1,1,None,None,None,None,None,None), + (4,35,"GtkWidget",2,2,None,1,None,None,None,None), + (4,40,"GtkWidget",1,1,None,None,None,None,None,None), + (4,33,"GtkWidget",1,1,None,None,None,None,None,None), + (4,33,"GtkWidget",2,2,None,1,None,None,None,None), + (4,45,"GtkWidget",1,1,None,None,None,None,None,None), + (4,45,"GtkWidget",2,2,None,1,None,None,None,None), + (1,9,"GtkStringList",2,5,"Disabled only",1,None,None,None,None), + (1,9,"GtkStringList",2,4,"Enabled only",1,None,None,None,None), + (1,9,"GtkStringList",2,3,"Pinned only",1,None,None,None,None), + (1,9,"GtkStringList",2,2,"All aliases",1,None,None,None,None), + (1,9,"GtkStringList",1,1,None,None,None,None,None,None), + (1,22,"GtkWidget",1,1,None,None,None,None,None,None), + (1,22,"GtkWidget",2,2,None,1,None,None,None,None), + (1,24,"GtkWidget",1,1,None,None,None,None,None,None), + (1,24,"GtkWidget",2,2,None,1,None,None,None,None), + (1,23,"GtkWidget",1,1,None,None,None,None,None,None), + (1,23,"GtkWidget",2,2,None,1,None,None,None,None), + (4,49,"GtkWidget",1,1,None,None,None,None,None,None), + (4,49,"GtkWidget",2,2,None,1,None,None,None,None), + (4,50,"GtkWidget",1,1,None,None,None,None,None,None), + (4,50,"GtkWidget",2,2,None,1,None,None,None,None), + (4,48,"GtkWidget",1,1,None,None,None,None,None,None), + (4,48,"GtkWidget",2,2,None,1,None,None,None,None), + (5,17,"GtkWidget",1,1,None,None,None,None,None,None), + (5,17,"GtkWidget",2,2,None,1,None,None,None,None), + (5,23,"GtkWidget",1,1,None,None,None,None,None,None), + (5,23,"GtkWidget",2,2,None,1,None,None,None,None), + (5,13,"GtkWidget",2,2,None,1,None,None,None,None), + (5,13,"GtkWidget",1,1,None,None,None,None,None,None), + (5,12,"GtkWidget",2,2,None,1,None,None,None,None), + (5,12,"GtkWidget",1,1,None,None,None,None,None,None), + (5,11,"GtkWidget",2,2,None,1,None,None,None,None), + (5,11,"GtkWidget",1,1,None,None,None,None,None,None), + (1,36,"GtkWidget",1,1,None,None,None,None,None,None), + (1,36,"GtkWidget",2,2,None,1,None,None,None,None), + (3,31,"GtkWidget",1,1,None,None,None,None,None,None), + (3,31,"GtkWidget",2,2,None,1,None,None,None,None), + (3,32,"GtkWidget",1,1,None,None,None,None,None,None), + (3,32,"GtkWidget",2,2,None,1,None,None,None,None), + (3,30,"GtkWidget",1,1,None,None,None,None,None,None), + (3,30,"GtkWidget",2,2,None,1,None,None,None,None), + (3,57,"GtkLabel",1,1,None,None,None,None,None,None), + (1,39,"GtkWidget",1,1,None,None,None,None,None,None), + (1,39,"GtkWidget",2,2,None,1,None,None,None,None), + (1,40,"GtkWidget",1,1,None,None,None,None,None,None), + (1,40,"GtkWidget",2,2,None,1,None,None,None,None), + (1,41,"GtkWidget",1,1,None,None,None,None,None,None), + (1,41,"GtkWidget",2,2,None,1,None,None,None,None), + (1,41,"GtkWidget",2,3,None,1,None,None,None,None), + (3,67,"GtkWidget",1,1,None,None,None,None,None,None), + (3,67,"GtkWidget",2,2,None,1,None,None,None,None), + (3,47,"GtkWidget",2,2,None,1,None,None,None,None), + (3,47,"GtkWidget",1,1,None,None,None,None,None,None), + (3,46,"GtkWidget",2,2,None,1,None,None,None,None), + (3,46,"GtkWidget",1,1,None,None,None,None,None,None), + (3,35,"GtkWidget",2,3,None,1,None,None,None,None), + (3,35,"GtkWidget",2,2,None,1,None,None,None,None), + (3,35,"GtkWidget",1,1,None,None,None,None,None,None) + + + (4,34,"GtkWidget",2,2,"name","login-title-label"), + (4,35,"GtkWidget",2,2,"name","error-message-label"), + (4,33,"GtkWidget",2,2,"name","login-form-warpper"), + (4,45,"GtkWidget",2,2,"name","login-title-label"), + (1,9,"GtkStringList",2,5,"translatable",None), + (1,9,"GtkStringList",2,5,"context",None), + (1,9,"GtkStringList",2,5,"comments",None), + (1,9,"GtkStringList",2,4,"translatable",None), + (1,9,"GtkStringList",2,4,"context",None), + (1,9,"GtkStringList",2,4,"comments",None), + (1,9,"GtkStringList",2,3,"translatable",None), + (1,9,"GtkStringList",2,3,"context",None), + (1,9,"GtkStringList",2,3,"comments",None), + (1,9,"GtkStringList",2,2,"translatable",None), + (1,9,"GtkStringList",2,2,"context",None), + (1,9,"GtkStringList",2,2,"comments",None), + (1,24,"GtkWidget",2,2,"name","loading-label"), + (1,22,"GtkWidget",2,2,"name","loading-label"), + (1,23,"GtkWidget",2,2,"name","loading-label"), + (4,49,"GtkWidget",2,2,"name","loading-label"), + (4,50,"GtkWidget",2,2,"name","loading-label"), + (4,48,"GtkWidget",2,2,"name","loading-label"), + (5,17,"GtkWidget",2,2,"name","login-title-label"), + (5,23,"GtkWidget",2,2,"name","error-message-label"), + (5,13,"GtkWidget",2,2,"name","loading-label"), + (5,12,"GtkWidget",2,2,"name","loading-label"), + (5,11,"GtkWidget",2,2,"name","loading-label"), + (1,36,"GtkWidget",2,2,"name","loading-label"), + (3,31,"GtkWidget",2,2,"name","loading-label"), + (3,32,"GtkWidget",2,2,"name","loading-label"), + (3,30,"GtkWidget",2,2,"name","loading-label"), + (1,39,"GtkWidget",2,2,"name","loading-label"), + (1,40,"GtkWidget",2,2,"name","loading-label"), + (1,41,"GtkWidget",2,2,"name","error-message-label"), + (1,41,"GtkWidget",2,3,"name","loading-label"), + (3,67,"GtkWidget",2,2,"name","login-title-label"), + (3,47,"GtkWidget",2,2,"name","loading-label"), + (3,46,"GtkWidget",2,2,"name","loading-label"), + (3,35,"GtkWidget",2,3,"name","loading-label"), + (3,35,"GtkWidget",2,2,"name","error-message-label") + + diff --git a/simplelogin-gui/res.old/subprojects/blueprint-compiler.wrap b/simplelogin-gui/res.old/subprojects/blueprint-compiler.wrap new file mode 100644 index 0000000..521d368 --- /dev/null +++ b/simplelogin-gui/res.old/subprojects/blueprint-compiler.wrap @@ -0,0 +1,8 @@ +[wrap-git] +directory = blueprint-compiler +url = https://gitlab.gnome.org/jwestman/blueprint-compiler.git +revision = v0.10.0 +depth = 1 + +[provide] +program_names = blueprint-compiler \ No newline at end of file diff --git a/simplelogin-gui/res.old/totp-dialog.ui b/simplelogin-gui/res.old/totp-dialog.ui new file mode 100644 index 0000000..e6dd234 --- /dev/null +++ b/simplelogin-gui/res.old/totp-dialog.ui @@ -0,0 +1,124 @@ + + + + + + + vertical + + + True + end + True + + + + + + Loading... + start + True + + + + + + diff --git a/simplelogin-gui/src/application.rs b/simplelogin-gui/src/application.rs new file mode 100644 index 0000000..4fe0b7d --- /dev/null +++ b/simplelogin-gui/src/application.rs @@ -0,0 +1,50 @@ +use crate::login_window::LoginWindow; + +use gtk4 as gtk; +use gtk::{glib, gio, prelude::*, subclass::prelude::*}; + +mod imp { + use super::*; + + #[derive(Debug)] + pub struct Application { + pub settings: gio::Settings, + } + + impl Default for Application { + fn default() -> Self { + Self { + settings: gio::Settings::new(crate::APP_ID) + } + } + } + + #[glib::object_subclass] + impl ObjectSubclass for Application { + const NAME: &'static str = "Application"; + type Type = super::Application; + type ParentType = gtk::Application; + } + + impl ApplicationImpl for Application { + fn activate(&self) { + let login_window = LoginWindow::new(&self.obj()); + login_window.present(); + } + } + + impl ObjectImpl for Application {} + impl GtkApplicationImpl for Application {} +} + +glib::wrapper! { + pub struct Application(ObjectSubclass) + @extends gtk::Application, gio::Application, + @implements gio::ActionMap, gio::ActionGroup; +} + +impl Application { + pub fn new(app_id: &str) -> Self { + glib::Object::builder().property("application-id", app_id).build() + } +} diff --git a/simplelogin-gui/src/login_window.rs b/simplelogin-gui/src/login_window.rs new file mode 100644 index 0000000..05f76a8 --- /dev/null +++ b/simplelogin-gui/src/login_window.rs @@ -0,0 +1,65 @@ +use gtk4 as gtk; +use gtk::{glib, gio, subclass::prelude::*}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/login_window.blp")] + pub struct LoginWindow { + #[template_child] + pub forms_overlay: TemplateChild, + #[template_child] + pub forms_wrapper: TemplateChild, + #[template_child] + pub error_label: TemplateChild, + #[template_child] + pub device_entry: TemplateChild, + #[template_child] + pub email_entry: TemplateChild, + #[template_child] + pub password_entry: TemplateChild, + #[template_child] + pub login_button: TemplateChild, + #[template_child] + pub api_key_entry: TemplateChild, + #[template_child] + pub api_key_button: TemplateChild, + #[template_child] + pub device_entry_popover: TemplateChild, + #[template_child] + pub loading_overlay: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for LoginWindow { + const NAME: &'static str = "LoginWindow"; + type Type = super::LoginWindow; + type ParentType = gtk::ApplicationWindow; + + fn class_init(class: &mut Self::Class) { + class.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for LoginWindow {} + impl WidgetImpl for LoginWindow {} + impl WindowImpl for LoginWindow {} + impl ApplicationWindowImpl for LoginWindow {} +} + +glib::wrapper! { + pub struct LoginWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, gtk::ApplicationWindow, + @implements gio::ActionMap, gio::ActionGroup; +} + +impl LoginWindow { + pub fn new(app: &crate::application::Application) -> Self { + glib::Object::builder().property("application", app).build() + } +} diff --git a/simplelogin-gui/src/main.rs b/simplelogin-gui/src/main.rs index e7a11a9..90adb39 100644 --- a/simplelogin-gui/src/main.rs +++ b/simplelogin-gui/src/main.rs @@ -1,3 +1,20 @@ -fn main() { - println!("Hello, world!"); +mod login_window; +mod totp_window; +mod application; + +use gtk4 as gtk; +use gtk::prelude::*; +use gtk::glib::{self, g_message}; + +const APP_ID: &str = "im.zander.SimpleLogin"; + +fn main() -> glib::ExitCode { + if cfg!(debug_assertions) { + // allow us to use a test schema for debug runs + let schema_dir = concat!(env!("OUT_DIR"), "/schemas/"); + g_message!("SimpleLogin", "Using schema dir: \"{}\"", schema_dir); + std::env::set_var("GSETTINGS_SCHEMA_DIR", schema_dir); + } + let app = application::Application::new(APP_ID); + app.run() } diff --git a/simplelogin-gui/src/totp_window.rs b/simplelogin-gui/src/totp_window.rs new file mode 100644 index 0000000..17a6866 --- /dev/null +++ b/simplelogin-gui/src/totp_window.rs @@ -0,0 +1,43 @@ +use gtk4 as gtk; +use gtk::{glib, gio, prelude::*, subclass::prelude::*}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/totp_window.blp")] + pub struct TotpWindow { + + } + + #[glib::object_subclass] + impl ObjectSubclass for TotpWindow { + const NAME: &'static str = "TotpWindow"; + type Type = super::TotpWindow; + type ParentType = gtk::Window; + + fn class_init(class: &mut Self::Class) { + class.bind_template(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for TotpWindow {} + impl WidgetImpl for TotpWindow {} + impl WindowImpl for TotpWindow {} +} + +glib::wrapper! { + pub struct TotpWindow(ObjectSubclass) + @extends gtk::Widget, gtk::Window, + @implements gio::ActionMap, gio::ActionGroup; +} + +impl TotpWindow { + pub fn new>(app: &P) -> Self { + glib::Object::builder().property("application", app).build() + } +} diff --git a/simplelogin/src/lib.rs b/simplelogin/src/lib.rs index e8f5ab1..c476e76 100644 --- a/simplelogin/src/lib.rs +++ b/simplelogin/src/lib.rs @@ -1,7 +1,9 @@ #![allow(dead_code)] +use core::fmt; + use serde::{Serialize, Deserialize}; -struct Context { +pub struct Context { client: reqwest::Client, api_key: Option, url: String, @@ -32,12 +34,60 @@ struct LoginResponse { api_key: Option, } -pub enum Error { - FIDO(String), - Json(String), - Auth(String), - Http(String), - Internal(String) +#[derive(Serialize)] +struct MfaRequest<'a> { + mfa_token: &'a str, + mfa_key: &'a str, + device: &'a str +} + +#[derive(Deserialize)] +struct MfaResponse { + name: String, + api_key: String, + email: String, +} + +pub enum ErrorKind { + State, + Json, + Http, + Auth, +} + +pub struct Error { + kind: ErrorKind, + msg: String, +} + +impl Error { + pub(crate) fn new( + kind: ErrorKind, msg: T + ) -> Self where T: Into { + Self {kind, msg: msg.into()} + } + + pub fn is_state(&self) -> bool { + matches!(self.kind, ErrorKind::State) + } + + pub fn is_http(&self) -> bool { + matches!(self.kind, ErrorKind::Http) + } + + pub fn is_auth(&self) -> bool { + matches!(self.kind, ErrorKind::Auth) + } + + pub fn is_json(&self) -> bool { + matches!(self.kind, ErrorKind::Json) + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.msg) + } } pub const DEFAULT_URL: &str = "https://app.simplelogin.io/"; @@ -63,6 +113,14 @@ impl Context { } } + pub fn api_key(&self) -> Option<&String> { + self.api_key.as_ref() + } + + pub fn set_api_key(&mut self, api_key: Option<&str>) { + self.api_key = api_key.map(|s| s.to_owned()); + } + fn parse_error_json(error_body: &String) -> String { match serde_json::from_str(error_body) { Ok::(obj) => obj.error, @@ -77,22 +135,43 @@ impl Context { Ok(resp) => { let status = resp.status(); let resp_text = resp.text().await - .map_err(|e| Error::Http(e.to_string()))?; + .map_err(|e| Error::new(ErrorKind::Http, e.to_string()))?; return if status.is_success() { Ok(resp_text) } else if status.is_client_error() { - Err(Error::Auth(Self::parse_error_json(&resp_text))) + Err(Error::new(ErrorKind::Auth, Self::parse_error_json(&resp_text))) } else { - Err(Error::Http("Unknown error! Response code: ".to_owned() - + status.as_str())) + Err(Error::new(ErrorKind::Http, status.to_string())) } }, Err(err) => { - Err(Error::Http(err.to_string())) + Err(Error::new(ErrorKind::Http, err.to_string())) }, } } + async fn post_request( + &mut self, endpoint: &str, body: &str + ) -> Result { + Self::perform_request(self.client.post(self.url.to_owned() + endpoint) + .header("Content-Type", "application/json") + .body(body.to_owned())).await + } + + pub fn obj_to_json( + obj: &T + ) -> Result where T: serde::Serialize { + serde_json::to_string(obj) + .map_err(|e| Error::new(ErrorKind::Json, e.to_string())) + } + + pub fn json_to_obj<'a, T>( + json: &'a str + ) -> Result where T: serde::Deserialize<'a> { + serde_json::from_str(json) + .map_err(|e| Error::new(ErrorKind::Json, e.to_string())) + } + pub async fn login( &mut self, email: &str, @@ -100,21 +179,16 @@ impl Context { device: &str ) -> Result { let req_body = LoginRequest {email, password, device}; - let response = Self::perform_request(self.client.post(self.url.to_owned()) - .header("Content-Type", "application/json") - .body(serde_json::to_string(&req_body).unwrap())); - let body = response.await?; - let resp_obj: LoginResponse = serde_json::from_str(body.as_str()) - .map_err(|_| Error::Json("Could not parse response json" - .to_owned()))?; + let body = self.post_request("api/auth/login", + Self::obj_to_json(&req_body)?.as_str()).await?; + let resp_obj: LoginResponse = Self::json_to_obj(body.as_str())?; if resp_obj.mfa_enabled { if resp_obj.mfa_key.is_some() { self.device = Some(device.to_owned()); self.mfa_key = resp_obj.mfa_key; Ok(true) } else { - Err(Error::Json("Error response did not contain mfa key" - .to_owned())) + Err(Error::new(ErrorKind::Json, "Error response did not contain mfa key")) } } else { if resp_obj.api_key.is_some() { @@ -123,13 +197,26 @@ impl Context { self.device = None; Ok(true) } else { - Err(Error::Json("Response did not contain api key".to_owned())) + Err(Error::new(ErrorKind::Json, "Response did not contain api key")) } } } pub async fn mfa(&mut self, mfa_token: &str) -> Result<(), Error> { - // TODO implement - Ok(()) + if self.mfa_key.is_some() && self.device.is_some() { + let req_json = Self::obj_to_json(&MfaRequest { + mfa_token, + mfa_key: self.mfa_key.as_ref().unwrap().as_str(), + device: self.device.as_ref().unwrap().as_str(), + })?; + let body = self.post_request("api/auth/mfa", req_json.as_str()).await?; + let resp_obj: MfaResponse = Self::json_to_obj(body.as_str())?; + self.api_key = Some(resp_obj.api_key); + self.device = None; + self.mfa_key = None; + Ok(()) + } else { + Err(Error::new(ErrorKind::State, "No login operation in progress")) + } } }