diff options
author | Miguel Jacq <mig@mig5.net> | 2018-10-11 13:04:48 +1100 |
---|---|---|
committer | Miguel Jacq <mig@mig5.net> | 2018-10-11 13:04:48 +1100 |
commit | 86b537a4a6376a4280f30912ab9b2e313083c058 (patch) | |
tree | 5d173265aeb7e2798ecb8f48ac2c1d23df9c3ef3 /onionshare_gui | |
parent | f45eae5768f75d2843f835a135708f1b521f0068 (diff) | |
parent | 93dd7a0effc8f5a450c84628ad2c640732d533ca (diff) | |
download | onionshare-86b537a4a6376a4280f30912ab9b2e313083c058.tar.gz onionshare-86b537a4a6376a4280f30912ab9b2e313083c058.zip |
Fix conflicts
Diffstat (limited to 'onionshare_gui')
-rw-r--r-- | onionshare_gui/__init__.py | 10 | ||||
-rw-r--r-- | onionshare_gui/mode/__init__.py (renamed from onionshare_gui/mode.py) | 79 | ||||
-rw-r--r-- | onionshare_gui/mode/history.py | 548 | ||||
-rw-r--r-- | onionshare_gui/mode/receive_mode/__init__.py | 196 | ||||
-rw-r--r-- | onionshare_gui/mode/share_mode/__init__.py (renamed from onionshare_gui/share_mode/__init__.py) | 224 | ||||
-rw-r--r-- | onionshare_gui/mode/share_mode/file_selection.py (renamed from onionshare_gui/share_mode/file_selection.py) | 5 | ||||
-rw-r--r-- | onionshare_gui/mode/share_mode/threads.py | 63 | ||||
-rw-r--r-- | onionshare_gui/onion_thread.py | 45 | ||||
-rw-r--r-- | onionshare_gui/onionshare_gui.py | 20 | ||||
-rw-r--r-- | onionshare_gui/receive_mode/__init__.py | 237 | ||||
-rw-r--r-- | onionshare_gui/receive_mode/uploads.py | 310 | ||||
-rw-r--r-- | onionshare_gui/server_status.py | 34 | ||||
-rw-r--r-- | onionshare_gui/settings_dialog.py | 10 | ||||
-rw-r--r-- | onionshare_gui/share_mode/downloads.py | 157 | ||||
-rw-r--r-- | onionshare_gui/threads.py | 77 |
15 files changed, 1064 insertions, 951 deletions
diff --git a/onionshare_gui/__init__.py b/onionshare_gui/__init__.py index 13f0e8c7..99db635a 100644 --- a/onionshare_gui/__init__.py +++ b/onionshare_gui/__init__.py @@ -18,7 +18,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ from __future__ import division -import os, sys, platform, argparse +import os +import sys +import platform +import argparse +import signal from .widgets import Alert from PyQt5 import QtCore, QtWidgets @@ -58,6 +62,10 @@ def main(): strings.load_strings(common) print(strings._('version_string').format(common.version)) + # Allow Ctrl-C to smoothly quit the program instead of throwing an exception + # https://stackoverflow.com/questions/42814093/how-to-handle-ctrlc-in-python-app-with-pyqt + signal.signal(signal.SIGINT, signal.SIG_DFL) + # Start the Qt app global qtapp qtapp = Application(common) diff --git a/onionshare_gui/mode.py b/onionshare_gui/mode/__init__.py index 418afffd..0971ff32 100644 --- a/onionshare_gui/mode.py +++ b/onionshare_gui/mode/__init__.py @@ -17,16 +17,14 @@ 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 <http://www.gnu.org/licenses/>. """ -import time -import threading from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.common import ShutdownTimer -from .server_status import ServerStatus -from .onion_thread import OnionThread -from .widgets import Alert +from ..server_status import ServerStatus +from ..threads import OnionThread +from ..widgets import Alert class Mode(QtWidgets.QWidget): """ @@ -39,7 +37,7 @@ class Mode(QtWidgets.QWidget): starting_server_error = QtCore.pyqtSignal(str) set_server_active = QtCore.pyqtSignal(bool) - def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None): + def __init__(self, common, qtapp, app, status_bar, server_status_label, system_tray, filenames=None, local_only=False): super(Mode, self).__init__() self.common = common self.qtapp = qtapp @@ -51,13 +49,18 @@ class Mode(QtWidgets.QWidget): self.filenames = filenames - self.setMinimumWidth(450) - # The web object gets created in init() self.web = None + # Local mode is passed from OnionShareGui + self.local_only = local_only + + # Threads start out as None + self.onion_thread = None + self.web_thread = None + # Server status - self.server_status = ServerStatus(self.common, self.qtapp, self.app) + self.server_status = ServerStatus(self.common, self.qtapp, self.app, None, self.local_only) self.server_status.server_started.connect(self.start_server) self.server_status.server_stopped.connect(self.stop_server) self.server_status.server_canceled.connect(self.cancel_server) @@ -67,16 +70,17 @@ class Mode(QtWidgets.QWidget): self.starting_server_step3.connect(self.start_server_step3) self.starting_server_error.connect(self.start_server_error) - # Primary action layout + # Primary action + # Note: It's up to the downstream Mode to add this to its layout self.primary_action_layout = QtWidgets.QVBoxLayout() self.primary_action_layout.addWidget(self.server_status) self.primary_action = QtWidgets.QWidget() self.primary_action.setLayout(self.primary_action_layout) - # Layout - self.layout = QtWidgets.QVBoxLayout() - self.layout.addWidget(self.primary_action) - self.setLayout(self.layout) + # Hack to allow a minimum width on the main layout + # Note: It's up to the downstream Mode to add this to its layout + self.min_width_widget = QtWidgets.QWidget() + self.min_width_widget.setMinimumWidth(600) def init(self): """ @@ -138,34 +142,11 @@ class Mode(QtWidgets.QWidget): self.status_bar.clearMessage() self.server_status_label.setText('') - # Start the onion service in a new thread - def start_onion_service(self): - # Choose a port for the web app - self.app.choose_port() - - # Start http service in new thread - t = threading.Thread(target=self.web.start, args=(self.app.port, not self.common.settings.get('close_after_first_download'), self.common.settings.get('public_mode'), self.common.settings.get('slug'))) - t.daemon = True - t.start() - - # Wait for the web app slug to generate before continuing - if not self.common.settings.get('public_mode'): - while self.web.slug == None: - time.sleep(0.1) - - # Now start the onion service - try: - self.app.start_onion_service() - self.starting_server_step2.emit() - - except Exception as e: - self.starting_server_error.emit(e.args[0]) - return - self.common.log('Mode', 'start_server', 'Starting an onion thread') - self.t = OnionThread(self.common, function=start_onion_service, kwargs={'self': self}) - self.t.daemon = True - self.t.start() + self.onion_thread = OnionThread(self) + self.onion_thread.success.connect(self.starting_server_step2.emit) + self.onion_thread.error.connect(self.starting_server_error.emit) + self.onion_thread.start() def start_server_custom(self): """ @@ -243,10 +224,22 @@ class Mode(QtWidgets.QWidget): """ Cancel the server while it is preparing to start """ - if self.t: - self.t.quit() + self.cancel_server_custom() + + if self.onion_thread: + self.common.log('Mode', 'cancel_server: quitting onion thread') + self.onion_thread.quit() + if self.web_thread: + self.common.log('Mode', 'cancel_server: quitting web thread') + self.web_thread.quit() self.stop_server() + def cancel_server_custom(self): + """ + Add custom initialization here. + """ + pass + def stop_server(self): """ Stop the onionshare server. diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py new file mode 100644 index 00000000..9e821868 --- /dev/null +++ b/onionshare_gui/mode/history.py @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +import time +import subprocess +import os +from datetime import datetime +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings +from ..widgets import Alert + + +class HistoryItem(QtWidgets.QWidget): + """ + The base history item + """ + def __init__(self): + super(HistoryItem, self).__init__() + + def update(self): + pass + + def cancel(self): + pass + + +class DownloadHistoryItem(HistoryItem): + """ + Download history item, for share mode + """ + def __init__(self, common, id, total_bytes): + super(DownloadHistoryItem, self).__init__() + self.common = common + + self.id = id + self.total_bytes = total_bytes + self.downloaded_bytes = 0 + self.started = time.time() + self.started_dt = datetime.fromtimestamp(self.started) + + # Label + self.label = QtWidgets.QLabel(strings._('gui_download_in_progress').format(self.started_dt.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.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 + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.progress_bar) + self.setLayout(layout) + + # 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 UploadHistoryItemFile(QtWidgets.QWidget): + def __init__(self, common, filename): + super(UploadHistoryItemFile, self).__init__() + self.common = common + + self.common.log('UploadHistoryItemFile', '__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('UploadHistoryItemFile', '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': + subprocess.call(['open', '-R', abs_filename]) + + # Windows + elif self.common.platform == 'Windows': + subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)]) + +class UploadHistoryItem(HistoryItem): + def __init__(self, common, id, content_length): + super(UploadHistoryItem, self).__init__() + self.common = common + self.id = id + self.content_length = content_length + self.started = datetime.now() + + # Label + self.label = QtWidgets.QLabel(strings._('gui_upload_in_progress', True).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, data): + """ + Using the progress from Web, update the progress bar and file size labels + for each file + """ + if data['action'] == 'progress': + total_uploaded_bytes = 0 + for filename in data['progress']: + total_uploaded_bytes += data['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(data['progress']): + # Add a new file if needed + if filename not in self.files: + self.files[filename] = UploadHistoryItemFile(self.common, filename) + self.files_layout.addWidget(self.files[filename]) + + # Update the file + self.files[filename].update(data['progress'][filename]['uploaded_bytes'], data['progress'][filename]['complete']) + + elif data['action'] == 'rename': + self.files[data['old_filename']].rename(data['new_filename']) + self.files[data['new_filename']] = self.files.pop(data['old_filename']) + + elif data['action'] == 'finished': + # 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', True).format( + self.started.strftime("%b %d, %I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range', True).format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%I:%M%p") + ) + else: + text = strings._('gui_upload_finished_range', True).format( + self.started.strftime("%b %d, %I:%M%p"), + self.ended.strftime("%b %d, %I:%M%p") + ) + self.label.setText(text) + + +class HistoryItemList(QtWidgets.QScrollArea): + """ + List of items + """ + def __init__(self, common): + super(HistoryItemList, self).__init__() + self.common = common + + self.items = {} + + # The layout that holds all of the items + self.items_layout = QtWidgets.QVBoxLayout() + self.items_layout.setContentsMargins(0, 0, 0, 0) + self.items_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + + # Wrapper layout that also contains a stretch + wrapper_layout = QtWidgets.QVBoxLayout() + wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + wrapper_layout.addLayout(self.items_layout) + wrapper_layout.addStretch() + + # The internal widget of the scroll area + widget = QtWidgets.QWidget() + widget.setLayout(wrapper_layout) + self.setWidget(widget) + self.setWidgetResizable(True) + + # Other scroll area settings + self.setBackgroundRole(QtGui.QPalette.Light) + self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.verticalScrollBar().setValue(maximum) + + def add(self, id, item): + """ + Add a new item. Override this method. + """ + self.items[id] = item + self.items_layout.addWidget(item) + + def update(self, id, data): + """ + Update an item. Override this method. + """ + self.items[id].update(data) + + def cancel(self, id): + """ + Cancel an item. Override this method. + """ + self.items[id].cancel() + + def reset(self): + """ + Reset all items, emptying the list. Override this method. + """ + for item in self.items.values(): + self.items_layout.removeWidget(item) + item.close() + self.items = {} + + +class History(QtWidgets.QWidget): + """ + A history of what's happened so far in this mode. This contains an internal + object full of a scrollable list of items. + """ + def __init__(self, common, empty_image, empty_text, header_text): + super(History, self).__init__() + self.common = common + + self.setMinimumWidth(350) + + # In progress and completed counters + self.in_progress_count = 0 + self.completed_count = 0 + + # In progress and completed labels + self.in_progress_label = QtWidgets.QLabel() + self.in_progress_label.setStyleSheet(self.common.css['mode_info_label']) + self.completed_label = QtWidgets.QLabel() + self.completed_label.setStyleSheet(self.common.css['mode_info_label']) + + # 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.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(self.header_label) + header_layout.addStretch() + header_layout.addWidget(self.in_progress_label) + header_layout.addWidget(self.completed_label) + header_layout.addWidget(clear_button) + + # When there are no items + self.empty_image = QtWidgets.QLabel() + self.empty_image.setAlignment(QtCore.Qt.AlignCenter) + self.empty_image.setPixmap(empty_image) + self.empty_text = QtWidgets.QLabel(empty_text) + self.empty_text.setAlignment(QtCore.Qt.AlignCenter) + self.empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) + empty_layout = QtWidgets.QVBoxLayout() + empty_layout.addStretch() + empty_layout.addWidget(self.empty_image) + empty_layout.addWidget(self.empty_text) + empty_layout.addStretch() + self.empty = QtWidgets.QWidget() + self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) + self.empty.setLayout(empty_layout) + + # When there are items + self.item_list = HistoryItemList(self.common) + self.not_empty_layout = QtWidgets.QVBoxLayout() + self.not_empty_layout.addLayout(header_layout) + self.not_empty_layout.addWidget(self.item_list) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(self.not_empty_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.empty) + layout.addWidget(self.not_empty) + self.setLayout(layout) + + # Reset once at the beginning + self.reset() + + def add(self, id, item): + """ + Add a new item. + """ + self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item)) + + # Hide empty, show not empty + self.empty.hide() + self.not_empty.show() + + # Add it to the list + self.item_list.add(id, item) + + def update(self, id, data): + """ + Update an item. + """ + self.item_list.update(id, data) + + def cancel(self, id): + """ + Cancel an item. + """ + self.item_list.cancel(id) + + def reset(self): + """ + Reset all items. + """ + self.item_list.reset() + + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() + + # Reset counters + self.completed_count = 0 + self.in_progress_count = 0 + self.update_completed() + self.update_in_progress() + + def update_completed(self): + """ + Update the 'completed' widget. + """ + if self.completed_count == 0: + image = self.common.get_resource_path('images/share_completed_none.png') + else: + image = self.common.get_resource_path('images/share_completed.png') + self.completed_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.completed_count)) + self.completed_label.setToolTip(strings._('history_completed_tooltip').format(self.completed_count)) + + def update_in_progress(self): + """ + Update the 'in progress' widget. + """ + if self.in_progress_count == 0: + image = self.common.get_resource_path('images/share_in_progress_none.png') + else: + image = self.common.get_resource_path('images/share_in_progress.png') + self.in_progress_label.setText('<img src="{0:s}" /> {1:d}'.format(image, self.in_progress_count)) + self.in_progress_label.setToolTip(strings._('history_in_progress_tooltip', True).format(self.in_progress_count)) + + +class ToggleHistory(QtWidgets.QPushButton): + """ + Widget for toggling showing or hiding the history, as well as keeping track + of the indicator counter if it's hidden + """ + def __init__(self, common, current_mode, history_widget, icon, selected_icon): + super(ToggleHistory, self).__init__() + self.common = common + self.current_mode = current_mode + self.history_widget = history_widget + self.icon = icon + self.selected_icon = selected_icon + + # Toggle button + self.setDefault(False) + self.setFixedWidth(35) + self.setFixedHeight(30) + self.setFlat(True) + self.setIcon(icon) + self.clicked.connect(self.toggle_clicked) + + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel(parent=self) + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Downloads is hidden. + """ + if increment and not self.history_widget.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) + self.indicator_label.show() + + def toggle_clicked(self): + """ + Toggle showing and hiding the history widget + """ + self.common.log('ToggleHistory', 'toggle_clicked') + + if self.history_widget.isVisible(): + self.history_widget.hide() + self.setIcon(self.icon) + self.setFlat(True) + else: + self.history_widget.show() + self.setIcon(self.selected_icon) + self.setFlat(False) + + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() diff --git a/onionshare_gui/mode/receive_mode/__init__.py b/onionshare_gui/mode/receive_mode/__init__.py new file mode 100644 index 00000000..b73acca2 --- /dev/null +++ b/onionshare_gui/mode/receive_mode/__init__.py @@ -0,0 +1,196 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings +from onionshare.web import Web + +from ..history import History, ToggleHistory, UploadHistoryItem +from .. import Mode + +class ReceiveMode(Mode): + """ + Parts of the main window UI for receiving files. + """ + def init(self): + """ + Custom initialization for ReceiveMode. + """ + # Create the Web object + self.web = Web(self.common, True, 'receive') + + # Server status + self.server_status.set_mode('receive') + self.server_status.server_started_finished.connect(self.update_primary_action) + self.server_status.server_stopped.connect(self.update_primary_action) + self.server_status.server_canceled.connect(self.update_primary_action) + + # Tell server_status about web, then update + self.server_status.web = self.web + self.server_status.update() + + # Upload history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/uploads_transparent.png'))), + strings._('gui_no_uploads'), + strings._('gui_uploads') + ) + self.history.hide() + + # Toggle history + self.toggle_history = ToggleHistory( + self.common, self, self.history, + QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/uploads_toggle_selected.png')) + ) + + # Receive mode warning + receive_warning = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) + receive_warning.setMinimumHeight(80) + receive_warning.setWordWrap(True) + + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addStretch() + top_bar_layout.addWidget(self.toggle_history) + + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(top_bar_layout) + self.main_layout.addWidget(receive_warning) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addStretch() + self.main_layout.addWidget(self.min_width_widget) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QHBoxLayout() + self.wrapper_layout.addLayout(self.main_layout) + self.wrapper_layout.addWidget(self.history) + self.setLayout(self.wrapper_layout) + + def get_stop_server_shutdown_timeout_text(self): + """ + 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) + + def timeout_finished_should_stop_server(self): + """ + The shutdown timer expired, should we stop the server? Returns a bool + """ + # TODO: wait until the final upload is done before stoppign the server? + return True + + def start_server_custom(self): + """ + Starting the server. + """ + # Reset web counters + self.web.receive_mode.upload_count = 0 + self.web.error404_count = 0 + + # Hide and reset the uploads if we have previously shared + self.reset_info_counters() + + def start_server_step2_custom(self): + """ + Step 2 in starting the server. + """ + # Continue + self.starting_server_step3.emit() + self.start_server_finished.emit() + + def handle_tor_broke_custom(self): + """ + Connection to Tor broke. + """ + self.primary_action.hide() + + def handle_request_load(self, event): + """ + Handle REQUEST_LOAD event. + """ + self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True)) + + def handle_request_started(self, event): + """ + Handle REQUEST_STARTED event. + """ + item = UploadHistoryItem(self.common, event["data"]["id"], event["data"]["content_length"]) + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + 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)) + + def handle_request_progress(self, event): + """ + Handle REQUEST_PROGRESS event. + """ + self.history.update(event["data"]["id"], { + 'action': 'progress', + 'progress': event["data"]["progress"] + }) + + def handle_request_close_server(self, event): + """ + Handle REQUEST_CLOSE_SERVER event. + """ + self.stop_server() + self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True)) + + def handle_request_upload_file_renamed(self, event): + """ + Handle REQUEST_UPLOAD_FILE_RENAMED event. + """ + self.history.update(event["data"]["id"], { + 'action': 'rename', + 'old_filename': event["data"]["old_filename"], + 'new_filename': event["data"]["new_filename"] + }) + + def handle_request_upload_finished(self, event): + """ + Handle REQUEST_UPLOAD_FINISHED event. + """ + self.history.update(event["data"]["id"], { + 'action': 'finished' + }) + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() + + def on_reload_settings(self): + """ + We should be ok to re-enable the 'Start Receive Mode' button now. + """ + self.primary_action.show() + + def reset_info_counters(self): + """ + Set the info counters back to zero. + """ + self.history.reset() + + def update_primary_action(self): + self.common.log('ReceiveMode', 'update_primary_action') diff --git a/onionshare_gui/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index 37315bbe..1c1f33ae 100644 --- a/onionshare_gui/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -17,7 +17,6 @@ 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 <http://www.gnu.org/licenses/>. """ -import threading import os from PyQt5 import QtCore, QtWidgets, QtGui @@ -27,9 +26,11 @@ from onionshare.common import Common from onionshare.web import Web from .file_selection import FileSelection -from .downloads import Downloads -from ..mode import Mode -from ..widgets import Alert +from .threads import CompressThread +from .. import Mode +from ..history import History, ToggleHistory, DownloadHistoryItem +from ...widgets import Alert + class ShareMode(Mode): """ @@ -39,8 +40,11 @@ class ShareMode(Mode): """ Custom initialization for ReceiveMode. """ + # Threads start out as None + self.compress_thread = None + # Create the Web object - self.web = Web(self.common, True, False) + self.web = Web(self.common, True, 'share') # File selection self.file_selection = FileSelection(self.common) @@ -67,40 +71,31 @@ class ShareMode(Mode): self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning']) self.filesize_warning.hide() - # Downloads - self.downloads = Downloads(self.common) - self.downloads_in_progress = 0 - self.downloads_completed = 0 + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))), + strings._('gui_no_downloads'), + strings._('gui_downloads') + ) + self.history.hide() - # Information about share, and show downloads button + # Info label self.info_label = QtWidgets.QLabel() - self.info_label.setStyleSheet(self.common.css['mode_info_label']) - - self.info_show_downloads = QtWidgets.QToolButton() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) - self.info_show_downloads.setCheckable(True) - self.info_show_downloads.toggled.connect(self.downloads_toggled) - self.info_show_downloads.setToolTip(strings._('gui_downloads_window_tooltip', True)) - - self.info_in_progress_downloads_count = QtWidgets.QLabel() - self.info_in_progress_downloads_count.setStyleSheet(self.common.css['mode_info_label']) + self.info_label.hide() - self.info_completed_downloads_count = QtWidgets.QLabel() - self.info_completed_downloads_count.setStyleSheet(self.common.css['mode_info_label']) + # Toggle history + self.toggle_history = ToggleHistory( + self.common, self, self.history, + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), + QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) + ) - self.update_downloads_completed() - self.update_downloads_in_progress() - - self.info_layout = QtWidgets.QHBoxLayout() - self.info_layout.addWidget(self.info_label) - self.info_layout.addStretch() - self.info_layout.addWidget(self.info_in_progress_downloads_count) - self.info_layout.addWidget(self.info_completed_downloads_count) - self.info_layout.addWidget(self.info_show_downloads) - - self.info_widget = QtWidgets.QWidget() - self.info_widget.setLayout(self.info_layout) - self.info_widget.hide() + # Top bar + top_bar_layout = QtWidgets.QHBoxLayout() + top_bar_layout.addWidget(self.info_label) + top_bar_layout.addStretch() + top_bar_layout.addWidget(self.toggle_history) # Primary action layout self.primary_action_layout.addWidget(self.filesize_warning) @@ -110,9 +105,18 @@ class ShareMode(Mode): # Status bar, zip progress bar self._zip_progress_bar = None - # Layout - self.layout.insertLayout(0, self.file_selection) - self.layout.insertWidget(0, self.info_widget) + # Main layout + self.main_layout = QtWidgets.QVBoxLayout() + self.main_layout.addLayout(top_bar_layout) + self.main_layout.addLayout(self.file_selection) + self.main_layout.addWidget(self.primary_action) + self.main_layout.addWidget(self.min_width_widget) + + # Wrapper layout + self.wrapper_layout = QtWidgets.QHBoxLayout() + self.wrapper_layout.addLayout(self.main_layout) + self.wrapper_layout.addWidget(self.history) + self.setLayout(self.wrapper_layout) # Always start with focus on file selection self.file_selection.setFocus() @@ -128,7 +132,7 @@ class ShareMode(Mode): The shutdown timer expired, should we stop the server? Returns a bool """ # If there were no attempts to download the share, or all downloads are done, we can stop - if self.web.download_count == 0 or self.web.done: + 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)) return True @@ -142,7 +146,7 @@ class ShareMode(Mode): Starting the server. """ # Reset web counters - self.web.download_count = 0 + self.web.share_mode.download_count = 0 self.web.error404_count = 0 # Hide and reset the downloads if we have previously shared @@ -161,28 +165,13 @@ class ShareMode(Mode): self._zip_progress_bar.total_files_size = ShareMode._compute_total_size(self.filenames) self.status_bar.insertWidget(0, self._zip_progress_bar) - # Prepare the files for sending in a new thread - def finish_starting_server(self): - # Prepare files to share - def _set_processed_size(x): - if self._zip_progress_bar != None: - self._zip_progress_bar.update_processed_size_signal.emit(x) - - try: - self.web.set_file_info(self.filenames, processed_size_callback=_set_processed_size) - self.app.cleanup_filenames.append(self.web.zip_filename) - - # Only continue if the server hasn't been canceled - if self.server_status.status != self.server_status.STATUS_STOPPED: - self.starting_server_step3.emit() - self.start_server_finished.emit() - except OSError as e: - self.starting_server_error.emit(e.strerror) - return - - t = threading.Thread(target=finish_starting_server, kwargs={'self': self}) - t.daemon = True - t.start() + # prepare the files for sending in a new thread + self.compress_thread = CompressThread(self) + self.compress_thread.success.connect(self.starting_server_step3.emit) + self.compress_thread.success.connect(self.start_server_finished.emit) + self.compress_thread.error.connect(self.starting_server_error.emit) + self.server_status.server_canceled.connect(self.compress_thread.cancel) + self.compress_thread.start() def start_server_step3_custom(self): """ @@ -195,7 +184,7 @@ class ShareMode(Mode): self._zip_progress_bar = None # Warn about sending large files over Tor - if self.web.zip_filesize >= 157286400: # 150mb + if self.web.share_mode.download_filesize >= 157286400: # 150mb self.filesize_warning.setText(strings._("large_filesize", True)) self.filesize_warning.show() @@ -217,17 +206,24 @@ class ShareMode(Mode): self._zip_progress_bar = None self.filesize_warning.hide() - self.downloads_in_progress = 0 - self.downloads_completed = 0 - self.update_downloads_in_progress() + self.history.in_progress_count = 0 + self.history.completed_count = 0 + self.history.update_in_progress() self.file_selection.file_list.adjustSize() + def cancel_server_custom(self): + """ + Stop the compression thread on cancel + """ + if self.compress_thread: + self.common.log('ShareMode', 'cancel_server: quitting compress thread') + self.compress_thread.quit() + def handle_tor_broke_custom(self): """ Connection to Tor broke. """ self.primary_action.hide() - self.info_widget.hide() def handle_request_load(self, event): """ @@ -239,9 +235,16 @@ class ShareMode(Mode): """ Handle REQUEST_STARTED event. """ - self.downloads.add(event["data"]["id"], self.web.zip_filesize) - self.downloads_in_progress += 1 - self.update_downloads_in_progress() + if event["data"]["use_gzip"]: + filesize = self.web.share_mode.gzip_filesize + else: + filesize = self.web.share_mode.download_filesize + + item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) + self.history.add(event["data"]["id"], item) + self.toggle_history.update_indicator(True) + 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)) @@ -249,18 +252,17 @@ class ShareMode(Mode): """ Handle REQUEST_PROGRESS event. """ - self.downloads.update(event["data"]["id"], event["data"]["bytes"]) + self.history.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? - if event["data"]["bytes"] == self.web.zip_filesize: + 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)) - # Update the total 'completed downloads' info - self.downloads_completed += 1 - self.update_downloads_completed() - # Update the 'in progress downloads' info - self.downloads_in_progress -= 1 - self.update_downloads_in_progress() + # Update completed and in progress labels + self.history.completed_count += 1 + self.history.in_progress_count -= 1 + self.history.update_completed() + self.history.update_in_progress() # Close on finish? if self.common.settings.get('close_after_first_download'): @@ -269,19 +271,19 @@ class ShareMode(Mode): self.server_status_label.setText(strings._('closing_automatically', True)) else: if self.server_status.status == self.server_status.STATUS_STOPPED: - self.downloads.cancel(event["data"]["id"]) - self.downloads_in_progress = 0 - self.update_downloads_in_progress() + self.history.cancel(event["data"]["id"]) + self.history.in_progress_count = 0 + self.history.update_in_progress() def handle_request_canceled(self, event): """ Handle REQUEST_CANCELED event. """ - self.downloads.cancel(event["data"]["id"]) + self.history.cancel(event["data"]["id"]) - # Update the 'in progress downloads' info - self.downloads_in_progress -= 1 - self.update_downloads_in_progress() + # Update in progress count + self.history.in_progress_count -= 1 + self.history.update_in_progress() self.system_tray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True)) def on_reload_settings(self): @@ -291,14 +293,16 @@ class ShareMode(Mode): """ if self.server_status.file_selection.get_num_files() > 0: self.primary_action.show() - self.info_widget.show() + self.info_label.show() def update_primary_action(self): + self.common.log('ShareMode', 'update_primary_action') + # Show or hide primary action layout file_count = self.file_selection.file_list.count() if file_count > 0: self.primary_action.show() - self.info_widget.show() + self.info_label.show() # Update the file count in the info label total_size_bytes = 0 @@ -314,54 +318,13 @@ class ShareMode(Mode): else: self.primary_action.hide() - self.info_widget.hide() - - # Resize window - self.adjustSize() - - def downloads_toggled(self, checked): - """ - When the 'Show/hide downloads' button is toggled, show or hide the downloads window. - """ - self.common.log('ShareMode', 'toggle_downloads') - if checked: - self.downloads.show() - else: - self.downloads.hide() + self.info_label.hide() def reset_info_counters(self): """ Set the info counters back to zero. """ - self.downloads_completed = 0 - self.downloads_in_progress = 0 - self.update_downloads_completed() - self.update_downloads_in_progress() - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_gray.png'))) - self.downloads.reset() - - def update_downloads_completed(self): - """ - Update the 'Downloads completed' info widget. - """ - if self.downloads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.info_completed_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_completed)) - self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(self.downloads_completed)) - - def update_downloads_in_progress(self): - """ - Update the 'Downloads in progress' info widget. - """ - if self.downloads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_downloads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/download_window_green.png'))) - self.info_in_progress_downloads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.downloads_in_progress)) - self.info_in_progress_downloads_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(self.downloads_in_progress)) + self.history.reset() @staticmethod def _compute_total_size(filenames): @@ -410,6 +373,7 @@ class ZipProgressBar(QtWidgets.QProgressBar): def update_processed_size(self, val): self._processed_size = val + if self.processed_size < self.total_files_size: self.setValue(int((self.processed_size * 100) / self.total_files_size)) elif self.total_files_size != 0: diff --git a/onionshare_gui/share_mode/file_selection.py b/onionshare_gui/mode/share_mode/file_selection.py index 628ad5ef..6bfa7dbf 100644 --- a/onionshare_gui/share_mode/file_selection.py +++ b/onionshare_gui/mode/share_mode/file_selection.py @@ -22,7 +22,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings -from ..widgets import Alert, AddFileDialog +from ...widgets import Alert, AddFileDialog class DropHereLabel(QtWidgets.QLabel): """ @@ -89,7 +89,7 @@ class FileList(QtWidgets.QListWidget): self.setAcceptDrops(True) self.setIconSize(QtCore.QSize(32, 32)) self.setSortingEnabled(True) - self.setMinimumHeight(205) + self.setMinimumHeight(160) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.drop_here_image = DropHereLabel(self.common, self, True) self.drop_here_text = DropHereLabel(self.common, self, False) @@ -261,6 +261,7 @@ class FileList(QtWidgets.QListWidget): # Item info widget, with a white background item_info_layout = QtWidgets.QHBoxLayout() + item_info_layout.setContentsMargins(0, 0, 0, 0) item_info_layout.addWidget(item_size) item_info_layout.addWidget(item.item_button) item_info = QtWidgets.QWidget() diff --git a/onionshare_gui/mode/share_mode/threads.py b/onionshare_gui/mode/share_mode/threads.py new file mode 100644 index 00000000..24e2c242 --- /dev/null +++ b/onionshare_gui/mode/share_mode/threads.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +from PyQt5 import QtCore + + +class CompressThread(QtCore.QThread): + """ + Compresses files to be shared + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(CompressThread, self).__init__() + self.mode = mode + self.mode.common.log('CompressThread', '__init__') + + # prepare files to share + def set_processed_size(self, x): + if self.mode._zip_progress_bar != None: + self.mode._zip_progress_bar.update_processed_size_signal.emit(x) + + def run(self): + self.mode.common.log('CompressThread', 'run') + + try: + if self.mode.web.share_mode.set_file_info(self.mode.filenames, processed_size_callback=self.set_processed_size): + self.success.emit() + else: + # Cancelled + pass + + self.mode.app.cleanup_filenames += self.mode.web.share_mode.cleanup_filenames + except OSError as e: + self.error.emit(e.strerror) + + def cancel(self): + self.mode.common.log('CompressThread', 'cancel') + + # Let the Web and ZipWriter objects know that we're canceling compression early + self.mode.web.cancel_compression = True + try: + self.mode.web.zip_writer.cancel_compression = True + except AttributeError: + # we never made it as far as creating a ZipWriter object + pass diff --git a/onionshare_gui/onion_thread.py b/onionshare_gui/onion_thread.py deleted file mode 100644 index 0a25e891..00000000 --- a/onionshare_gui/onion_thread.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> - -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 <http://www.gnu.org/licenses/>. -""" -from PyQt5 import QtCore - -class OnionThread(QtCore.QThread): - """ - A QThread for starting our Onion Service. - By using QThread rather than threading.Thread, we are able - to call quit() or terminate() on the startup if the user - decided to cancel (in which case do not proceed with obtaining - the Onion address and starting the web server). - """ - def __init__(self, common, function, kwargs=None): - super(OnionThread, self).__init__() - - self.common = common - - self.common.log('OnionThread', '__init__') - self.function = function - if not kwargs: - self.kwargs = {} - else: - self.kwargs = kwargs - - def run(self): - self.common.log('OnionThread', 'run') - - self.function(**self.kwargs) diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py index b63119bb..c2e6657b 100644 --- a/onionshare_gui/onionshare_gui.py +++ b/onionshare_gui/onionshare_gui.py @@ -23,8 +23,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from onionshare.web import Web -from .share_mode import ShareMode -from .receive_mode import ReceiveMode +from .mode.share_mode import ShareMode +from .mode.receive_mode import ReceiveMode from .tor_connection_dialog import TorConnectionDialog from .settings_dialog import SettingsDialog @@ -45,6 +45,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.common = common self.common.log('OnionShareGui', '__init__') + self.setMinimumWidth(820) + self.setMinimumHeight(660) self.onion = onion self.qtapp = qtapp @@ -55,7 +57,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setWindowTitle('OnionShare') self.setWindowIcon(QtGui.QIcon(self.common.get_resource_path('images/logo.png'))) - self.setMinimumWidth(450) # Load settings self.config = config @@ -66,7 +67,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.settings_action = menu.addAction(strings._('gui_settings_window_title', True)) self.settings_action.triggered.connect(self.open_settings) help_action = menu.addAction(strings._('gui_settings_button_help', True)) - help_action.triggered.connect(SettingsDialog.help_clicked) + help_action.triggered.connect(SettingsDialog.open_help) exit_action = menu.addAction(strings._('systray_menu_exit', True)) exit_action.triggered.connect(self.close) @@ -121,7 +122,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.setStatusBar(self.status_bar) # Share mode - self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames) + self.share_mode = ShareMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, filenames, self.local_only) self.share_mode.init() self.share_mode.server_status.server_started.connect(self.update_server_status_indicator) self.share_mode.server_status.server_stopped.connect(self.update_server_status_indicator) @@ -135,7 +136,7 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode.set_server_active.connect(self.set_server_active) # Receive mode - self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray) + self.receive_mode = ReceiveMode(self.common, qtapp, app, self.status_bar, self.server_status_label, self.system_tray, None, self.local_only) self.receive_mode.init() self.receive_mode.server_status.server_started.connect(self.update_server_status_indicator) self.receive_mode.server_status.server_stopped.connect(self.update_server_status_indicator) @@ -153,7 +154,7 @@ class OnionShareGui(QtWidgets.QMainWindow): # Layouts contents_layout = QtWidgets.QVBoxLayout() - contents_layout.setContentsMargins(10, 10, 10, 10) + contents_layout.setContentsMargins(10, 0, 10, 0) contents_layout.addWidget(self.receive_mode) contents_layout.addWidget(self.share_mode) @@ -194,8 +195,8 @@ class OnionShareGui(QtWidgets.QMainWindow): self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) - self.share_mode.show() self.receive_mode.hide() + self.share_mode.show() else: self.share_mode_button.setStyleSheet(self.common.css['mode_switcher_unselected_style']) self.receive_mode_button.setStyleSheet(self.common.css['mode_switcher_selected_style']) @@ -205,9 +206,6 @@ class OnionShareGui(QtWidgets.QMainWindow): self.update_server_status_indicator() - # Wait 1ms for the event loop to finish, then adjust size - QtCore.QTimer.singleShot(1, self.adjustSize) - def share_mode_clicked(self): if self.mode != self.MODE_SHARE: self.common.log('OnionShareGui', 'share_mode_clicked') diff --git a/onionshare_gui/receive_mode/__init__.py b/onionshare_gui/receive_mode/__init__.py deleted file mode 100644 index d414f3b0..00000000 --- a/onionshare_gui/receive_mode/__init__.py +++ /dev/null @@ -1,237 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> - -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 <http://www.gnu.org/licenses/>. -""" -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings -from onionshare.web import Web - -from .uploads import Uploads -from ..mode import Mode - -class ReceiveMode(Mode): - """ - Parts of the main window UI for receiving files. - """ - def init(self): - """ - Custom initialization for ReceiveMode. - """ - # Create the Web object - self.web = Web(self.common, True, True) - - # Server status - self.server_status.set_mode('receive') - self.server_status.server_started_finished.connect(self.update_primary_action) - self.server_status.server_stopped.connect(self.update_primary_action) - self.server_status.server_canceled.connect(self.update_primary_action) - - # Tell server_status about web, then update - self.server_status.web = self.web - self.server_status.update() - - # Downloads - self.uploads = Uploads(self.common) - self.uploads_in_progress = 0 - self.uploads_completed = 0 - self.new_upload = False # For scrolling to the bottom of the uploads list - - # Information about share, and show uploads button - self.info_show_uploads = QtWidgets.QToolButton() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) - self.info_show_uploads.setCheckable(True) - self.info_show_uploads.toggled.connect(self.uploads_toggled) - self.info_show_uploads.setToolTip(strings._('gui_uploads_window_tooltip', True)) - - self.info_in_progress_uploads_count = QtWidgets.QLabel() - self.info_in_progress_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.info_completed_uploads_count = QtWidgets.QLabel() - self.info_completed_uploads_count.setStyleSheet(self.common.css['mode_info_label']) - - self.update_uploads_completed() - self.update_uploads_in_progress() - - self.info_layout = QtWidgets.QHBoxLayout() - self.info_layout.addStretch() - self.info_layout.addWidget(self.info_in_progress_uploads_count) - self.info_layout.addWidget(self.info_completed_uploads_count) - self.info_layout.addWidget(self.info_show_uploads) - - self.info_widget = QtWidgets.QWidget() - self.info_widget.setLayout(self.info_layout) - self.info_widget.hide() - - # Receive mode info - self.receive_info = QtWidgets.QLabel(strings._('gui_receive_mode_warning', True)) - self.receive_info.setMinimumHeight(80) - self.receive_info.setWordWrap(True) - - # Layout - self.layout.insertWidget(0, self.receive_info) - self.layout.insertWidget(0, self.info_widget) - - def get_stop_server_shutdown_timeout_text(self): - """ - 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) - - def timeout_finished_should_stop_server(self): - """ - The shutdown timer expired, should we stop the server? Returns a bool - """ - # TODO: wait until the final upload is done before stoppign the server? - return True - - def start_server_custom(self): - """ - Starting the server. - """ - # Reset web counters - self.web.upload_count = 0 - self.web.error404_count = 0 - - # Hide and reset the uploads if we have previously shared - self.reset_info_counters() - - def start_server_step2_custom(self): - """ - Step 2 in starting the server. - """ - # Continue - self.starting_server_step3.emit() - self.start_server_finished.emit() - - def handle_tor_broke_custom(self): - """ - Connection to Tor broke. - """ - self.primary_action.hide() - self.info_widget.hide() - - def handle_request_load(self, event): - """ - Handle REQUEST_LOAD event. - """ - self.system_tray.showMessage(strings._('systray_page_loaded_title', True), strings._('systray_upload_page_loaded_message', True)) - - def handle_request_started(self, event): - """ - Handle REQUEST_STARTED event. - """ - self.uploads.add(event["data"]["id"], event["data"]["content_length"]) - self.uploads_in_progress += 1 - self.update_uploads_in_progress() - - self.system_tray.showMessage(strings._('systray_upload_started_title', True), strings._('systray_upload_started_message', True)) - - def handle_request_progress(self, event): - """ - Handle REQUEST_PROGRESS event. - """ - self.uploads.update(event["data"]["id"], event["data"]["progress"]) - - def handle_request_close_server(self, event): - """ - Handle REQUEST_CLOSE_SERVER event. - """ - self.stop_server() - self.system_tray.showMessage(strings._('systray_close_server_title', True), strings._('systray_close_server_message', True)) - - def handle_request_upload_file_renamed(self, event): - """ - Handle REQUEST_UPLOAD_FILE_RENAMED event. - """ - self.uploads.rename(event["data"]["id"], event["data"]["old_filename"], event["data"]["new_filename"]) - - def handle_request_upload_finished(self, event): - """ - Handle REQUEST_UPLOAD_FINISHED event. - """ - self.uploads.finished(event["data"]["id"]) - # Update the total 'completed uploads' info - self.uploads_completed += 1 - self.update_uploads_completed() - # Update the 'in progress uploads' info - self.uploads_in_progress -= 1 - self.update_uploads_in_progress() - - def on_reload_settings(self): - """ - We should be ok to re-enable the 'Start Receive Mode' button now. - """ - self.primary_action.show() - self.info_widget.show() - - def reset_info_counters(self): - """ - Set the info counters back to zero. - """ - self.uploads_completed = 0 - self.uploads_in_progress = 0 - self.update_uploads_completed() - self.update_uploads_in_progress() - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_gray.png'))) - self.uploads.reset() - - def update_uploads_completed(self): - """ - Update the 'Uploads completed' info widget. - """ - if self.uploads_completed == 0: - image = self.common.get_resource_path('images/share_completed_none.png') - else: - image = self.common.get_resource_path('images/share_completed.png') - self.info_completed_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_completed)) - self.info_completed_uploads_count.setToolTip(strings._('info_completed_uploads_tooltip', True).format(self.uploads_completed)) - - def update_uploads_in_progress(self): - """ - Update the 'Uploads in progress' info widget. - """ - if self.uploads_in_progress == 0: - image = self.common.get_resource_path('images/share_in_progress_none.png') - else: - image = self.common.get_resource_path('images/share_in_progress.png') - self.info_show_uploads.setIcon(QtGui.QIcon(self.common.get_resource_path('images/upload_window_green.png'))) - self.info_in_progress_uploads_count.setText('<img src="{0:s}" /> {1:d}'.format(image, self.uploads_in_progress)) - self.info_in_progress_uploads_count.setToolTip(strings._('info_in_progress_uploads_tooltip', True).format(self.uploads_in_progress)) - - def update_primary_action(self): - self.common.log('ReceiveMode', 'update_primary_action') - - # Show the info widget when the server is active - if self.server_status.status == self.server_status.STATUS_STARTED: - self.info_widget.show() - else: - self.info_widget.hide() - - # Resize window - self.adjustSize() - - def uploads_toggled(self, checked): - """ - When the 'Show/hide uploads' button is toggled, show or hide the uploads window. - """ - self.common.log('ReceiveMode', 'toggle_uploads') - if checked: - self.uploads.show() - else: - self.uploads.hide() diff --git a/onionshare_gui/receive_mode/uploads.py b/onionshare_gui/receive_mode/uploads.py deleted file mode 100644 index 8d95d984..00000000 --- a/onionshare_gui/receive_mode/uploads.py +++ /dev/null @@ -1,310 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> - -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 <http://www.gnu.org/licenses/>. -""" -import os -import subprocess -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': - subprocess.call(['open', '-R', abs_filename]) - - # Windows - elif self.common.platform == 'Windows': - subprocess.Popen(['explorer', '/select,{}'.format(abs_filename)]) - - -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', True).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', True).format( - self.started.strftime("%b %d, %I:%M%p") - ) - else: - text = strings._('gui_upload_finished_range', True).format( - self.started.strftime("%b %d, %I:%M%p"), - self.ended.strftime("%I:%M%p") - ) - else: - text = strings._('gui_upload_finished_range', True).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', True)) - self.setWidgetResizable(True) - self.setMaximumHeight(600) - 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', True)) - uploads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - self.no_uploads_label = QtWidgets.QLabel(strings._('gui_no_uploads', True)) - - self.uploads_layout = QtWidgets.QVBoxLayout() - - widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout() - layout.addWidget(uploads_label) - layout.addWidget(self.no_uploads_label) - 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() - - # 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(): - self.uploads_layout.removeWidget(upload) - self.uploads = {} - - self.no_uploads_label.show() - 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(): - if item.filename_label_width > width: - item.filename_label.setText(item.filename[:25] + '[...]') - item.adjustSize() - if width > item.filename_label_width: - item.filename_label.setText(item.filename) - except: - pass diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index 9afceb38..0267d826 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -44,7 +44,7 @@ class ServerStatus(QtWidgets.QWidget): STATUS_WORKING = 1 STATUS_STARTED = 2 - def __init__(self, common, qtapp, app, file_selection=None): + def __init__(self, common, qtapp, app, file_selection=None, local_only=False): super(ServerStatus, self).__init__() self.common = common @@ -56,17 +56,23 @@ class ServerStatus(QtWidgets.QWidget): self.app = app self.web = None + self.local_only = local_only self.resizeEvent(None) # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout = QtWidgets.QDateTimeEdit() - # Set proposed timeout to be 5 minutes into the future self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy") - self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if self.local_only: + # For testing + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(15)) + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime()) + else: + # Set proposed timeout to be 5 minutes into the future + self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) + # Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 60s from now + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection) shutdown_timeout_layout = QtWidgets.QHBoxLayout() shutdown_timeout_layout.addWidget(self.shutdown_timeout_label) @@ -84,20 +90,20 @@ class ServerStatus(QtWidgets.QWidget): self.server_button.clicked.connect(self.server_button_clicked) # URL layout - url_font = QtGui.QFont() + url_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont) self.url_description = QtWidgets.QLabel() self.url_description.setWordWrap(True) self.url_description.setMinimumHeight(50) self.url = QtWidgets.QLabel() self.url.setFont(url_font) self.url.setWordWrap(True) - self.url.setMinimumHeight(65) 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.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.setFlat(True) @@ -136,12 +142,12 @@ class ServerStatus(QtWidgets.QWidget): When the widget is resized, try and adjust the display of a v3 onion URL. """ try: - self.get_url() + # Wrap the URL label url_length=len(self.get_url()) if url_length > 60: width = self.frameGeometry().width() if width < 530: - wrapped_onion_url = textwrap.fill(self.get_url(), 50) + wrapped_onion_url = textwrap.fill(self.get_url(), 46) self.url.setText(wrapped_onion_url) else: self.url.setText(self.get_url()) @@ -154,7 +160,8 @@ class ServerStatus(QtWidgets.QWidget): Reset the timeout in the UI after stopping a share """ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300)) - self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120)) + if not self.local_only: + self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(60)) def update(self): """ @@ -255,8 +262,11 @@ class ServerStatus(QtWidgets.QWidget): """ if self.status == self.STATUS_STOPPED: if self.common.settings.get('shutdown_timeout'): - # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen - self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) + if self.local_only: + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime() + else: + # Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen + self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0) # If the timeout has actually passed already before the user hit Start, refuse to start the server. if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout: Alert(self.common, strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning)) diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index c31d4630..39f08128 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -746,7 +746,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)) + Alert(self.common, strings._('settings_test_success', True).format(onion.tor_version, onion.supports_ephemeral, onion.supports_stealth, onion.supports_next_gen_onions)) # Clean up onion.cleanup() @@ -883,8 +883,12 @@ class SettingsDialog(QtWidgets.QDialog): Help button clicked. """ self.common.log('SettingsDialog', 'help_clicked') - help_site = 'https://github.com/micahflee/onionshare/wiki' - QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_site)) + SettingsDialog.open_help() + + @staticmethod + def open_help(): + help_url = 'https://github.com/micahflee/onionshare/wiki' + QtGui.QDesktopServices.openUrl(QtCore.QUrl(help_url)) def settings_from_fields(self): """ diff --git a/onionshare_gui/share_mode/downloads.py b/onionshare_gui/share_mode/downloads.py deleted file mode 100644 index 538ddfd0..00000000 --- a/onionshare_gui/share_mode/downloads.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> - -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 <http://www.gnu.org/licenses/>. -""" -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', True)) - self.setWidgetResizable(True) - self.setMaximumHeight(600) - 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', True)) - downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - self.no_downloads_label = QtWidgets.QLabel(strings._('gui_no_downloads', True)) - - self.downloads_layout = QtWidgets.QVBoxLayout() - - widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout() - layout.addWidget(downloads_label) - layout.addWidget(self.no_downloads_label) - 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() - - # 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.resize(self.sizeHint()) diff --git a/onionshare_gui/threads.py b/onionshare_gui/threads.py new file mode 100644 index 00000000..3b05bebf --- /dev/null +++ b/onionshare_gui/threads.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee <micah@micahflee.com> + +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 <http://www.gnu.org/licenses/>. +""" +import time +from PyQt5 import QtCore + +from onionshare.onion import * + + +class OnionThread(QtCore.QThread): + """ + Starts the onion service, and waits for it to finish + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(OnionThread, self).__init__() + self.mode = mode + self.mode.common.log('OnionThread', '__init__') + + # allow this thread to be terminated + self.setTerminationEnabled() + + def run(self): + self.mode.common.log('OnionThread', 'run') + + self.mode.app.stay_open = not self.mode.common.settings.get('close_after_first_download') + + # start onionshare http service in new thread + self.mode.web_thread = WebThread(self.mode) + self.mode.web_thread.start() + + # wait for modules in thread to load, preventing a thread-related cx_Freeze crash + time.sleep(0.2) + + try: + self.mode.app.start_onion_service() + self.success.emit() + + except (TorTooOld, TorErrorInvalidSetting, TorErrorAutomatic, TorErrorSocketPort, TorErrorSocketFile, TorErrorMissingPassword, TorErrorUnreadableCookieFile, TorErrorAuthError, TorErrorProtocolError, BundledTorTimeout, OSError) as e: + self.error.emit(e.args[0]) + return + + +class WebThread(QtCore.QThread): + """ + Starts the web service + """ + success = QtCore.pyqtSignal() + error = QtCore.pyqtSignal(str) + + def __init__(self, mode): + super(WebThread, self).__init__() + self.mode = mode + self.mode.common.log('WebThread', '__init__') + + def run(self): + self.mode.common.log('WebThread', 'run') + self.mode.app.choose_port() + self.mode.web.start(self.mode.app.port, self.mode.app.stay_open, self.mode.common.settings.get('public_mode'), self.mode.common.settings.get('slug')) |