new alpha release

This commit is contained in:
Bilal Elmoussaoui 2017-01-10 04:08:59 +01:00
parent ccaefc30ad
commit f3a94a2651
24 changed files with 836 additions and 214 deletions

0
.travis.yml Normal file
View file

14
Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM ubuntu:16.04
RUN apt-get -y update
# Install dependecies
RUN apt-get install -y python3 libzbar-dev gnome-screenshot gnome-keyring git python-gobject
RUN pip install python-pyotp python-yaml python-pillow zbarlight setuptools
RUN pip install ninja
# Install latest git version of meson
RUN git clone https://github.com/mesonbuild/meson && && cd ./meson && python3 setup.py install
# Build Gnome-TwoFactorAuth usinn Meson
RUN git clone https://github.com/bil-elmoussaoui/Gnome-TwoFactorAuth && cd ./Gnome-TwoFactorAuth
RUN mkdir build && cd ./build
RUN meson .. && ninja && sudo ninja install
CMD gnome-twofactorauth

View file

@ -26,12 +26,14 @@ from TwoFactorAuth.widgets.window import Window
from TwoFactorAuth.models.database import Database
from TwoFactorAuth.widgets.settings import SettingsWindow
from TwoFactorAuth.models.settings import SettingsReader
from TwoFactorAuth.interfaces.application_observrable import ApplicaitonObservable
from TwoFactorAuth.utils import *
import logging
import signal
from gettext import gettext as _
from os import environ as env
class Application(Gtk.Application):
win = None
alive = True
@ -46,6 +48,8 @@ class Application(Gtk.Application):
GLib.set_application_name(_("TwoFactorAuth"))
GLib.set_prgname("Gnome-TwoFactorAuth")
self.observable = ApplicaitonObservable()
self.menu = Gio.Menu()
self.db = Database()
self.cfg = SettingsReader()
@ -59,14 +63,15 @@ class Application(Gtk.Application):
cssFileName = "gnome-twofactorauth-post3.20.css"
else:
cssFileName = "gnome-twofactorauth-pre3.20.css"
cssProviderFile = Gio.File.new_for_uri('resource:///org/gnome/TwoFactorAuth/%s' % cssFileName)
cssProviderFile = Gio.File.new_for_uri(
'resource:///org/gnome/TwoFactorAuth/%s' % cssFileName)
cssProvider = Gtk.CssProvider()
screen = Gdk.Screen.get_default()
styleContext = Gtk.StyleContext()
try:
cssProvider.load_from_file(cssProviderFile)
styleContext.add_provider_for_screen(screen, cssProvider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
Gtk.STYLE_PROVIDER_PRIORITY_USER)
logging.debug("Loading css file ")
except Exception as e:
logging.error("Error message %s" % str(e))
@ -80,6 +85,8 @@ class Application(Gtk.Application):
settings_content = Gio.Menu.new()
settings_content.append_item(
Gio.MenuItem.new(_("Settings"), "app.settings"))
self.is_dark_mode_menu = Gio.MenuItem.new(_("Night mode"), "app.night_mode")
settings_content.append_item(self.is_dark_mode_menu)
settings_section = Gio.MenuItem.new_section(None, settings_content)
self.menu.append_item(settings_section)
@ -94,6 +101,10 @@ class Application(Gtk.Application):
help_section = Gio.MenuItem.new_section(None, help_content)
self.menu.append_item(help_section)
self.dark_mode_action = Gio.SimpleAction.new_stateful("night_mode", GLib.VariantType.new("b"), GLib.Variant.new_boolean(False))
self.dark_mode_action.connect("activate", self.enable_dark_mode)
self.add_action(self.dark_mode_action)
self.settings_action = Gio.SimpleAction.new("settings", None)
self.settings_action.connect("activate", self.on_settings)
self.settings_action.set_enabled(not self.locked)
@ -111,11 +122,22 @@ class Application(Gtk.Application):
action = Gio.SimpleAction.new("quit", None)
action.connect("activate", self.on_quit)
self.add_action(action)
self.refresh_menu_night_mode()
if is_gnome():
self.set_app_menu(self.menu)
logging.debug("Adding gnome shell menu")
def enable_dark_mode(self, *args):
is_dark_mode = self.cfg.read("night-mode", "preferences")
self.cfg.update("night-mode", not is_dark_mode, "preferences")
self.refresh_menu_night_mode()
def refresh_menu_night_mode(self):
is_dark_mode = self.cfg.read("night-mode", "preferences")
settings = Gtk.Settings.get_default()
settings.set_property("gtk-application-prefer-dark-theme", is_dark_mode)
self.dark_mode_action.set_state(GLib.Variant.new_boolean(is_dark_mode))
def do_activate(self, *args):
if not self.win:
self.win = Window(self)
@ -184,8 +206,7 @@ class Application(Gtk.Application):
clipboard.clear()
except Exception as e:
logging.error(str(e))
self.alive = False
signal.signal(signal.SIGINT, lambda x, y: self.alive)
self.observable.update_observers(alive=False)
if self.win:
self.win.save_window_state()
self.win.destroy()

View file

@ -0,0 +1,13 @@
from TwoFactorAuth.interfaces.observable import Observable
class AccountObservable(Observable):
def update_observers(self, *args, **kwargs):
for observer in self.observers:
observer.update(*args, **kwargs)
class AccountRowObservable(Observable):
def update_observers(self, *args, **kwargs):
for observer in self.observers:
observer.update(*args, **kwargs)

View file

@ -0,0 +1,7 @@
from TwoFactorAuth.interfaces.observable import Observable
class ApplicaitonObservable(Observable):
def update_observers(self, *args, **kwargs):
for observer in self.observers:
observer.update(*args, **kwargs)

View file

@ -0,0 +1,20 @@
class Observable(object):
def __init__(self):
self.observers = []
def register(self, observer):
if not observer in self.observers:
self.observers.append(observer)
def unregister(self, observer):
if observer in self.observers:
self.observers.remove(observer)
def unregister_all(self):
if self.observers:
del self.observers[:]
def update_observers(self, *args, **kwargs):
for observer in self.observers:
observer.update(*args, **kwargs)

View file

@ -0,0 +1,110 @@
from TwoFactorAuth.models.settings import SettingsReader
from TwoFactorAuth.models.code import Code
from TwoFactorAuth.models.database import Database
import logging
from gi.repository import GObject
from threading import Thread
from time import sleep
from TwoFactorAuth.models.observer import Observer
from TwoFactorAuth.interfaces.account_observrable import AccountObservable, AccountRowObservable
class Account(GObject.GObject, Thread, Observer):
__gsignals__ = {
'code_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
'name_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
'counter_updated': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
'removed': (GObject.SignalFlags.RUN_LAST, None, (bool,)),
}
counter_max = 30
counter = 30
alive = True
code_generated = True
def __init__(self, app, db):
Thread.__init__(self)
GObject.GObject.__init__(self)
self.db = db
cfg = SettingsReader()
self.counter_max = cfg.read("refresh-time", "preferences")
self.counter = self.counter_max
self.account_id = app[0]
self.account_name = app[1]
self.secret_code = Database.fetch_secret_code(app[2])
if self.secret_code:
self.code = Code(self.secret_code)
else:
self.code_generated = False
logging.error("Could not read the secret code,"
"the keyring keys were reset manually")
self.logo = app[3]
self.account_observerable = AccountObservable()
self.row_observerable = AccountRowObservable()
self.start()
def do_name_updated(self, *args):
self.account_observerable.update_observers(name=self.get_name())
def do_counter_updated(self, *args):
self.account_observerable.update_observers(counter=self.get_counter())
def do_code_updated(self, *args):
self.account_observerable.update_observers(code=self.get_code())
def do_removed(self, *args):
self.row_observerable.update_observers(removed=self.get_id())
def update(self, alive):
if not alive:
self.kill()
def run(self):
while self.code_generated and self.alive:
self.counter -= 1
if self.counter == 0:
self.counter = self.counter_max
self.code.update()
self.emit("code_updated", True)
self.emit("counter_updated", True)
sleep(1)
def get_id(self):
"""
Get the application id
:return: (int): row id
"""
return self.account_id
def get_name(self):
"""
Get the application name
:return: (str): application name
"""
return self.account_name
def get_logo(self):
return self.logo
def get_code(self):
return self.code.get_secret_code()
def get_counter(self):
return self.counter
def get_counter_max(self):
return self.counter_max
def kill(self):
"""
Kill the row thread once it's removed
"""
self.alive = False
def remove(self):
self.db.remove_by_id(self.get_id())
self.emit("removed", True)
def set_name(self, name):
self.db.update_name_by_id(self.get_id(), name)
self.account_name = name
self.emit("name_updated", True)

View file

@ -0,0 +1,8 @@
from abc import ABCMeta, abstractmethod
class Observer(object):
__metaclass__ = ABCMeta
@abstractmethod
def update(self, *args, **kwargs):
pass

View file

@ -39,7 +39,7 @@ def get_home_path():
return path.expanduser("~")
def get_icon(image):
def get_icon(image, size):
"""
Generate a GdkPixbuf image
:param image: icon name or image path
@ -48,15 +48,15 @@ def get_icon(image):
directory = path.join(env.get("DATA_DIR"), "applications", "images") + "/"
theme = Gtk.IconTheme.get_default()
if theme.has_icon(path.splitext(image)[0]):
icon = theme.load_icon(path.splitext(image)[0], 48, 0)
icon = theme.load_icon(path.splitext(image)[0], size, 0)
elif path.exists(directory + image):
icon = GdkPixbuf.Pixbuf.new_from_file(directory + image)
elif path.exists(image):
icon = GdkPixbuf.Pixbuf.new_from_file(image)
else:
icon = theme.load_icon("image-missing", 48, 0)
if icon.get_width() != 48 or icon.get_height() != 48:
icon = icon.scale_simple(48, 48, GdkPixbuf.InterpType.BILINEAR)
icon = theme.load_icon("image-missing", size, 0)
if icon.get_width() != size or icon.get_height() != size:
icon = icon.scale_simple(size, size, GdkPixbuf.InterpType.BILINEAR)
return icon

View file

@ -20,15 +20,13 @@
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, GLib, Pango
from TwoFactorAuth.models.code import Code
from TwoFactorAuth.models.settings import SettingsReader
from TwoFactorAuth.models.database import Database
from TwoFactorAuth.widgets.confirmation import ConfirmationMessage
from TwoFactorAuth.utils import get_icon
from threading import Thread
from time import sleep
import logging
from gettext import gettext as _
from TwoFactorAuth.models.observer import Observer
class RowEntryName(Gtk.Entry):
@ -60,34 +58,18 @@ class RowEntryName(Gtk.Entry):
def focus(self):
self.grab_focus_without_selecting()
class AccountRow(Thread, Gtk.ListBoxRow):
counter_max = 30
counter = 30
timer = 0
code = None
code_generated = True
alive = True
notification = None
def __init__(self, parent, window, app):
Thread.__init__(self)
Gtk.ListBoxRow.__init__(self)
class AccountRow(Observer):
notification = None
timer = 0
remove_timer = 0
def __init__(self, parent, window, account):
# Read default values
cfg = SettingsReader()
self.counter_max = cfg.read("refresh-time", "preferences")
self.counter = self.counter_max
self.window = window
self.parent = parent
self.id = app[0]
self.name = app[1]
self.secret_code = Database.fetch_secret_code(app[2])
if self.secret_code:
self.code = Code(self.secret_code)
else:
self.code_generated = False
logging.error(
"Could not read the secret code from, the keyring keys were reset manually")
self.logo = app[3]
self.account = account
self.account.account_observerable.register(self)
# Create needed widgets
self.code_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
self.revealer = Gtk.Revealer()
@ -95,31 +77,36 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.application_name = Gtk.Label(xalign=0)
self.code_label = Gtk.Label(xalign=0)
self.timer_label = Gtk.Label(xalign=0)
# Create the list row
self.create_row()
self.start()
self.window.connect("key-press-event", self.__on_key_press)
self.is_grid_view = isinstance(self, AccountRowGrid)
def get_id(self):
"""
Get the application id
:return: (int): row id
"""
return self.id
def update(self, *args, **kwargs):
self.set_account_code(kwargs.get("code", None))
self.set_account_name(kwargs.get("name", None))
self.set_account_counter(kwargs.get("counter", None))
def get_name(self):
"""
Get the application name
:return: (str): application name
"""
return self.name
def set_account_name(self, name):
if name:
self.application_name.props.tooltip_text = name
self.application_name .set_text(name)
def get_code(self):
return self.code
def set_account_code(self, code):
if code:
self.code_label.set_text(str(code))
def set_account_counter(self, counter):
if counter:
if self.is_grid_view:
label = str(counter)
else:
label = _("Expires in %s seconds" % str(counter))
self.timer_label.set_label(label)
def get_code_label(self):
return self.code_label
def get_name(self):
return self.account.get_name()
def get_checkbox(self):
"""
Get ListBowRow's checkbox
@ -140,21 +127,16 @@ class AccountRow(Thread, Gtk.ListBoxRow):
"""
self.revealer.set_reveal_child(not self.revealer.get_reveal_child())
def kill(self):
"""
Kill the row thread once it's removed
"""
self.alive = False
def copy_code(self, *args):
"""
Copy code shows the code box for a while (10s by default)
"""
self.timer = 0
code = self.get_code().get_secret_code()
code = self.account.get_code()
try:
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
self.window.notification.update(_('Code "%s" copied to clipboard' % str(code)))
self.window.notification.update(
_('Code "%s" copied to clipboard' % str(code)))
self.window.notification.show()
clipboard.clear()
clipboard.set_text(code, len(code))
@ -164,6 +146,13 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.revealer.set_reveal_child(True)
GLib.timeout_add_seconds(1, self.update_timer)
def update_remove_countdown(self, *args):
if self.remove_timer > 0:
self.remove_timer -= 1
if self.remove_timer == 0:
self.account.remove()
return self.timer <= self.window.notification.timeout
def update_timer(self, *args):
"""
Update timer
@ -173,6 +162,40 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.revealer.set_reveal_child(False)
return self.timer <= 10
def toggle_code(self, *args):
self.toggle_code_box()
def toggle_action_box(self, visible):
self.actions_box.set_visible(visible)
self.actions_box.set_no_show_all(not visible)
def remove(self, *args):
"""
Remove an account
"""
message = _('Do you really want to remove "%s"?' % self.account.get_name())
confirmation = ConfirmationMessage(self.window, message)
confirmation.show()
if confirmation.get_confirmation():
self.window.notification.update(_('"%s" was removed' % self.account.get_name()), self.undo_remove)
self.window.notification.show()
self.remove_timer = self.window.notification.timeout
GLib.timeout_add_seconds(1, self.update_remove_countdown)
confirmation.destroy()
self.window.refresh_window()
def undo_remove(self):
self.remove_timer = 0
self.window.notification.hide()
class AccountRowList(AccountRow, Gtk.ListBoxRow):
def __init__(self, parent, window, account):
AccountRow.__init__(self, parent, window, account)
Gtk.ListBoxRow.__init__(self)
self.create_row()
self.window.connect("key-press-event", self.__on_key_press)
def create_row(self):
"""
Create ListBoxRow
@ -191,14 +214,14 @@ class AccountRow(Thread, Gtk.ListBoxRow):
h_box.pack_start(self.checkbox, False, True, 6)
# account logo
auth_icon = get_icon(self.logo)
auth_icon = get_icon(self.account.get_logo(), 48)
auth_logo = Gtk.Image(xalign=0)
auth_logo.set_from_pixbuf(auth_icon)
h_box.pack_start(auth_logo, False, True, 6)
# Account name entry
name_entry_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.name_entry = RowEntryName(self.name)
self.name_entry = RowEntryName(self.account.get_name())
name_entry_box.pack_start(self.name_entry, False, False, 6)
h_box.pack_start(name_entry_box, False, False, 0)
name_entry_box.set_visible(False)
@ -207,8 +230,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
name_event = Gtk.EventBox()
self.application_name .get_style_context().add_class("application-name")
self.application_name.set_ellipsize(Pango.EllipsizeMode.END)
self.application_name.props.tooltip_text = self.name
self.application_name .set_text(self.name)
self.set_account_name(self.account.get_name())
name_event.connect("button-press-event", self.toggle_code)
name_event.add(self.application_name)
h_box.pack_start(name_event, False, True, 6)
@ -238,7 +260,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
edit_event = Gtk.EventBox()
self.edit_button = Gtk.Image(xalign=0)
self.edit_button.set_from_icon_name("document-edit-symbolic",
Gtk.IconSize.SMALL_TOOLBAR)
Gtk.IconSize.SMALL_TOOLBAR)
self.edit_button.set_tooltip_text(_("Edit the account"))
edit_event.add(self.edit_button)
edit_event.connect("button-press-event", self.edit)
@ -248,7 +270,7 @@ class AccountRow(Thread, Gtk.ListBoxRow):
apply_event = Gtk.EventBox()
self.apply_button = Gtk.Image(xalign=0)
self.apply_button.set_from_icon_name("emblem-ok-symbolic",
Gtk.IconSize.SMALL_TOOLBAR)
Gtk.IconSize.SMALL_TOOLBAR)
self.apply_button.set_tooltip_text(_("Save the new account name"))
apply_event.add(self.apply_button)
apply_event.connect("button-press-event", self.apply_edit_name)
@ -257,13 +279,13 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.toggle_action_box(True)
self.toggle_edit_mode(False)
self.timer_label.set_label(_("Expires in %s seconds") % self.counter)
self.set_account_counter(self.account.get_counter())
self.timer_label.get_style_context().add_class("account-timer")
self.code_label.get_style_context().add_class("account-secret-code")
if self.code_generated:
self.update_code(self.code_label)
if self.account.code_generated:
self.set_account_code(self.account.get_code())
else:
self.code_label.set_text(_("Error during the generation of code"))
self.set_account_code(_("Error during the generation of code"))
self.code_box.pack_end(self.timer_label, False, True, 6)
self.code_box.pack_start(self.code_label, False, True, 6)
@ -271,50 +293,10 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.revealer.set_reveal_child(False)
self.add(box)
def get_counter(self):
return self.counter
def toggle_code(self, *args):
self.toggle_code_box()
def toggle_action_box(self, visible):
self.actions_box.set_visible(visible)
self.actions_box.set_no_show_all(not visible)
def run(self):
while self.code_generated and self.window.app.alive and self.alive:
self.counter -= 1
if self.counter == 0:
self.counter = self.counter_max
self.regenerate_code()
if self.timer != 0:
self.timer = 0
self.update_timer_label()
self.changed()
sleep(1)
def regenerate_code(self):
label = self.code_label
if label:
self.code.update()
self.update_code(label)
def update_code(self, label):
try:
code = self.code.get_secret_code()
if code:
label.set_text(code)
else:
raise TypeError
except TypeError as e:
logging.error("Couldn't generate the secret code : %s" % str(e))
label.set_text(_("Couldn't generate the secret code"))
self.code_generated = False
def toggle_edit_mode(self, visible):
if visible:
self.name_entry.show()
self.name_entry.set_text(self.get_name())
self.name_entry.set_text(self.account.get_name())
self.name_entry.focus()
else:
self.name_entry.hide()
@ -325,11 +307,10 @@ class AccountRow(Thread, Gtk.ListBoxRow):
self.edit_button.get_parent().set_visible(not visible)
self.edit_button.get_parent().set_no_show_all(visible)
def __on_key_press(self, widget, event):
keyname = Gdk.keyval_name(event.keyval).lower()
if not self.window.is_locked():
if self.parent.get_selected_row_id() == self.get_id():
if self.parent.get_selected_row_id() == self.account.get_id():
is_search_bar = self.window.search_bar.is_visible()
is_editing_name = self.name_entry.is_visible()
if keyname == "delete" and not is_search_bar and not is_editing_name:
@ -362,25 +343,101 @@ class AccountRow(Thread, Gtk.ListBoxRow):
def apply_edit_name(self, *args):
new_name = self.name_entry.get_text()
self.application_name.set_text(new_name)
self.window.app.db.update_name_by_id(self.get_id(), new_name)
self.name = new_name
self.account.set_name(new_name)
self.toggle_edit_mode(False)
self.parent.window.refresh_window()
def remove(self, *args):
"""
Remove an account
"""
message = _('Do you really want to remove "%s"?' % self.get_name())
confirmation = ConfirmationMessage(self.window, message)
confirmation.show()
if confirmation.get_confirmation():
self.window.notification.update(_('"%s" was removed' % self.name))
self.window.notification.show()
self.kill()
self.window.accounts_list.remove(self)
self.window.app.db.remove_by_id(self.get_id())
confirmation.destroy()
self.window.refresh_window()
def update_timer_label(self):
self.timer_label.set_label(_("Expires in %s seconds") % self.counter)
class AccountRowGrid(AccountRow, Gtk.Box):
def __init__(self, parent, window, account):
AccountRow.__init__(self, parent, window, account)
Gtk.Box.__init__(self)
self.create_row()
self.window.connect("key-press-event", self.__on_key_press)
def create_row(self):
"""
Create ListBoxRow
"""
self.actions_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
v_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
# Checkbox
overlay_box = Gtk.Overlay()
checkbox_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.checkbox.set_visible(False)
self.checkbox.get_style_context().add_class("checkbutton-grid")
self.checkbox.set_no_show_all(True)
self.checkbox.connect("toggled", self.parent.select_account)
checkbox_box.pack_end(self.checkbox, False, False, 0)
overlay_box.add_overlay(checkbox_box)
# account logo
logo_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
auth_icon = get_icon(self.account.get_logo(), 96)
auth_logo = Gtk.Image(xalign=0)
auth_logo.set_from_pixbuf(auth_icon)
logo_box.add(auth_logo)
overlay_box.add(logo_box)
v_box.pack_start(overlay_box, False, False, 6)
# accout name
name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
name_event = Gtk.EventBox()
self.application_name .get_style_context().add_class("application-name-grid")
self.application_name.set_ellipsize(Pango.EllipsizeMode.END)
self.set_account_name(self.account.get_name())
name_event.connect("button-press-event", self.toggle_code)
name_event.add(self.application_name)
name_box.pack_start(name_event, True, False, 6)
v_box.pack_start(name_box, False, False, 0)
# 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)
self.actions_box.pack_end(copy_event, False, False, 6)
self.set_account_counter(self.account.get_counter())
self.timer_label.get_style_context().add_class("account-timer-grid")
self.code_label.get_style_context().add_class("account-secret-code-grid")
if self.account.code_generated:
self.set_account_code(self.account.get_code())
else:
self.set_account_code(_("Error during the generation of code"))
self.code_box.pack_end(self.timer_label, False, True, 6)
self.code_box.pack_start(self.code_label, True, False, 6)
self.revealer.add(self.code_box)
self.revealer.set_reveal_child(False)
v_box.pack_start(self.revealer, False, False, 0)
# self.pack_start(checkbox_box, False, False, 0)
self.pack_start(v_box, True, False, 0)
def __on_key_press(self, widget, event):
keyname = Gdk.keyval_name(event.keyval).lower()
if not self.window.is_locked():
if self.parent.get_selected_row_id() == self.account.get_id():
is_search_bar = self.window.search_bar.is_visible()
if keyname == "delete" and not is_search_bar:
self.remove()
return True
if keyname == "return":
self.toggle_code_box()
return True
if event.state & Gdk.ModifierType.CONTROL_MASK:
if keyname == 'c':
self.copy_code()
return True
return False

View file

@ -0,0 +1,163 @@
# -*- coding: utf-8 -*-
"""
Copyright © 2016 Bilal Elmoussaoui <bil.elmoussaoui@gmail.com>
This file is part of Gnome-TwoFactorAuth.
Gnome-TwoFactorAuth is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
TwoFactorAuth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
"""
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from TwoFactorAuth.widgets.confirmation import ConfirmationMessage
from TwoFactorAuth.widgets.account_row import AccountRowGrid
from gettext import gettext as _
from hashlib import sha256
import logging
class AccountsGrid(Gtk.FlowBox):
scrolled_win = None
def __init__(self, window, accounts):
self.accounts = accounts
self.window = window
self.generate()
self.connect("child-activated", self.activate_child)
self.connect("selected-children-changed", self.selected_child)
GLib.timeout_add_seconds(1, self.refresh)
def generate(self):
Gtk.FlowBox.__init__(self,orientation=Gtk.Orientation.HORIZONTAL)
# Create a ScrolledWindow for accounts
self.set_min_children_per_line(2)
self.set_max_children_per_line(8)
self.set_valign(Gtk.Align.START)
self.set_column_spacing(0)
self.set_row_spacing(0)
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.scrolled_win = Gtk.ScrolledWindow()
self.scrolled_win.add_with_viewport(self)
self.set_homogeneous(True)
count = len(self.accounts)
for account in self.accounts:
self.add(AccountRowGrid(self, self.window, account))
if count != 0:
self.select_child(self.get_child_at_index(0))
self.show_all()
def selected_child(self, account_list):
for box in self.get_children():
row = box.get_children()[0]
checkbutton = row.get_checkbox()
if not checkbutton.get_active() and self.window.is_select_mode:
self.unselect_child(box)
def activate_child(self, account_list, selected_child):
if self.window.is_select_mode and selected_child:
selected_box = selected_child.get_children()[0]
self.select_account(selected_box.get_checkbox())
def toggle_select_mode(self):
is_select_mode = self.window.is_select_mode
if is_select_mode:
self.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
else:
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
if len(self.get_children()) != 0:
self.select_child(self.get_child_at_index(0))
for box in self.get_children():
row = box.get_children()[0]
checkbox = row.get_checkbox()
code_label = row.get_code_label()
visible = checkbox.get_visible()
style_context = code_label.get_style_context()
if is_select_mode:
self.select_account(checkbox)
row.toggle_action_box(visible)
checkbox.set_visible(not visible)
checkbox.set_no_show_all(visible)
def remove_selected(self, *args):
"""
Remove selected accounts
"""
for row in self.get_selected_children():
checkbox = row.get_checkbox()
if checkbox.get_active():
row.remove()
self.unselect_all()
self.window.toggle_select()
self.window.refresh_window()
def select_account(self, checkbutton):
"""
Select an account
:param checkbutton:
"""
is_active = checkbutton.get_active()
is_visible = checkbutton.get_visible()
flowbox_child = checkbutton.get_parent().get_parent().get_parent().get_parent().get_parent()
if is_active:
self.select_child(flowbox_child)
else:
self.unselect_child(flowbox_child)
selected_count = len(self.get_selected_children())
self.window.hb.remove_button.set_sensitive(selected_count > 0)
def get_selected_child_id(self):
selected_box = self.get_selected_children()
if selected_box:
return selected_box[0].get_children()[0].account.get_id()
else:
return None
def get_selected_row_id(self):
return self.get_selected_child_id()
def toggle(self, visible):
self.set_visible(visible)
self.set_no_show_all(not visible)
def is_visible(self):
return self.get_visible()
def hide(self):
self.toggle(False)
def show(self):
self.toggle(True)
def get_scrolled_win(self):
return self.scrolled_win
def refresh(self):
self.scrolled_win.hide()
self.scrolled_win.show_all()
def remove_by_id(self, _id):
for row in self.get_children():
row = row.get_children()[0]
if row.account.get_id() == _id:
self.remove(row)
break
self.refresh()

View file

@ -21,7 +21,7 @@ from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from TwoFactorAuth.widgets.confirmation import ConfirmationMessage
from TwoFactorAuth.widgets.account_row import AccountRow
from TwoFactorAuth.widgets.account_row import AccountRowList
from gettext import gettext as _
from hashlib import sha256
import logging
@ -30,8 +30,8 @@ import logging
class AccountsList(Gtk.ListBox):
scrolled_win = None
def __init__(self, application, window):
self.app = application
def __init__(self, window, accounts):
self.accounts = accounts
self.window = window
self.generate()
self.window.connect("key-press-event", self.on_key_press)
@ -53,11 +53,10 @@ class AccountsList(Gtk.ListBox):
self.scrolled_win = Gtk.ScrolledWindow()
self.scrolled_win.add_with_viewport(box)
apps = self.app.db.fetch_apps()
count = len(apps)
count = len(self.accounts)
for app in apps:
self.add(AccountRow(self, self.window, app))
for account in self.accounts:
self.add(AccountRowList(self, self.window, account))
if count != 0:
self.select_row(self.get_row_at_index(0))
@ -116,14 +115,6 @@ class AccountsList(Gtk.ListBox):
checkbox.set_visible(not visible)
checkbox.set_no_show_all(visible)
def append(self, app):
"""
Add an element to the ListBox
"""
app[2] = sha256(app[2].encode('utf-8')).hexdigest()
self.add(AccountRow(self, self.window, app))
self.show_all()
def remove_selected(self, *args):
"""
Remove selected accounts
@ -154,7 +145,7 @@ class AccountsList(Gtk.ListBox):
def get_selected_row_id(self):
selected_row = self.get_selected_row()
if selected_row:
return selected_row.get_id()
return selected_row.account.get_id()
else:
return None
@ -177,3 +168,10 @@ class AccountsList(Gtk.ListBox):
def refresh(self):
self.scrolled_win.hide()
self.scrolled_win.show_all()
def remove_by_id(self, _id):
for row in self.get_children():
if row.account.get_id() == _id:
self.remove(row)
break
self.refresh()

View file

@ -9,6 +9,7 @@
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
TwoFactorAuth is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
@ -21,43 +22,85 @@ from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, Gdk, GObject, GLib
from TwoFactorAuth.widgets.accounts_list import AccountsList
from TwoFactorAuth.widgets.accounts_grid import AccountsGrid
from TwoFactorAuth.widgets.account_row import AccountRowGrid, AccountRowList
from TwoFactorAuth.widgets.search_bar import SearchBar
from TwoFactorAuth.models.account import Account
from gettext import gettext as _
from hashlib import sha256
import logging
from TwoFactorAuth.models.observer import Observer
class AccountsWindow(Gtk.Box):
class AccountsWindow(Gtk.Box, Observer):
def __init__(self, application, window):
Gtk.Box.__init__(self, orientation=Gtk.Orientation.VERTICAL)
self.app = application
self.window = window
self.scrolled_win = None
self.generate()
def generate(self):
self.generate_accounts_list()
self.generate_search_bar()
self.pack_start(self.search_bar, False, True, 0)
self.pack_start(self.scrolled_win, True, True, 0)
self.reorder_child(self.search_bar, 0)
def generate_accounts_list(self):
"""
Generate an account ListBox inside of a ScrolledWindow
"""
self.accounts_list = AccountsList(self.app, self.window)
self.scrolled_win = self.accounts_list.get_scrolled_win()
apps = self.app.db.fetch_apps()
count = len(apps)
self.accounts = []
for app in apps:
account = Account(app, self.app.db)
account.row_observerable.register(self)
self.accounts.append(account)
self.app.observable.register(account)
self.accounts_list = AccountsList(self.window, self.accounts)
self.accounts_grid = AccountsGrid(self.window, self.accounts)
self.pack_start(self.accounts_list.get_scrolled_win(), True, True, 0)
self.pack_start(self.accounts_grid.get_scrolled_win(), True, True, 0)
is_grid = self.app.cfg.read(
"view-mode", "preferences").lower() == "grid"
self.set_mode_view(is_grid)
def generate_search_bar(self):
"""
Generate search bar box and entry
"""
self.search_bar = SearchBar(self.accounts_list, self.window,
self.window.hb.search_button)
self.search_bar = SearchBar(self.window, self.window.hb.search_button,
[self.accounts_list, self.accounts_grid])
def update(self, *args, **kwargs):
removed_id = kwargs.get("removed", None)
if removed_id:
self.accounts_list.remove_by_id(removed_id)
self.accounts_grid.remove_by_id(removed_id)
def get_accounts_list(self):
return self.accounts_list
def get_accounts_grid(self):
return self.accounts_grid
def set_mode_view(self, is_grid):
if is_grid:
self.scrolled_win = self.accounts_grid.get_scrolled_win()
self.accounts_list.get_scrolled_win().set_visible(False)
self.accounts_list.get_scrolled_win().set_no_show_all(True)
self.accounts_grid.refresh()
else:
self.scrolled_win = self.accounts_list.get_scrolled_win()
self.accounts_grid.get_scrolled_win().set_no_show_all(True)
self.accounts_grid.get_scrolled_win().set_visible(False)
self.accounts_list.refresh()
self.scrolled_win.set_no_show_all(False)
self.scrolled_win.set_visible(True)
def get_search_bar(self):
return self.search_bar
@ -73,3 +116,20 @@ class AccountsWindow(Gtk.Box):
def show(self):
self.toggle(True)
def refresh(self, *args):
self.accounts_list.refresh()
self.accounts_grid.refresh()
def append(self, app):
"""
Add an element to the ListBox
"""
app[2] = sha256(app[2].encode('utf-8')).hexdigest()
account = Account(app, self.app.db)
account.row_observerable.register(self)
self.accounts.append(account)
self.app.observable.register(account)
self.accounts_list.add(AccountRowList(self.accounts_list, self.window, account))
self.accounts_grid.add(AccountRowGrid(self.accounts_grid, self.window, account))
self.show_all()

View file

@ -17,9 +17,7 @@
You should have received a copy of the GNU General Public License
along with Gnome-TwoFactorAuth. If not, see <http://www.gnu.org/licenses/>.
"""
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, Gio
import logging
from TwoFactorAuth.utils import screenshot_area, current_date_time
from TwoFactorAuth.widgets.applications_list import ApplicationChooserWindow
@ -28,6 +26,9 @@ from TwoFactorAuth.models.code import Code
from TwoFactorAuth.models.qr_reader import QRReader
from TwoFactorAuth.utils import get_icon
from gettext import gettext as _
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gdk, Gio
class AddAccount(Gtk.Window):
@ -47,7 +48,8 @@ class AddAccount(Gtk.Window):
self.generate_header_bar()
def generate_window(self):
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL, title=_("Add a new account"),
Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL,
title=_("Add a new account"),
modal=True, destroy_with_parent=True)
self.connect("delete-event", self.close_window)
self.resize(500, 400)
@ -65,7 +67,6 @@ class AddAccount(Gtk.Window):
Generate the header bar box
"""
self.hb.props.title = _("Add a new account")
left_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
@ -118,7 +119,7 @@ class AddAccount(Gtk.Window):
hbox_secret_code.pack_end(self.secret_code, False, True, 0)
hbox_secret_code.pack_end(secret_code_label, False, True, 0)
account_logo = get_icon("image-missing")
account_logo = get_icon("image-missing", 48)
self.account_image.set_from_pixbuf(account_logo)
self.account_image.get_style_context().add_class("application-logo-add")
logo_box.pack_start(self.account_image, True, False, 6)
@ -142,7 +143,9 @@ class AddAccount(Gtk.Window):
self.secret_code.set_text(data["secret"])
self.apply_button.set_sensitive(True)
else:
self.notification.update(_("Selected area is not a valid QR code"))
self.notification.update(
_("Selected area is not a valid QR code"))
self.notification.set_message_type(Gtk.MessageType.ERROR)
self.notification.show()
def on_key_press(self, key, key_event):
@ -171,7 +174,7 @@ class AddAccount(Gtk.Window):
Update image logo
"""
self.selected_logo = image
account_icon = get_icon(image)
account_icon = get_icon(image, 48)
self.account_image.clear()
self.account_image.set_from_pixbuf(account_icon)
@ -199,11 +202,11 @@ class AddAccount(Gtk.Window):
try:
self.parent.app.db.add_account(name, secret_code, logo)
uid = self.parent.app.db.get_latest_id()
self.parent.accounts_list.append([uid, name, secret_code, logo])
self.parent.accounts_box.append([uid, name, secret_code, logo])
self.parent.refresh_window()
self.close_window()
except Exception as e:
logging.error("Error in adding a new account")
logging.error("Error in addin.accounts_boxg a new account")
logging.error(str(e))
def show_window(self):

View file

@ -53,7 +53,7 @@ class ApplicationRow(Gtk.ListBoxRow):
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
# Application logo
application_logo = get_icon(self.image)
application_logo = get_icon(self.image, 48)
application_image = Gtk.Image(xalign=0)
application_image.set_from_pixbuf(application_logo)
hbox.pack_start(application_image, False, True, 6)

View file

@ -137,7 +137,7 @@ class ApplicationChooserWindow(Gtk.Window, Thread, GObject.GObject):
"""
Generate the search bar
"""
self.search_bar = SearchBar(self.listbox, self, self.search_button)
self.search_bar = SearchBar(self, self.search_button, [self.listbox])
self.main_box.pack_start(self.search_bar, False, True, 0)
def is_valid_app(self, app):

View file

@ -24,11 +24,13 @@ from TwoFactorAuth.utils import is_gnome
import logging
from gettext import gettext as _
# view-grid-symbolic
# view-list-symoblic
class HeaderBar(Gtk.HeaderBar):
search_button = Gtk.ToggleButton()
add_button = Gtk.Button()
view_mode_button = Gtk.Button()
settings_button = Gtk.Button()
remove_button = Gtk.Button()
cancel_button = Gtk.Button()
@ -86,7 +88,7 @@ class HeaderBar(Gtk.HeaderBar):
def generate_right_box(self):
count = self.app.db.count()
is_grid = self.app.cfg.read("view-mode", "preferences").lower() == "grid"
right_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
select_icon = Gio.ThemedIcon(name="object-select-symbolic")
select_image = Gtk.Image.new_from_gicon(
@ -102,10 +104,14 @@ class HeaderBar(Gtk.HeaderBar):
self.search_button.set_image(search_image)
self.toggle_search_button(count > 0)
self.set_toggle_view_mode_icon(is_grid)
self.view_mode_button.connect("clicked", self.toggle_view_mode)
self.cancel_button.set_label(_("Cancel"))
self.toggle_cancel_button(False)
right_box.add(self.search_button)
right_box.add(self.view_mode_button)
right_box.add(self.select_button)
right_box.add(self.cancel_button)
return right_box
@ -130,6 +136,25 @@ class HeaderBar(Gtk.HeaderBar):
else:
self.popover.show_all()
def toggle_view_mode(self, *args):
is_grid = self.app.cfg.read("view-mode", "preferences").lower() == "grid"
if not is_grid:
self.app.cfg.update("view-mode", "grid" , "preferences")
else:
self.app.cfg.update("view-mode", "list" , "preferences")
self.set_toggle_view_mode_icon(not is_grid)
self.window.toggle_view_mode(not is_grid)
def set_toggle_view_mode_icon(self, is_grid):
if not is_grid:
view_mode_icon = Gio.ThemedIcon(name="view-grid-symbolic")
self.view_mode_button.set_tooltip_text(_("Grid mode"))
else:
view_mode_icon = Gio.ThemedIcon(name="view-list-symbolic")
self.view_mode_button.set_tooltip_text(_("List mode"))
view_mode_image = Gtk.Image.new_from_gicon(view_mode_icon, Gtk.IconSize.BUTTON)
self.view_mode_button.set_image(view_mode_image)
def toggle_select_mode(self):
is_select_mode = self.window.is_select_mode
pass_enabled = self.app.cfg.read("state", "login")
@ -180,6 +205,10 @@ class HeaderBar(Gtk.HeaderBar):
self.remove_button.set_visible(visible)
self.remove_button.set_no_show_all(not visible)
def toggle_view_mode_button(self, visible):
self.view_mode_button.set_visible(visible)
self.view_mode_button.set_no_show_all(not visible)
def hide(self):
self.toggle_add_button(False)
self.toggle_lock_button(False)
@ -188,6 +217,7 @@ class HeaderBar(Gtk.HeaderBar):
self.toggle_search_button(False)
self.toggle_settings_button(True)
self.toggle_select_button(False)
self.toggle_view_mode_button(False)
def refresh(self):
is_locked = self.app.locked
@ -202,6 +232,7 @@ class HeaderBar(Gtk.HeaderBar):
self.toggle_select_button(False)
self.toggle_remove_button(False)
self.toggle_search_button(False)
self.toggle_view_mode_button(False)
self.toggle_lock_button(can_be_locked)
self.toggle_settings_button(True)
self.toggle_cancel_button(False)
@ -210,6 +241,7 @@ class HeaderBar(Gtk.HeaderBar):
self.toggle_select_button(True)
self.toggle_remove_button(False)
self.toggle_search_button(True)
self.toggle_view_mode_button(True)
self.toggle_lock_button(can_be_locked)
self.toggle_settings_button(True)
self.toggle_cancel_button(False)

View file

@ -1,8 +1,8 @@
from gettext import gettext as _
import logging
from gi import require_version
require_version("Gtk", "3.0")
from gi.repository import Gtk, Gio, GLib
from gettext import gettext as _
import logging
class InAppNotification(Gtk.Revealer):
@ -19,35 +19,27 @@ class InAppNotification(Gtk.Revealer):
def generate_components(self):
self.get_style_context().add_class("top")
frame = Gtk.Frame()
self.main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
frame.props.width_request = 100
frame.get_style_context().add_class("app-notification")
self.infobar = Gtk.InfoBar()
self.infobar.set_show_close_button(True)
self.infobar.connect("response", self.response)
self.infobar.set_default_response(Gtk.ResponseType.CLOSE)
self.infobar.set_message_type(Gtk.MessageType.INFO)
self.undo_button = Gtk.Button()
self.undo_button.set_label(_("Undo"))
if self.undo_action is not None:
self.undo_button.connect("clicked", self.undo_action)
else:
self.undo_button.set_visible(False)
self.undo_button.set_no_show_all(True)
delete_button = Gtk.Button()
delete_icon = Gio.ThemedIcon(name="edit-delete-symbolic")
delete_image = Gtk.Image.new_from_gicon(
delete_icon, Gtk.IconSize.BUTTON)
delete_button.set_tooltip_text(_("Hide notification"))
delete_button.set_image(delete_image)
delete_button.connect("clicked", self.on_hide_notification)
content_area = self.infobar.get_content_area()
action_area = self.infobar.get_action_area()
self.undo_button = None
if self.undo_action:
self.undo_button = self.infobar.add_button(
_("Undo"), Gtk.ResponseType.CANCEL)
self.message_label = Gtk.Label()
self.message_label.set_text(self.message)
self.main_box.pack_end(delete_button, False, False, 6)
self.main_box.pack_end(self.undo_button, False, False, 6)
self.main_box.pack_start(self.message_label, False, False, 6)
frame.add(self.main_box)
self.add(frame)
content_area.add(self.message_label)
self.add(self.infobar)
def set_message_type(self, message_type):
self.infobar.set_message_type(message_type)
def on_hide_notification(self, *args):
self.hide()
@ -58,14 +50,27 @@ class InAppNotification(Gtk.Revealer):
def hide(self):
self.set_reveal_child(False)
def update(self, message, undo_action = None):
def update(self, message, undo_action=None):
self.message_label.set_text(message)
self.timer = 0
if undo_action is not None:
self.undo_button.set_visible(True)
self.undo_button.set_no_show_all(False)
if undo_action:
if not self.undo_button:
self.undo_button = self.infobar.add_button(
_("Undo"), Gtk.ResponseType.CANCEL)
else:
self.undo_button.set_visible(True)
self.undo_button.set_no_show_all(False)
self.undo_action = undo_action
self.undo_button.connect("clicked", self.undo_action)
else:
if self.undo_button:
self.undo_button.set_visible(False)
self.undo_button.set_no_show_all(True)
def response(self, infobar, response_id):
if response_id == Gtk.ResponseType.CLOSE:
self.hide()
elif response_id == Gtk.ResponseType.CANCEL:
self.undo_action()
def update_timer(self):
if self.get_reveal_child():

View file

@ -25,9 +25,9 @@ import logging
class SearchBar(Gtk.Revealer):
def __init__(self, listbox, window, search_button):
def __init__(self, window, search_button, *args):
self.search_entry = Gtk.SearchEntry()
self.listbox = listbox
self.search_list = args[0]
self.search_button = search_button
self.window = window
self.generate()
@ -49,10 +49,10 @@ class SearchBar(Gtk.Revealer):
def toggle(self, *args):
if self.is_visible():
self.set_reveal_child(False)
self.search_entry.set_text("")
self.listbox.set_filter_func(lambda x, y, z: True,
None, False)
self.set_reveal_child(False)
for search_list in self.search_list:
search_list.set_filter_func(lambda x, y, z: True, None, False)
else:
self.set_reveal_child(True)
self.focus()
@ -61,6 +61,8 @@ class SearchBar(Gtk.Revealer):
"""
Filter function, used to check if the entered data exists on the application ListBox
"""
if isinstance(row, Gtk.FlowBoxChild):
row = row.get_children()[0]
app_label = row.get_name()
data = data.lower()
if len(data) > 0:
@ -101,4 +103,5 @@ class SearchBar(Gtk.Revealer):
def filter_applications(self, entry):
data = entry.get_text().strip()
self.listbox.set_filter_func(self.filter_func, data, False)
for search_list in self.search_list:
search_list.set_filter_func(self.filter_func, data, False)

View file

@ -57,7 +57,7 @@ class Window(Gtk.ApplicationWindow):
self.set_icon_name("Gnome-TwoFactorAuth")
self.resize(500, 650)
self.set_size_request(500, 650)
self.set_resizable(False)
self.set_resizable(True)
self.connect("key_press_event", self.on_key_press)
self.connect("delete-event", lambda x, y: self.app.on_quit())
self.notification = InAppNotification()
@ -107,6 +107,7 @@ class Window(Gtk.ApplicationWindow):
def generate_accounts_box(self):
self.accounts_box = AccountsWindow(self.app, self)
self.accounts_list = self.accounts_box.get_accounts_list()
self.accounts_grid = self.accounts_box.get_accounts_grid()
self.hb.remove_button.connect(
"clicked", self.accounts_list.remove_selected)
self.search_bar = self.accounts_box.get_search_bar()
@ -130,6 +131,10 @@ class Window(Gtk.ApplicationWindow):
add_account = AddAccount(self)
add_account.show_window()
def toggle_view_mode(self, is_grid):
self.accounts_box.set_mode_view(is_grid)
self.refresh_window()
def toggle_select(self, *args):
"""
Toggle select mode
@ -137,6 +142,7 @@ class Window(Gtk.ApplicationWindow):
self.is_select_mode = not self.is_select_mode
self.hb.toggle_select_mode()
self.accounts_list.toggle_select_mode()
self.accounts_grid.toggle_select_mode()
def generate_no_accounts_box(self):
"""
@ -150,6 +156,8 @@ class Window(Gtk.ApplicationWindow):
Refresh windows components
"""
count = self.app.db.count()
self.accounts_grid.show_all()
self.accounts_list.show_all()
if self.is_locked():
self.login_box.show()
self.no_account_box.hide()

View file

@ -4,10 +4,13 @@
.application-list-row:selected GtkImage {
color: @theme_fg_selected_color;
}
.application-name {
.application-name,
.application-name-grid {
font-weight: bold;
font-size: 14px;
}
.application-logo-add {
margin-right: 5px;
margin-bottom: 10px;
@ -19,9 +22,21 @@
margin-bottom: 5px;
margin-left: 60px;
}
.account-secret-code-grid,
.account-timer-grid {
font-size: 18px;
margin-top: 10px;
margin-bottom: 5px;
}
.application-secret-code-select-mode {
margin-left: 98px;
}
.choose-popover GtkLabel {
padding: 10px;
}
.checkbutton-grid{
color:@theme_fg_color;
}

View file

@ -36,6 +36,20 @@
Default window postiton on y axes
</description>
</key>
<key name="view-mode" type="s">
<default>"list"</default>
<summary>Default view mode grid/list</summary>
<description>
The way the generated codes will be shown list/grid mode
</description>
</key>
<key name="night-mode" type="b">
<default>false</default>
<summary>Night mode</summary>
<description>
Enable/disable night mode within the application
</description>
</key>
</schema>
<schema path="/org/gnome/TwoFactorAuth/login/" id="org.gnome.TwoFactorAuth.login" gettext-domain="TwoFactorAuth">
<key name="password" type="s">

View file

@ -0,0 +1,68 @@
{
"app-id": "org.gnome.TwoFactorAuth",
"branch": "0.1.1",
"runtime": "org.gnome.Platform",
"runtime-version": "3.22",
"sdk": "org.gnome.Sdk",
"command": "gnome-twofactorauth",
"copy-icon": true,
"separate-locales": false,
"no-debuginfo": true,
"desktop-file-name-prefix": "",
"finish-args": [
"--socket=x11",
"--socket=wayland",
"--filesystem=host",
"--talk-name=org.freedesktop.Notifications",
"--talk-name=org.freedesktop.secrets",
"--own-name=org.gnome.TwoFactorAuth"
],
"modules": [{
"name" :"python-pyotp",
"no-autogen": true,
"sources": [{
"type": "archive",
"url": "https://pypi.python.org/packages/42/71/85c28b2d373eee3592d6f079e556df4a28d5c133c2ba484d7311dc9a746c/pylast-1.6.0.tar.gz",
"sha256": "6bf325ee0fdeb35780554843cf64df99304abb98c5ce2e451c0df7e95e08b42e"
},{
"type": "file",
"path": "pip-Makefile",
"dest-filename": "Makefile"
}],
"modules": [{
"name": "python-yaml",
"no-autogen": true,
"sources": [{
"type": "archive",
"url": "https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz#md5=34eed507548117b2ab523ab14b2f8b55",
"sha256": "105f8d68616f8248e24bf0e9372ef04d3cc10104f1980f54d57b2ce73a5ad56a"
},{
"type": "file",
"path": "pip-Makefile",
"dest-filename": "Makefile"
}]
}]
},{
"name": "python-pillow",
"no-autogen": true,
"sources": [{
"type": "archive",
"url": "https://pypi.python.org/packages/d9/03/155b3e67fe35fe5b6f4227a8d9e96a14fda828b18199800d161bcefc1359/requests-2.12.3.tar.gz#md5=0cafdecd95a46b081dd5fcfa4978db7a",
"sha256": "de5d266953875e9647e37ef7bfe6ef1a46ff8ddfe61b5b3652edf7ea717ee2b2"
},{
"type": "file",
"path": "pip-Makefile",
"dest-filename": "Makefile"
}]
},{
"name": "lollypop",
"make-install-args": ["prefix=/app"],
"no-autogen": true,
"sources": [{
"type": "archive",
"url": "file:///tmp/lollypop-0.9.223.tar.xz",
"sha256": "d1cab2e73fd223387f518573b1ccd7d5fca7bd5f48bbdc7b1afe43d141fa0649"
}]
}]
}

View file

@ -73,6 +73,9 @@ if __name__ == "__main__":
about_dialog.destroy()
sys.exit()
else:
app = application.Application()
exit_status = app.run(None)
sys.exit(exit_status)
try:
app = application.Application()
exit_status = app.run(None)
sys.exit(exit_status)
except KeyboardInterrupt:
exit()