class VentanaPrincipal(QTabWidget): def __init__(self): super(VentanaPrincipal, self).__init__() self.ui = Ui_TabWidget() self.ui.setupUi(self) # DB self.db = QSqlDatabase.addDatabase("QSQLITE") self.db.setDatabaseName("cuentas.db") self.conector = QSqlDatabase.database() self.general_query = QSqlQuery() self.organizar_db() # Querys para psw_tool self.master_query = QSqlQuery() self.save_query = QSqlQuery() self.passwords_query_retrieve = QSqlQuery() self.passwords_query_write = QSqlQuery() self.popular = QSqlQuery() self.comboBoxes_query_categoria = QSqlQuery() self.comboBoxes_query_mail = QSqlQuery() self.comboBoxes_query_usuario = QSqlQuery() self.verificar_columna_contrasenas = QSqlQuery() self.filtro = QSqlQuery() # Querys para pswItemDelegate self.all_data = QSqlQuery() # BASE DE DATOS en UI self.model = QSqlTableModel() # pswItemDelegate.CustomSqlModel() self.organizar_tabla_ui() self.model.setEditStrategy(QSqlTableModel.OnManualSubmit) # Va aca abajo por self.model.setTable('passwords') # Para cuando se cambian datos self.model.dataChanged.connect(self.celdas_cambiadas) self.ui.tabla_db.setContextMenuPolicy(Qt.CustomContextMenu) self.ui.tabla_db.customContextMenuRequested.connect(self.context_menu) # Filtro DB # .connect(lambda: self. self.tabBarClicked.connect(self.actualizar_tabs) # Iconos DB en UI self.icon_seguridad = QIcon() self.icon_seguridad.addPixmap(QPixmap(":/media/iconografia/locked.png"), QIcon.Normal, QIcon.Off) self.icon_desbloqueado = QIcon() self.icon_desbloqueado.addPixmap(QPixmap(":/media/iconografia/unlocked.png"), QIcon.Normal, QIcon.Off) self.icon_editar = QIcon() self.icon_editar.addPixmap(QPixmap(":/media/iconografia/pencil.png"), QIcon.Normal, QIcon.Off) self.icon_guardar = QIcon() self.icon_guardar.addPixmap(QPixmap(":/media/iconografia/broken-pencil.png"), QIcon.Normal, QIcon.Off) # Procesos iniciales self.cargar_config() self.master_key = None self.seguridad_alterada = False self.candado = "cerrado" self.modo_boton_editar_guardar = "editar" self.contrasenas_nuevas = {} self.edits = {} self.revisar_columna_contrasenas() self.cargar_opciones_combo_boxes() # Alertas # Iconos para las alertas sin UI self.icon_ventana = QIcon() self.icon_ventana.addPixmap(QPixmap(":/media/imagenes/main_frame.png"), QIcon.Normal, QIcon.Off) self.alerta_config = QMessageBox(QMessageBox.Warning, "Problema con la configuración actual", "Existen dos posibilidades para este error:\n\n1. El archivo de configuración está dañando\n2. Usted tiene todas las opciones desmarcadas (el amor no cuenta)\n\nPara solucionarlo, borre el archivo de configuración ('opciones.ini'),\no marque alguna opción en la pestaña de configuración y guarde su selección\n" ) self.alerta_master_psw_mala = QMessageBox(QMessageBox.Warning, "Problema con la contraseña ingresada", "Por favor tome precauciones con la elección de la contraseña maestra.\nPara poder proseguir debe ingresar una contraseña con más de 5 y menos de 17 caracteres, o sino presione cancelar." ) self.alerta_master_psw_incorrecta = QMessageBox(QMessageBox.Warning, "Problema con la contraseña ingresada", "La contraseña que ingresaste es incorrecta" ) self.alerta_guardado_exitoso = QMessageBox(QMessageBox.Information, "Información guardada", "Toda la información que ingresaste se guardó con éxito." ) self.alerta_config.setWindowIcon(self.icon_ventana) self.alerta_master_psw_mala.setWindowIcon(self.icon_ventana) self.alerta_master_psw_incorrecta.setWindowIcon(self.icon_ventana) self.alerta_guardado_exitoso.setWindowIcon(self.icon_ventana) # Alertas con su propia UI # Dialog info self.dialogo_info = QDialog() self.info_app = Ui_dialogo_info() self.info_app.setupUi(self.dialogo_info) # Alerta master psw self.dialogo_master_psw = QDialog() self.alerta_master_psw = Ui_dialogo_master_psw() self.alerta_master_psw.setupUi(self.dialogo_master_psw) # Botones self.ui.boton_guardar_config.clicked.connect(self.guardar_config) self.ui.boton_info.clicked.connect(self.cargar_info) self.ui.boton_generar.clicked.connect(self.llamar_generar_contrasena) # Boton generar contrasñea self.ui.boton_guardar.clicked.connect(self.guardar_contrasena) # boton guardar data # Si presionan el boton revelar contraseña self.ui.reveal_psw.clicked.connect(self.mostrar_contrasena) self.alerta_master_psw.reveal_master_psw.clicked.connect(self.mostrar_contrasena_maestra) # Icono del boton revelar contraseña self.icon_not_view = QIcon() self.icon_not_view.addPixmap(QPixmap(":/media/iconografia/not_view.png"), QIcon.Normal, QIcon.Off) self.icon_view = QIcon() self.icon_view.addPixmap(QPixmap(":/media/iconografia/view.png"), QIcon.Normal, QIcon.Off) # Click en botones copiar # Otra manera de hacerlo: partial(self.llamar_copiar(n)) using functools.partial self.ui.cp1.clicked.connect(lambda: self.llamar_copiar(1)) self.ui.cp2.clicked.connect(lambda: self.llamar_copiar(2)) self.ui.cp3.clicked.connect(lambda: self.llamar_copiar(3)) # Botones DB self.ui.boton_filtro.clicked.connect(lambda: self.filtrar()) self.ui.boton_editar.clicked.connect(lambda: self.gestor_boton_editar_guardar()) self.ui.boton_seguridad.clicked.connect(lambda: self.mostrar_contrasenas()) # Botones Info self.info_app.boton_steam.clicked.connect(lambda: backend.abrir_link("https://steamcommunity.com/id/JosephKm")) self.info_app.boton_discord.clicked.connect(lambda: backend.abrir_link("https://discord.gg/wYuXPQS")) self.info_app.boton_github.clicked.connect( lambda: backend.abrir_link("https://github.com/kuttz-dev/Password-manager")) # Si se presiona la pestaña de configuracion # self.ui.tab_3.connect(self.cargar_config) # SETEAR COMBOBOXES self.ui.comboBox_usuario.setInsertPolicy(QComboBox.InsertAlphabetically) self.ui.comboBox_mail.setInsertPolicy(QComboBox.InsertAlphabetically) self.ui.comboBox_categoria.setInsertPolicy(QComboBox.InsertAlphabetically) self.ui.comboBox_usuario.setDuplicatesEnabled(False) self.ui.comboBox_mail.setDuplicatesEnabled(False) self.ui.comboBox_categoria.setDuplicatesEnabled(False) self.ui.comboBox_usuario.clearEditText() self.ui.comboBox_mail.clearEditText() self.ui.comboBox_categoria.clearEditText() def organizar_db(self): self.general_query.exec_( 'CREATE TABLE IF NOT EXISTS passwords (id INTEGER PRIMARY KEY ASC, categoria TEXT, icono BLOB, servicio TEXT, mail TEXT, usuario TEXT, contraseña_encriptada BLOB);' ) self.general_query.exec_( 'CREATE TABLE IF NOT EXISTS maestra (id INTEGER PRIMARY KEY, muestra BLOB);' ) self.general_query.exec_( 'DELETE FROM passwords WHERE usuario = "" AND mail = "" AND contraseña_encriptada = ""' ) self.db.commit() def borrar_columna_contrasenas(self): self.general_query.exec_( 'DROP TABLE IF EXISTS temporal' ) self.general_query.exec_( 'CREATE TABLE temporal (id INTEGER PRIMARY KEY ASC, categoria TEXT, icono BLOB, servicio TEXT, mail TEXT, usuario TEXT, contraseña_encriptada BLOB);' ) self.popular.exec_( 'INSERT INTO temporal(id, categoria, icono, servicio, mail, usuario, contraseña_encriptada) SELECT id, categoria, icono, servicio, mail, usuario, contraseña_encriptada FROM passwords' ) self.db.commit() self.general_query.exec_( 'DROP TABLE IF EXISTS passwords' ) self.popular.exec_( 'ALTER TABLE temporal RENAME TO passwords' ) self.general_query.exec_( 'DROP TABLE IF EXISTS temporal' ) self.db.commit() def cargar_config(self): largo, mayus, minus, numeros, special, icono = backend.obtener_cfg() self.ui.spinBox_largo.setProperty("value", int(largo)) self.ui.check_mayus.setChecked(backend.string2bool(mayus)) self.ui.check_min.setChecked(backend.string2bool(minus)) self.ui.check_numeros.setChecked(backend.string2bool(numeros)) self.ui.check_caracteres.setChecked(backend.string2bool(special)) self.ui.check_amor.setChecked(backend.string2bool(icono)) def guardar_config(self): hay_opcion_verdadera = False largo = self.ui.spinBox_largo.value() mayus = self.ui.check_mayus.checkState() minus = self.ui.check_min.checkState() numeros = self.ui.check_numeros.checkState() special = self.ui.check_caracteres.checkState() icono = self.ui.check_amor.checkState() estado_checkeado = [mayus, minus, numeros, special, icono] for i in range(len(estado_checkeado)): if str(estado_checkeado[i]) == "PySide2.QtCore.Qt.CheckState.Checked": if i != 4: # len de estado_checkeado - 1 hay_opcion_verdadera = True estado_checkeado[i] = True else: estado_checkeado[i] = False if hay_opcion_verdadera is False: return self.alerta_config.exec() backend.generar_cfg(largo, estado_checkeado[0], estado_checkeado[1], estado_checkeado[2], estado_checkeado[3], estado_checkeado[4] ) self.cargar_config() def cargar_info(self): return self.dialogo_info.exec() def llamar_generar_contrasena(self): # Primero obtenemos que tipo de contraseña quiere el usuario largo, mayus, minus, numeros, special, icono = backend.obtener_cfg() mayus = backend.string2bool(mayus) minus = backend.string2bool(minus) numeros = backend.string2bool(numeros) special = backend.string2bool(special) if mayus is False and minus is False and numeros is False and special is False: self.alerta_config.exec() texto_contrasena = backend.generar_contraseña(int(largo), mayus, minus, numeros, special) self.ui.input_psw.setText(str(texto_contrasena)) # Ponemos la contraseña en la aplicacion def mostrar_contrasena(self): # Si esta en password a normal y viceversa if self.ui.input_psw.echoMode() == QLineEdit.EchoMode.Password: self.ui.reveal_psw.setIcon(self.icon_not_view) self.ui.input_psw.setEchoMode(QLineEdit.Normal) else: self.ui.reveal_psw.setIcon(self.icon_view) self.ui.input_psw.setEchoMode(QLineEdit.Password) def mostrar_contrasena_maestra(self, modo_echo=False): # Si no se cambio el estado va a estar en False y se va a usar el echoMode del input if modo_echo is False: modo_echo = self.alerta_master_psw.input_master_psw.echoMode() # Si esta en password a normal y viceversa if modo_echo == QLineEdit.EchoMode.Password: self.alerta_master_psw.reveal_master_psw.setIcon(self.icon_not_view) self.alerta_master_psw.input_master_psw.setEchoMode(QLineEdit.Normal) else: self.alerta_master_psw.reveal_master_psw.setIcon(self.icon_view) self.alerta_master_psw.input_master_psw.setEchoMode(QLineEdit.Password) def llamar_copiar(self, numero_boton): if numero_boton == 1: backend.copiar(str(self.ui.input_psw.text())) if numero_boton == 2: backend.copiar(str(self.ui.comboBox_usuario.currentText())) if numero_boton == 3: backend.copiar(str(self.ui.comboBox_mail.currentText())) def preparar_favicon(self, url): try: archivo_ico = backend.descargar_favico(url) except Exception: # Si lo que se ingreso era un link pero no se consiguio favicon with open("media/favicons/domain.ico") as ico: return QByteArray(ico.read()) # Si no se consiguio la imagen if archivo_ico is None: return None with open(archivo_ico, "rb") as ico: return QByteArray(ico.read()) def guardar_contrasena(self): # self.ui.comboBox_usuario.currentText() / self.ui.comboBox_mail.currentText() # self.ui.comboBox_categoria.currentText() / self.ui.input_url.text() # self.ui.input_psw.text() if self.master_key is None: try: self.master_key = self.pedir_contrasena_maestra() except Exception: return if self.ui.input_psw.text() != "": contrasena_ingresada_encriptada = QByteArray(pswCrypto.encriptar(self.ui.input_psw.text(), self.master_key)) else: contrasena_ingresada_encriptada = "" try: fav_icon = self.preparar_favicon(self.ui.input_url.text()) self.save_query.prepare( 'INSERT INTO passwords (categoria, icono, servicio, mail, usuario, contraseña_encriptada) VALUES(?,?,?,?,?,?)' ) self.save_query.addBindValue(self.ui.comboBox_categoria.currentText()) self.save_query.addBindValue(fav_icon) self.save_query.addBindValue(self.ui.input_url.text()) self.save_query.addBindValue(self.ui.comboBox_mail.currentText()) self.save_query.addBindValue(self.ui.comboBox_usuario.currentText()) self.save_query.addBindValue(contrasena_ingresada_encriptada) self.save_query.exec_() self.db.commit() self.model.select() self.ui.tabla_db.resizeColumnsToContents() self.cargar_opciones_combo_boxes() return self.alerta_guardado_exitoso.exec() except Exception as ex: template = "An exception of type {0} occurred. Arguments:\n{1!r}" message = template.format(type(ex).__name__, ex.args) print(message) def pedir_contrasena_maestra(self): self.dialogo_master_psw.exec() contrasena_maestra = self.alerta_master_psw.input_master_psw.text() # Borrarmos el texto porque no se borra solo, y lo volvemos secreto de nuevo self.alerta_master_psw.input_master_psw.setText("") self.mostrar_contrasena_maestra(QLineEdit.EchoMode.Normal) # Si le dio a cancelar if bool(self.dialogo_master_psw.result()) is False: raise Exception("Accion canelada") # Comprobamos que cumpla requisitos if contrasena_maestra == "" or len(contrasena_maestra) < 6 or len(contrasena_maestra) > 16: self.alerta_master_psw_mala.exec() return self.pedir_contraseña_maestra() # Encriptacion key_contrasena_maestra = pswCrypto.generar_key(contrasena_maestra) # La convertimos en una key # Verificacion # Obtenemos la muestra guardada en la db muestra_db = backend.obtener_muestra_db() # Si no habia guardamos una nueva con esta contraseña maestra if muestra_db is None: array = QByteArray(backend.generar_muestra(key_contrasena_maestra)) self.master_query.prepare("INSERT INTO maestra (id, muestra) VALUES(1, ?)") self.master_query.addBindValue(array) self.master_query.exec_() return key_contrasena_maestra else: # Ahora si verificamos psw_correcta = backend.verificar_key(muestra_db, key_contrasena_maestra) if psw_correcta is True: return key_contrasena_maestra else: self.alerta_master_psw_incorrecta.exec_() raise Exception("Contraseña maestra incorrecta") def filtrar(self): if self.ui.input_filtro.text() == "": self.model.setFilter("") return self.model.select() else: self.model.setFilter( "{} LIKE '{}%'".format(self.ui.combobox_filtro.currentText().lower(), self.ui.input_filtro.text()) ) return self.model.select() def mostrar_contrasenas(self): if self.master_key is None: try: self.master_key = self.pedir_contrasena_maestra() except Exception: return if self.candado == "abierto": # Si esta abierto self.borrar_columna_contrasenas() self.ui.combobox_filtro.removeItem(4) self.organizar_tabla_ui() self.ui.boton_seguridad.setIcon(self.icon_seguridad) self.candado = "cerrado" # lo cerramos return else: # Si estaba cerrado self.candado = "abierto" # lo abrimos # y se abre asi: self.ui.boton_seguridad.setIcon(self.icon_desbloqueado) # Conseguimos las contraseñas encriptadas self.passwords_query_retrieve.exec_('SELECT id, contraseña_encriptada FROM passwords') # Creamos una columna para las contraseñas descifradas self.general_query.exec_('ALTER TABLE passwords ADD contraseña TEXT') self.db.commit() # Para cada contraseña while self.passwords_query_retrieve.next(): # Si se guardo un texto vacio contrasena_descifrada = self.passwords_query_retrieve.value(1) if type(contrasena_descifrada) != str: contrasena_descifrada = pswCrypto.descifrar(contrasena_descifrada.data(), self.master_key) self.passwords_query_write.prepare('UPDATE passwords SET contraseña = ? WHERE id= ?') self.passwords_query_write.addBindValue(contrasena_descifrada) self.passwords_query_write.addBindValue(self.passwords_query_retrieve.value(0)) self.passwords_query_write.exec_() self.ui.combobox_filtro.addItem("Contraseña") self.organizar_tabla_ui() # Las hacemos visibles el la tabla def actualizar_tabs(self, index): if index == 0: self.resize(437, 200) self.cargar_opciones_combo_boxes() if index == 1 and self.ui.boton_seguridad.isEnabled() is True: self.organizar_tabla_ui() elif index == 2: self.resize(437, 200) self.cargar_config() def organizar_tabla_ui(self): self.model.setTable('passwords') self.model.setSort(1, Qt.AscendingOrder) self.model.select() self.ui.tabla_db.setModel(self.model) self.ui.tabla_db.setItemDelegateForColumn(2, pswItemDelegate.ImageDelegate(self.ui.tabla_db)) self.ui.tabla_db.hideColumn(0) # Escondemos id self.ui.tabla_db.hideColumn(6) # Escondemos las contraseñas encriptadas self.ui.tabla_db.setWindowTitle("Lista de cuentas") # Tamaño columnas #self.ui.tabla_db.resizeColumnsToContents() self.ui.tabla_db.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.ui.tabla_db.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed) # iconos self.ui.tabla_db.setColumnWidth(2, 32) # tamaño iconos self.ui.tabla_db.verticalHeader().setVisible(False) self.ui.tabla_db.setSortingEnabled(True) def cargar_opciones_combo_boxes(self): a = self.ui.comboBox_categoria.currentText() b = self.ui.comboBox_mail.currentText() c = self.ui.comboBox_usuario.currentText() self.comboBoxes_query_categoria.exec_( 'SELECT DISTINCT categoria FROM passwords' ) self.comboBoxes_query_mail.exec_( 'SELECT DISTINCT mail FROM passwords' ) self.comboBoxes_query_usuario.exec_( 'SELECT DISTINCT usuario FROM passwords' ) # Borramos las anteriores self.ui.comboBox_usuario.clear() self.ui.comboBox_mail.clear() self.ui.comboBox_categoria.clear() # Cargamos las nuevas while self.comboBoxes_query_categoria.next(): if self.comboBoxes_query_categoria.value(0) == "" or self.comboBoxes_query_categoria.value(0) is None: continue self.ui.comboBox_categoria.addItem(self.comboBoxes_query_categoria.value(0)) while self.comboBoxes_query_mail.next(): if self.comboBoxes_query_mail.value(0) == "" or self.comboBoxes_query_mail.value(0) is None: continue self.ui.comboBox_mail.addItem(self.comboBoxes_query_mail.value(0)) while self.comboBoxes_query_usuario.next(): if self.comboBoxes_query_usuario.value(0) == "" or self.comboBoxes_query_usuario.value(0) is None: continue self.ui.comboBox_usuario.addItem(self.comboBoxes_query_usuario.value(0)) self.ui.comboBox_categoria.setCurrentText(a) self.ui.comboBox_mail.setCurrentText(b) self.ui.comboBox_usuario.setCurrentText(c) def revisar_columna_contrasenas(self, borrar=True): existe = self.verificar_columna_contrasenas.exec_('SELECT contraseña FROM passwords LIMIT 1') if existe is True and borrar is True: self.verificar_columna_contrasenas.finish() self.borrar_columna_contrasenas() return self.organizar_tabla_ui() elif existe is True: return "Existe" def gestor_boton_editar_guardar(self): if self.modo_boton_editar_guardar == "editar": try: self.editar_tabla_ui() except Exception: return self.modo_boton_editar_guardar = "guardar" # Cambiamos al proximo modo return self.ui.boton_editar.setIcon(self.icon_guardar) elif self.modo_boton_editar_guardar == "guardar": self.borrar_columna_contrasenas() self.organizar_tabla_ui() self.candado = "cerrado" self.ui.boton_seguridad.setDisabled(False) self.ui.boton_seguridad.setIcon(self.icon_seguridad) self.ui.boton_editar.setIcon(self.icon_editar) self.modo_boton_editar_guardar = "editar" # Cambiamos al proximo modo return def editar_tabla_ui(self): if self.master_key is None: try: self.master_key = self.pedir_contrasena_maestra() except Exception: raise Exception("No se pudo conseguir master key") self.ui.boton_seguridad.setDisabled(True) if self.candado == "cerrado": self.mostrar_contrasenas() def celdas_cambiadas(self, top_left, bottom_right): if self.modo_boton_editar_guardar == "editar": return else: if top_left.column() == 7: contrasena_editada_encriptada = "" if self.model.record(top_left.row()).value('contraseña') != "": contrasena_editada_encriptada = QByteArray( pswCrypto.encriptar(self.model.record(top_left.row()).value('contraseña'), self.master_key)) casilla_editada = QSqlField("contraseña_encriptada") casilla_editada.setValue(contrasena_editada_encriptada) valores_fila = self.model.record(top_left.row()) valores_fila.replace(6, casilla_editada) return self.model.updateRowInTable(top_left.row(), valores_fila) else: self.model.updateRowInTable(top_left.row(), self.model.record(top_left.row())) return self.db.commit() def borrar_linea(self, row): self.model.deleteRowFromTable(row) return self.model.select() def context_menu(self): # Menu con click derecho if self.ui.tabla_db.selectedIndexes() and self.modo_boton_editar_guardar == "guardar": menu = QMenu() borrar_data = menu.addAction("Borrar linea de la base de datos") borrar_data.triggered.connect(lambda: self.borrar_linea(self.ui.tabla_db.currentIndex().row())) cursor = QCursor() menu.exec_(cursor.pos())
class Settings(object): class _MigrationFailed(ExpectedError): pass def __init__(self, cfg, main_cfg, start_service, exit_service, parent=None, size=None, migrate=False, dp=1, get_offline_dirs=lambda: None, set_offline_dirs=lambda o, no: None): super(Settings, self).__init__() self._cfg = cfg self._main_cfg = main_cfg self._start_service = start_service self._exit_service = exit_service self._parent = parent self._size = size self._dp = dp self._get_offline_dirs = get_offline_dirs self._set_offline_dirs = set_offline_dirs self._dialog = QDialog(parent) self._dialog.setWindowIcon(QIcon(':/images/icon.png')) self._dialog.setAttribute(Qt.WA_MacFrameworkScaled) self._ui = settings.Ui_Dialog() self._ui.setupUi(self._dialog) self._max_root_len = get_max_root_len(self._cfg) self._migrate = migrate self._migration = None self._migration_cancelled = False try: self._ui.account_type.setText( license_display_name_from_constant(self._cfg.license_type)) self._ui.account_type.setVisible(True) self._ui.account_type_header.setVisible(True) self._ui.account_upgrade.setVisible(True) except KeyError: pass upgrade_license_types = (FREE_LICENSE, FREE_TRIAL_LICENSE) if self._cfg.license_type in upgrade_license_types: self._ui.account_upgrade.setText('<a href="{}">{}</a>'.format( GET_PRO_URI.format(self._cfg.host), tr('Upgrade'))) self._ui.account_upgrade.setTextFormat(Qt.RichText) self._ui.account_upgrade.setTextInteractionFlags( Qt.TextBrowserInteraction) self._ui.account_upgrade.setOpenExternalLinks(True) self._ui.account_upgrade.setAlignment(Qt.AlignLeft) else: self._ui.account_upgrade.setText("") self._ui.centralWidget.setFrameShape(QFrame.NoFrame) self._ui.centralWidget.setLineWidth(1) self._ui.language_comboBox.addItem(tr('English')) self._ui.language_comboBox.setEnabled(False) self._connect_slots() self._set_fonts() self._ui.tabWidget.setCurrentIndex(0) self._smart_sync_dialog = None self.logged_out = Signal(bool) self.logging_disabled_changed = Signal(bool) # FIXMe: without line below app crashes on exit after settings opened self._dialog.mousePressEvent = self.on_mouse_press_event def on_mouse_press_event(self, ev): pass def _connect_slots(self): ui = self._ui ui.logout_button.clicked.connect(self._logout) ui.download_auto_radioButton.clicked.connect( lambda: ui.download_limit_edit.setEnabled( False) or ui.download_limit_edit.clear()) ui.download_limit_radioButton.clicked.connect( lambda: ui.download_limit_edit.setEnabled(True)) ui.upload_auto_radioButton.clicked.connect( lambda: ui.upload_limit_edit.setEnabled( False) or ui.upload_limit_edit.clear()) ui.upload_limit_radioButton.clicked.connect( lambda: ui.upload_limit_edit.setEnabled(True)) ui.buttonBox.accepted.connect(self._dialog.accept) ui.buttonBox.rejected.connect(self._dialog.reject) ui.smart_sync_button.clicked.connect( self._on_smart_sync_button_clicked) ui.location_button.clicked.connect( self._on_sync_folder_location_button_clicked) ui.location_button.enterEvent = lambda _: \ ui.location_button.setIcon(QIcon( ':/images/settings/pencil_hovered.svg')) ui.location_button.leaveEvent = lambda _: \ ui.location_button.setIcon(QIcon( ':/images/settings/pencil.svg')) ui.smart_sync_button.enterEvent = lambda _: \ ui.smart_sync_button.setIcon(QIcon( ':/images/settings/folder_sync_hovered.svg')) ui.smart_sync_button.leaveEvent = lambda _: \ ui.smart_sync_button.setIcon(QIcon( ':/images/settings/folder_sync.svg')) ui.logout_button.enterEvent = lambda _: \ ui.logout_button.setIcon(QIcon( ':/images/settings/logout_hovered.svg')) ui.logout_button.leaveEvent = lambda _: \ ui.logout_button.setIcon(QIcon( ':/images/settings/logout.svg')) def _set_fonts(self): ui = self._ui controls = [ui.tabWidget, ui.language_comboBox] controls.extend([c for c in ui.tabWidget.findChildren(QLabel)]) controls.extend([c for c in ui.tabWidget.findChildren(QLineEdit)]) controls.extend([c for c in ui.tabWidget.findChildren(QPushButton)]) controls.extend([c for c in ui.tabWidget.findChildren(QCheckBox)]) controls.extend([c for c in ui.tabWidget.findChildren(QRadioButton)]) for control in controls: font = control.font() font_size = control.font().pointSize() * self._dp if font_size > 0: control_font = QFont(font.family(), font_size) control_font.setBold(font.bold()) control.setFont(control_font) def _logout(self): userAnswer = msgbox(tr('Keep local files on device?'), buttons=[ (tr('Clear all'), 'Wipe'), (tr('Keep'), 'Keep'), ], parent=self._dialog, default_index=1, enable_close_button=True) if userAnswer == '': return wipe_all = userAnswer == 'Wipe' if not wipe_all: self._cfg.set_settings({'user_password_hash': ""}) self.logged_out.emit(wipe_all) self._dialog.reject() def show(self, on_finished): def finished(): if self._dialog.result() == QDialog.Accepted: self._apply_settings() self._dialog.finished.disconnect(finished) on_finished() self._setup_to_ui() if self._migrate: self._ui.tabWidget.setCurrentIndex(1) # Account page QTimer.singleShot(100, self._on_sync_folder_location_button_clicked) self._dialog.finished.connect(finished) self._dialog.raise_() self._dialog.setModal(True) self._dialog.show() def _setup_to_ui(self): ui = self._ui cfg = self._cfg portable = is_portable() if cfg.get_setting('lang', None) is None: self._ui.language_comboBox.setCurrentIndex(0) else: lang = cfg.lang if cfg.lang in get_available_languages() else 'en' assert lang in get_available_languages() for i in range(1, ui.language_comboBox.count()): if ui.language_comboBox.itemText(i) == lang: ui.language_comboBox.setCurrentIndex(i) break ui.location_edit.setText( FilePath(cfg.sync_directory) if cfg.sync_directory else '') ui.location_button.setEnabled(not portable) if portable: ui.location_button.setToolTip(tr("Disabled in portable version")) ui.email_label.setText(cfg.user_email if cfg.user_email else '') def set_limit(limit, auto_btn, manual_btn, edit): edit.setValidator(QRegExpValidator(QRegExp("\\d{1,9}"))) if limit: manual_btn.setChecked(True) edit.setText(str(limit)) else: auto_btn.setChecked(True) auto_btn.click() set_limit(limit=cfg.download_limit, auto_btn=ui.download_auto_radioButton, manual_btn=ui.download_limit_radioButton, edit=ui.download_limit_edit) set_limit(limit=cfg.upload_limit, auto_btn=ui.upload_auto_radioButton, manual_btn=ui.upload_limit_radioButton, edit=ui.upload_limit_edit) ui.autologin_checkbox.setChecked(self._main_cfg.autologin) ui.autologin_checkbox.setEnabled(not portable) if portable: ui.autologin_checkbox.setToolTip( tr("Disabled in portable version")) ui.tracking_checkbox.setChecked(cfg.send_statistics) ui.autoupdate_checkbox.setChecked(self._main_cfg.autoupdate) ui.download_backups_checkBox.setChecked(cfg.download_backups) ui.is_smart_sync_checkBox.setChecked(cfg.smart_sync) ui.disable_logging_checkBox.setChecked(self._main_cfg.logging_disabled) # Disable smart sync for free license if not cfg.license_type or cfg.license_type == FREE_LICENSE: ui.is_smart_sync_checkBox.setText( tr("SmartSync+ is not available for your license")) ui.is_smart_sync_checkBox.setChecked(False) ui.is_smart_sync_checkBox.setCheckable(False) ui.smart_sync_button.setEnabled(False) ui.startup_checkbox.setChecked(is_in_system_startup()) ui.startup_checkbox.setEnabled(not portable) if portable: ui.startup_checkbox.setToolTip(tr("Disabled in portable version")) def _apply_settings(self): service_settings, main_settings = self._get_configs_from_ui() if main_settings['logging_disabled'] != \ self._main_cfg.logging_disabled: self.logging_disabled_changed.emit( main_settings['logging_disabled']) self._cfg.set_settings(service_settings) self._main_cfg.set_settings(main_settings) if self._ui.startup_checkbox.isChecked(): if not is_in_system_startup(): add_to_system_startup() else: if is_in_system_startup(): remove_from_system_startup() def _config_is_changed(self): service_settings, main_settings = self._get_configs_from_ui() for param, value in service_settings.items(): if self._cfg.get_setting(param) != value: return True for param, value in main_settings.items(): if self._main_cfg.get_setting(param) != value: return True return False def _get_configs_from_ui(self): ui = self._ui return { 'lang': (str(ui.language_comboBox.currentText()) if ui.language_comboBox.currentIndex() > 0 else None), 'upload_limit': (0 if ui.upload_auto_radioButton.isChecked() or not ui.upload_limit_edit.text() else int( ui.upload_limit_edit.text())), 'download_limit': (0 if ui.download_auto_radioButton.isChecked() or not ui.download_limit_edit.text() else int( ui.download_limit_edit.text())), 'send_statistics': bool(ui.tracking_checkbox.isChecked()), 'download_backups': bool(ui.download_backups_checkBox.isChecked()), 'smart_sync': bool(ui.is_smart_sync_checkBox.isChecked()), 'autologin': bool(ui.autologin_checkbox.isChecked()), }, { 'autologin': bool(ui.autologin_checkbox.isChecked()), 'autoupdate': bool(ui.autoupdate_checkbox.isChecked()), 'logging_disabled': bool(ui.disable_logging_checkBox.isChecked()), 'download_backups': bool(ui.download_backups_checkBox.isChecked()), } def _on_smart_sync_button_clicked(self): self._get_offline_dirs() root = str(self._ui.location_edit.text()) self._smart_sync_dialog = SmartSyncDialog(self._dialog) offline, online = self._smart_sync_dialog.show(root_path=root, hide_dotted=True) if offline or online: logger.info("Directories set to be offline: (%s)", ", ".join(map(lambda s: u"'%s'" % s, offline))) self._set_offline_dirs(offline, online) def offline_dirs(self, offline_dirs): root = str(self._ui.location_edit.text()) pc = PathConverter(root) offline_dirs_abs_paths = set( map(lambda p: pc.create_abspath(p), offline_dirs)) if self._smart_sync_dialog: self._smart_sync_dialog.set_offline_paths(offline_dirs_abs_paths) def _on_sync_folder_location_button_clicked(self): selected_folder = QFileDialog.getExistingDirectory( self._dialog, tr('Choose Pvtbox folder location'), get_parent_dir(FilePath(self._cfg.sync_directory))) selected_folder = ensure_unicode(selected_folder) try: if not selected_folder: raise self._MigrationFailed("Folder is not selected") if len(selected_folder + "/Pvtbox") > self._max_root_len: if not self._migrate: msgbox(tr("Destination path too long. " "Please select shorter path."), tr("Path too long"), parent=self._dialog) raise self._MigrationFailed("Destination path too long") free_space = get_free_space(selected_folder) selected_folder = get_data_dir(dir_parent=selected_folder, create=False) if FilePath(selected_folder) == FilePath(self._cfg.sync_directory): raise self._MigrationFailed("Same path selected") if FilePath(selected_folder) in FilePath(self._cfg.sync_directory): msgbox(tr("Can't migrate into existing Pvtbox folder.\n" "Please choose other location"), tr("Invalid Pvtbox folder location"), parent=self._dialog) raise self._MigrationFailed( "Can't migrate into existing Pvtbox folder") if self._size and free_space < self._size: logger.debug( "No disk space in %s. Free space: %s. Needed: %s.", selected_folder, free_space, self._size) msgbox(tr( "Insufficient disk space for migration to\n{}.\n" "Please clean disk", selected_folder), tr("No disk space"), parent=self._dialog) raise self._MigrationFailed( "Insufficient disk space for migration") self._migration_cancelled = False dialog = QProgressDialog(self._dialog) dialog.setWindowTitle(tr('Migrating to new Pvtbox folder')) dialog.setWindowIcon(QIcon(':/images/icon.svg')) dialog.setModal(True) dialog.setMinimum(0) dialog.setMaximum(100) dialog.setMinimumSize(400, 80) dialog.setAutoClose(False) def progress(value): logger.debug("Migration dialog progress received: %s", value) dialog.setValue(value) def migration_failed(error): logger.warning("Migration failed with error: %s", error) msgbox(error, tr('Migration to new Pvtbox folder error'), parent=dialog) dialog.cancel() self._migration_cancelled = True done() def cancel(): logger.debug("Migration dialog cancelled") self._migration_cancelled = True self._migration.cancel() def done(): logger.debug("Migration done") try: self._migration.progress.disconnect(progress) self._migration.failed.disconnect(migration_failed) self._migration.done.disconnect(done) dialog.canceled.disconnect(cancel) except Exception as e: logger.warning("Can't disconnect signal %s", e) dialog.hide() dialog.done(QDialog.Accepted) dialog.close() self._migration = SyncDirMigration(self._cfg, parent=self._dialog) self._migration.progress.connect(progress, Qt.QueuedConnection) self._migration.failed.connect(migration_failed, Qt.QueuedConnection) self._migration.done.connect(done, Qt.QueuedConnection) dialog.canceled.connect(cancel) self._exit_service() old_dir = self._cfg.sync_directory self._migration.migrate(old_dir, selected_folder) def on_finished(): logger.info("Migration dialog closed") if not self._migration_cancelled: logger.debug("Setting new location") self._ui.location_edit.setText(FilePath(selected_folder)) disable_file_logging(logger) shutil.rmtree(op.join(old_dir, '.pvtbox'), ignore_errors=True) set_root_directory(FilePath(selected_folder)) enable_file_logging(logger) make_dir_hidden(get_patches_dir(selected_folder)) self._start_service() dialog.finished.connect(on_finished) dialog.show() except self._MigrationFailed as e: logger.warning("Sync dir migration failed. Reason: %s", e) finally: if self._migrate: self._dialog.accept()