diff --git a/.buildconfig b/.buildconfig deleted file mode 100644 index 9d1be62..0000000 --- a/.buildconfig +++ /dev/null @@ -1,6 +0,0 @@ -[default] -name=Configuration par défaut -device=local -runtime=xdg-app:org.gnome.Platform/3.20/x86_64 -prefix=/app -default=true diff --git a/Makefile.am b/Makefile.am index 58b5c6f..7c8da0d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,4 +1,2 @@ SUBDIRS = src \ - data \ - plugins - + data diff --git a/createtable.py b/createtable.py deleted file mode 100644 index 29c54f7..0000000 --- a/createtable.py +++ /dev/null @@ -1,18 +0,0 @@ -import sqlite3 -conn = sqlite3.connect('example.db') - -c = conn.cursor() - -# Create table -c.execute('''CREATE TABLE providers - (date text, trans text, symbol text, qty real, price real)''') - -# Insert a row of data -c.execute("INSERT INTO stocks VALUES ('2006-01-05','BUY','RHAT',100,35.14)") - -# Save (commit) the changes -conn.commit() - -# We can also close the connection if we are done with it. -# Just be sure any changes have been committed or they will be lost. -conn.close() diff --git a/data/Makefile.am b/data/Makefile.am index 15f3e38..ed27719 100644 --- a/data/Makefile.am +++ b/data/Makefile.am @@ -1,15 +1,26 @@ desktopdir = $(DATADIR)/applications -desktop_DATA = two-factor.desktop +desktop_DATA = twofactorauth.desktop UPDATE_DESKTOP = update-desktop-database $(datadir)/applications || : uidir = $(pkgdatadir)/data -ui_DATA = about.glade +ui_DATA = about.glade \ + shortcuts.glade \ + menu.glade \ + style.css + +logodir = $(pkgdatadir)/data +logo_DATA = about.glade \ + shortcuts.glade \ + menu.glade \ + style.css + appdatadir = $(DATADIR)/appdata appdata_DATA = \ two-factor.appdata.xml EXTRA_DIST = \ + $(logo_DATA) \ $(ui_DATA) \ $(appdata_DATA) diff --git a/data/shortcuts.glade b/data/shortcuts.glade index fa79b3a..3cd1130 100644 --- a/data/shortcuts.glade +++ b/data/shortcuts.glade @@ -25,7 +25,13 @@ <Primary>C - + + + True + Add a new provider + <Primary>N + + True diff --git a/data/style.css b/data/style.css index 8addc26..186914c 100644 --- a/data/style.css +++ b/data/style.css @@ -2,7 +2,7 @@ background-color: @theme_bg_color; } -.application-list-row:selected * { +.application-list-row:selected GtkImage { color: @theme_fg_selected_color; } @@ -12,13 +12,13 @@ } .provider-logo-add { - margin-left: 15px; - margin-bottom: 15px; + margin-right: 5px; + margin-bottom: 10px; } .application-secret-code { font-size: 10px; margin-top: 0; margin-bottom: 5px; - margin-left: 70px; + margin-left: 65px; } diff --git a/data/two-factor.appdata.xml b/data/twofactorauth.appdata.xml similarity index 93% rename from data/two-factor.appdata.xml rename to data/twofactorauth.appdata.xml index 40a2b61..8c762c5 100644 --- a/data/two-factor.appdata.xml +++ b/data/twofactorauth.appdata.xml @@ -1,6 +1,6 @@ - two-factor.desktop + twofactorauth.desktop CC0 Two-factor Auth Two Factor Authentication code generator diff --git a/data/two-factor.desktop.in b/data/twofactorauth.desktop.in similarity index 100% rename from data/two-factor.desktop.in rename to data/twofactorauth.desktop.in diff --git a/database.db b/database.db index 7c2cdd1..effd21e 100644 Binary files a/database.db and b/database.db differ diff --git a/src/Makefile.am b/src/Makefile.am index 1d0d64c..478cd65 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,14 +1,14 @@ -SUBDIRS = unitsconverter +SUBDIRS = twofactorauth -bin_SCRIPTS = units-converter +bin_SCRIPTS = twofactorauth CLEANFILES = $(bin_SCRIPTS) -EXTRA_DIST = unit-converter.in +EXTRA_DIST = twofactorauth.in do_substitution = sed -e 's,[@]pythondir[@],$(pythondir),g' \ -e 's,[@]pkgdatadir[@],$(pkgdatadir),g' \ -e 's,[@]PACKAGE[@],$(PACKAGE),g' \ - -e 's,[@]VERSION[@],$(VERSION),g' + -e 's,[@]VERSION[@],$(VERSION),g' -units-converter: units-converter.in Makefile - $(do_substitution) < $(srcdir)/units-converter.in > units-converter - chmod +x units-converter +units-converter: twofactorauth.in Makefile + $(do_substitution) < $(srcdir)/twofactorauth.in > twofactorauth + chmod +x twofactorauth diff --git a/src/two-factor.in b/src/twofactorauth.in similarity index 91% rename from src/two-factor.in rename to src/twofactorauth.in index d7ed312..e236865 100644 --- a/src/two-factor.in +++ b/src/twofactorauth.in @@ -20,10 +20,10 @@ """ import sys sys.path.insert(1, '@pythondir@') -from twofactor import application +from twofactorauth import application if __name__ == "__main__": - app = application.UnitsConverter(package="@PACKAGE@", + app = application.TwoFactor(package="@PACKAGE@", version="@VERSION@", pkgdatadir="@pkgdatadir@") app.run(None) diff --git a/src/twofactorauth/Makefile.am b/src/twofactorauth/Makefile.am new file mode 100644 index 0000000..87e3891 --- /dev/null +++ b/src/twofactorauth/Makefile.am @@ -0,0 +1,8 @@ +SUBDIRS = models \ + ui + +app_PYTHON = \ + application.py \ + __init__.py + +appdir = $(pythondir)/TwoFactorAuth diff --git a/src/twofactorauth/__init__.py b/src/twofactorauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/twofactor/application.py b/src/twofactorauth/application.py similarity index 71% rename from src/twofactor/application.py rename to src/twofactorauth/application.py index 55944a1..8177e98 100644 --- a/src/twofactor/application.py +++ b/src/twofactorauth/application.py @@ -1,6 +1,6 @@ from gi import require_version require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gio, Gdk +from gi.repository import Gtk, GLib, Gio, Gdk, GObject from ui.window import TwoFactorWindow import logging from models.provider import Provider @@ -9,19 +9,19 @@ logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(message)s', ) -# TODO : https://pypi.python.org/pypi/pyotp - class TwoFactor(Gtk.Application): win = None - def __init__(self): + def __init__(self, *args, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) Gtk.Application.__init__(self, - application_id='org.gnome.twofactor', + application_id='org.gnome.twofactorauth', flags=Gio.ApplicationFlags.FLAGS_NONE) - GLib.set_application_name("Two-factor") - GLib.set_prgname('twofactor') - + GLib.set_application_name("Two-Factor Auth") + GLib.set_prgname('two_factor_auth') + GObject.threads_init() provider = Gtk.CssProvider() css_file = "/home/bilal/Projects/Two-factor-gtk/data/style.css" try: @@ -29,10 +29,10 @@ class TwoFactor(Gtk.Application): Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_USER) - logging.debug("[CSS]: Loading css file %s" % css_file) + logging.debug("Loading css file %s" % css_file) except Exception as e: - logging.debug("[CSS]: File not found %s" % css_file) - logging.debug("[CSS]: Error message %s" % str(e)) + logging.debug("File not found %s" % css_file) + logging.debug("Error message %s" % str(e)) def do_startup(self): Gtk.Application.do_startup(self) @@ -60,6 +60,7 @@ class TwoFactor(Gtk.Application): TwoFactorWindow(self) self.win.show() self.add_window(self.win) + self.get_active_window().present() def on_shortcuts(self, *args): logging.debug("Shortcuts window") @@ -70,11 +71,12 @@ class TwoFactor(Gtk.Application): self.win.show_about() def on_quit(self, *args): - Gtk.main_quit() - self.win.destroy() + # Clear the clipboard once the application is closed, for safety resasons + try: + clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) + clipboard.clear() + except Exception as e: + logging.error(str(e)) + for win in self.get_windows(): + win.destroy() self.quit() - - - -app = TwoFactor() -app.run(None) diff --git a/src/twofactorauth/models/Makefile.am b/src/twofactorauth/models/Makefile.am new file mode 100644 index 0000000..4d4cf96 --- /dev/null +++ b/src/twofactorauth/models/Makefile.am @@ -0,0 +1,5 @@ +app_PYTHON = \ + code.py \ + provider.py + +appdir = $(pythondir)/TwoFactorAuth/models diff --git a/src/twofactorauth/models/code.py b/src/twofactorauth/models/code.py new file mode 100644 index 0000000..a29df84 --- /dev/null +++ b/src/twofactorauth/models/code.py @@ -0,0 +1,34 @@ +from pyotp import TOTP +import logging +logging.basicConfig(level=logging.DEBUG, + format='[%(levelname)s] %(message)s', + ) +class Code: + password = None + label = None + + def __init__(self, secret_code): + self.secret_code = secret_code + self.create() + + def create(self): + try: + self.totp = TOTP(self.secret_code) + self.password = self.totp.now() + except Exception as e: + logging.error("Canno't generate a two-factor auth code") + logging.error(str(e)) + def update(self): + self.password = self.totp.now() + + + def get_secret_code(self): + try: + if self.password: + return self.password + else: + raise AttributeError + except AttributeError as e: + logging.error("Couldn't generate the code") + logging.error(str(e)) + return None diff --git a/src/twofactor/models/provider.py b/src/twofactorauth/models/provider.py similarity index 82% rename from src/twofactor/models/provider.py rename to src/twofactorauth/models/provider.py index f44bbb0..55173db 100644 --- a/src/twofactor/models/provider.py +++ b/src/twofactorauth/models/provider.py @@ -15,7 +15,6 @@ class Provider: try: self.conn.execute(query, t) self.conn.commit() - logging.debug("Proivder '%s' added to the database" % name) except Exception as e: logging.error(query) logging.error("Couldn't add a new provider to database") @@ -25,7 +24,7 @@ class Provider: query = "DELETE FROM providers WHERE id=?" try: self.conn.execute(query, (id,)) - self.conn.commit() + self.conn.commit() except Exception as e: logging.error("Couldn't remove the application with id : %s", id) logging.error(str(e)) @@ -35,7 +34,6 @@ class Provider: query = "SELECT COUNT(id) AS count FROM providers" try: data = c.execute(query) - logging.debug("Providers list fetched successfully") return data.fetchone()[0] except Exception as e: logging.error(query) @@ -48,7 +46,6 @@ class Provider: query = "SELECT * FROM providers" try: data = c.execute(query) - logging.debug("Providers list fetched successfully") return data.fetchall() except Exception as e: logging.error(query) @@ -59,12 +56,16 @@ class Provider: def get_provider_image(self, image): img = Gtk.Image(xalign=0) directory = "/home/bilal/Projects/Two-factor-gtk/data/logos/" - image = directory + image - if path.exists(image): + theme = Gtk.IconTheme.get_default() + if path.isfile(directory + image) and path.exists(directory + image): + img.set_from_file(directory + image) + elif path.isfile(image) and path.exists(image): img.set_from_file(image) + elif theme.has_icon(path.splitext(image)[0]): + img.set_from_icon_name(path.splitext(image)[0], + Gtk.IconSize.DIALOG) else: - img.set_from_icon_name(image, - Gtk.IconSize.DIALOG) + img.set_from_icon_name("image-missing", Gtk.IconSize.DIALOG) return img def get_latest_id(self): diff --git a/src/twofactorauth/ui/Makefile.am b/src/twofactorauth/ui/Makefile.am new file mode 100644 index 0000000..f8966ed --- /dev/null +++ b/src/twofactorauth/ui/Makefile.am @@ -0,0 +1,8 @@ +app_PYTHON = \ + add_provider.py \ + confirmation.py \ + listrow.py \ + logo_provider.py \ + window.py + +appdir = $(pythondir)/TwoFactorAuth/ui diff --git a/src/twofactor/ui/add_provider.py b/src/twofactorauth/ui/add_provider.py similarity index 90% rename from src/twofactor/ui/add_provider.py rename to src/twofactorauth/ui/add_provider.py index d65ce07..05e78ae 100644 --- a/src/twofactor/ui/add_provider.py +++ b/src/twofactorauth/ui/add_provider.py @@ -30,9 +30,9 @@ class AddProviderWindow(Gtk.Window): Gtk.Window.__init__(self, title="Add a new provider", modal=True, destroy_with_parent=True) self.connect("delete-event", lambda x, y: self.destroy()) - self.resize(350, 100) + self.resize(300, 100) self.set_border_width(18) - self.set_size_request(350, 100) + self.set_size_request(300, 100) self.set_position(Gtk.WindowPosition.CENTER) self.set_resizable(False) self.set_transient_for(self.parent) @@ -55,7 +55,11 @@ class AddProviderWindow(Gtk.Window): secret_entry, image_entry) id = self.parent.app.provider.get_latest_id() - self.parent.refresh_window() + print() + if self.parent.app.provider.count_providers() == 1: + self.parent.refresh_window(True) + else: + self.parent.update_list(id, name_entry, secret_entry, image_entry) self.close_window() except Exception as e: logging.error("Error in adding a new provider") @@ -82,10 +86,8 @@ class AddProviderWindow(Gtk.Window): hbox_two_factor.pack_end(two_factor_entry, False, True, 0) logo_event = Gtk.EventBox() - logo_image = Gtk.Image() - logo_image.set_from_icon_name("image-missing", - Gtk.IconSize.DIALOG) - logo_event.get_style_context().add_class("provider-logo-add") + logo_image = self.parent.app.provider.get_provider_image("image-missing") + logo_image.get_style_context().add_class("provider-logo-add") logo_event.add(logo_image) logo_event.connect("button-press-event", self.select_logo) logo_box.pack_start(logo_event, False, False, 6) diff --git a/src/twofactor/ui/confirmation.py b/src/twofactorauth/ui/confirmation.py similarity index 100% rename from src/twofactor/ui/confirmation.py rename to src/twofactorauth/ui/confirmation.py diff --git a/src/twofactorauth/ui/listrow.py b/src/twofactorauth/ui/listrow.py new file mode 100644 index 0000000..311df35 --- /dev/null +++ b/src/twofactorauth/ui/listrow.py @@ -0,0 +1,155 @@ + +from gi import require_version +require_version("Gtk", "3.0") +from gi.repository import Gtk, GLib, Gio, Gdk, GObject +from models.code import Code +from threading import Thread +import time + +import logging +from math import pi + +class ListBoxRow(Thread): + counter_max = 30 + counter = 30 + code = None + code_generated = True + + def __init__(self, parent, id, name, secret_code, logo): + Thread.__init__(self) + self.parent = parent + self.id = id + self.name = name + self.secret_code = secret_code + self.code = Code(secret_code) + self.logo = logo + self.create_row() + self.start() + GObject.timeout_add_seconds(1, self.refresh_listbox) + + + def on_button_press_event(self, widget, event) : + if event.button == Gdk.EventType._2BUTTON_PRESS: + #data = widget.get_path_at_pos(int(event.x), int(event.y)) + print("helloooo") + + def create_row(self): + self.row = Gtk.ListBoxRow() + self.row.get_style_context().add_class("application-list-row") + self.row.connect("button-press-event", self.on_button_press_event) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + pass_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + pass_box.set_visible(False) + vbox.pack_start(hbox, True, True, 6) + vbox.pack_start(pass_box, True, True, 6) + + # ID + label_id = Gtk.Label() + label_id.set_text(str(self.id)) + label_id.set_visible(False) + label_id.set_no_show_all(True) + vbox.pack_end(label_id, False, False, 0) + + # Checkbox + checkbox = Gtk.CheckButton() + checkbox.set_visible(False) + checkbox.set_no_show_all(True) + checkbox.connect("toggled", self.parent.select_application) + hbox.pack_start(checkbox, False, True, 6) + + # Provider logo + provider_logo = self.parent.app.provider.get_provider_image(self.logo) + hbox.pack_start(provider_logo, False, True, 6) + + # Provider name + application_name = Gtk.Label(xalign=0) + application_name.get_style_context().add_class("application-name") + application_name.set_text(self.name) + hbox.pack_start(application_name, True, True, 6) + # Copy button + copy_event = Gtk.EventBox() + copy_button = Gtk.Image(xalign=0) + copy_button.set_from_icon_name("edit-copy-symbolic", + Gtk.IconSize.SMALL_TOOLBAR) + copy_button.set_tooltip_text("Copy the generated code..") + copy_event.connect("button-press-event", self.parent.copy_code) + copy_event.add(copy_button) + hbox.pack_end(copy_event, False, True, 6) + + # Remove button + remove_event = Gtk.EventBox() + remove_button = Gtk.Image(xalign=0) + remove_button.set_from_icon_name("list-remove-symbolic", + Gtk.IconSize.SMALL_TOOLBAR) + remove_button.set_tooltip_text("Remove the source..") + remove_event.add(remove_button) + remove_event.connect("button-press-event", self.parent.remove_provider) + hbox.pack_end(remove_event, False, True, 6) + + self.darea = Gtk.DrawingArea() + self.darea.set_size_request(24, 24) + + code_label = Gtk.Label(xalign=0) + code_label.get_style_context().add_class("application-secret-code") + # TODO : show the real secret code + self.update_code(code_label) + pass_box.set_no_show_all(True) + pass_box.pack_end(self.darea, False, True, 6) + pass_box.pack_start(code_label, False, True, 6) + + self.row.add(vbox) + + + def get_counter(self): + return self.counter + + def run(self): + while self.code_generated: + self.counter -= 1 + if self.counter < 0: + self.counter = self.counter_max + self.regenerate_code() + self.darea.connect("draw", self.expose) + self.row.changed() + time.sleep(1) + + def get_listrow(self): + return self.row + + def refresh_listbox(self): + self.parent.listbox.hide() + self.parent.listbox.show_all() + return self.code_generated + + def regenerate_code(self): + label = self.row.get_children()[0].get_children()[1].get_children()[0] + if label: + self.code.update() + self.update_code(label) + + def update_code(self, label): + try: + label.set_text(self.code.get_secret_code()) + except TypeError as e: + logging.error("Canno't generate secret code") + logging.error(str(e)) + label.set_text("Couldn't generate the secret code") + self.code_generated = False + + def expose(self, darea, cairo): + try: + if self.code_generated: + cairo.arc(12, 12, 12, 0, (self.counter*2*pi/self.counter_max)) + cairo.set_source_rgba(0, 0, 0, 0.4) + cairo.fill_preserve() + if self.counter < self.counter_max/2: + cairo.set_source_rgb(0, 0, 0) + else: + cairo.set_source_rgb(1, 1, 1) + cairo.move_to(8, 15) + cairo.show_text(str(self.counter)) + # self.draw(cairo) + except Exception as e: + logging.error(str(e)) + return False diff --git a/src/twofactor/ui/logo_provider.py b/src/twofactorauth/ui/logo_provider.py similarity index 82% rename from src/twofactor/ui/logo_provider.py rename to src/twofactorauth/ui/logo_provider.py index 4722b45..9a58e2c 100644 --- a/src/twofactor/ui/logo_provider.py +++ b/src/twofactorauth/ui/logo_provider.py @@ -9,6 +9,9 @@ logging.basicConfig(level=logging.DEBUG, class LogoProviderWindow(Gtk.Window): def __init__(self, window): + directory = "/home/bilal/Projects/Two-factor-gtk/data/logos/" + self.logos = listdir(directory) + self.logos.sort() self.window = window self.generate_window() self.genereate_searchbar() @@ -31,7 +34,7 @@ class LogoProviderWindow(Gtk.Window): def filter_func(self, row, data, notify_destroy): provider_label = row.get_children()[0].get_children()[0].get_children() - data = data.strip() + data = data.strip().lower() if len(data) > 0: return data in provider_label[1].get_text().lower() else: @@ -39,6 +42,12 @@ class LogoProviderWindow(Gtk.Window): def filter_providers(self, entry): data = entry.get_text() + if len(data) != 0: + entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, + "edit-clear-symbolic") + else: + entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, + None) self.listbox.set_filter_func(self.filter_func, data, False) def on_key_press(self, provider, keyevent): @@ -54,7 +63,7 @@ class LogoProviderWindow(Gtk.Window): search_box.set_visible(is_visible) search_box.show_all() if is_visible: - search_box.get_children()[1].grab_focus_without_selecting() + search_box.get_children()[0].grab_focus_without_selecting() else: self.listbox.set_filter_func(lambda x,y,z : True, None, False) elif keypressed == "return": @@ -62,40 +71,29 @@ class LogoProviderWindow(Gtk.Window): def genereate_searchbar(self): hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.set_margin_left(40) - - search_image = Gtk.Image(xalign=0) - search_image.set_from_icon_name("system-search-symbolic", - Gtk.IconSize.SMALL_TOOLBAR) - search_image.set_tooltip_text("Type to search") + hbox.set_margin_left(60) search_entry = Gtk.Entry() search_entry.connect("changed", self.filter_providers) + search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, + "system-search-symbolic") - hbox.pack_start(search_image, False, True, 6) hbox.pack_start(search_entry, False, True, 6) hbox.set_visible(False) - self.get_children()[0].pack_start(hbox, False, True, 0) + self.get_children()[0].pack_start(hbox, False, True, 6) self.get_children()[0].get_children()[0].set_no_show_all(True) def select_logo(self, *args): index = self.listbox.get_selected_row().get_index() - directory = "/home/bilal/Projects/Two-factor-gtk/data/logos/" - files = listdir(directory) - files.sort() - count = len(files) - if count > 0: - img_path = files[index] + if len(self.logos) > 0: + img_path = self.logos[index] self.window.update_logo(img_path) self.close_window() def generate_compenents(self): box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) directory = "/home/bilal/Projects/Two-factor-gtk/data/logos/" - files = listdir(directory) - files.sort() - count = len(files) - if count > 0: + if len(self.logos) > 0: # Create a ScrolledWindow for installed applications scrolled_win = Gtk.ScrolledWindow() scrolled_win.add_with_viewport(box_outer) @@ -107,8 +105,8 @@ class LogoProviderWindow(Gtk.Window): self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) box_outer.pack_start(self.listbox, True, True, 0) i = 0 - while i < count: - logo = files[i] + while i < len(self.logos): + logo = self.logos[i] provider_name = path.splitext(logo)[0].strip(".").title() row = Gtk.ListBoxRow() row.get_style_context().add_class("application-list-row") @@ -116,11 +114,8 @@ class LogoProviderWindow(Gtk.Window): hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) # Provider logo - provider_logo = Gtk.Image(xalign=0) - provider_logo.set_from_file(directory + logo) - #provider_logo.scale_simple(Gtk.IconSize.DIALOG, - # Gtk.IconSize.DIALOG, - # GdkPixbuf.InterpType.BILINEAR) + provider = self.window.parent.app.provider + provider_logo = provider.get_provider_image(logo) hbox.pack_start(provider_logo, False, True, 6) # Provider name diff --git a/src/twofactor/ui/window.py b/src/twofactorauth/ui/window.py similarity index 52% rename from src/twofactor/ui/window.py rename to src/twofactorauth/ui/window.py index 842bd49..2c43471 100644 --- a/src/twofactor/ui/window.py +++ b/src/twofactorauth/ui/window.py @@ -1,19 +1,21 @@ from gi import require_version require_version("Gtk", "3.0") -from gi.repository import Gtk, GLib, Gio, Gdk +from gi.repository import Gtk, GLib, Gio, Gdk, GObject from ui.add_provider import AddProviderWindow from ui.confirmation import ConfirmationMessage -import logging +from ui.listrow import ListBoxRow +from threading import Thread +import logging +from math import pi logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(message)s', ) -class TwoFactorWindow(Gtk.Window): +class TwoFactorWindow(Gtk.ApplicationWindow): app = None selected_app_idx = None - checkboxes = [] def __init__(self, application): self.app = application @@ -24,15 +26,16 @@ class TwoFactorWindow(Gtk.Window): self.get_children()[0].get_children()[0].set_visible(False) def generate_window(self, *args): - Gtk.Window.__init__(self, application=self.app) - self.connect("delete-event", lambda x, y: self.app.quit()) + Gtk.ApplicationWindow.__init__(self, Gtk.WindowType.TOPLEVEL, + application=self.app) self.set_position(Gtk.WindowPosition.CENTER) - self.set_wmclass("twofactor", "Two-Factor") + self.set_wmclass("two_factor_auth", "Two-Factor Auth") self.resize(350, 500) self.set_size_request(350, 500) self.set_resizable(False) self.connect("key_press_event", self.on_key_press) self.app.win = self + self.connect("delete-event", lambda x, y: self.app.on_quit()) self.add(Gtk.Box(orientation=Gtk.Orientation.VERTICAL)) def on_key_press(self, provider, keyevent): @@ -44,17 +47,20 @@ class TwoFactorWindow(Gtk.Window): elif keypressed == "f": if keyevent.state == CONTROL_MASK: if self.app.provider.count_providers() > 0: - search_box = self.get_children()[0].get_children()[0] + search_box = self.get_children()[0].get_children()[0].get_children()[0] is_visible = search_box.get_no_show_all() search_box.set_no_show_all(not is_visible) search_box.set_visible(is_visible) search_box.show_all() if is_visible: - search_box.get_children()[1].grab_focus_without_selecting() + search_box.get_children()[0].grab_focus_without_selecting() else: self.listbox.set_filter_func(lambda x,y,z : True, None, False) + elif keypressed == "n": + if keyevent.state == CONTROL_MASK: + self.add_provider() elif keypressed == "delete": - self.remove_application() + self.remove_provider() elif keypressed == "return": if self.app.provider.count_providers() > 0: if self.listbox.get_selected_row(): @@ -71,40 +77,42 @@ class TwoFactorWindow(Gtk.Window): def filter_providers(self, entry): data = entry.get_text() + if len(data) != 0: + entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, + "edit-clear-symbolic") + else: + entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, + None) self.listbox.set_filter_func(self.filter_func, data, False) def genereate_searchbar(self): - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - hbox.set_margin_left(40) - - search_image = Gtk.Image(xalign=0) - search_image.set_from_icon_name("system-search-symbolic", - Gtk.IconSize.SMALL_TOOLBAR) - search_image.set_tooltip_text("Type to search") - + hbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + search_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) search_entry = Gtk.Entry() + search_box.set_margin_left(60) + search_entry.set_width_chars(21) search_entry.connect("changed", self.filter_providers) + search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, + "system-search-symbolic") - hbox.pack_start(search_image, False, True, 6) - hbox.pack_start(search_entry, False, True, 6) + search_box.pack_start(search_entry, False, True, 0) + hbox.pack_start(search_box, False, True, 6) hbox.set_visible(False) - self.get_children()[0].pack_start(hbox, False, True, 6) - self.get_children()[0].get_children()[0].set_no_show_all(True) - + self.get_children()[0].pack_start(hbox, True, True, 0) + search_box.set_no_show_all(True) def remove_selected(self, *args): - i = 0 - confirmation = ConfirmationMessage(self, "Are you sure??") + message = "Do you really want to remove the two-factor auth provider?" + confirmation = ConfirmationMessage(self, message) confirmation.show() if confirmation.get_confirmation(): - while i < len(self.checkboxes): - if self.checkboxes[i].get_active(): - selected_row = self.listbox.get_row_at_index(i) - label_id = selected_row.get_children()[0].get_children()[2] - self.app.provider.remove_from_database(int(label_id.get_text())) - self.listbox.remove(selected_row) - del self.checkboxes[i] - i += 1 + for row in self.listbox.get_children(): + checkbox = self.get_checkbox_from_row(row) + if checkbox.get_active(): + label_id = row.get_children()[0].get_children()[2] + label_id = int(label_id.get_text()) + self.app.provider.remove_from_database(label_id) + self.listbox.remove(row) self.listbox.unselect_all() confirmation.destroy() self.refresh_window() @@ -174,14 +182,14 @@ class TwoFactorWindow(Gtk.Window): listrow_box = self.listbox.get_row_at_index(index) self.listbox.select_row(listrow_box) - while i < len(self.checkboxes): - visible = self.checkboxes[i].get_visible() - selected = self.checkboxes[i].get_active() + for row in self.listbox.get_children(): + checkbox = self.get_checkbox_from_row(row) + visible = checkbox.get_visible() + selected = checkbox.get_active() if not button_visible: - self.select_application(self.checkboxes[i]) - self.checkboxes[i].set_visible(not visible) - self.checkboxes[i].set_no_show_all(visible) - i += 1 + self.select_application(checkbox) + checkbox.set_visible(not visible) + checkbox.set_no_show_all(visible) def select_application(self, checkbutton): is_active = checkbutton.get_active() @@ -199,135 +207,84 @@ class TwoFactorWindow(Gtk.Window): else: return True + def get_checkbox_from_row(self, row): + if row: + return row.get_children()[0].get_children()[0].get_children()[0] + else: + return None + def select_row(self, listbox, listbox_row): index = listbox_row.get_index() button_visible = self.remove_button.get_visible() - - if self.checkboxes[index]: - if button_visible: - clicked = self.checkboxes[index].get_active() - self.checkboxes[index].set_active(not clicked) - else: - if self.selected_app_idx: - listrow_box = self.listbox.get_row_at_index( - self.selected_app_idx) - self.listbox.unselect_row(listbox_row) - self.selected_app_idx = index - listrow_box = self.listbox.get_row_at_index(index) - self.listbox.select_row(listbox_row) + checkbox = self.get_checkbox_from_row(listbox_row) + if button_visible: + checkbox.set_active(not checkbox.get_active()) + else: + if self.selected_app_idx: + listrow_box = self.listbox.get_row_at_index( + self.selected_app_idx) + self.listbox.unselect_row(listbox_row) + self.selected_app_idx = index + listrow_box = self.listbox.get_row_at_index(index) + self.listbox.select_row(listbox_row) # TODO : show a nice message when no application is added def generate_applications_list(self): - box_outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + list_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) count = self.app.provider.count_providers() - if count > 0: - # Create a ScrolledWindow for installed applications - scrolled_win = Gtk.ScrolledWindow() - scrolled_win.add_with_viewport(box_outer) - self.get_children()[0].pack_start(scrolled_win, True, True, 0) + # Create a ScrolledWindow for installed applications + self.listbox = Gtk.ListBox() + self.listbox.get_style_context().add_class("applications-list") + self.listbox.set_adjustment() + self.listbox.connect("row_activated", self.select_row) + self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) + list_box.pack_start(self.listbox, True, True, 0) - self.listbox = Gtk.ListBox() - self.listbox.get_style_context().add_class("applications-list") - self.listbox.set_adjustment() - self.listbox.connect("row_activated", self.select_row) - self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) - box_outer.pack_start(self.listbox, True, True, 0) - providers = self.app.provider.fetch_providers() - i = 0 - while i < count: - row = self.generate_listrow(providers[i][0], providers[i][1], - providers[i][2], providers[i][3]) - self.listbox.add(row) - i += 1 + scrolled_win = Gtk.ScrolledWindow() + scrolled_win.add_with_viewport(list_box) + self.get_children()[0].get_children()[0].pack_start(scrolled_win, True, True, 0) + + providers = self.app.provider.fetch_providers() + i = 0 + while i < len(providers): + row = ListBoxRow(self, providers[i][0], providers[i][1], + providers[i][2], providers[i][3]) + self.listbox.add(row.get_listrow()) + i += 1 + + nolist_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + vbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + logo_image = Gtk.Image() + logo_image.set_from_icon_name("dialog-information-symbolic", + Gtk.IconSize.DIALOG) + vbox.pack_start(logo_image, False, False, 6) + + no_proivders_label = Gtk.Label() + no_proivders_label.set_text("There's no providers at the moment") + vbox.pack_start(no_proivders_label, False, False, 6) + + nolist_box.pack_start(vbox, True, True, 0) + self.get_children()[0].pack_start(nolist_box, True, True, 0) + if len(providers) == 0: + self.get_children()[0].get_children()[0].set_no_show_all(True) + self.get_children()[0].get_children()[0].set_visible(False) else: - vbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + self.get_children()[0].get_children()[1].set_no_show_all(True) + self.get_children()[0].get_children()[1].set_visible(False) - logo_image = Gtk.Image() - logo_image.set_from_icon_name("dialog-information-symbolic", - Gtk.IconSize.DIALOG) - vbox.pack_start(logo_image, False, False, 6) - - no_proivders_label = Gtk.Label() - no_proivders_label.set_text("There's no providers at the moment") - vbox.pack_start(no_proivders_label, False, False, 6) - - box_outer.pack_start(vbox, True, True, 0) - self.get_children()[0].pack_start(box_outer, True, True, 0) def update_list(self, id, name, secret_code, image): - row = self.generate_listrow(id, name, secret_code, image) - self.listbox.add(row) + row = ListBoxRow(self, id, name, secret_code, image) + self.listbox.add(row.get_listrow()) self.listbox.show_all() - def generate_listrow(self, id, name, secret_code, logo): - row = Gtk.ListBoxRow() - row.get_style_context().add_class("application-list-row") - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - pass_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - pass_box.set_visible(False) - vbox.pack_start(hbox, True, True, 6) - vbox.pack_start(pass_box, True, True, 6) - - # ID - label_id = Gtk.Label() - label_id.set_text(str(id)) - label_id.set_visible(False) - label_id.set_no_show_all(True) - vbox.pack_end(label_id, False, False, 0) - - # Checkbox - checkbox = Gtk.CheckButton() - checkbox.set_visible(False) - checkbox.set_no_show_all(True) - checkbox.connect("toggled", self.select_application) - hbox.pack_start(checkbox, False, True, 6) - self.checkboxes.append(checkbox) - - # Provider logo - provider_logo = self.app.provider.get_provider_image(logo) - hbox.pack_start(provider_logo, False, True, 6) - - # Provider name - application_name = Gtk.Label(xalign=0) - application_name.get_style_context().add_class("application-name") - application_name.set_text(name) - hbox.pack_start(application_name, True, True, 6) - # Copy button - copy_event = Gtk.EventBox() - copy_button = Gtk.Image(xalign=0) - copy_button.set_from_icon_name("edit-copy-symbolic", - Gtk.IconSize.SMALL_TOOLBAR) - copy_button.set_tooltip_text("Copy the generated code..") - copy_event.connect("button-press-event", self.copy_code) - copy_event.add(copy_button) - hbox.pack_end(copy_event, False, True, 6) - - # Remove button - remove_event = Gtk.EventBox() - remove_button = Gtk.Image(xalign=0) - remove_button.set_from_icon_name("list-remove-symbolic", - Gtk.IconSize.SMALL_TOOLBAR) - remove_button.set_tooltip_text("Remove the source..") - remove_event.add(remove_button) - remove_event.connect("button-press-event", self.remove_application) - hbox.pack_end(remove_event, False, True, 6) - - code_label = Gtk.Label(xalign=0) - code_label.get_style_context().add_class("application-secret-code") - # TODO : show the real secret code - code_label.set_text(secret_code) - pass_box.set_no_show_all(True) - - pass_box.pack_start(code_label, False, True, 0) - - row.add(vbox) - return row - - def copy_code(self, *args): + if len(args) > 0: + row = args[0].get_parent().get_parent().get_parent() + self.listbox.select_row(row) selected_row = self.listbox.get_selected_row() label = selected_row.get_children()[0].get_children()[1].get_children() code = label[0].get_text() @@ -338,40 +295,35 @@ class TwoFactorWindow(Gtk.Window): except Exception as e: logging.error(str(e)) - def refresh_window(self, *args): + def refresh_window(self, force_refresh=False): mainbox = self.get_children()[0] - self.checkboxes = [] count = self.app.provider.count_providers() - for widget in mainbox: - mainbox.remove(widget) - self.genereate_searchbar() - self.generate_applications_list() + if count == 0: + self.get_children()[0].get_children()[0].set_no_show_all(True) + self.get_children()[0].get_children()[0].set_visible(False) + else: + self.get_children()[0].get_children()[1].set_no_show_all(True) + self.get_children()[0].get_children()[1].set_visible(False) headerbar = self.get_children()[1] left_box = headerbar.get_children()[0] right_box = headerbar.get_children()[1] right_box.get_children()[0].set_visible(count > 0) - if count == 0: - self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) - if count > 0 and self.listbox: - if self.listbox.get_selection_mode() == Gtk.SelectionMode.MULTIPLE: - left_box.get_children()[0].set_visible(count > 0) - else: - left_box.get_children()[0].set_visible(False) - self.get_children()[0].show_all() + self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) + left_box.get_children()[0].set_visible(False) - # TODO : add remove from database - def remove_application(self, *args): - confirmation = ConfirmationMessage(self, "Are you sure??") + + def remove_provider(self, *args): + if len(args) > 0: + row = args[0].get_parent().get_parent().get_parent() + self.listbox.select_row(row) + + message = "Do you really want to remove the two-factor auth provider?" + confirmation = ConfirmationMessage(self, message) confirmation.show() if confirmation.get_confirmation(): if self.listbox.get_selected_row(): selected_row = self.listbox.get_selected_row() - del self.checkboxes[selected_row.get_index()] - index = selected_row.get_index() + 1 - if index > len(self.listbox.get_children()) - 1: - index = selected_row.get_index() - 1 - self.listbox.select_row(self.listbox.get_row_at_index(index)) self.listbox.remove(selected_row) label_id = selected_row.get_children()[0].get_children()[2] self.app.provider.remove_from_database(int(label_id.get_text()))