mirror of
https://gitlab.gnome.org/World/Authenticator.git
synced 2025-03-04 08:44:40 +01:00
new alpha release
This commit is contained in:
parent
ccaefc30ad
commit
f3a94a2651
24 changed files with 836 additions and 214 deletions
0
.travis.yml
Normal file
0
.travis.yml
Normal file
14
Dockerfile
Normal file
14
Dockerfile
Normal 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
|
|
@ -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()
|
||||
|
|
13
TwoFactorAuth/interfaces/account_observrable.py
Normal file
13
TwoFactorAuth/interfaces/account_observrable.py
Normal 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)
|
7
TwoFactorAuth/interfaces/application_observrable.py
Normal file
7
TwoFactorAuth/interfaces/application_observrable.py
Normal 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)
|
20
TwoFactorAuth/interfaces/observable.py
Normal file
20
TwoFactorAuth/interfaces/observable.py
Normal 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)
|
110
TwoFactorAuth/models/account.py
Normal file
110
TwoFactorAuth/models/account.py
Normal 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)
|
8
TwoFactorAuth/models/observer.py
Normal file
8
TwoFactorAuth/models/observer.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
class Observer(object):
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abstractmethod
|
||||
def update(self, *args, **kwargs):
|
||||
pass
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
163
TwoFactorAuth/widgets/accounts_grid.py
Normal file
163
TwoFactorAuth/widgets/accounts_grid.py
Normal 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()
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
68
flatpak/org.gnome.TwoFactorAuth.json
Normal file
68
flatpak/org.gnome.TwoFactorAuth.json
Normal 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"
|
||||
}]
|
||||
}]
|
||||
}
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue