diff --git a/onionshare/__init__.py b/onionshare/__init__.py index 715c5571..069559c5 100644 --- a/onionshare/__init__.py +++ b/onionshare/__init__.py @@ -33,7 +33,15 @@ def main(cwd=None): """ common = Common() + # Load the default settings and strings early, for the sake of being able to parse options. + # These won't be in the user's chosen locale necessarily, but we need to parse them + # early in order to even display the option to pass alternate settings (which might + # contain a preferred locale). + # If an alternate --config is passed, we'll reload strings later. + common.load_settings() strings.load_strings(common) + + # Display OnionShare banner print(strings._('version_string').format(common.version)) # OnionShare CLI in OSX needs to change current working directory (#132) @@ -88,8 +96,11 @@ def main(cwd=None): if not valid: sys.exit() - # Load settings - common.load_settings(config) + # Re-load settings, if a custom config was passed in + if config: + common.load_settings(config) + # Re-load the strings, in case the provided config has changed locale + strings.load_strings(common) # Debug mode? common.debug = debug diff --git a/onionshare/onion.py b/onionshare/onion.py index c45ae72e..cb73e976 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -247,7 +247,7 @@ class Onion(object): self.c = Controller.from_socket_file(path=self.tor_control_socket) self.c.authenticate() except Exception as e: - raise BundledTorBroken(strings._('settings_error_bundled_tor_broken', True).format(e.args[0])) + raise BundledTorBroken(strings._('settings_error_bundled_tor_broken').format(e.args[0])) while True: try: diff --git a/onionshare/settings.py b/onionshare/settings.py index adcfc7a3..ed827cbd 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -21,6 +21,7 @@ along with this program. If not, see . import json import os import platform +import locale from . import strings @@ -47,6 +48,25 @@ class Settings(object): else: self.common.log('Settings', '__init__', 'Supplied config does not exist or is unreadable. Falling back to default location') + # Dictionary of available languages in this version of OnionShare, + # mapped to the language name, in that language + self.available_locales = { + 'cs': 'Hrvatski', # Croatian + 'da': 'Dansk', # Danish + 'de': 'Deutsch', # German + 'en': 'English', # English + 'eo': 'Esperanto', # Esperanto + 'es': 'Español', # Spanish + 'fi': 'Suomi', # Finnish + 'fr': 'Français', # French + 'it': 'Italiano', # Italian + 'nl': 'Nederlands', # Dutch + 'no': 'Norsk', # Norweigan + 'pt': 'Português', # Portuguese + 'ru': 'Русский', # Russian + 'tr': 'Türkçe' # Turkish + } + # These are the default settings. They will get overwritten when loading from disk self.default_settings = { 'version': self.common.version, @@ -74,7 +94,8 @@ class Settings(object): 'slug': '', 'hidservauth_string': '', 'downloads_dir': self.build_default_downloads_dir(), - 'receive_allow_receiver_shutdown': True + 'receive_allow_receiver_shutdown': True, + 'locale': None # this gets defined in fill_in_defaults() } self._settings = {} self.fill_in_defaults() @@ -88,14 +109,26 @@ class Settings(object): if key not in self._settings: self._settings[key] = self.default_settings[key] + # Choose the default locale based on the OS preference, and fall-back to English + if self._settings['locale'] is None: + default_locale = locale.getdefaultlocale()[0][:2] + if default_locale not in self.available_locales: + default_locale = 'en' + self._settings['locale'] = default_locale + def build_filename(self): """ Returns the path of the settings file. """ p = platform.system() if p == 'Windows': - appdata = os.environ['APPDATA'] - return '{}\\OnionShare\\onionshare.json'.format(appdata) + try: + appdata = os.environ['APPDATA'] + return '{}\\OnionShare\\onionshare.json'.format(appdata) + except: + # If for some reason we don't have the 'APPDATA' environment variable + # (like running tests in Linux while pretending to be in Windows) + return os.path.expanduser('~/.config/onionshare/onionshare.json') elif p == 'Darwin': return os.path.expanduser('~/Library/Application Support/OnionShare/onionshare.json') else: @@ -136,7 +169,7 @@ class Settings(object): except: pass open(self.filename, 'w').write(json.dumps(self._settings)) - print(strings._('settings_saved').format(self.filename)) + self.common.log('Settings', 'save', 'Settings saved in {}'.format(self.filename)) def get(self, key): return self._settings[key] diff --git a/onionshare/strings.py b/onionshare/strings.py index 3e9df56d..b730933d 100644 --- a/onionshare/strings.py +++ b/onionshare/strings.py @@ -22,39 +22,36 @@ import locale import os strings = {} +translations = {} -def load_strings(common, default="en"): +def load_strings(common): """ Loads translated strings and fallback to English if the translation does not exist. """ - global strings + global strings, translations - # find locale dir - locale_dir = common.get_resource_path('locale') - - # load all translations + # Load all translations translations = {} - for filename in os.listdir(locale_dir): - abs_filename = os.path.join(locale_dir, filename) - lang, ext = os.path.splitext(filename) - if ext == '.json': - with open(abs_filename, encoding='utf-8') as f: - translations[lang] = json.load(f) + for locale in common.settings.available_locales: + locale_dir = common.get_resource_path('locale') + filename = os.path.join(locale_dir, "{}.json".format(locale)) + with open(filename, encoding='utf-8') as f: + translations[locale] = json.load(f) - strings = translations[default] - lc, enc = locale.getdefaultlocale() - if lc: - lang = lc[:2] - if lang in translations: - # if a string doesn't exist, fallback to English - for key in translations[default]: - if key in translations[lang]: - strings[key] = translations[lang][key] + # Build strings + default_locale = 'en' + current_locale = common.settings.get('locale') + strings = {} + for s in translations[default_locale]: + if s in translations[current_locale]: + strings[s] = translations[current_locale][s] + else: + strings[s] = translations[default_locale][s] -def translated(k, gui=False): +def translated(k): """ Returns a translated string. """ diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 99db635a..675bb52d 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -59,7 +59,15 @@ def main(): common = Common() common.define_css() + # Load the default settings and strings early, for the sake of being able to parse options. + # These won't be in the user's chosen locale necessarily, but we need to parse them + # early in order to even display the option to pass alternate settings (which might + # contain a preferred locale). + # If an alternate --config is passed, we'll reload strings later. + common.load_settings() strings.load_strings(common) + + # Display OnionShare banner print(strings._('version_string').format(common.version)) # Allow Ctrl-C to smoothly quit the program instead of throwing an exception @@ -84,6 +92,10 @@ def main(): filenames[i] = os.path.abspath(filenames[i]) config = args.config + if config: + # Re-load the strings, in case the provided config has changed locale + common.load_settings(config) + strings.load_strings(common) local_only = bool(args.local_only) debug = bool(args.debug) @@ -96,10 +108,10 @@ def main(): valid = True for filename in filenames: if not os.path.isfile(filename) and not os.path.isdir(filename): - Alert(common, strings._("not_a_file", True).format(filename)) + Alert(common, strings._("not_a_file").format(filename)) valid = False if not os.access(filename, os.R_OK): - Alert(common, strings._("not_a_readable_file", True).format(filename)) + Alert(common, strings._("not_a_readable_file").format(filename)) valid = False if not valid: sys.exit() diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py index 8cfa0ed5..b446b9fb 100644 --- a/onionshare_gui/mode/history.py +++ b/onionshare_gui/mode/history.py @@ -196,7 +196,7 @@ class UploadHistoryItem(HistoryItem): self.started = datetime.now() # Label - self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).format(self.started.strftime("%b %d, %I:%M%p"))) + self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress').format(self.started.strftime("%b %d, %I:%M%p"))) # Progress bar self.progress_bar = QtWidgets.QProgressBar() @@ -274,16 +274,16 @@ class UploadHistoryItem(HistoryItem): self.ended = self.started = datetime.now() if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day: if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute: - text = strings._('gui_upload_finished', True).format( + text = strings._('gui_upload_finished').format( self.started.strftime("%b %d, %I:%M%p") ) else: - text = strings._('gui_upload_finished_range', True).format( + text = strings._('gui_upload_finished_range').format( self.started.strftime("%b %d, %I:%M%p"), self.ended.strftime("%I:%M%p") ) else: - text = strings._('gui_upload_finished_range', True).format( + text = strings._('gui_upload_finished_range').format( self.started.strftime("%b %d, %I:%M%p"), self.ended.strftime("%b %d, %I:%M%p") ) @@ -380,7 +380,7 @@ class History(QtWidgets.QWidget): # Header self.header_label = QtWidgets.QLabel(header_text) self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) - clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history')) clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) clear_button.setFlat(True) clear_button.clicked.connect(self.reset) @@ -486,7 +486,7 @@ class History(QtWidgets.QWidget): else: image = self.common.get_resource_path('images/share_in_progress.png') self.in_progress_label.setText(' {1:d}'.format(image, self.in_progress_count)) - self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip', True).format(self.in_progress_count)) + self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip').format(self.in_progress_count)) class ToggleHistory(QtWidgets.QPushButton): diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py index b73acca2..f070f963 100644 --- a/onionshare_gui/mode/receive_mode/__init__.py +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -63,7 +63,7 @@ class ReceiveMode(Mode): ) # Receive mode warning - receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) + receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning')) receive_warning.setMinimumHeight(80) receive_warning.setWordWrap(True) @@ -90,7 +90,7 @@ class ReceiveMode(Mode): """ Return the string to put on the stop server button, if there's a shutdown timeout """ - return strings._('gui_receive_stop_server_shutdown_timeout', True) + return strings._('gui_receive_stop_server_shutdown_timeout') def timeout_finished_should_stop_server(self): """ @@ -128,7 +128,7 @@ class ReceiveMode(Mode): """ Handle REQUEST_LOAD event. """ - self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True)) + self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_upload_page_loaded_message')) def handle_request_started(self, event): """ @@ -140,7 +140,7 @@ class ReceiveMode(Mode): self.history.in_progress_count += 1 self.history.update_in_progress() - self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True)) + self.system_tray.showMessage(strings._('systray_upload_started_title'), strings._('systray_upload_started_message')) def handle_request_progress(self, event): """ @@ -156,7 +156,7 @@ class ReceiveMode(Mode): Handle REQUEST_CLOSE_SERVER event. """ self.stop_server() - self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True)) + self.system_tray.showMessage(strings._('systray_close_server_title'), strings._('systray_close_server_message')) def handle_request_upload_file_renamed(self, event): """ diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 1c1f33ae..62c5d8a7 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -125,7 +125,7 @@ class ShareMode(Mode): """ Return the string to put on the stop server button, if there's a shutdown timeout """ - return strings._('gui_share_stop_server_shutdown_timeout', True) + return strings._('gui_share_stop_server_shutdown_timeout') def timeout_finished_should_stop_server(self): """ @@ -134,11 +134,11 @@ class ShareMode(Mode): # If there were no attempts to download the share, or all downloads are done, we can stop if self.web.share_mode.download_count == 0 or self.web.done: self.server_status.stop_server() - self.server_status_label.setText(strings._('close_on_timeout', True)) + self.server_status_label.setText(strings._('close_on_timeout')) return True # A download is probably still running - hold off on stopping the share else: - self.server_status_label.setText(strings._('timeout_download_still_running', True)) + self.server_status_label.setText(strings._('timeout_download_still_running')) return False def start_server_custom(self): @@ -185,7 +185,7 @@ class ShareMode(Mode): # Warn about sending large files over Tor if self.web.share_mode.download_filesize >= 157286400: # 150mb - self.filesize_warning.setText(strings._("large_filesize", True)) + self.filesize_warning.setText(strings._("large_filesize")) self.filesize_warning.show() def start_server_error_custom(self): @@ -229,7 +229,7 @@ class ShareMode(Mode): """ Handle REQUEST_LOAD event. """ - self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_download_page_loaded_message', True)) + self.system_tray.showMessage(strings._('systray_page_loaded_title'), strings._('systray_download_page_loaded_message')) def handle_request_started(self, event): """ @@ -246,7 +246,7 @@ class ShareMode(Mode): self.history.in_progress_count += 1 self.history.update_in_progress() - self.system_tray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True)) + self.system_tray.showMessage(strings._('systray_download_started_title'), strings._('systray_download_started_message')) def handle_request_progress(self, event): """ @@ -256,7 +256,7 @@ class ShareMode(Mode): # Is the download complete? if event["data"]["bytes"] == self.web.share_mode.filesize: - self.system_tray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True)) + self.system_tray.showMessage(strings._('systray_download_completed_title'), strings._('systray_download_completed_message')) # Update completed and in progress labels self.history.completed_count += 1 @@ -268,7 +268,7 @@ class ShareMode(Mode): if self.common.settings.get('close_after_first_download'): self.server_status.stop_server() self.status_bar.clearMessage() - self.server_status_label.setText(strings._('closing_automatically', True)) + self.server_status_label.setText(strings._('closing_automatically')) else: if self.server_status.status == self.server_status.STATUS_STOPPED: self.history.cancel(event["data"]["id"]) @@ -312,9 +312,9 @@ class ShareMode(Mode): total_size_readable = self.common.human_readable_filesize(total_size_bytes) if file_count > 1: - self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable)) + self.info_label.setText(strings._('gui_file_info').format(file_count, total_size_readable)) else: - self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable)) + self.info_label.setText(strings._('gui_file_info_single').format(file_count, total_size_readable)) else: self.primary_action.hide() diff --git a/onionshare_gui/mode/share_mode/file_selection.py b/onionshare_gui/mode/share_mode/file_selection.py index 6bfa7dbf..ec3b5ea5 100644 --- a/onionshare_gui/mode/share_mode/file_selection.py +++ b/onionshare_gui/mode/share_mode/file_selection.py @@ -41,7 +41,7 @@ class DropHereLabel(QtWidgets.QLabel): if image: self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/logo_transparent.png')))) else: - self.setText(strings._('gui_drag_and_drop', True)) + self.setText(strings._('gui_drag_and_drop')) self.setStyleSheet(self.common.css['share_file_selection_drop_here_label']) self.hide() @@ -65,7 +65,7 @@ class DropCountLabel(QtWidgets.QLabel): self.setAcceptDrops(True) self.setAlignment(QtCore.Qt.AlignCenter) - self.setText(strings._('gui_drag_and_drop', True)) + self.setText(strings._('gui_drag_and_drop')) self.setStyleSheet(self.common.css['share_file_selection_drop_count_label']) self.hide() @@ -216,7 +216,7 @@ class FileList(QtWidgets.QListWidget): if filename not in filenames: if not os.access(filename, os.R_OK): - Alert(self.common, strings._("not_a_readable_file", True).format(filename)) + Alert(self.common, strings._("not_a_readable_file").format(filename)) return fileinfo = QtCore.QFileInfo(filename) @@ -302,9 +302,9 @@ class FileSelection(QtWidgets.QVBoxLayout): self.file_list.files_updated.connect(self.update) # Buttons - self.add_button = QtWidgets.QPushButton(strings._('gui_add', True)) + self.add_button = QtWidgets.QPushButton(strings._('gui_add')) self.add_button.clicked.connect(self.add) - self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True)) + self.delete_button = QtWidgets.QPushButton(strings._('gui_delete')) self.delete_button.clicked.connect(self.delete) button_layout = QtWidgets.QHBoxLayout() button_layout.addStretch() @@ -341,7 +341,7 @@ class FileSelection(QtWidgets.QVBoxLayout): """ Add button clicked. """ - file_dialog = AddFileDialog(self.common, caption=strings._('gui_choose_items', True)) + file_dialog = AddFileDialog(self.common, caption=strings._('gui_choose_items')) if file_dialog.exec_() == QtWidgets.QDialog.Accepted: for filename in file_dialog.selectedFiles(): self.file_list.add_file(filename) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index c2e6657b..8c8e4e73 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -58,17 +58,18 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - # Load settings + # Load settings, if a custom config was passed in self.config = config - self.common.load_settings(self.config) + if self.config: + self.common.load_settings(self.config) # System tray menu = QtWidgets.QMenu() - self.settings_action = menu.addAction(strings._('gui_settings_window_title', True)) + self.settings_action = menu.addAction(strings._('gui_settings_window_title')) self.settings_action.triggered.connect(self.open_settings) - help_action = menu.addAction(strings._('gui_settings_button_help', True)) - help_action.triggered.connect(SettingsDialog.open_help) - exit_action = menu.addAction(strings._('systray_menu_exit', True)) + help_action = menu.addAction(strings._('gui_settings_button_help')) + help_action.triggered.connect(SettingsDialog.help_clicked) + exit_action = menu.addAction(strings._('systray_menu_exit')) exit_action.triggered.connect(self.close) self.system_tray = QtWidgets.QSystemTrayIcon(self) @@ -81,10 +82,10 @@ class OnionShareGui(QtWidgets.QMainWindow): self.system_tray.show() # Mode switcher, to switch between share files and receive files - self.share_mode_button = QtWidgets.QPushButton(strings._('gui_mode_share_button', True)); + self.share_mode_button = QtWidgets.QPushButton(strings._('gui_mode_share_button')); self.share_mode_button.setFixedHeight(50) self.share_mode_button.clicked.connect(self.share_mode_clicked) - self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button', True)); + self.receive_mode_button = QtWidgets.QPushButton(strings._('gui_mode_receive_button')); self.receive_mode_button.setFixedHeight(50) self.receive_mode_button.clicked.connect(self.receive_mode_clicked) self.settings_button = QtWidgets.QPushButton() @@ -224,24 +225,24 @@ class OnionShareGui(QtWidgets.QMainWindow): # Share mode if self.share_mode.server_status.status == ServerStatus.STATUS_STOPPED: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) - self.server_status_label.setText(strings._('gui_status_indicator_share_stopped', True)) + self.server_status_label.setText(strings._('gui_status_indicator_share_stopped')) elif self.share_mode.server_status.status == ServerStatus.STATUS_WORKING: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) - self.server_status_label.setText(strings._('gui_status_indicator_share_working', True)) + self.server_status_label.setText(strings._('gui_status_indicator_share_working')) elif self.share_mode.server_status.status == ServerStatus.STATUS_STARTED: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) - self.server_status_label.setText(strings._('gui_status_indicator_share_started', True)) + self.server_status_label.setText(strings._('gui_status_indicator_share_started')) else: # Receive mode if self.receive_mode.server_status.status == ServerStatus.STATUS_STOPPED: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped)) - self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped', True)) + self.server_status_label.setText(strings._('gui_status_indicator_receive_stopped')) elif self.receive_mode.server_status.status == ServerStatus.STATUS_WORKING: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working)) - self.server_status_label.setText(strings._('gui_status_indicator_receive_working', True)) + self.server_status_label.setText(strings._('gui_status_indicator_receive_working')) elif self.receive_mode.server_status.status == ServerStatus.STATUS_STARTED: self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started)) - self.server_status_label.setText(strings._('gui_status_indicator_receive_started', True)) + self.server_status_label.setText(strings._('gui_status_indicator_receive_started')) def stop_server_finished(self): # When the server stopped, cleanup the ephemeral onion service @@ -255,9 +256,9 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common.log('OnionShareGui', '_tor_connection_canceled') def ask(): - a = Alert(self.common, strings._('gui_tor_connection_ask', True), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False) - settings_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_open_settings', True)) - quit_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_quit', True)) + a = Alert(self.common, strings._('gui_tor_connection_ask'), QtWidgets.QMessageBox.Question, buttons=QtWidgets.QMessageBox.NoButton, autostart=False) + settings_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_open_settings')) + quit_button = QtWidgets.QPushButton(strings._('gui_tor_connection_ask_quit')) a.addButton(settings_button, QtWidgets.QMessageBox.AcceptRole) a.addButton(quit_button, QtWidgets.QMessageBox.RejectRole) a.setDefaultButton(settings_button) @@ -328,7 +329,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if self.common.platform == 'Windows' or self.common.platform == 'Darwin': if self.common.settings.get('use_autoupdate'): def update_available(update_url, installed_version, latest_version): - Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version)) + Alert(self.common, strings._("update_available").format(update_url, installed_version, latest_version)) self.update_thread = UpdateThread(self.common, self.onion, self.config) self.update_thread.update_available.connect(update_available) @@ -345,8 +346,8 @@ class OnionShareGui(QtWidgets.QMainWindow): # Have we lost connection to Tor somehow? if not self.onion.is_authenticated(): self.timer.stop() - self.status_bar.showMessage(strings._('gui_tor_connection_lost', True)) - self.system_tray.showMessage(strings._('gui_tor_connection_lost', True), strings._('gui_tor_connection_error_settings', True)) + self.status_bar.showMessage(strings._('gui_tor_connection_lost')) + self.system_tray.showMessage(strings._('gui_tor_connection_lost'), strings._('gui_tor_connection_error_settings')) self.share_mode.handle_tor_broke() self.receive_mode.handle_tor_broke() @@ -400,7 +401,7 @@ class OnionShareGui(QtWidgets.QMainWindow): if event["type"] == Web.REQUEST_OTHER: if event["path"] != '/favicon.ico' and event["path"] != "/{}/shutdown".format(mode.web.shutdown_slug): - self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded', True), event["path"])) + self.status_bar.showMessage('[#{0:d}] {1:s}: {2:s}'.format(mode.web.error404_count, strings._('other_page_loaded'), event["path"])) mode.timer_callback() @@ -409,14 +410,14 @@ class OnionShareGui(QtWidgets.QMainWindow): When the URL gets copied to the clipboard, display this in the status bar. """ self.common.log('OnionShareGui', 'copy_url') - self.system_tray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True)) + self.system_tray.showMessage(strings._('gui_copied_url_title'), strings._('gui_copied_url')) def copy_hidservauth(self): """ When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar. """ self.common.log('OnionShareGui', 'copy_hidservauth') - self.system_tray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True)) + self.system_tray.showMessage(strings._('gui_copied_hidservauth_title'), strings._('gui_copied_hidservauth')) def clear_message(self): """ @@ -454,14 +455,14 @@ class OnionShareGui(QtWidgets.QMainWindow): if server_status.status != server_status.STATUS_STOPPED: self.common.log('OnionShareGui', 'closeEvent, opening warning dialog') dialog = QtWidgets.QMessageBox() - dialog.setWindowTitle(strings._('gui_quit_title', True)) + dialog.setWindowTitle(strings._('gui_quit_title')) if self.mode == OnionShareGui.MODE_SHARE: - dialog.setText(strings._('gui_share_quit_warning', True)) + dialog.setText(strings._('gui_share_quit_warning')) else: - dialog.setText(strings._('gui_receive_quit_warning', True)) + dialog.setText(strings._('gui_receive_quit_warning')) dialog.setIcon(QtWidgets.QMessageBox.Critical) - quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole) - dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole) + quit_button = dialog.addButton(strings._('gui_quit_warning_quit'), QtWidgets.QMessageBox.YesRole) + dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit'), QtWidgets.QMessageBox.NoRole) dialog.setDefaultButton(dont_quit_button) reply = dialog.exec_() diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py new file mode 100644 index 00000000..33d993b3 --- /dev/null +++ b/onionshare_gui/receive_mode/uploads.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program 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. + +This program 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 this program. If not, see . +""" +import os +import subprocess +import textwrap +from datetime import datetime +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings +from ..widgets import Alert + + +class File(QtWidgets.QWidget): + def __init__(self, common, filename): + super(File, self).__init__() + self.common = common + + self.common.log('File', '__init__', 'filename: {}'.format(filename)) + + self.filename = filename + self.started = datetime.now() + + # Filename label + self.filename_label = QtWidgets.QLabel(self.filename) + self.filename_label_width = self.filename_label.width() + + # File size label + self.filesize_label = QtWidgets.QLabel() + self.filesize_label.setStyleSheet(self.common.css['receive_file_size']) + self.filesize_label.hide() + + # Folder button + folder_pixmap = QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/open_folder.png'))) + folder_icon = QtGui.QIcon(folder_pixmap) + self.folder_button = QtWidgets.QPushButton() + self.folder_button.clicked.connect(self.open_folder) + self.folder_button.setIcon(folder_icon) + self.folder_button.setIconSize(folder_pixmap.rect().size()) + self.folder_button.setFlat(True) + self.folder_button.hide() + + # Layouts + layout = QtWidgets.QHBoxLayout() + layout.addWidget(self.filename_label) + layout.addWidget(self.filesize_label) + layout.addStretch() + layout.addWidget(self.folder_button) + self.setLayout(layout) + + def update(self, uploaded_bytes, complete): + self.filesize_label.setText(self.common.human_readable_filesize(uploaded_bytes)) + self.filesize_label.show() + + if complete: + self.folder_button.show() + + def rename(self, new_filename): + self.filename = new_filename + self.filename_label.setText(self.filename) + + def open_folder(self): + """ + Open the downloads folder, with the file selected, in a cross-platform manner + """ + self.common.log('File', 'open_folder') + + abs_filename = os.path.join(self.common.settings.get('downloads_dir'), self.filename) + + # Linux + if self.common.platform == 'Linux' or self.common.platform == 'BSD': + try: + # If nautilus is available, open it + subprocess.Popen(['nautilus', abs_filename]) + except: + Alert(self.common, strings._('gui_open_folder_error_nautilus').format(abs_filename)) + + # macOS + elif self.common.platform == 'Darwin': + # TODO: Implement opening folder with file selected in macOS + # This seems helpful: https://stackoverflow.com/questions/3520493/python-show-in-finder + self.common.log('File', 'open_folder', 'not implemented for Darwin yet') + + # Windows + elif self.common.platform == 'Windows': + # TODO: Implement opening folder with file selected in Windows + # This seems helpful: https://stackoverflow.com/questions/6631299/python-opening-a-folder-in-explorer-nautilus-mac-thingie + self.common.log('File', 'open_folder', 'not implemented for Windows yet') + + +class Upload(QtWidgets.QWidget): + def __init__(self, common, upload_id, content_length): + super(Upload, self).__init__() + self.common = common + self.upload_id = upload_id + self.content_length = content_length + self.started = datetime.now() + + # Label + self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress').format(self.started.strftime("%b %d, %I:%M%p"))) + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) + + # This layout contains file widgets + self.files_layout = QtWidgets.QVBoxLayout() + self.files_layout.setContentsMargins(0, 0, 0, 0) + files_widget = QtWidgets.QWidget() + files_widget.setStyleSheet(self.common.css['receive_file']) + files_widget.setLayout(self.files_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.progress_bar) + layout.addWidget(files_widget) + layout.addStretch() + self.setLayout(layout) + + # We're also making a dictionary of file widgets, to make them easier to access + self.files = {} + + def update(self, progress): + """ + Using the progress from Web, update the progress bar and file size labels + for each file + """ + total_uploaded_bytes = 0 + for filename in progress: + total_uploaded_bytes += progress[filename]['uploaded_bytes'] + + # Update the progress bar + self.progress_bar.setMaximum(self.content_length) + self.progress_bar.setValue(total_uploaded_bytes) + + elapsed = datetime.now() - self.started + if elapsed.seconds < 10: + pb_fmt = strings._('gui_download_upload_progress_starting').format( + self.common.human_readable_filesize(total_uploaded_bytes)) + else: + estimated_time_remaining = self.common.estimated_time_remaining( + total_uploaded_bytes, + self.content_length, + self.started.timestamp()) + pb_fmt = strings._('gui_download_upload_progress_eta').format( + self.common.human_readable_filesize(total_uploaded_bytes), + estimated_time_remaining) + + # Using list(progress) to avoid "RuntimeError: dictionary changed size during iteration" + for filename in list(progress): + # Add a new file if needed + if filename not in self.files: + self.files[filename] = File(self.common, filename) + self.files_layout.addWidget(self.files[filename]) + + # Update the file + self.files[filename].update(progress[filename]['uploaded_bytes'], progress[filename]['complete']) + + def rename(self, old_filename, new_filename): + self.files[old_filename].rename(new_filename) + self.files[new_filename] = self.files.pop(old_filename) + + def finished(self): + # Hide the progress bar + self.progress_bar.hide() + + # Change the label + self.ended = self.started = datetime.now() + if self.started.year == self.ended.year and self.started.month == self.ended.month and self.started.day == self.ended.day: + if self.started.hour == self.ended.hour and self.started.minute == self.ended.minute: + text = strings._('gui_upload_finished').format( + self.started.strftime("%b %d, %I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range').format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range').format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%b %d, %I:%M%p") + ) + self.label.setText(text) + + +class Uploads(QtWidgets.QScrollArea): + """ + The uploads chunk of the GUI. This lists all of the active upload + progress bars, as well as information about each upload. + """ + def __init__(self, common): + super(Uploads, self).__init__() + self.common = common + self.common.log('Uploads', '__init__') + + self.resizeEvent = None + + self.uploads = {} + + self.setWindowTitle(strings._('gui_uploads')) + self.setWidgetResizable(True) + self.setMinimumHeight(150) + self.setMinimumWidth(350) + self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) + self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) + + uploads_label = QtWidgets.QLabel(strings._('gui_uploads')) + uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) + self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads')) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history')) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() + + + self.uploads_layout = QtWidgets.QVBoxLayout() + + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout() + layout.addWidget(uploads_label) + layout.addWidget(self.no_uploads_label) + layout.addWidget(self.clear_history_button) + layout.addLayout(self.uploads_layout) + layout.addStretch() + widget.setLayout(layout) + self.setWidget(widget) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + + def add(self, upload_id, content_length): + """ + Add a new upload. + """ + self.common.log('Uploads', 'add', 'upload_id: {}, content_length: {}'.format(upload_id, content_length)) + # Hide the no_uploads_label + self.no_uploads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() + + # Add it to the list + upload = Upload(self.common, upload_id, content_length) + self.uploads[upload_id] = upload + self.uploads_layout.addWidget(upload) + + def update(self, upload_id, progress): + """ + Update the progress of an upload. + """ + self.uploads[upload_id].update(progress) + + def rename(self, upload_id, old_filename, new_filename): + """ + Rename a file, which happens if the filename already exists in downloads_dir. + """ + self.uploads[upload_id].rename(old_filename, new_filename) + + def finished(self, upload_id): + """ + An upload has finished. + """ + self.uploads[upload_id].finished() + + def cancel(self, upload_id): + """ + Update an upload progress bar to show that it has been canceled. + """ + self.common.log('Uploads', 'cancel', 'upload_id: {}'.format(upload_id)) + self.uploads[upload_id].cancel() + + def reset(self): + """ + Reset the uploads back to zero + """ + self.common.log('Uploads', 'reset') + for upload in self.uploads.values(): + upload.close() + self.uploads_layout.removeWidget(upload) + self.uploads = {} + + self.no_uploads_label.show() + self.clear_history_button.hide() + self.resize(self.sizeHint()) + + def resizeEvent(self, event): + width = self.frameGeometry().width() + try: + for upload in self.uploads.values(): + for item in upload.files.values(): + item.filename_label.setText(textwrap.fill(item.filename, 30)) + item.adjustSize() + except: + pass diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 0267d826..b86155f0 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -61,7 +61,7 @@ class ServerStatus(QtWidgets.QWidget): self.resizeEvent(None) # Shutdown timeout layout - self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) + self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout')) self.shutdown_timeout = QtWidgets.QDateTimeEdit() self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") if self.local_only: @@ -100,12 +100,12 @@ class ServerStatus(QtWidgets.QWidget): self.url.setMinimumSize(self.url.sizeHint()) self.url.setStyleSheet(self.common.css['server_status_url']) - self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True)) + self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url')) self.copy_url_button.setFlat(True) self.copy_url_button.setStyleSheet(self.common.css['server_status_url_buttons']) self.copy_url_button.setMinimumHeight(65) self.copy_url_button.clicked.connect(self.copy_url) - self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth')) self.copy_hidservauth_button.setFlat(True) self.copy_hidservauth_button.setStyleSheet(self.common.css['server_status_url_buttons']) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) @@ -174,21 +174,21 @@ class ServerStatus(QtWidgets.QWidget): info_image = self.common.get_resource_path('images/info.png') if self.mode == ServerStatus.MODE_SHARE: - self.url_description.setText(strings._('gui_share_url_description', True).format(info_image)) + self.url_description.setText(strings._('gui_share_url_description').format(info_image)) else: - self.url_description.setText(strings._('gui_receive_url_description', True).format(info_image)) + self.url_description.setText(strings._('gui_receive_url_description').format(info_image)) # Show a Tool Tip explaining the lifecycle of this URL if self.common.settings.get('save_private_key'): if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True)) + self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent')) else: - self.url_description.setToolTip(strings._('gui_url_label_persistent', True)) + self.url_description.setToolTip(strings._('gui_url_label_persistent')) else: if self.mode == ServerStatus.MODE_SHARE and self.common.settings.get('close_after_first_download'): - self.url_description.setToolTip(strings._('gui_url_label_onetime', True)) + self.url_description.setToolTip(strings._('gui_url_label_onetime')) else: - self.url_description.setToolTip(strings._('gui_url_label_stay_open', True)) + self.url_description.setToolTip(strings._('gui_url_label_stay_open')) self.url.setText(self.get_url()) self.url.show() @@ -223,9 +223,9 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setStyleSheet(self.common.css['server_status_button_stopped']) self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setText(strings._('gui_share_start_server', True)) + self.server_button.setText(strings._('gui_share_start_server')) else: - self.server_button.setText(strings._('gui_receive_start_server', True)) + self.server_button.setText(strings._('gui_receive_start_server')) self.server_button.setToolTip('') if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.show() @@ -233,15 +233,15 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.setStyleSheet(self.common.css['server_status_button_started']) self.server_button.setEnabled(True) if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setText(strings._('gui_share_stop_server', True)) + self.server_button.setText(strings._('gui_share_stop_server')) else: - self.server_button.setText(strings._('gui_receive_stop_server', True)) + self.server_button.setText(strings._('gui_receive_stop_server')) if self.common.settings.get('shutdown_timeout'): self.shutdown_timeout_container.hide() if self.mode == ServerStatus.MODE_SHARE: - self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip', True).format(self.timeout)) + self.server_button.setToolTip(strings._('gui_share_stop_server_shutdown_timeout_tooltip').format(self.timeout)) else: - self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip', True).format(self.timeout)) + self.server_button.setToolTip(strings._('gui_receive_stop_server_shutdown_timeout_tooltip').format(self.timeout)) elif self.status == self.STATUS_WORKING: self.server_button.setStyleSheet(self.common.css['server_status_button_working']) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 39f08128..edd2528b 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -47,7 +47,7 @@ class SettingsDialog(QtWidgets.QDialog): self.local_only = local_only self.setModal(True) - self.setWindowTitle(strings._('gui_settings_window_title', True)) + self.setWindowTitle(strings._('gui_settings_window_title')) self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) self.system = platform.system() @@ -57,8 +57,8 @@ class SettingsDialog(QtWidgets.QDialog): # Use a slug or not ('public mode') self.public_mode_checkbox = QtWidgets.QCheckBox() self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox", True)) - public_mode_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Public-Mode")) + self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox")) + public_mode_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Public-Mode")) public_mode_label.setStyleSheet(self.common.css['settings_whats_this']) public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) public_mode_label.setOpenExternalLinks(True) @@ -74,8 +74,8 @@ class SettingsDialog(QtWidgets.QDialog): # Whether or not to use a shutdown ('auto-stop') timer self.shutdown_timeout_checkbox = QtWidgets.QCheckBox() self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) - self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True)) - shutdown_timeout_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer")) + self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox")) + shutdown_timeout_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-the-Auto-Stop-Timer")) shutdown_timeout_label.setStyleSheet(self.common.css['settings_whats_this']) shutdown_timeout_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) shutdown_timeout_label.setOpenExternalLinks(True) @@ -91,9 +91,9 @@ class SettingsDialog(QtWidgets.QDialog): # Whether or not to use legacy v2 onions self.use_legacy_v2_onions_checkbox = QtWidgets.QCheckBox() self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.use_legacy_v2_onions_checkbox.setText(strings._("gui_use_legacy_v2_onions_checkbox", True)) + self.use_legacy_v2_onions_checkbox.setText(strings._("gui_use_legacy_v2_onions_checkbox")) self.use_legacy_v2_onions_checkbox.clicked.connect(self.use_legacy_v2_onions_checkbox_clicked) - use_legacy_v2_onions_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Legacy-Addresses")) + use_legacy_v2_onions_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Legacy-Addresses")) use_legacy_v2_onions_label.setStyleSheet(self.common.css['settings_whats_this']) use_legacy_v2_onions_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) use_legacy_v2_onions_label.setOpenExternalLinks(True) @@ -108,9 +108,9 @@ class SettingsDialog(QtWidgets.QDialog): # Whether or not to save the Onion private key for reuse (persistent URL mode) self.save_private_key_checkbox = QtWidgets.QCheckBox() self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True)) + self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox")) self.save_private_key_checkbox.clicked.connect(self.save_private_key_checkbox_clicked) - save_private_key_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL")) + save_private_key_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Using-a-Persistent-URL")) save_private_key_label.setStyleSheet(self.common.css['settings_whats_this']) save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) save_private_key_label.setOpenExternalLinks(True) @@ -125,9 +125,9 @@ class SettingsDialog(QtWidgets.QDialog): # Stealth self.stealth_checkbox = QtWidgets.QCheckBox() self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True)) + self.stealth_checkbox.setText(strings._("gui_settings_stealth_option")) self.stealth_checkbox.clicked.connect(self.stealth_checkbox_clicked_connect) - use_stealth_label = QtWidgets.QLabel(strings._("gui_settings_whats_this", True).format("https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services")) + use_stealth_label = QtWidgets.QLabel(strings._("gui_settings_whats_this").format("https://github.com/micahflee/onionshare/wiki/Stealth-Onion-Services")) use_stealth_label.setStyleSheet(self.common.css['settings_whats_this']) use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) use_stealth_label.setOpenExternalLinks(True) @@ -140,12 +140,12 @@ class SettingsDialog(QtWidgets.QDialog): self.use_stealth_widget = QtWidgets.QWidget() self.use_stealth_widget.setLayout(use_stealth_layout) - hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True)) + hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string')) hidservauth_details.setWordWrap(True) hidservauth_details.setMinimumSize(hidservauth_details.sizeHint()) hidservauth_details.hide() - self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth')) self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked) self.hidservauth_copy_button.hide() @@ -159,7 +159,7 @@ class SettingsDialog(QtWidgets.QDialog): general_group_layout.addWidget(hidservauth_details) general_group_layout.addWidget(self.hidservauth_copy_button) - general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label", True)) + general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label")) general_group.setLayout(general_group_layout) # Sharing options @@ -167,19 +167,19 @@ class SettingsDialog(QtWidgets.QDialog): # Close after first download self.close_after_first_download_checkbox = QtWidgets.QCheckBox() self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked) - self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option", True)) + self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option")) # Sharing options layout sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout.addWidget(self.close_after_first_download_checkbox) - sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True)) + sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label")) sharing_group.setLayout(sharing_group_layout) # Downloads dir - downloads_label = QtWidgets.QLabel(strings._('gui_settings_downloads_label', True)); + downloads_label = QtWidgets.QLabel(strings._('gui_settings_downloads_label')); self.downloads_dir_lineedit = QtWidgets.QLineEdit() self.downloads_dir_lineedit.setReadOnly(True) - downloads_button = QtWidgets.QPushButton(strings._('gui_settings_downloads_button', True)) + downloads_button = QtWidgets.QPushButton(strings._('gui_settings_downloads_button')) downloads_button.clicked.connect(self.downloads_button_clicked) downloads_layout = QtWidgets.QHBoxLayout() downloads_layout.addWidget(downloads_label) @@ -189,13 +189,13 @@ class SettingsDialog(QtWidgets.QDialog): # Allow the receiver to shutdown the server self.receive_allow_receiver_shutdown_checkbox = QtWidgets.QCheckBox() self.receive_allow_receiver_shutdown_checkbox.setCheckState(QtCore.Qt.Checked) - self.receive_allow_receiver_shutdown_checkbox.setText(strings._("gui_settings_receive_allow_receiver_shutdown_checkbox", True)) + self.receive_allow_receiver_shutdown_checkbox.setText(strings._("gui_settings_receive_allow_receiver_shutdown_checkbox")) # Receiving options layout receiving_group_layout = QtWidgets.QVBoxLayout() receiving_group_layout.addLayout(downloads_layout) receiving_group_layout.addWidget(self.receive_allow_receiver_shutdown_checkbox) - receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label", True)) + receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label")) receiving_group.setLayout(receiving_group_layout) # Automatic updates options @@ -203,13 +203,13 @@ class SettingsDialog(QtWidgets.QDialog): # Autoupdate self.autoupdate_checkbox = QtWidgets.QCheckBox() self.autoupdate_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option", True)) + self.autoupdate_checkbox.setText(strings._("gui_settings_autoupdate_option")) # Last update time self.autoupdate_timestamp = QtWidgets.QLabel() # Check for updates button - self.check_for_updates_button = QtWidgets.QPushButton(strings._('gui_settings_autoupdate_check_button', True)) + self.check_for_updates_button = QtWidgets.QPushButton(strings._('gui_settings_autoupdate_check_button')) self.check_for_updates_button.clicked.connect(self.check_for_updates) # We can't check for updates if not connected to Tor if not self.onion.connected_to_tor: @@ -220,17 +220,32 @@ class SettingsDialog(QtWidgets.QDialog): autoupdate_group_layout.addWidget(self.autoupdate_checkbox) autoupdate_group_layout.addWidget(self.autoupdate_timestamp) autoupdate_group_layout.addWidget(self.check_for_updates_button) - autoupdate_group = QtWidgets.QGroupBox(strings._("gui_settings_autoupdate_label", True)) + autoupdate_group = QtWidgets.QGroupBox(strings._("gui_settings_autoupdate_label")) autoupdate_group.setLayout(autoupdate_group_layout) # Autoupdate is only available for Windows and Mac (Linux updates using package manager) if self.system != 'Windows' and self.system != 'Darwin': autoupdate_group.hide() + # Language settings + language_label = QtWidgets.QLabel(strings._("gui_settings_language_label")) + self.language_combobox = QtWidgets.QComboBox() + # Populate the dropdown with all of OnionShare's available languages + language_names_to_locales = {v: k for k, v in self.common.settings.available_locales.items()} + language_names = list(language_names_to_locales) + language_names.sort() + for language_name in language_names: + locale = language_names_to_locales[language_name] + self.language_combobox.addItem(language_name, QtCore.QVariant(locale)) + language_layout = QtWidgets.QHBoxLayout() + language_layout.addWidget(language_label) + language_layout.addWidget(self.language_combobox) + language_layout.addStretch() + # Connection type: either automatic, control port, or socket file # Bundled Tor - self.connection_type_bundled_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_bundled_option', True)) + self.connection_type_bundled_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_bundled_option')) self.connection_type_bundled_radio.toggled.connect(self.connection_type_bundled_toggled) # Bundled Tor doesn't work on dev mode in Windows or Mac @@ -240,27 +255,27 @@ class SettingsDialog(QtWidgets.QDialog): # Bridge options for bundled tor # No bridges option radio - self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_no_bridges_radio_option', True)) + self.tor_bridges_no_bridges_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_no_bridges_radio_option')) self.tor_bridges_no_bridges_radio.toggled.connect(self.tor_bridges_no_bridges_radio_toggled) # obfs4 option radio # if the obfs4proxy binary is missing, we can't use obfs4 transports (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() if not os.path.isfile(self.obfs4proxy_file_path): - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy', True)) + self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option_no_obfs4proxy')) self.tor_bridges_use_obfs4_radio.setEnabled(False) else: - self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option', True)) + self.tor_bridges_use_obfs4_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_obfs4_radio_option')) self.tor_bridges_use_obfs4_radio.toggled.connect(self.tor_bridges_use_obfs4_radio_toggled) # meek_lite-azure option radio # if the obfs4proxy binary is missing, we can't use meek_lite-azure transports (self.tor_path, self.tor_geo_ip_file_path, self.tor_geo_ipv6_file_path, self.obfs4proxy_file_path) = self.common.get_tor_paths() if not os.path.isfile(self.obfs4proxy_file_path): - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy', True)) + self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option_no_obfs4proxy')) self.tor_bridges_use_meek_lite_azure_radio.setEnabled(False) else: - self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option', True)) + self.tor_bridges_use_meek_lite_azure_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_meek_lite_azure_radio_option')) self.tor_bridges_use_meek_lite_azure_radio.toggled.connect(self.tor_bridges_use_meek_lite_azure_radio_toggled) # meek_lite currently not supported on the version of obfs4proxy bundled with TorBrowser @@ -268,10 +283,10 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_bridges_use_meek_lite_azure_radio.hide() # Custom bridges radio and textbox - self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_custom_radio_option', True)) + self.tor_bridges_use_custom_radio = QtWidgets.QRadioButton(strings._('gui_settings_tor_bridges_custom_radio_option')) self.tor_bridges_use_custom_radio.toggled.connect(self.tor_bridges_use_custom_radio_toggled) - self.tor_bridges_use_custom_label = QtWidgets.QLabel(strings._('gui_settings_tor_bridges_custom_label', True)) + self.tor_bridges_use_custom_label = QtWidgets.QLabel(strings._('gui_settings_tor_bridges_custom_label')) self.tor_bridges_use_custom_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) self.tor_bridges_use_custom_label.setOpenExternalLinks(True) self.tor_bridges_use_custom_textbox = QtWidgets.QPlainTextEdit() @@ -298,14 +313,14 @@ class SettingsDialog(QtWidgets.QDialog): self.bridges.setLayout(bridges_layout) # Automatic - self.connection_type_automatic_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_automatic_option', True)) + self.connection_type_automatic_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_automatic_option')) self.connection_type_automatic_radio.toggled.connect(self.connection_type_automatic_toggled) # Control port - self.connection_type_control_port_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_control_port_option', True)) + self.connection_type_control_port_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_control_port_option')) self.connection_type_control_port_radio.toggled.connect(self.connection_type_control_port_toggled) - connection_type_control_port_extras_label = QtWidgets.QLabel(strings._('gui_settings_control_port_label', True)) + connection_type_control_port_extras_label = QtWidgets.QLabel(strings._('gui_settings_control_port_label')) self.connection_type_control_port_extras_address = QtWidgets.QLineEdit() self.connection_type_control_port_extras_port = QtWidgets.QLineEdit() connection_type_control_port_extras_layout = QtWidgets.QHBoxLayout() @@ -318,10 +333,10 @@ class SettingsDialog(QtWidgets.QDialog): self.connection_type_control_port_extras.hide() # Socket file - self.connection_type_socket_file_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_socket_file_option', True)) + self.connection_type_socket_file_radio = QtWidgets.QRadioButton(strings._('gui_settings_connection_type_socket_file_option')) self.connection_type_socket_file_radio.toggled.connect(self.connection_type_socket_file_toggled) - connection_type_socket_file_extras_label = QtWidgets.QLabel(strings._('gui_settings_socket_file_label', True)) + connection_type_socket_file_extras_label = QtWidgets.QLabel(strings._('gui_settings_socket_file_label')) self.connection_type_socket_file_extras_path = QtWidgets.QLineEdit() connection_type_socket_file_extras_layout = QtWidgets.QHBoxLayout() connection_type_socket_file_extras_layout.addWidget(connection_type_socket_file_extras_label) @@ -332,7 +347,7 @@ class SettingsDialog(QtWidgets.QDialog): self.connection_type_socket_file_extras.hide() # Tor SOCKS address and port - gui_settings_socks_label = QtWidgets.QLabel(strings._('gui_settings_socks_label', True)) + gui_settings_socks_label = QtWidgets.QLabel(strings._('gui_settings_socks_label')) self.connection_type_socks_address = QtWidgets.QLineEdit() self.connection_type_socks_port = QtWidgets.QLineEdit() connection_type_socks_layout = QtWidgets.QHBoxLayout() @@ -347,14 +362,14 @@ class SettingsDialog(QtWidgets.QDialog): # Authentication options # No authentication - self.authenticate_no_auth_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_no_auth_option', True)) + self.authenticate_no_auth_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_no_auth_option')) self.authenticate_no_auth_radio.toggled.connect(self.authenticate_no_auth_toggled) # Password - self.authenticate_password_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_password_option', True)) + self.authenticate_password_radio = QtWidgets.QRadioButton(strings._('gui_settings_authenticate_password_option')) self.authenticate_password_radio.toggled.connect(self.authenticate_password_toggled) - authenticate_password_extras_label = QtWidgets.QLabel(strings._('gui_settings_password_label', True)) + authenticate_password_extras_label = QtWidgets.QLabel(strings._('gui_settings_password_label')) self.authenticate_password_extras_password = QtWidgets.QLineEdit('') authenticate_password_extras_layout = QtWidgets.QHBoxLayout() authenticate_password_extras_layout.addWidget(authenticate_password_extras_label) @@ -369,7 +384,7 @@ class SettingsDialog(QtWidgets.QDialog): authenticate_group_layout.addWidget(self.authenticate_no_auth_radio) authenticate_group_layout.addWidget(self.authenticate_password_radio) authenticate_group_layout.addWidget(self.authenticate_password_extras) - self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label", True)) + self.authenticate_group = QtWidgets.QGroupBox(strings._("gui_settings_authenticate_label")) self.authenticate_group.setLayout(authenticate_group_layout) # Put the radios into their own group so they are exclusive @@ -378,18 +393,18 @@ class SettingsDialog(QtWidgets.QDialog): connection_type_radio_group_layout.addWidget(self.connection_type_automatic_radio) connection_type_radio_group_layout.addWidget(self.connection_type_control_port_radio) connection_type_radio_group_layout.addWidget(self.connection_type_socket_file_radio) - connection_type_radio_group = QtWidgets.QGroupBox(strings._("gui_settings_connection_type_label", True)) + connection_type_radio_group = QtWidgets.QGroupBox(strings._("gui_settings_connection_type_label")) connection_type_radio_group.setLayout(connection_type_radio_group_layout) # The Bridges options are not exclusive (enabling Bridges offers obfs4 or custom bridges) connection_type_bridges_radio_group_layout = QtWidgets.QVBoxLayout() connection_type_bridges_radio_group_layout.addWidget(self.bridges) - self.connection_type_bridges_radio_group = QtWidgets.QGroupBox(strings._("gui_settings_tor_bridges", True)) + self.connection_type_bridges_radio_group = QtWidgets.QGroupBox(strings._("gui_settings_tor_bridges")) self.connection_type_bridges_radio_group.setLayout(connection_type_bridges_radio_group_layout) self.connection_type_bridges_radio_group.hide() # Test tor settings button - self.connection_type_test_button = QtWidgets.QPushButton(strings._('gui_settings_connection_type_test_button', True)) + self.connection_type_test_button = QtWidgets.QPushButton(strings._('gui_settings_connection_type_test_button')) self.connection_type_test_button.clicked.connect(self.test_tor_clicked) connection_type_test_button_layout = QtWidgets.QHBoxLayout() connection_type_test_button_layout.addWidget(self.connection_type_test_button) @@ -405,13 +420,13 @@ class SettingsDialog(QtWidgets.QDialog): connection_type_layout.addLayout(connection_type_test_button_layout) # Buttons - self.save_button = QtWidgets.QPushButton(strings._('gui_settings_button_save', True)) + self.save_button = QtWidgets.QPushButton(strings._('gui_settings_button_save')) self.save_button.clicked.connect(self.save_clicked) - self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True)) + self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel')) self.cancel_button.clicked.connect(self.cancel_clicked) version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(self.common.version)) version_label.setStyleSheet(self.common.css['settings_version']) - self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True)) + self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help')) self.help_button.clicked.connect(self.help_clicked) buttons_layout = QtWidgets.QHBoxLayout() buttons_layout.addWidget(version_label) @@ -431,6 +446,7 @@ class SettingsDialog(QtWidgets.QDialog): left_col_layout.addWidget(sharing_group) left_col_layout.addWidget(receiving_group) left_col_layout.addWidget(autoupdate_group) + left_col_layout.addLayout(language_layout) left_col_layout.addStretch() right_col_layout = QtWidgets.QVBoxLayout() @@ -524,6 +540,10 @@ class SettingsDialog(QtWidgets.QDialog): autoupdate_timestamp = self.old_settings.get('autoupdate_timestamp') self._update_autoupdate_timestamp(autoupdate_timestamp) + locale = self.old_settings.get('locale') + locale_index = self.language_combobox.findData(QtCore.QVariant(locale)) + self.language_combobox.setCurrentIndex(locale_index) + connection_type = self.old_settings.get('connection_type') if connection_type == 'bundled': if self.connection_type_bundled_radio.isEnabled(): @@ -603,7 +623,7 @@ class SettingsDialog(QtWidgets.QDialog): self.tor_bridges_use_custom_textbox_options.hide() # Alert the user about meek's costliness if it looks like they're turning it on if not self.old_settings.get('tor_bridges_use_meek_lite_azure'): - Alert(self.common, strings._('gui_settings_meek_lite_expensive_warning', True), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('gui_settings_meek_lite_expensive_warning'), QtWidgets.QMessageBox.Warning) def tor_bridges_use_custom_radio_toggled(self, checked): """ @@ -716,7 +736,7 @@ class SettingsDialog(QtWidgets.QDialog): """ downloads_dir = self.downloads_dir_lineedit.text() selected_dir = QtWidgets.QFileDialog.getExistingDirectory(self, - strings._('gui_settings_downloads_label', True), downloads_dir) + strings._('gui_settings_downloads_label'), downloads_dir) if selected_dir: self.common.log('SettingsDialog', 'downloads_button_clicked', 'selected dir: {}'.format(selected_dir)) @@ -746,7 +766,7 @@ class SettingsDialog(QtWidgets.QDialog): onion.connect(custom_settings=settings, config=self.config, tor_status_update_func=tor_status_update_func) # If an exception hasn't been raised yet, the Tor settings work - Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) + Alert(self.common, strings._('settings_test_success').format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) # Clean up onion.cleanup() @@ -782,19 +802,19 @@ class SettingsDialog(QtWidgets.QDialog): # Check for updates def update_available(update_url, installed_version, latest_version): - Alert(self.common, strings._("update_available", True).format(update_url, installed_version, latest_version)) + Alert(self.common, strings._("update_available").format(update_url, installed_version, latest_version)) close_forced_update_thread() def update_not_available(): - Alert(self.common, strings._('update_not_available', True)) + Alert(self.common, strings._('update_not_available')) close_forced_update_thread() def update_error(): - Alert(self.common, strings._('update_error_check_error', True), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('update_error_check_error'), QtWidgets.QMessageBox.Warning) close_forced_update_thread() def update_invalid_version(latest_version): - Alert(self.common, strings._('update_error_invalid_latest_version', True).format(latest_version), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('update_error_invalid_latest_version').format(latest_version), QtWidgets.QMessageBox.Warning) close_forced_update_thread() forced_update_thread = UpdateThread(self.common, self.onion, self.config, force=True) @@ -810,8 +830,29 @@ class SettingsDialog(QtWidgets.QDialog): """ self.common.log('SettingsDialog', 'save_clicked') + def changed(s1, s2, keys): + """ + Compare the Settings objects s1 and s2 and return true if any values + have changed for the given keys. + """ + for key in keys: + if s1.get(key) != s2.get(key): + return True + return False + settings = self.settings_from_fields() if settings: + # If language changed, inform user they need to restart OnionShare + if changed(settings, self.old_settings, ['locale']): + # Look up error message in different locale + new_locale = settings.get('locale') + if new_locale in strings.translations and 'gui_settings_language_changed_notice' in strings.translations[new_locale]: + notice = strings.translations[new_locale]['gui_settings_language_changed_notice'] + else: + notice = strings._('gui_settings_language_changed_notice') + Alert(self.common, notice, QtWidgets.QMessageBox.Information) + + # Save the new settings settings.save() # If Tor isn't connected, or if Tor settings have changed, Reinitialize @@ -820,15 +861,6 @@ class SettingsDialog(QtWidgets.QDialog): if not self.local_only: if self.onion.is_authenticated(): self.common.log('SettingsDialog', 'save_clicked', 'Connected to Tor') - def changed(s1, s2, keys): - """ - Compare the Settings objects s1 and s2 and return true if any values - have changed for the given keys. - """ - for key in keys: - if s1.get(key) != s2.get(key): - return True - return False if changed(settings, self.old_settings, [ 'connection_type', 'control_port_address', @@ -873,7 +905,7 @@ class SettingsDialog(QtWidgets.QDialog): """ self.common.log('SettingsDialog', 'cancel_clicked') if not self.local_only and not self.onion.is_authenticated(): - Alert(self.common, strings._('gui_tor_connection_canceled', True), QtWidgets.QMessageBox.Warning) + Alert(self.common, strings._('gui_tor_connection_canceled'), QtWidgets.QMessageBox.Warning) sys.exit() else: self.close() @@ -940,6 +972,12 @@ class SettingsDialog(QtWidgets.QDialog): if not self.stealth_checkbox.isChecked(): settings.set('hidservauth_string', '') + # Language + locale_index = self.language_combobox.currentIndex() + locale = self.language_combobox.itemData(locale_index) + settings.set('locale', locale) + + # Tor connection if self.connection_type_bundled_radio.isChecked(): settings.set('connection_type', 'bundled') if self.connection_type_automatic_radio.isChecked(): @@ -1013,7 +1051,7 @@ class SettingsDialog(QtWidgets.QDialog): new_bridges = ''.join(new_bridges) settings.set('tor_bridges_use_custom_bridges', new_bridges) else: - Alert(self.common, strings._('gui_settings_tor_bridges_invalid', True)) + Alert(self.common, strings._('gui_settings_tor_bridges_invalid')) settings.set('no_bridges', True) return False @@ -1037,11 +1075,11 @@ class SettingsDialog(QtWidgets.QDialog): dt = datetime.datetime.fromtimestamp(autoupdate_timestamp) last_checked = dt.strftime('%B %d, %Y %H:%M') else: - last_checked = strings._('gui_settings_autoupdate_timestamp_never', True) - self.autoupdate_timestamp.setText(strings._('gui_settings_autoupdate_timestamp', True).format(last_checked)) + last_checked = strings._('gui_settings_autoupdate_timestamp_never') + self.autoupdate_timestamp.setText(strings._('gui_settings_autoupdate_timestamp').format(last_checked)) def _tor_status_update(self, progress, summary): - self.tor_status.setText('{}
{}% {}'.format(strings._('connecting_to_tor', True), progress, summary)) + self.tor_status.setText('{}
{}% {}'.format(strings._('connecting_to_tor'), progress, summary)) self.qtapp.processEvents() if 'Done' in summary: self.tor_status.hide() diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py new file mode 100644 index 00000000..fe1e91b8 --- /dev/null +++ b/onionshare_gui/share_mode/downloads.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program 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. + +This program 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 this program. If not, see . +""" +import time +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + + +class Download(object): + def __init__(self, common, download_id, total_bytes): + self.common = common + + self.download_id = download_id + self.started = time.time() + self.total_bytes = total_bytes + self.downloaded_bytes = 0 + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(total_bytes) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) + self.progress_bar.total_bytes = total_bytes + + # Start at 0 + self.update(0) + + def update(self, downloaded_bytes): + self.downloaded_bytes = downloaded_bytes + + self.progress_bar.setValue(downloaded_bytes) + if downloaded_bytes == self.progress_bar.total_bytes: + pb_fmt = strings._('gui_download_upload_progress_complete').format( + self.common.format_seconds(time.time() - self.started)) + else: + elapsed = time.time() - self.started + if elapsed < 10: + # Wait a couple of seconds for the download rate to stabilize. + # This prevents a "Windows copy dialog"-esque experience at + # the beginning of the download. + pb_fmt = strings._('gui_download_upload_progress_starting').format( + self.common.human_readable_filesize(downloaded_bytes)) + else: + pb_fmt = strings._('gui_download_upload_progress_eta').format( + self.common.human_readable_filesize(downloaded_bytes), + self.estimated_time_remaining) + + self.progress_bar.setFormat(pb_fmt) + + def cancel(self): + self.progress_bar.setFormat(strings._('gui_canceled')) + + @property + def estimated_time_remaining(self): + return self.common.estimated_time_remaining(self.downloaded_bytes, + self.total_bytes, + self.started) + + +class Downloads(QtWidgets.QScrollArea): + """ + The downloads chunk of the GUI. This lists all of the active download + progress bars. + """ + def __init__(self, common): + super(Downloads, self).__init__() + self.common = common + + self.downloads = {} + + self.setWindowTitle(strings._('gui_downloads')) + self.setWidgetResizable(True) + self.setMinimumHeight(150) + self.setMinimumWidth(350) + self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png'))) + self.setWindowFlags(QtCore.Qt.Sheet | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.CustomizeWindowHint) + self.vbar = self.verticalScrollBar() + self.vbar.rangeChanged.connect(self.resizeScroll) + + downloads_label = QtWidgets.QLabel(strings._('gui_downloads')) + downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) + self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads')) + self.clear_history_button = QtWidgets.QPushButton(strings._('gui_clear_history')) + self.clear_history_button.clicked.connect(self.reset) + self.clear_history_button.hide() + + self.downloads_layout = QtWidgets.QVBoxLayout() + + widget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout() + layout.addWidget(downloads_label) + layout.addWidget(self.no_downloads_label) + layout.addWidget(self.clear_history_button) + layout.addLayout(self.downloads_layout) + layout.addStretch() + widget.setLayout(layout) + self.setWidget(widget) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.vbar.setValue(maximum) + + def add(self, download_id, total_bytes): + """ + Add a new download progress bar. + """ + # Hide the no_downloads_label + self.no_downloads_label.hide() + # Show the clear_history_button + self.clear_history_button.show() + + # Add it to the list + download = Download(self.common, download_id, total_bytes) + self.downloads[download_id] = download + self.downloads_layout.addWidget(download.progress_bar) + + def update(self, download_id, downloaded_bytes): + """ + Update the progress of a download progress bar. + """ + self.downloads[download_id].update(downloaded_bytes) + + def cancel(self, download_id): + """ + Update a download progress bar to show that it has been canceled. + """ + self.downloads[download_id].cancel() + + def reset(self): + """ + Reset the downloads back to zero + """ + for download in self.downloads.values(): + self.downloads_layout.removeWidget(download.progress_bar) + download.progress_bar.close() + self.downloads = {} + + self.no_downloads_label.show() + self.clear_history_button.hide() + self.resize(self.sizeHint()) diff --git a/onionshare_gui/tor_connection_dialog.py b/onionshare_gui/tor_connection_dialog.py index b3bd1fe5..2bcbf1a6 100644 --- a/onionshare_gui/tor_connection_dialog.py +++ b/onionshare_gui/tor_connection_dialog.py @@ -51,7 +51,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): self.setFixedSize(400, 150) # Label - self.setLabelText(strings._('connecting_to_tor', True)) + self.setLabelText(strings._('connecting_to_tor')) # Progress bar ticks from 0 to 100 self.setRange(0, 100) @@ -81,7 +81,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def _tor_status_update(self, progress, summary): self.setValue(int(progress)) - self.setLabelText("{}
{}".format(strings._('connecting_to_tor', True), summary)) + self.setLabelText("{}
{}".format(strings._('connecting_to_tor'), summary)) def _connected_to_tor(self): self.common.log('TorConnectionDialog', '_connected_to_tor') @@ -104,7 +104,7 @@ class TorConnectionDialog(QtWidgets.QProgressDialog): def alert_and_open_settings(): # Display the exception in an alert box - Alert(self.common, "{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings', True)), QtWidgets.QMessageBox.Warning) + Alert(self.common, "{}\n\n{}".format(msg, strings._('gui_tor_connection_error_settings')), QtWidgets.QMessageBox.Warning) # Open settings self.open_settings.emit() diff --git a/share/locale/en.json b/share/locale/en.json index e5d9a3be..db416c9b 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -106,7 +106,6 @@ "gui_settings_button_help": "Help", "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", "gui_settings_shutdown_timeout": "Stop the share at:", - "settings_saved": "Settings saved in {}", "settings_error_unknown": "Can't connect to Tor controller because your settings don't make sense.", "settings_error_automatic": "Could not connect to the Tor controller. Is Tor Browser (available from torproject.org) running in the background?", "settings_error_socket_port": "Can't connect to the Tor controller at {}:{}.", @@ -181,5 +180,7 @@ "gui_upload_finished_range": "Uploaded {} to {}", "gui_upload_finished": "Uploaded {}", "gui_download_in_progress": "Download Started {}", - "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}" + "gui_open_folder_error_nautilus": "Cannot open folder because nautilus is not available. The file is here: {}", + "gui_settings_language_label": "Preferred language", + "gui_settings_language_changed_notice": "Restart OnionShare for your change in language to take effect." } diff --git a/share/locale/fr.json b/share/locale/fr.json index 967e456e..ee206662 100644 --- a/share/locale/fr.json +++ b/share/locale/fr.json @@ -29,5 +29,6 @@ "gui_please_wait": "Attendez-vous...", "gui_quit_warning_quit": "Quitter", "gui_quit_warning_dont_quit": "Ne quitter pas", - "gui_settings_autoupdate_timestamp_never": "Jamais" + "gui_settings_autoupdate_timestamp_never": "Jamais", + "gui_settings_language_changed_notice": "Redémarrez OnionShare pour que votre changement de langue prenne effet" } diff --git a/tests/conftest.py b/tests/conftest.py index 8ac7efb8..3ae6fd52 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ import tempfile import pytest -from onionshare import common, web, settings +from onionshare import common, web, settings, strings @pytest.fixture def temp_dir_1024(): @@ -151,7 +151,10 @@ def time_strftime(monkeypatch): @pytest.fixture def common_obj(): - return common.Common() + _common = common.Common() + _common.settings = settings.Settings(_common) + strings.load_strings(_common) + return _common @pytest.fixture def settings_obj(sys_onionshare_dev_mode, platform_linux): diff --git a/tests/test_onionshare_settings.py b/tests/test_onionshare_settings.py index 1f1ef528..371b2d27 100644 --- a/tests/test_onionshare_settings.py +++ b/tests/test_onionshare_settings.py @@ -40,7 +40,7 @@ def settings_obj(sys_onionshare_dev_mode, platform_linux): class TestSettings: def test_init(self, settings_obj): - assert settings_obj._settings == settings_obj.default_settings == { + expected_settings = { 'version': 'DUMMY_VERSION_1.2.3', 'connection_type': 'bundled', 'control_port_address': '127.0.0.1', @@ -68,6 +68,11 @@ class TestSettings: 'receive_allow_receiver_shutdown': True, 'public_mode': False } + for key in settings_obj._settings: + # Skip locale, it will not always default to the same thing + if key != 'locale': + assert settings_obj._settings[key] == settings_obj.default_settings[key] + assert settings_obj._settings[key] == expected_settings[key] def test_fill_in_defaults(self, settings_obj): del settings_obj._settings['version'] diff --git a/tests/test_onionshare_strings.py b/tests/test_onionshare_strings.py index d3d40c8f..6d39598c 100644 --- a/tests/test_onionshare_strings.py +++ b/tests/test_onionshare_strings.py @@ -32,12 +32,6 @@ from onionshare import strings # return path # common.get_resource_path = get_resource_path - -def test_starts_with_empty_strings(): - """ Creates an empty strings dict by default """ - assert strings.strings == {} - - def test_underscore_is_function(): assert callable(strings._) and isinstance(strings._, types.FunctionType) @@ -53,11 +47,13 @@ class TestLoadStrings: def test_load_strings_loads_other_languages( self, common_obj, locale_fr, sys_onionshare_dev_mode): """ load_strings() loads other languages in different locales """ - strings.load_strings(common_obj, "fr") + common_obj.settings.set('locale', 'fr') + strings.load_strings(common_obj) assert strings._('preparing_files') == "Préparation des fichiers à partager." def test_load_invalid_locale( self, common_obj, locale_invalid, sys_onionshare_dev_mode): """ load_strings() raises a KeyError for an invalid locale """ with pytest.raises(KeyError): - strings.load_strings(common_obj, 'XX') + common_obj.settings.set('locale', 'XX') + strings.load_strings(common_obj) diff --git a/tests_gui_local/commontests.py b/tests_gui_local/commontests.py index 39011b4a..27e406c1 100644 --- a/tests_gui_local/commontests.py +++ b/tests_gui_local/commontests.py @@ -3,14 +3,62 @@ import requests import socket import socks import zipfile +import json +import shutil from PyQt5 import QtCore, QtTest + from onionshare import strings -from onionshare_gui.mode.receive_mode import ReceiveMode +from onionshare.common import Common +from onionshare.settings import Settings +from onionshare.onion import Onion +from onionshare.web import Web +from onionshare_gui import Application, OnionShare, OnionShareGui from onionshare_gui.mode.share_mode import ShareMode +from onionshare_gui.mode.receive_mode import ReceiveMode class CommonTests(object): + @staticmethod + def set_up(test_settings): + '''Create GUI with given settings''' + # Create our test file + testfile = open('/tmp/test.txt', 'w') + testfile.write('onionshare') + testfile.close() + + common = Common() + common.settings = Settings(common) + common.define_css() + strings.load_strings(common) + + # Get all of the settings in test_settings + test_settings['downloads_dir'] = '/tmp/OnionShare' + for key, val in common.settings.default_settings.items(): + if key not in test_settings: + test_settings[key] = val + + # Start the Onion + testonion = Onion(common) + global qtapp + qtapp = Application(common) + app = OnionShare(common, testonion, True, 0) + + web = Web(common, False, True) + settings_filename = '/tmp/testsettings.json' + open(settings_filename, 'w').write(json.dumps(test_settings)) + + gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], settings_filename, True) + return gui + + @staticmethod + def tear_down(): + try: + os.remove('/tmp/test.txt') + shutil.rmtree('/tmp/OnionShare') + except: + pass + def test_gui_loaded(self): '''Test that the GUI actually is shown''' self.assertTrue(self.gui.show) @@ -190,12 +238,12 @@ class CommonTests(object): def test_server_status_indicator_says_closed(self, mode, stay_open): '''Test that the Server Status indicator shows we closed''' if type(mode) == ReceiveMode: - self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped', True)) + self.assertEquals(self.gui.receive_mode.server_status_label.text(), strings._('gui_status_indicator_receive_stopped')) if type(mode) == ShareMode: if stay_open: - self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped', True)) + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('gui_status_indicator_share_stopped')) else: - self.assertEqual(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically', True)) + self.assertEquals(self.gui.share_mode.server_status_label.text(), strings._('closing_automatically')) # Auto-stop timer tests def test_set_timeout(self, mode, timeout): diff --git a/tests_gui_local/onionshare_receive_mode_upload_test.py b/tests_gui_local/onionshare_receive_mode_upload_test.py index 1ce91ba2..91013d92 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test.py @@ -15,68 +15,17 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": False, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": False, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "receive_allow_receiver_shutdown": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') - os.remove('/tmp/OnionShare/test.txt') - os.remove('/tmp/OnionShare/test-2.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py index cd02e012..42f237c9 100644 --- a/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py +++ b/tests_gui_local/onionshare_receive_mode_upload_test_public_mode.py @@ -8,6 +8,7 @@ import json from PyQt5 import QtWidgets from onionshare.common import Common +from onionshare.settings import Settings from onionshare.web import Web from onionshare import onion, strings from onionshare_gui import * @@ -15,71 +16,17 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": True, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": False, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "receive_allow_receiver_shutdown": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - try: - os.remove('/tmp/test.txt') - os.remove('/tmp/OnionShare/test.txt') - os.remove('/tmp/OnionShare/test-2.txt') - except: - pass + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_share_mode_download_test.py b/tests_gui_local/onionshare_share_mode_download_test.py index 6842f1a6..9caad8b1 100644 --- a/tests_gui_local/onionshare_share_mode_download_test.py +++ b/tests_gui_local/onionshare_share_mode_download_test.py @@ -15,66 +15,17 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": False, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": False, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "close_after_first_download": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py index 82f1989c..c7b05543 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_public_mode.py +++ b/tests_gui_local/onionshare_share_mode_download_test_public_mode.py @@ -15,66 +15,16 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", - "public_mode": True, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": False, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "public_mode": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py index df9bc857..478177c0 100644 --- a/tests_gui_local/onionshare_share_mode_download_test_stay_open.py +++ b/tests_gui_local/onionshare_share_mode_download_test_stay_open.py @@ -15,66 +15,17 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": False, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": True, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": False, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "close_after_first_download": False } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_slug_persistent_test.py b/tests_gui_local/onionshare_slug_persistent_test.py index 5b825dad..f4139afb 100644 --- a/tests_gui_local/onionshare_slug_persistent_test.py +++ b/tests_gui_local/onionshare_slug_persistent_test.py @@ -15,68 +15,18 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' - slug = '' - @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": False, - "receive_allow_receiver_shutdown": True, - "save_private_key": True, - "shutdown_timeout": False, "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "save_private_key": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_local/onionshare_timer_test.py b/tests_gui_local/onionshare_timer_test.py index 4aaaf364..ef55886e 100644 --- a/tests_gui_local/onionshare_timer_test.py +++ b/tests_gui_local/onionshare_timer_test.py @@ -15,66 +15,17 @@ from onionshare_gui import * from .commontests import CommonTests class OnionShareGuiTest(unittest.TestCase): - '''Test the OnionShare GUI''' @classmethod def setUpClass(cls): - '''Create the GUI''' - # Create our test file - testfile = open('/tmp/test.txt', 'w') - testfile.write('onionshare') - testfile.close() - common = Common() - common.define_css() - - # Start the Onion - strings.load_strings(common) - - testonion = onion.Onion(common) - global qtapp - qtapp = Application(common) - app = OnionShare(common, testonion, True, 0) - - web = Web(common, False, True) - test_settings = { - "auth_password": "", - "auth_type": "no_auth", - "autoupdate_timestamp": "", - "close_after_first_download": True, - "connection_type": "bundled", - "control_port_address": "127.0.0.1", - "control_port_port": 9051, - "downloads_dir": "/tmp/OnionShare", - "hidservauth_string": "", - "no_bridges": True, - "private_key": "", "public_mode": False, - "receive_allow_receiver_shutdown": True, - "save_private_key": False, - "shutdown_timeout": True, - "slug": "", - "socks_address": "127.0.0.1", - "socks_port": 9050, - "socket_file_path": "/var/run/tor/control", - "systray_notifications": True, - "tor_bridges_use_meek_lite_azure": False, - "tor_bridges_use_meek_lite_amazon": False, - "tor_bridges_use_custom_bridges": "", - "tor_bridges_use_obfs4": False, - "use_stealth": False, - "use_legacy_v2_onions": False, - "use_autoupdate": True, - "version": "1.3.1" + "shutdown_timeout": True } - testsettings = '/tmp/testsettings.json' - open(testsettings, 'w').write(json.dumps(test_settings)) - - cls.gui = OnionShareGui(common, testonion, qtapp, app, ['/tmp/test.txt'], testsettings, True) + cls.gui = CommonTests.set_up(test_settings) @classmethod def tearDownClass(cls): - '''Clean up after tests''' - os.remove('/tmp/test.txt') + CommonTests.tear_down() @pytest.mark.run(order=1) def test_gui_loaded(self): diff --git a/tests_gui_tor/commontests.py b/tests_gui_tor/commontests.py index a1e420fd..89ebf669 100644 --- a/tests_gui_tor/commontests.py +++ b/tests_gui_tor/commontests.py @@ -56,6 +56,6 @@ class CommonTests(LocalCommonTests): self.gui.app.onion.cleanup(stop_tor=True) QtTest.QTest.qWait(2500) if mode == 'share': - self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) + self.assertTrue(self.gui.share_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost')) if mode == 'receive': - self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost', True)) + self.assertTrue(self.gui.receive_mode.status_bar.currentMessage(), strings._('gui_tor_connection_lost')) diff --git a/tests_gui_tor/conftest.py b/tests_gui_tor/conftest.py index 8ac7efb8..3ae6fd52 100644 --- a/tests_gui_tor/conftest.py +++ b/tests_gui_tor/conftest.py @@ -8,7 +8,7 @@ import tempfile import pytest -from onionshare import common, web, settings +from onionshare import common, web, settings, strings @pytest.fixture def temp_dir_1024(): @@ -151,7 +151,10 @@ def time_strftime(monkeypatch): @pytest.fixture def common_obj(): - return common.Common() + _common = common.Common() + _common.settings = settings.Settings(_common) + strings.load_strings(_common) + return _common @pytest.fixture def settings_obj(sys_onionshare_dev_mode, platform_linux):