aboutsummaryrefslogtreecommitdiff
path: root/desktop/onionshare/tab/mode
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/onionshare/tab/mode')
-rw-r--r--desktop/onionshare/tab/mode/__init__.py576
-rw-r--r--desktop/onionshare/tab/mode/chat_mode/__init__.py159
-rw-r--r--desktop/onionshare/tab/mode/file_selection.py534
-rw-r--r--desktop/onionshare/tab/mode/history.py893
-rw-r--r--desktop/onionshare/tab/mode/mode_settings_widget.py288
-rw-r--r--desktop/onionshare/tab/mode/receive_mode/__init__.py427
-rw-r--r--desktop/onionshare/tab/mode/share_mode/__init__.py478
-rw-r--r--desktop/onionshare/tab/mode/share_mode/threads.py62
-rw-r--r--desktop/onionshare/tab/mode/website_mode/__init__.py388
9 files changed, 3805 insertions, 0 deletions
diff --git a/desktop/onionshare/tab/mode/__init__.py b/desktop/onionshare/tab/mode/__init__.py
new file mode 100644
index 00000000..c9b5cad1
--- /dev/null
+++ b/desktop/onionshare/tab/mode/__init__.py
@@ -0,0 +1,576 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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 PySide2 import QtCore, QtWidgets
+
+from onionshare_cli.common import AutoStopTimer
+
+from .history import IndividualFileHistoryItem
+from .mode_settings_widget import ModeSettingsWidget
+
+from ..server_status import ServerStatus
+from ... import strings
+from ...threads import OnionThread, AutoStartTimer
+from ...widgets import Alert, MinimumSizeWidget
+
+
+class Mode(QtWidgets.QWidget):
+ """
+ The class that all modes inherit from
+ """
+
+ start_server_finished = QtCore.Signal()
+ stop_server_finished = QtCore.Signal()
+ starting_server_step2 = QtCore.Signal()
+ starting_server_step3 = QtCore.Signal()
+ starting_server_error = QtCore.Signal(str)
+ starting_server_early = QtCore.Signal()
+ set_server_active = QtCore.Signal(bool)
+ change_persistent = QtCore.Signal(int, bool)
+
+ def __init__(self, tab):
+ super(Mode, self).__init__()
+ self.tab = tab
+ self.settings = tab.settings
+
+ self.common = tab.common
+ self.qtapp = self.common.gui.qtapp
+ self.app = tab.app
+
+ self.status_bar = tab.status_bar
+ self.server_status_label = tab.status_bar.server_status_label
+ self.system_tray = tab.system_tray
+
+ self.filenames = tab.filenames
+
+ # The web object gets created in init()
+ self.web = None
+
+ # Threads start out as None
+ self.onion_thread = None
+ self.web_thread = None
+ self.startup_thread = None
+
+ # Mode settings widget
+ self.mode_settings_widget = ModeSettingsWidget(
+ self.common, self.tab, self.settings
+ )
+ self.mode_settings_widget.change_persistent.connect(self.change_persistent)
+
+ # Server status
+ self.server_status = ServerStatus(
+ self.common,
+ self.qtapp,
+ self.app,
+ self.settings,
+ self.mode_settings_widget,
+ None,
+ self.common.gui.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)
+ self.start_server_finished.connect(self.server_status.start_server_finished)
+ self.stop_server_finished.connect(self.server_status.stop_server_finished)
+ self.starting_server_step2.connect(self.start_server_step2)
+ self.starting_server_step3.connect(self.start_server_step3)
+ self.starting_server_early.connect(self.start_server_early)
+ self.starting_server_error.connect(self.start_server_error)
+
+ # 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.mode_settings_widget)
+ self.primary_action = QtWidgets.QWidget()
+ self.primary_action.setLayout(self.primary_action_layout)
+
+ # It's up to the downstream Mode to add stuff to self.content_layout
+ # self.content_layout shows the actual content of the mode
+ # self.tor_not_connected_layout is displayed when Tor isn't connected
+ self.content_layout = QtWidgets.QVBoxLayout()
+ self.content_widget = QtWidgets.QWidget()
+ self.content_widget.setLayout(self.content_layout)
+
+ tor_not_connected_label = QtWidgets.QLabel(
+ strings._("mode_tor_not_connected_label")
+ )
+ tor_not_connected_label.setAlignment(QtCore.Qt.AlignHCenter)
+ tor_not_connected_label.setStyleSheet(
+ self.common.gui.css["tor_not_connected_label"]
+ )
+ self.tor_not_connected_layout = QtWidgets.QVBoxLayout()
+ self.tor_not_connected_layout.addStretch()
+ self.tor_not_connected_layout.addWidget(tor_not_connected_label)
+ self.tor_not_connected_layout.addWidget(MinimumSizeWidget(700, 0))
+ self.tor_not_connected_layout.addStretch()
+ self.tor_not_connected_widget = QtWidgets.QWidget()
+ self.tor_not_connected_widget.setLayout(self.tor_not_connected_layout)
+
+ self.wrapper_layout = QtWidgets.QVBoxLayout()
+ self.wrapper_layout.addWidget(self.content_widget)
+ self.wrapper_layout.addWidget(self.tor_not_connected_widget)
+ self.setLayout(self.wrapper_layout)
+
+ if self.common.gui.onion.connected_to_tor:
+ self.tor_connection_started()
+ else:
+ self.tor_connection_stopped()
+
+ def init(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def get_type(self):
+ """
+ Returns the type of mode as a string (e.g. "share", "receive", etc.)
+ """
+ pass
+
+ def human_friendly_time(self, secs):
+ """
+ Returns a human-friendly time delta from given seconds.
+ """
+ days = secs // 86400
+ hours = (secs - days * 86400) // 3600
+ minutes = (secs - days * 86400 - hours * 3600) // 60
+ seconds = secs - days * 86400 - hours * 3600 - minutes * 60
+ if not seconds:
+ seconds = "0"
+ result = (
+ (f"{days}{strings._('days_first_letter')}, " if days else "")
+ + (f"{hours}{strings._('hours_first_letter')}, " if hours else "")
+ + (f"{minutes}{strings._('minutes_first_letter')}, " if minutes else "")
+ + f"{seconds}{strings._('seconds_first_letter')}"
+ )
+
+ return result
+
+ def timer_callback(self):
+ """
+ This method is called regularly on a timer.
+ """
+ # If this is a scheduled share, display the countdown til the share starts
+ if self.server_status.status == ServerStatus.STATUS_WORKING:
+ if self.settings.get("general", "autostart_timer"):
+ now = QtCore.QDateTime.currentDateTime()
+ if self.server_status.local_only:
+ seconds_remaining = now.secsTo(
+ self.mode_settings_widget.autostart_timer_widget.dateTime()
+ )
+ else:
+ seconds_remaining = now.secsTo(
+ self.server_status.autostart_timer_datetime.replace(
+ second=0, microsecond=0
+ )
+ )
+ # Update the server button
+ if seconds_remaining > 0:
+ self.server_status.server_button.setText(
+ strings._("gui_waiting_to_start").format(
+ self.human_friendly_time(seconds_remaining)
+ )
+ )
+ else:
+ if self.common.platform == "Windows" or self.settings.get(
+ "general", "autostart_timer"
+ ):
+ self.server_status.server_button.setText(
+ strings._("gui_please_wait")
+ )
+ else:
+ self.server_status.server_button.setText(
+ strings._("gui_please_wait_no_button")
+ )
+
+ # If the auto-stop timer has stopped, stop the server
+ if self.server_status.status == ServerStatus.STATUS_STARTED:
+ if self.app.autostop_timer_thread and self.settings.get(
+ "general", "autostop_timer"
+ ):
+ if self.autostop_timer_datetime_delta > 0:
+ now = QtCore.QDateTime.currentDateTime()
+ seconds_remaining = now.secsTo(
+ self.server_status.autostop_timer_datetime
+ )
+
+ # Update the server button
+ server_button_text = self.get_stop_server_autostop_timer_text()
+ self.server_status.server_button.setText(
+ server_button_text.format(
+ self.human_friendly_time(seconds_remaining)
+ )
+ )
+
+ self.status_bar.clearMessage()
+ if not self.app.autostop_timer_thread.is_alive():
+ self.autostop_timer_finished_should_stop_server()
+
+ def timer_callback_custom(self):
+ """
+ Add custom timer code.
+ """
+ pass
+
+ def get_stop_server_autostop_timer_text(self):
+ """
+ Return the string to put on the stop server button, if there's an auto-stop timer
+ """
+ pass
+
+ def autostop_timer_finished_should_stop_server(self):
+ """
+ The auto-stop timer expired, should we stop the server? Returns a bool
+ """
+ pass
+
+ def start_server(self):
+ """
+ Start the onionshare server. This uses multiple threads to start the Tor onion
+ server and the web app.
+ """
+ self.common.log("Mode", "start_server")
+
+ self.start_server_custom()
+ self.set_server_active.emit(True)
+
+ # Clear the status bar
+ self.status_bar.clearMessage()
+ self.server_status_label.setText("")
+
+ # Hide the mode settings
+ self.mode_settings_widget.hide()
+
+ # Ensure we always get a new random port each time we might launch an OnionThread
+ self.app.port = None
+
+ # Start the onion thread. If this share was scheduled for a future date,
+ # the OnionThread will start and exit 'early' to obtain the port, password
+ # and onion address, but it will not start the WebThread yet.
+ if self.settings.get("general", "autostart_timer"):
+ self.start_onion_thread(obtain_onion_early=True)
+ self.common.log("Mode", "start_server", "Starting auto-start timer")
+ self.startup_thread = AutoStartTimer(self)
+ # Once the timer has finished, start the real share, with a WebThread
+ self.startup_thread.success.connect(self.start_scheduled_service)
+ self.startup_thread.error.connect(self.start_server_error)
+ self.startup_thread.canceled = False
+ self.startup_thread.start()
+ else:
+ self.start_onion_thread()
+
+ def start_onion_thread(self, obtain_onion_early=False):
+ # If we tried to start with Client Auth and our Tor is too old to support it,
+ # bail out early
+ if (
+ not self.server_status.local_only
+ and not self.app.onion.supports_stealth
+ and not self.settings.get("general", "public")
+ ):
+ self.stop_server()
+ self.start_server_error(strings._("gui_server_doesnt_support_stealth"))
+ else:
+ self.common.log("Mode", "start_server", "Starting an onion thread")
+ self.obtain_onion_early = obtain_onion_early
+ self.onion_thread = OnionThread(self)
+ self.onion_thread.success.connect(self.starting_server_step2.emit)
+ self.onion_thread.success_early.connect(self.starting_server_early.emit)
+ self.onion_thread.error.connect(self.starting_server_error.emit)
+ self.onion_thread.start()
+
+ def start_scheduled_service(self, obtain_onion_early=False):
+ # We start a new OnionThread with the saved scheduled key from settings
+ self.common.settings.load()
+ self.obtain_onion_early = obtain_onion_early
+ self.common.log("Mode", "start_server", "Starting a scheduled onion thread")
+ 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):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def start_server_early(self):
+ """
+ An 'early' start of an onion service in order to obtain the onion
+ address for a scheduled start. Shows the onion address in the UI
+ in advance of actually starting the share.
+ """
+ self.server_status.show_url()
+
+ def start_server_step2(self):
+ """
+ Step 2 in starting the onionshare server.
+ """
+ self.common.log("Mode", "start_server_step2")
+
+ self.start_server_step2_custom()
+
+ # Nothing to do here.
+
+ # start_server_step2_custom has call these to move on:
+ # self.starting_server_step3.emit()
+ # self.start_server_finished.emit()
+
+ def start_server_step2_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def start_server_step3(self):
+ """
+ Step 3 in starting the onionshare server.
+ """
+ self.common.log("Mode", "start_server_step3")
+
+ self.start_server_step3_custom()
+
+ if self.settings.get("general", "autostop_timer"):
+ # Convert the date value to seconds between now and then
+ now = QtCore.QDateTime.currentDateTime()
+ self.autostop_timer_datetime_delta = now.secsTo(
+ self.server_status.autostop_timer_datetime
+ )
+ # Start the auto-stop timer
+ if self.autostop_timer_datetime_delta > 0:
+ self.app.autostop_timer_thread = AutoStopTimer(
+ self.common, self.autostop_timer_datetime_delta
+ )
+ self.app.autostop_timer_thread.start()
+ # The auto-stop timer has actually already passed since the user clicked Start. Probably the Onion service took too long to start.
+ else:
+ self.stop_server()
+ self.start_server_error(
+ strings._("gui_server_started_after_autostop_timer")
+ )
+
+ def start_server_step3_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def start_server_error(self, error):
+ """
+ If there's an error when trying to start the onion service
+ """
+ self.common.log("Mode", "start_server_error")
+
+ Alert(self.common, error, QtWidgets.QMessageBox.Warning)
+ self.set_server_active.emit(False)
+ self.server_status.stop_server()
+ self.status_bar.clearMessage()
+
+ self.start_server_error_custom()
+
+ def start_server_error_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def cancel_server(self):
+ """
+ Cancel the server while it is preparing to start
+ """
+ self.cancel_server_custom()
+ if self.startup_thread:
+ self.common.log("Mode", "cancel_server: quitting startup thread")
+ self.startup_thread.canceled = True
+ self.app.onion.scheduled_key = None
+ self.app.onion.scheduled_auth_cookie = None
+ self.startup_thread.quit()
+
+ # Canceling only works in Windows
+ # https://github.com/onionshare/onionshare/issues/1371
+ if self.common.platform == "Windows":
+ if self.onion_thread:
+ self.common.log("Mode", "cancel_server: quitting onion thread")
+ self.onion_thread.terminate()
+ self.onion_thread.wait()
+ if self.web_thread:
+ self.common.log("Mode", "cancel_server: quitting web thread")
+ self.web_thread.terminate()
+ self.web_thread.wait()
+
+ self.stop_server()
+
+ def cancel_server_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def stop_server(self):
+ """
+ Stop the onionshare server.
+ """
+ self.common.log("Mode", "stop_server")
+
+ if self.server_status.status != ServerStatus.STATUS_STOPPED:
+ try:
+ self.web.stop(self.app.port)
+ except Exception:
+ # Probably we had no port to begin with (Onion service didn't start)
+ pass
+ self.web.cleanup()
+
+ self.stop_server_custom()
+
+ self.set_server_active.emit(False)
+ self.stop_server_finished.emit()
+
+ # Show the mode settings
+ self.mode_settings_widget.show()
+
+ def stop_server_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ def handle_tor_broke(self):
+ """
+ Handle connection from Tor breaking.
+ """
+ if self.server_status.status != ServerStatus.STATUS_STOPPED:
+ self.server_status.stop_server()
+ self.handle_tor_broke_custom()
+
+ def handle_tor_broke_custom(self):
+ """
+ Add custom initialization here.
+ """
+ pass
+
+ # Handle web server events
+
+ def handle_request_load(self, event):
+ """
+ Handle REQUEST_LOAD event.
+ """
+ pass
+
+ def handle_request_started(self, event):
+ """
+ Handle REQUEST_STARTED event.
+ """
+ pass
+
+ def handle_request_progress(self, event):
+ """
+ Handle REQUEST_PROGRESS event.
+ """
+ pass
+
+ def handle_request_canceled(self, event):
+ """
+ Handle REQUEST_CANCELED event.
+ """
+ pass
+
+ def handle_request_upload_includes_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_INCLUDES_MESSAGE event.
+ """
+ pass
+
+ def handle_request_upload_file_renamed(self, event):
+ """
+ Handle REQUEST_UPLOAD_FILE_RENAMED event.
+ """
+ pass
+
+ def handle_request_upload_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_MESSAGE event.
+ """
+ pass
+
+ def handle_request_upload_set_dir(self, event):
+ """
+ Handle REQUEST_UPLOAD_SET_DIR event.
+ """
+ pass
+
+ def handle_request_upload_finished(self, event):
+ """
+ Handle REQUEST_UPLOAD_FINISHED event.
+ """
+ pass
+
+ def handle_request_upload_canceled(self, event):
+ """
+ Handle REQUEST_UPLOAD_CANCELED event.
+ """
+ pass
+
+ def handle_request_individual_file_started(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_STARTED event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.toggle_history.update_indicator(True)
+ self.history.requests_count += 1
+ self.history.update_requests()
+
+ item = IndividualFileHistoryItem(self.common, event["data"], event["path"])
+ self.history.add(event["data"]["id"], item)
+
+ def handle_request_individual_file_progress(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_PROGRESS event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.history.update(event["data"]["id"], event["data"]["bytes"])
+
+ if self.server_status.status == self.server_status.STATUS_STOPPED:
+ self.history.cancel(event["data"]["id"])
+
+ def handle_request_individual_file_canceled(self, event):
+ """
+ Handle REQUEST_INDVIDIDUAL_FILES_CANCELED event.
+ Used in both Share and Website modes, so implemented here.
+ """
+ self.history.cancel(event["data"]["id"])
+
+ def tor_connection_started(self):
+ """
+ This is called on every Mode when Tor is connected
+ """
+ self.content_widget.show()
+ self.tor_not_connected_widget.hide()
+
+ def tor_connection_stopped(self):
+ """
+ This is called on every Mode when Tor is disconnected
+ """
+ if self.common.gui.local_only:
+ self.tor_connection_started()
+ return
+
+ self.content_widget.hide()
+ self.tor_not_connected_widget.show()
diff --git a/desktop/onionshare/tab/mode/chat_mode/__init__.py b/desktop/onionshare/tab/mode/chat_mode/__init__.py
new file mode 100644
index 00000000..1081fe9d
--- /dev/null
+++ b/desktop/onionshare/tab/mode/chat_mode/__init__.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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 PySide2 import QtCore, QtWidgets, QtGui
+
+from onionshare_cli.web import Web
+
+from .. import Mode
+from .... import strings
+from ....widgets import MinimumSizeWidget
+from ....gui_common import GuiCommon
+
+
+class ChatMode(Mode):
+ """
+ Parts of the main window UI for sharing files.
+ """
+
+ success = QtCore.Signal()
+ error = QtCore.Signal(str)
+
+ def init(self):
+ """
+ Custom initialization for ChatMode.
+ """
+ # Create the Web object
+ self.web = Web(self.common, True, self.settings, "chat")
+
+ # Chat image
+ self.image_label = QtWidgets.QLabel()
+ self.image_label.setPixmap(
+ QtGui.QPixmap.fromImage(
+ QtGui.QImage(
+ GuiCommon.get_resource_path(
+ "images/{}_mode_chat.png".format(self.common.gui.color_mode)
+ )
+ )
+ )
+ )
+ self.image_label.setFixedSize(300, 300)
+ image_layout = QtWidgets.QVBoxLayout()
+ image_layout.addStretch()
+ image_layout.addWidget(self.image_label)
+ image_layout.addStretch()
+ self.image = QtWidgets.QWidget()
+ self.image.setLayout(image_layout)
+
+ # Set title placeholder
+ self.mode_settings_widget.title_lineedit.setPlaceholderText(
+ strings._("gui_tab_name_chat")
+ )
+
+ # Server status
+ self.server_status.set_mode("chat")
+ 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()
+
+ # Header
+ header_label = QtWidgets.QLabel(strings._("gui_new_tab_chat_button"))
+ header_label.setStyleSheet(self.common.gui.css["mode_header_label"])
+
+ # Top bar
+ top_bar_layout = QtWidgets.QHBoxLayout()
+ # Add space at the top, same height as the toggle history bar in other modes
+ top_bar_layout.addWidget(MinimumSizeWidget(0, 30))
+
+ # Main layout
+ self.main_layout = QtWidgets.QVBoxLayout()
+ self.main_layout.addLayout(top_bar_layout)
+ self.main_layout.addWidget(header_label)
+ self.main_layout.addWidget(self.primary_action, stretch=1)
+ self.main_layout.addWidget(self.server_status)
+ self.main_layout.addWidget(MinimumSizeWidget(700, 0))
+
+ # Column layout
+ self.column_layout = QtWidgets.QHBoxLayout()
+ self.column_layout.addWidget(self.image)
+ self.column_layout.addLayout(self.main_layout)
+
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
+
+ def get_type(self):
+ """
+ Returns the type of mode as a string (e.g. "share", "receive", etc.)
+ """
+ return "chat"
+
+ def get_stop_server_autostop_timer_text(self):
+ """
+ Return the string to put on the stop server button, if there's an auto-stop timer
+ """
+ return strings._("gui_share_stop_server_autostop_timer")
+
+ def autostop_timer_finished_should_stop_server(self):
+ """
+ The auto-stop timer expired, should we stop the server? Returns a bool
+ """
+
+ self.server_status.stop_server()
+ self.server_status_label.setText(strings._("close_on_autostop_timer"))
+ return True
+
+ def start_server_custom(self):
+ """
+ Starting the server.
+ """
+ # Reset web counters
+ self.web.chat_mode.cur_history_id = 0
+
+ def start_server_step2_custom(self):
+ """
+ Step 2 in starting the server. Zipping up files.
+ """
+ # Continue
+ self.starting_server_step3.emit()
+ self.start_server_finished.emit()
+
+ def cancel_server_custom(self):
+ """
+ Log that the server has been cancelled
+ """
+ self.common.log("ChatMode", "cancel_server")
+
+ def handle_tor_broke_custom(self):
+ """
+ Connection to Tor broke.
+ """
+ self.primary_action.hide()
+
+ def on_reload_settings(self):
+ """
+ We should be ok to re-enable the 'Start Receive Mode' button now.
+ """
+ self.primary_action.show()
+
+ def update_primary_action(self):
+ self.common.log("ChatMode", "update_primary_action")
diff --git a/desktop/onionshare/tab/mode/file_selection.py b/desktop/onionshare/tab/mode/file_selection.py
new file mode 100644
index 00000000..1addba22
--- /dev/null
+++ b/desktop/onionshare/tab/mode/file_selection.py
@@ -0,0 +1,534 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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
+from PySide2 import QtCore, QtWidgets, QtGui
+
+from ... import strings
+from ...widgets import Alert, AddFileDialog
+from ...gui_common import GuiCommon
+
+
+class DropHereWidget(QtWidgets.QWidget):
+ """
+ When there are no files or folders in the FileList yet, display the
+ 'drop files here' message and graphic.
+ """
+
+ def __init__(self, common, image_filename, header_text, w, h, parent):
+ super(DropHereWidget, self).__init__(parent)
+ self.common = common
+ self.setAcceptDrops(True)
+
+ self.image_label = QtWidgets.QLabel(parent=self)
+ self.image_label.setPixmap(
+ QtGui.QPixmap.fromImage(
+ QtGui.QImage(GuiCommon.get_resource_path(image_filename))
+ )
+ )
+ self.image_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.image_label.show()
+
+ self.header_label = QtWidgets.QLabel(parent=self)
+ self.header_label.setText(header_text)
+ self.header_label.setStyleSheet(
+ self.common.gui.css["share_file_selection_drop_here_header_label"]
+ )
+ self.header_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.header_label.show()
+
+ self.text_label = QtWidgets.QLabel(parent=self)
+ self.text_label.setText(strings._("gui_drag_and_drop"))
+ self.text_label.setStyleSheet(
+ self.common.gui.css["share_file_selection_drop_here_label"]
+ )
+ self.text_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.text_label.show()
+
+ self.resize(w, h)
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.hide()
+ event.accept()
+
+ def resize(self, w, h):
+ self.setGeometry(0, 0, w, h)
+ self.image_label.setGeometry(0, 0, w, h - 100)
+ self.header_label.setGeometry(0, 290, w, h - 360)
+ self.text_label.setGeometry(0, 340, w, h - 380)
+
+
+class DropCountLabel(QtWidgets.QLabel):
+ """
+ While dragging files over the FileList, this counter displays the
+ number of files you're dragging.
+ """
+
+ def __init__(self, common, parent):
+ self.parent = parent
+ super(DropCountLabel, self).__init__(parent=parent)
+
+ self.common = common
+
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setText(strings._("gui_drag_and_drop"))
+ self.setStyleSheet(self.common.gui.css["share_file_selection_drop_count_label"])
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.hide()
+ event.accept()
+
+
+class FileList(QtWidgets.QListWidget):
+ """
+ The list of files and folders in the GUI.
+ """
+
+ files_dropped = QtCore.Signal()
+ files_updated = QtCore.Signal()
+
+ def __init__(self, common, background_image_filename, header_text, parent=None):
+ super(FileList, self).__init__(parent)
+ self.common = common
+ self.setAcceptDrops(True)
+
+ self.setIconSize(QtCore.QSize(32, 32))
+ self.setSortingEnabled(True)
+ self.setMinimumHeight(160)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.drop_here = DropHereWidget(
+ self.common,
+ background_image_filename,
+ header_text,
+ self.width(),
+ self.height(),
+ self,
+ )
+ self.drop_count = DropCountLabel(self.common, self)
+ self.resizeEvent(None)
+ self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
+
+ def update(self):
+ """
+ Update the GUI elements based on the current state.
+ """
+ # file list should have a background image if empty
+ if self.count() == 0:
+ self.drop_here.show()
+ else:
+ self.drop_here.hide()
+
+ def server_started(self):
+ """
+ Update the GUI when the server starts, by hiding delete buttons.
+ """
+ self.setAcceptDrops(False)
+ self.setCurrentItem(None)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+ for index in range(self.count()):
+ self.item(index).item_button.hide()
+
+ def server_stopped(self):
+ """
+ Update the GUI when the server stops, by showing delete buttons.
+ """
+ self.setAcceptDrops(True)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ for index in range(self.count()):
+ self.item(index).item_button.show()
+
+ def resizeEvent(self, event):
+ """
+ When the widget is resized, resize the drop files image and text.
+ """
+ self.drop_here.resize(self.width(), self.height())
+
+ if self.count() > 0:
+ # Add and delete an empty item, to force all items to get redrawn
+ # This is ugly, but the only way I could figure out how to proceed
+ item = QtWidgets.QListWidgetItem("fake item")
+ self.addItem(item)
+ self.takeItem(self.row(item))
+ self.update()
+
+ # Extend any filenames that were truncated to fit the window
+ # We use 200 as a rough guess at how wide the 'file size + delete button' widget is
+ # and extend based on the overall width minus that amount.
+ for index in range(self.count()):
+ metrics = QtGui.QFontMetrics(self.item(index).font())
+ elided = metrics.elidedText(
+ self.item(index).basename, QtCore.Qt.ElideRight, self.width() - 200
+ )
+ self.item(index).setText(elided)
+
+ def dragEnterEvent(self, event):
+ """
+ dragEnterEvent for dragging files and directories into the widget.
+ """
+ # Drag and drop doesn't work in Flatpak, because of the sandbox
+ if self.common.is_flatpak():
+ Alert(self.common, strings._("gui_dragdrop_sandbox_flatpak").format())
+ event.ignore()
+ return
+
+ if event.mimeData().hasUrls:
+ self.setStyleSheet(self.common.gui.css["share_file_list_drag_enter"])
+ count = len(event.mimeData().urls())
+ self.drop_count.setText(f"+{count}")
+
+ size_hint = self.drop_count.sizeHint()
+ self.drop_count.setGeometry(
+ self.width() - size_hint.width() - 30,
+ self.height() - size_hint.height() - 10,
+ size_hint.width(),
+ size_hint.height(),
+ )
+ self.drop_count.show()
+ event.accept()
+ else:
+ event.ignore()
+
+ def dragLeaveEvent(self, event):
+ """
+ dragLeaveEvent for dragging files and directories into the widget.
+ """
+ # Drag and drop doesn't work in Flatpak, because of the sandbox
+ if self.common.is_flatpak():
+ event.ignore()
+ return
+
+ self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"])
+ self.drop_count.hide()
+ event.accept()
+ self.update()
+
+ def dragMoveEvent(self, event):
+ """
+ dragMoveEvent for dragging files and directories into the widget.
+ """
+ # Drag and drop doesn't work in Flatpak, because of the sandbox
+ if self.common.is_flatpak():
+ event.ignore()
+ return
+
+ if event.mimeData().hasUrls:
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ else:
+ event.ignore()
+
+ def dropEvent(self, event):
+ """
+ dropEvent for dragging files and directories into the widget.
+ """
+ # Drag and drop doesn't work in Flatpak, because of the sandbox
+ if self.common.is_flatpak():
+ event.ignore()
+ return
+
+ if event.mimeData().hasUrls:
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ for url in event.mimeData().urls():
+ filename = str(url.toLocalFile())
+ self.add_file(filename)
+ else:
+ event.ignore()
+
+ self.setStyleSheet(self.common.gui.css["share_file_list_drag_leave"])
+ self.drop_count.hide()
+
+ self.files_dropped.emit()
+
+ def add_file(self, filename):
+ """
+ Add a file or directory to this widget.
+ """
+ filenames = []
+ for index in range(self.count()):
+ filenames.append(self.item(index).filename)
+
+ if filename not in filenames:
+ if not os.access(filename, os.R_OK):
+ Alert(self.common, strings._("not_a_readable_file").format(filename))
+ return
+
+ fileinfo = QtCore.QFileInfo(filename)
+ ip = QtWidgets.QFileIconProvider()
+ icon = ip.icon(fileinfo)
+
+ if os.path.isfile(filename):
+ size_bytes = fileinfo.size()
+ size_readable = self.common.human_readable_filesize(size_bytes)
+ else:
+ size_bytes = self.common.dir_size(filename)
+ size_readable = self.common.human_readable_filesize(size_bytes)
+
+ # Create a new item
+ item = QtWidgets.QListWidgetItem()
+ item.setIcon(icon)
+ item.size_bytes = size_bytes
+
+ # Item's filename attribute and size labels
+ item.filename = filename
+ item_size = QtWidgets.QLabel(size_readable)
+ item_size.setStyleSheet(self.common.gui.css["share_file_list_item_size"])
+
+ item.basename = os.path.basename(filename.rstrip("/"))
+ # Use the basename as the method with which to sort the list
+ metrics = QtGui.QFontMetrics(item.font())
+ elided = metrics.elidedText(
+ item.basename, QtCore.Qt.ElideRight, self.sizeHint().width()
+ )
+ item.setData(QtCore.Qt.DisplayRole, elided)
+
+ # Item's delete button
+ def delete_item():
+ itemrow = self.row(item)
+ self.takeItem(itemrow)
+ self.files_updated.emit()
+
+ item.item_button = QtWidgets.QPushButton()
+ item.item_button.setDefault(False)
+ item.item_button.setFlat(True)
+ item.item_button.setIcon(
+ QtGui.QIcon(GuiCommon.get_resource_path("images/file_delete.png"))
+ )
+ item.item_button.clicked.connect(delete_item)
+ item.item_button.setSizePolicy(
+ QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
+ )
+
+ # 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()
+ item_info.setObjectName("item-info")
+ item_info.setLayout(item_info_layout)
+
+ # Create the item's widget and layouts
+ item_hlayout = QtWidgets.QHBoxLayout()
+ item_hlayout.addStretch()
+ item_hlayout.addWidget(item_info)
+ widget = QtWidgets.QWidget()
+ widget.setLayout(item_hlayout)
+
+ item.setSizeHint(widget.sizeHint())
+
+ self.addItem(item)
+ self.setItemWidget(item, widget)
+
+ self.files_updated.emit()
+
+
+class FileSelection(QtWidgets.QVBoxLayout):
+ """
+ The list of files and folders in the GUI, as well as buttons to add and
+ delete the files and folders.
+ """
+
+ def __init__(self, common, background_image_filename, header_text, parent):
+ super(FileSelection, self).__init__()
+
+ self.common = common
+ self.parent = parent
+
+ self.server_on = False
+
+ # File list
+ self.file_list = FileList(self.common, background_image_filename, header_text)
+ self.file_list.itemSelectionChanged.connect(self.update)
+ self.file_list.files_dropped.connect(self.update)
+ self.file_list.files_updated.connect(self.update)
+
+ # Sandboxes (for masOS, Flatpak, etc.) need separate add files and folders buttons, in
+ # order to use native file selection dialogs
+ if self.common.platform == "Darwin" or self.common.is_flatpak():
+ self.sandbox = True
+ else:
+ self.sandbox = False
+
+ # Buttons
+ if self.sandbox:
+ # The macOS sandbox makes it so the Mac version needs separate add files
+ # and folders buttons, in order to use native file selection dialogs
+ self.add_files_button = QtWidgets.QPushButton(strings._("gui_add_files"))
+ self.add_files_button.clicked.connect(self.add_files)
+ self.add_folder_button = QtWidgets.QPushButton(strings._("gui_add_folder"))
+ self.add_folder_button.clicked.connect(self.add_folder)
+ else:
+ self.add_button = QtWidgets.QPushButton(strings._("gui_add"))
+ self.add_button.clicked.connect(self.add)
+ self.remove_button = QtWidgets.QPushButton(strings._("gui_remove"))
+ self.remove_button.clicked.connect(self.delete)
+ button_layout = QtWidgets.QHBoxLayout()
+ button_layout.addStretch()
+ if self.sandbox:
+ button_layout.addWidget(self.add_files_button)
+ button_layout.addWidget(self.add_folder_button)
+ else:
+ button_layout.addWidget(self.add_button)
+ button_layout.addWidget(self.remove_button)
+
+ # Add the widgets
+ self.addWidget(self.file_list)
+ self.addLayout(button_layout)
+
+ self.update()
+
+ def update(self):
+ """
+ Update the GUI elements based on the current state.
+ """
+ # All buttons should be hidden if the server is on
+ if self.server_on:
+ if self.sandbox:
+ self.add_files_button.hide()
+ self.add_folder_button.hide()
+ else:
+ self.add_button.hide()
+ self.remove_button.hide()
+ else:
+ if self.sandbox:
+ self.add_files_button.show()
+ self.add_folder_button.show()
+ else:
+ self.add_button.show()
+
+ # Delete button should be hidden if item isn't selected
+ if len(self.file_list.selectedItems()) == 0:
+ self.remove_button.hide()
+ else:
+ self.remove_button.show()
+
+ # Update the file list
+ self.file_list.update()
+
+ # Save the latest file list to mode settings
+ self.save_filenames()
+
+ def add(self):
+ """
+ Add button clicked.
+ """
+ file_dialog = AddFileDialog(self.common, caption=strings._("gui_choose_items"))
+ if file_dialog.exec_() == QtWidgets.QDialog.Accepted:
+ self.common.log("FileSelection", "add", file_dialog.selectedFiles())
+ for filename in file_dialog.selectedFiles():
+ self.file_list.add_file(filename)
+
+ self.file_list.setCurrentItem(None)
+ self.update()
+
+ def add_files(self):
+ """
+ Add Files button clicked.
+ """
+ files = QtWidgets.QFileDialog.getOpenFileNames(
+ self.parent, caption=strings._("gui_choose_items")
+ )
+ self.common.log("FileSelection", "add_files", files)
+
+ filenames = files[0]
+ for filename in filenames:
+ self.file_list.add_file(filename)
+
+ self.file_list.setCurrentItem(None)
+ self.update()
+
+ def add_folder(self):
+ """
+ Add Folder button clicked.
+ """
+ filename = QtWidgets.QFileDialog.getExistingDirectory(
+ self.parent,
+ caption=strings._("gui_choose_items"),
+ options=QtWidgets.QFileDialog.ShowDirsOnly,
+ )
+ self.common.log("FileSelection", "add_folder", filename)
+ if filename:
+ self.file_list.add_file(filename)
+ self.file_list.setCurrentItem(None)
+ self.update()
+
+ def delete(self):
+ """
+ Delete button clicked
+ """
+ selected = self.file_list.selectedItems()
+ for item in selected:
+ itemrow = self.file_list.row(item)
+ self.file_list.takeItem(itemrow)
+ self.file_list.files_updated.emit()
+
+ self.file_list.setCurrentItem(None)
+ self.update()
+
+ def server_started(self):
+ """
+ Gets called when the server starts.
+ """
+ self.server_on = True
+ self.file_list.server_started()
+ self.update()
+
+ def server_stopped(self):
+ """
+ Gets called when the server stops.
+ """
+ self.server_on = False
+ self.file_list.server_stopped()
+ self.update()
+
+ def get_num_files(self):
+ """
+ Returns the total number of files and folders in the list.
+ """
+ return len(range(self.file_list.count()))
+
+ def get_filenames(self):
+ """
+ Return the list of file and folder names
+ """
+ filenames = []
+ for index in range(self.file_list.count()):
+ filenames.append(self.file_list.item(index).filename)
+ return filenames
+
+ def save_filenames(self):
+ """
+ Save the filenames to mode settings
+ """
+ filenames = self.get_filenames()
+ if self.parent.tab.mode == self.common.gui.MODE_SHARE:
+ self.parent.settings.set("share", "filenames", filenames)
+ elif self.parent.tab.mode == self.common.gui.MODE_WEBSITE:
+ self.parent.settings.set("website", "filenames", filenames)
+
+ def setFocus(self):
+ """
+ Set the Qt app focus on the file selection box.
+ """
+ self.file_list.setFocus()
diff --git a/desktop/onionshare/tab/mode/history.py b/desktop/onionshare/tab/mode/history.py
new file mode 100644
index 00000000..4e8fcf8e
--- /dev/null
+++ b/desktop/onionshare/tab/mode/history.py
@@ -0,0 +1,893 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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 PySide2 import QtCore, QtWidgets, QtGui
+
+from ... import strings
+from ...widgets import Alert
+from ...gui_common import GuiCommon
+
+
+class HistoryItem(QtWidgets.QWidget):
+ """
+ The base history item
+ """
+
+ STATUS_STARTED = 0
+ STATUS_FINISHED = 1
+ STATUS_CANCELED = 2
+
+ def __init__(self):
+ super(HistoryItem, self).__init__()
+
+ def update(self):
+ pass
+
+ def cancel(self):
+ pass
+
+ def get_finished_label_text(self, started):
+ """
+ When an item finishes, returns a string displaying the start/end datetime range.
+ started is a datetime object.
+ """
+ return self._get_label_text(
+ "gui_all_modes_transfer_finished",
+ "gui_all_modes_transfer_finished_range",
+ started,
+ )
+
+ def get_canceled_label_text(self, started):
+ """
+ When an item is canceled, returns a string displaying the start/end datetime range.
+ started is a datetime object.
+ """
+ return self._get_label_text(
+ "gui_all_modes_transfer_canceled",
+ "gui_all_modes_transfer_canceled_range",
+ started,
+ )
+
+ def _get_label_text(self, string_name, string_range_name, started):
+ """
+ Return a string that contains a date, or date range.
+ """
+ ended = datetime.now()
+ if (
+ started.year == ended.year
+ and started.month == ended.month
+ and started.day == ended.day
+ ):
+ if started.hour == ended.hour and started.minute == ended.minute:
+ text = strings._(string_name).format(started.strftime("%b %d, %I:%M%p"))
+ else:
+ text = strings._(string_range_name).format(
+ started.strftime("%b %d, %I:%M%p"), ended.strftime("%I:%M%p")
+ )
+ else:
+ text = strings._(string_range_name).format(
+ started.strftime("%b %d, %I:%M%p"), ended.strftime("%b %d, %I:%M%p")
+ )
+ return text
+
+
+class ShareHistoryItem(HistoryItem):
+ """
+ Download history item, for share mode
+ """
+
+ def __init__(self, common, id, total_bytes):
+ super(ShareHistoryItem, 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)
+ self.status = HistoryItem.STATUS_STARTED
+
+ # Label
+ self.label = QtWidgets.QLabel(
+ strings._("gui_all_modes_transfer_started").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 / 1024)
+ self.progress_bar.setValue(0)
+ self.progress_bar.setStyleSheet(
+ self.common.gui.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 / 1024)
+ if (downloaded_bytes / 1024) == (self.progress_bar.total_bytes / 1024):
+ pb_fmt = strings._("gui_all_modes_progress_complete").format(
+ self.common.format_seconds(time.time() - self.started)
+ )
+
+ # Change the label
+ self.label.setText(self.get_finished_label_text(self.started_dt))
+ self.label.setStyleSheet(self.common.gui.css["history_default_label"])
+ self.status = HistoryItem.STATUS_FINISHED
+
+ 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_all_modes_progress_starting").format(
+ self.common.human_readable_filesize(downloaded_bytes)
+ )
+ else:
+ pb_fmt = strings._("gui_all_modes_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"))
+ self.status = HistoryItem.STATUS_CANCELED
+
+ @property
+ def estimated_time_remaining(self):
+ return self.common.estimated_time_remaining(
+ self.downloaded_bytes, self.total_bytes, self.started
+ )
+
+
+class ReceiveHistoryItemFile(QtWidgets.QWidget):
+ def __init__(self, common, filename):
+ super(ReceiveHistoryItemFile, self).__init__()
+ self.common = common
+
+ self.common.log("ReceiveHistoryItemFile", "__init__", f"filename: {filename}")
+
+ self.filename = filename
+ self.dir = None
+ 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.gui.css["receive_file_size"])
+ self.filesize_label.hide()
+
+ # Folder button
+ folder_pixmap = QtGui.QPixmap.fromImage(
+ QtGui.QImage(GuiCommon.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 set_dir(self, dir):
+ self.dir = dir
+
+ def open_folder(self):
+ """
+ Open the downloads folder, with the file selected, in a cross-platform manner
+ """
+ self.common.log("ReceiveHistoryItemFile", "open_folder")
+
+ if not self.dir:
+ self.common.log(
+ "ReceiveHistoryItemFile",
+ "open_folder",
+ "dir has not been set yet, can't open folder",
+ )
+ return
+
+ abs_filename = os.path.join(self.dir, self.filename)
+
+ # Linux
+ if self.common.platform == "Linux" or self.common.platform == "BSD":
+ try:
+ # If nautilus is available, open it
+ subprocess.Popen(["xdg-open", self.dir])
+ except Exception:
+ Alert(
+ self.common,
+ strings._("gui_open_folder_error").format(abs_filename),
+ )
+
+ # macOS
+ elif self.common.platform == "Darwin":
+ subprocess.call(["open", "-R", abs_filename])
+
+ # Windows
+ elif self.common.platform == "Windows":
+ subprocess.Popen(["explorer", f"/select,{abs_filename}"])
+
+
+class ReceiveHistoryItemMessage(QtWidgets.QWidget):
+ def __init__(
+ self,
+ common,
+ ):
+ super(ReceiveHistoryItemMessage, self).__init__()
+ self.common = common
+ self.filename = None
+
+ # Read message button
+ message_pixmap = QtGui.QPixmap.fromImage(
+ QtGui.QImage(GuiCommon.get_resource_path("images/open_message.png"))
+ )
+ message_icon = QtGui.QIcon(message_pixmap)
+ self.message_button = QtWidgets.QPushButton(
+ strings._("history_receive_read_message_button")
+ )
+ self.message_button.setStyleSheet(self.common.gui.css["receive_message_button"])
+ self.message_button.clicked.connect(self.open_message)
+ self.message_button.setIcon(message_icon)
+ self.message_button.setIconSize(message_pixmap.rect().size())
+
+ # Layouts
+ layout = QtWidgets.QHBoxLayout()
+ layout.addWidget(self.message_button)
+ layout.addStretch()
+ self.setLayout(layout)
+
+ self.hide()
+
+ def set_filename(self, new_filename):
+ self.filename = new_filename
+ self.show()
+
+ def open_message(self):
+ """
+ Open the message in the operating system's default text editor
+ """
+ self.common.log("ReceiveHistoryItemMessage", "open_message", self.filename)
+
+ # Linux
+ if self.common.platform == "Linux" or self.common.platform == "BSD":
+ # If nautilus is available, open it
+ subprocess.Popen(["xdg-open", self.filename])
+
+ # macOS
+ elif self.common.platform == "Darwin":
+ subprocess.call(["open", self.filename])
+
+ # Windows
+ elif self.common.platform == "Windows":
+ subprocess.Popen(["notepad", self.filename])
+
+
+class ReceiveHistoryItem(HistoryItem):
+ def __init__(self, common, id, content_length):
+ super(ReceiveHistoryItem, self).__init__()
+ self.common = common
+ self.id = id
+ self.content_length = content_length
+ self.started = datetime.now()
+ self.status = HistoryItem.STATUS_STARTED
+
+ self.common.log(
+ "ReceiveHistoryItem",
+ "__init__",
+ f"id={self.id} content_length={self.content_length}",
+ )
+
+ # Label
+ self.label = QtWidgets.QLabel(
+ strings._("gui_all_modes_transfer_started").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.gui.css["downloads_uploads_progress_bar"]
+ )
+
+ # The message widget, if a message was included
+ self.message = ReceiveHistoryItemMessage(self.common)
+
+ # 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.gui.css["receive_file"])
+ files_widget.setLayout(self.files_layout)
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.label)
+ layout.addWidget(self.message)
+ 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 includes_message(self, message_filename):
+ self.message.set_filename(message_filename)
+
+ 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 / 1024)
+ self.progress_bar.setValue(total_uploaded_bytes / 1024)
+
+ elapsed = datetime.now() - self.started
+ if elapsed.seconds < 10:
+ pb_fmt = strings._("gui_all_modes_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_all_modes_progress_eta").format(
+ self.common.human_readable_filesize(total_uploaded_bytes),
+ estimated_time_remaining,
+ )
+
+ self.progress_bar.setFormat(pb_fmt)
+
+ # 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] = ReceiveHistoryItemFile(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"] == "set_dir":
+ self.files[data["filename"]].set_dir(data["dir"])
+
+ elif data["action"] == "finished":
+ # Change the status
+ self.status = HistoryItem.STATUS_FINISHED
+
+ # Hide the progress bar
+ self.progress_bar.hide()
+
+ # Change the label
+ self.label.setText(self.get_finished_label_text(self.started))
+ self.label.setStyleSheet(self.common.gui.css["history_default_label"])
+
+ elif data["action"] == "canceled":
+ # Change the status
+ self.status = HistoryItem.STATUS_CANCELED
+
+ # Hide the progress bar
+ self.progress_bar.hide()
+
+ # Change the label
+ self.label.setText(self.get_canceled_label_text(self.started))
+
+
+class IndividualFileHistoryItem(HistoryItem):
+ """
+ Individual file history item, for share mode viewing of individual files
+ """
+
+ def __init__(self, common, data, path):
+ super(IndividualFileHistoryItem, self).__init__()
+ self.status = HistoryItem.STATUS_STARTED
+ self.common = common
+
+ self.id = id
+ self.path = path
+ self.total_bytes = 0
+ self.downloaded_bytes = 0
+ self.started = time.time()
+ self.started_dt = datetime.fromtimestamp(self.started)
+ self.status = HistoryItem.STATUS_STARTED
+
+ self.directory_listing = "directory_listing" in data
+
+ # Labels
+ self.timestamp_label = QtWidgets.QLabel(
+ self.started_dt.strftime("%b %d, %I:%M%p")
+ )
+ self.timestamp_label.setStyleSheet(
+ self.common.gui.css["history_individual_file_timestamp_label"]
+ )
+ self.path_label = QtWidgets.QLabel(self.path)
+ self.path_label.setTextFormat(QtCore.Qt.PlainText)
+ self.path_label.setStyleSheet(self.common.gui.css["history_default_label"])
+ self.status_code_label = QtWidgets.QLabel()
+
+ # 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.setValue(0)
+ self.progress_bar.setStyleSheet(
+ self.common.gui.css["downloads_uploads_progress_bar"]
+ )
+
+ # Text layout
+ labels_layout = QtWidgets.QHBoxLayout()
+ labels_layout.addWidget(self.timestamp_label)
+ labels_layout.addWidget(self.path_label)
+ labels_layout.addWidget(self.status_code_label)
+ labels_layout.addStretch()
+
+ # Layout
+ layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(labels_layout)
+ layout.addWidget(self.progress_bar)
+ self.setLayout(layout)
+
+ # Is a status code already sent?
+ if "status_code" in data:
+ self.status_code_label.setText(str(data["status_code"]))
+ if data["status_code"] >= 200 and data["status_code"] < 300:
+ self.status_code_label.setStyleSheet(
+ self.common.gui.css["history_individual_file_status_code_label_2xx"]
+ )
+ if data["status_code"] >= 400 and data["status_code"] < 500:
+ self.status_code_label.setStyleSheet(
+ self.common.gui.css["history_individual_file_status_code_label_4xx"]
+ )
+ self.status = HistoryItem.STATUS_FINISHED
+ self.progress_bar.hide()
+ return
+
+ else:
+ self.total_bytes = data["filesize"]
+ self.progress_bar.setMinimum(0)
+ self.progress_bar.setMaximum(data["filesize"] / 1024)
+ self.progress_bar.total_bytes = data["filesize"]
+
+ # Start at 0
+ self.update(0)
+
+ def update(self, downloaded_bytes):
+ self.downloaded_bytes = downloaded_bytes
+
+ self.progress_bar.setValue(downloaded_bytes / 1024)
+ if (downloaded_bytes / 1024) == (self.progress_bar.total_bytes / 1024):
+ self.status_code_label.setText("200")
+ self.status_code_label.setStyleSheet(
+ self.common.gui.css["history_individual_file_status_code_label_2xx"]
+ )
+ self.progress_bar.hide()
+ self.status = HistoryItem.STATUS_FINISHED
+
+ 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_all_modes_progress_starting").format(
+ self.common.human_readable_filesize(downloaded_bytes)
+ )
+ else:
+ pb_fmt = strings._("gui_all_modes_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"))
+ self.status = HistoryItem.STATUS_CANCELED
+
+ @property
+ def estimated_time_remaining(self):
+ return self.common.estimated_time_remaining(
+ self.downloaded_bytes, self.total_bytes, self.started
+ )
+
+
+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.
+ """
+ if id in self.items:
+ self.items[id].update(data)
+
+ def cancel(self, id):
+ """
+ Cancel an item. Override this method.
+ """
+ if id in self.items:
+ self.items[id].cancel()
+
+ def includes_message(self, id, message_filename):
+ """
+ Show message button for receive mode
+ """
+ if id in self.items:
+ self.items[id].includes_message(message_filename)
+
+ def reset(self):
+ """
+ Reset all items, emptying the list. Override this method.
+ """
+ for key, item in self.items.copy().items():
+ self.items_layout.removeWidget(item)
+ item.close()
+ del self.items[key]
+
+
+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, mode=""):
+ super(History, self).__init__()
+ self.common = common
+ self.mode = mode
+
+ self.setMinimumWidth(350)
+
+ # In progress and completed counters
+ self.in_progress_count = 0
+ self.completed_count = 0
+ self.requests_count = 0
+
+ # In progress, completed, and requests labels
+ self.in_progress_label = QtWidgets.QLabel()
+ self.in_progress_label.setStyleSheet(self.common.gui.css["mode_info_label"])
+ self.completed_label = QtWidgets.QLabel()
+ self.completed_label.setStyleSheet(self.common.gui.css["mode_info_label"])
+ self.requests_label = QtWidgets.QLabel()
+ self.requests_label.setStyleSheet(self.common.gui.css["mode_info_label"])
+
+ # Header
+ self.header_label = QtWidgets.QLabel(header_text)
+ self.header_label.setStyleSheet(self.common.gui.css["downloads_uploads_label"])
+ self.clear_button = QtWidgets.QPushButton(
+ strings._("gui_all_modes_clear_history")
+ )
+ self.clear_button.setStyleSheet(self.common.gui.css["downloads_uploads_clear"])
+ self.clear_button.setFlat(True)
+ self.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(self.requests_label)
+ header_layout.addWidget(self.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.gui.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.gui.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.setStyleSheet(self.common.gui.css["downloads_uploads_not_empty"])
+ 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", f"id: {id}")
+
+ # 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 includes_message(self, id, message_filename):
+ """
+ Show the message button
+ """
+ self.item_list.includes_message(id, message_filename)
+
+ def reset(self):
+ """
+ Reset all items.
+ """
+ self.item_list.reset()
+ if len(self.item_list.items) == 0:
+ # Hide not empty, show empty
+ self.not_empty.hide()
+ self.empty.show()
+ # Reset in-progress counter
+ self.in_progress_count = 0
+ self.update_in_progress()
+
+ # Reset completed counter
+ self.completed_count = 0
+ self.update_completed()
+
+ # Reset web requests counter
+ self.requests_count = 0
+ self.update_requests()
+
+ def update_completed(self):
+ """
+ Update the 'completed' widget.
+ """
+ if self.completed_count == 0:
+ image = GuiCommon.get_resource_path("images/history_completed_none.png")
+ else:
+ image = GuiCommon.get_resource_path("images/history_completed.png")
+ self.completed_label.setText(f'<img src="{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 = GuiCommon.get_resource_path("images/history_in_progress_none.png")
+ else:
+ image = GuiCommon.get_resource_path("images/history_in_progress.png")
+
+ self.in_progress_label.setText(
+ f'<img src="{image}" /> {self.in_progress_count}'
+ )
+ self.in_progress_label.setToolTip(
+ strings._("history_in_progress_tooltip").format(self.in_progress_count)
+ )
+
+ def update_requests(self):
+ """
+ Update the 'web requests' widget.
+ """
+ if self.requests_count == 0:
+ image = GuiCommon.get_resource_path("images/history_requests_none.png")
+ else:
+ image = GuiCommon.get_resource_path("images/history_requests.png")
+
+ self.requests_label.setText(f'<img src="{image}" /> {self.requests_count}')
+ self.requests_label.setToolTip(
+ strings._("history_requests_tooltip").format(self.requests_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.gui.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 History is hidden.
+ """
+ if increment and not self.history_widget.isVisible():
+ self.indicator_count += 1
+
+ self.indicator_label.setText(str(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/desktop/onionshare/tab/mode/mode_settings_widget.py b/desktop/onionshare/tab/mode/mode_settings_widget.py
new file mode 100644
index 00000000..0e80023e
--- /dev/null
+++ b/desktop/onionshare/tab/mode/mode_settings_widget.py
@@ -0,0 +1,288 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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 PySide2 import QtCore, QtWidgets
+
+from ... import strings
+
+
+class ModeSettingsWidget(QtWidgets.QScrollArea):
+ """
+ All of the common settings for each mode are in this widget
+ """
+
+ change_persistent = QtCore.Signal(int, bool)
+
+ def __init__(self, common, tab, mode_settings):
+ super(ModeSettingsWidget, self).__init__()
+ self.common = common
+ self.tab = tab
+ self.settings = mode_settings
+
+ # Downstream Mode need to fill in this layout with its settings
+ self.mode_specific_layout = QtWidgets.QVBoxLayout()
+
+ # Persistent
+ self.persistent_checkbox = QtWidgets.QCheckBox()
+ self.persistent_checkbox.clicked.connect(self.persistent_checkbox_clicked)
+ self.persistent_checkbox.setText(strings._("mode_settings_persistent_checkbox"))
+ if self.settings.get("persistent", "enabled"):
+ self.persistent_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.persistent_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ # Public
+ self.public_checkbox = QtWidgets.QCheckBox()
+ self.public_checkbox.clicked.connect(self.public_checkbox_clicked)
+ self.public_checkbox.setText(strings._("mode_settings_public_checkbox"))
+ if self.settings.get("general", "public"):
+ self.public_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.public_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ # Title
+ title_label = QtWidgets.QLabel(strings._("mode_settings_title_label"))
+ self.title_lineedit = QtWidgets.QLineEdit()
+ self.title_lineedit.editingFinished.connect(self.title_editing_finished)
+ if self.settings.get("general", "title"):
+ self.title_lineedit.setText(self.settings.get("general", "title"))
+ title_layout = QtWidgets.QHBoxLayout()
+ title_layout.addWidget(title_label)
+ title_layout.addWidget(self.title_lineedit)
+
+ # Whether or not to use an auto-start timer
+ self.autostart_timer_checkbox = QtWidgets.QCheckBox()
+ self.autostart_timer_checkbox.clicked.connect(
+ self.autostart_timer_checkbox_clicked
+ )
+ self.autostart_timer_checkbox.setText(
+ strings._("mode_settings_autostart_timer_checkbox")
+ )
+ if self.settings.get("general", "autostart_timer"):
+ self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.autostart_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ # The autostart timer widget
+ self.autostart_timer_widget = QtWidgets.QDateTimeEdit()
+ self.autostart_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
+ self.autostart_timer_reset()
+ self.autostart_timer_widget.setCurrentSection(
+ QtWidgets.QDateTimeEdit.MinuteSection
+ )
+ if self.settings.get("general", "autostart_timer"):
+ self.autostart_timer_widget.show()
+ else:
+ self.autostart_timer_widget.hide()
+
+ # Autostart timer layout
+ autostart_timer_layout = QtWidgets.QHBoxLayout()
+ autostart_timer_layout.setContentsMargins(0, 0, 0, 0)
+ autostart_timer_layout.addWidget(self.autostart_timer_checkbox)
+ autostart_timer_layout.addWidget(self.autostart_timer_widget)
+
+ # Whether or not to use an auto-stop timer
+ self.autostop_timer_checkbox = QtWidgets.QCheckBox()
+ self.autostop_timer_checkbox.clicked.connect(
+ self.autostop_timer_checkbox_clicked
+ )
+ self.autostop_timer_checkbox.setText(
+ strings._("mode_settings_autostop_timer_checkbox")
+ )
+ if self.settings.get("general", "autostop_timer"):
+ self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.autostop_timer_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ # The autostop timer widget
+ self.autostop_timer_widget = QtWidgets.QDateTimeEdit()
+ self.autostop_timer_widget.setDisplayFormat("hh:mm A MMM d, yy")
+ self.autostop_timer_reset()
+ self.autostop_timer_widget.setCurrentSection(
+ QtWidgets.QDateTimeEdit.MinuteSection
+ )
+ if self.settings.get("general", "autostop_timer"):
+ self.autostop_timer_widget.show()
+ else:
+ self.autostop_timer_widget.hide()
+
+ # Autostop timer layout
+ autostop_timer_layout = QtWidgets.QHBoxLayout()
+ autostop_timer_layout.setContentsMargins(0, 0, 0, 0)
+ autostop_timer_layout.addWidget(self.autostop_timer_checkbox)
+ autostop_timer_layout.addWidget(self.autostop_timer_widget)
+
+ # Toggle advanced settings
+ self.toggle_advanced_button = QtWidgets.QPushButton()
+ self.toggle_advanced_button.clicked.connect(self.toggle_advanced_clicked)
+ self.toggle_advanced_button.setFlat(True)
+ self.toggle_advanced_button.setStyleSheet(
+ self.common.gui.css["mode_settings_toggle_advanced"]
+ )
+
+ # Advanced group itself
+ advanced_layout = QtWidgets.QVBoxLayout()
+ advanced_layout.setContentsMargins(0, 0, 0, 0)
+ advanced_layout.addLayout(title_layout)
+ advanced_layout.addLayout(autostart_timer_layout)
+ advanced_layout.addLayout(autostop_timer_layout)
+ self.advanced_widget = QtWidgets.QWidget()
+ self.advanced_widget.setLayout(advanced_layout)
+ self.advanced_widget.hide()
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.addLayout(self.mode_specific_layout)
+ layout.addWidget(self.persistent_checkbox)
+ layout.addWidget(self.public_checkbox)
+ layout.addWidget(self.advanced_widget)
+ layout.addWidget(self.toggle_advanced_button)
+ layout.addStretch()
+ main_widget = QtWidgets.QWidget()
+ main_widget.setLayout(layout)
+
+ self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self.setWidgetResizable(True)
+ self.setFrameShape(QtWidgets.QFrame.NoFrame)
+ self.setWidget(main_widget)
+
+ self.update_ui()
+
+ def update_ui(self):
+ # Update text on advanced group toggle button
+ if self.advanced_widget.isVisible():
+ self.toggle_advanced_button.setText(
+ strings._("mode_settings_advanced_toggle_hide")
+ )
+ else:
+ self.toggle_advanced_button.setText(
+ strings._("mode_settings_advanced_toggle_show")
+ )
+
+ def title_editing_finished(self):
+ if self.title_lineedit.text().strip() == "":
+ self.title_lineedit.setText("")
+ self.settings.set("general", "title", None)
+ if self.tab.mode == self.common.gui.MODE_SHARE:
+ self.tab.change_title.emit(
+ self.tab.tab_id, strings._("gui_tab_name_share")
+ )
+ elif self.tab.mode == self.common.gui.MODE_RECEIVE:
+ self.tab.change_title.emit(
+ self.tab.tab_id, strings._("gui_tab_name_receive")
+ )
+ elif self.tab.mode == self.common.gui.MODE_WEBSITE:
+ self.tab.change_title.emit(
+ self.tab.tab_id, strings._("gui_tab_name_website")
+ )
+ elif self.tab.mode == self.common.gui.MODE_CHAT:
+ self.tab.change_title.emit(
+ self.tab.tab_id, strings._("gui_tab_name_chat")
+ )
+ elif self.tab_mode is None:
+ pass
+ else:
+ title = self.title_lineedit.text()
+ self.settings.set("general", "title", title)
+ self.tab.change_title.emit(self.tab.tab_id, title)
+
+ def persistent_checkbox_clicked(self):
+ self.settings.set("persistent", "enabled", self.persistent_checkbox.isChecked())
+ self.settings.set("persistent", "mode", self.tab.mode)
+ self.change_persistent.emit(
+ self.tab.tab_id, self.persistent_checkbox.isChecked()
+ )
+
+ # If disabling persistence, delete the file from disk
+ if not self.persistent_checkbox.isChecked():
+ self.settings.delete()
+
+ def public_checkbox_clicked(self):
+ self.settings.set("general", "public", self.public_checkbox.isChecked())
+
+ def autostart_timer_checkbox_clicked(self):
+ self.settings.set(
+ "general", "autostart_timer", self.autostart_timer_checkbox.isChecked()
+ )
+
+ if self.autostart_timer_checkbox.isChecked():
+ self.autostart_timer_widget.show()
+ else:
+ self.autostart_timer_widget.hide()
+
+ def autostop_timer_checkbox_clicked(self):
+ self.settings.set(
+ "general", "autostop_timer", self.autostop_timer_checkbox.isChecked()
+ )
+
+ if self.autostop_timer_checkbox.isChecked():
+ self.autostop_timer_widget.show()
+ else:
+ self.autostop_timer_widget.hide()
+
+ def toggle_advanced_clicked(self):
+ if self.advanced_widget.isVisible():
+ self.advanced_widget.hide()
+ else:
+ self.advanced_widget.show()
+
+ self.update_ui()
+
+ def autostart_timer_reset(self):
+ """
+ Reset the auto-start timer in the UI after stopping a share
+ """
+ if self.common.gui.local_only:
+ # For testing
+ self.autostart_timer_widget.setDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(15)
+ )
+ self.autostart_timer_widget.setMinimumDateTime(
+ QtCore.QDateTime.currentDateTime()
+ )
+ else:
+ self.autostart_timer_widget.setDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(
+ 300
+ ) # 5 minutes in the future
+ )
+ self.autostart_timer_widget.setMinimumDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(60)
+ )
+
+ def autostop_timer_reset(self):
+ """
+ Reset the auto-stop timer in the UI after stopping a share
+ """
+ if self.common.gui.local_only:
+ # For testing
+ self.autostop_timer_widget.setDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(15)
+ )
+ self.autostop_timer_widget.setMinimumDateTime(
+ QtCore.QDateTime.currentDateTime()
+ )
+ else:
+ self.autostop_timer_widget.setDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(300)
+ )
+ self.autostop_timer_widget.setMinimumDateTime(
+ QtCore.QDateTime.currentDateTime().addSecs(60)
+ )
diff --git a/desktop/onionshare/tab/mode/receive_mode/__init__.py b/desktop/onionshare/tab/mode/receive_mode/__init__.py
new file mode 100644
index 00000000..b2b2fc5a
--- /dev/null
+++ b/desktop/onionshare/tab/mode/receive_mode/__init__.py
@@ -0,0 +1,427 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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
+from PySide2 import QtCore, QtWidgets, QtGui
+
+from onionshare_cli.web import Web
+
+from ..history import History, ToggleHistory, ReceiveHistoryItem
+from .. import Mode
+from .... import strings
+from ....widgets import MinimumSizeWidget, Alert
+from ....gui_common import GuiCommon
+
+
+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, self.settings, "receive")
+
+ # Receive image
+ self.image_label = QtWidgets.QLabel()
+ self.image_label.setPixmap(
+ QtGui.QPixmap.fromImage(
+ QtGui.QImage(
+ GuiCommon.get_resource_path(
+ "images/{}_mode_receive.png".format(self.common.gui.color_mode)
+ )
+ )
+ )
+ )
+ self.image_label.setFixedSize(250, 250)
+ image_layout = QtWidgets.QVBoxLayout()
+ image_layout.addWidget(self.image_label)
+ self.image = QtWidgets.QWidget()
+ self.image.setLayout(image_layout)
+
+ # Settings
+
+ # Data dir
+ data_dir_label = QtWidgets.QLabel(
+ strings._("mode_settings_receive_data_dir_label")
+ )
+ self.data_dir_lineedit = QtWidgets.QLineEdit()
+ self.data_dir_lineedit.setReadOnly(True)
+ self.data_dir_lineedit.setText(self.settings.get("receive", "data_dir"))
+ data_dir_button = QtWidgets.QPushButton(
+ strings._("mode_settings_receive_data_dir_browse_button")
+ )
+ data_dir_button.clicked.connect(self.data_dir_button_clicked)
+ data_dir_layout = QtWidgets.QHBoxLayout()
+ data_dir_layout.addWidget(data_dir_label)
+ data_dir_layout.addWidget(self.data_dir_lineedit)
+ data_dir_layout.addWidget(data_dir_button)
+ self.mode_settings_widget.mode_specific_layout.addLayout(data_dir_layout)
+
+ # Disable text or files
+ self.disable_text_checkbox = self.settings.get("receive", "disable_files")
+ self.disable_text_checkbox = QtWidgets.QCheckBox()
+ self.disable_text_checkbox.clicked.connect(self.disable_text_checkbox_clicked)
+ self.disable_text_checkbox.setText(
+ strings._("mode_settings_receive_disable_text_checkbox")
+ )
+ self.disable_files_checkbox = self.settings.get("receive", "disable_files")
+ self.disable_files_checkbox = QtWidgets.QCheckBox()
+ self.disable_files_checkbox.clicked.connect(self.disable_files_checkbox_clicked)
+ self.disable_files_checkbox.setText(
+ strings._("mode_settings_receive_disable_files_checkbox")
+ )
+ disable_layout = QtWidgets.QHBoxLayout()
+ disable_layout.addWidget(self.disable_text_checkbox)
+ disable_layout.addWidget(self.disable_files_checkbox)
+ disable_layout.addStretch()
+ self.mode_settings_widget.mode_specific_layout.addLayout(disable_layout)
+
+ # Webhook URL
+ webhook_url = self.settings.get("receive", "webhook_url")
+ self.webhook_url_checkbox = QtWidgets.QCheckBox()
+ self.webhook_url_checkbox.clicked.connect(self.webhook_url_checkbox_clicked)
+ self.webhook_url_checkbox.setText(
+ strings._("mode_settings_receive_webhook_url_checkbox")
+ )
+ self.webhook_url_lineedit = QtWidgets.QLineEdit()
+ self.webhook_url_lineedit.editingFinished.connect(
+ self.webhook_url_editing_finished
+ )
+ self.webhook_url_lineedit.setPlaceholderText(
+ "https://example.com/post-when-file-uploaded"
+ )
+ webhook_url_layout = QtWidgets.QHBoxLayout()
+ webhook_url_layout.addWidget(self.webhook_url_checkbox)
+ webhook_url_layout.addWidget(self.webhook_url_lineedit)
+ if webhook_url is not None and webhook_url != "":
+ self.webhook_url_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.webhook_url_lineedit.setText(
+ self.settings.get("receive", "webhook_url")
+ )
+ self.show_webhook_url()
+ else:
+ self.webhook_url_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.hide_webhook_url()
+ self.mode_settings_widget.mode_specific_layout.addLayout(webhook_url_layout)
+
+ # Set title placeholder
+ self.mode_settings_widget.title_lineedit.setPlaceholderText(
+ strings._("gui_tab_name_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(
+ GuiCommon.get_resource_path("images/receive_icon_transparent.png")
+ )
+ ),
+ strings._("gui_receive_mode_no_files"),
+ strings._("gui_all_modes_history"),
+ )
+ self.history.hide()
+
+ # Toggle history
+ self.toggle_history = ToggleHistory(
+ self.common,
+ self,
+ self.history,
+ QtGui.QIcon(GuiCommon.get_resource_path("images/receive_icon_toggle.png")),
+ QtGui.QIcon(
+ GuiCommon.get_resource_path("images/receive_icon_toggle_selected.png")
+ ),
+ )
+
+ # Header
+ header_label = QtWidgets.QLabel(strings._("gui_new_tab_receive_button"))
+ header_label.setStyleSheet(self.common.gui.css["mode_header_label"])
+
+ # Receive mode warning
+ receive_warning = QtWidgets.QLabel(strings._("gui_receive_mode_warning"))
+ 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.addWidget(header_label)
+ self.main_layout.addWidget(receive_warning)
+ self.main_layout.addWidget(self.primary_action, stretch=1)
+ self.main_layout.addWidget(self.server_status)
+
+ # Row layout
+ content_row = QtWidgets.QHBoxLayout()
+ content_row.addLayout(self.main_layout, stretch=1)
+ content_row.addWidget(self.image)
+ row_layout = QtWidgets.QVBoxLayout()
+ row_layout.addLayout(top_bar_layout)
+ row_layout.addLayout(content_row, stretch=1)
+
+ # Column layout
+ self.column_layout = QtWidgets.QHBoxLayout()
+ self.column_layout.addLayout(row_layout)
+ self.column_layout.addWidget(self.history, stretch=1)
+
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
+
+ def get_type(self):
+ """
+ Returns the type of mode as a string (e.g. "share", "receive", etc.)
+ """
+ return "receive"
+
+ def data_dir_button_clicked(self):
+ """
+ Browse for a new OnionShare data directory, and save to tab settings
+ """
+ data_dir = self.data_dir_lineedit.text()
+ selected_dir = QtWidgets.QFileDialog.getExistingDirectory(
+ self, strings._("mode_settings_receive_data_dir_label"), data_dir
+ )
+
+ if selected_dir:
+ # If we're running inside a flatpak package, the data dir must be inside ~/OnionShare
+ if self.common.gui.is_flatpak:
+ if not selected_dir.startswith(os.path.expanduser("~/OnionShare")):
+ Alert(self.common, strings._("gui_receive_flatpak_data_dir"))
+ return
+
+ self.common.log(
+ "ReceiveMode",
+ "data_dir_button_clicked",
+ f"selected dir: {selected_dir}",
+ )
+ self.data_dir_lineedit.setText(selected_dir)
+ self.settings.set("receive", "data_dir", selected_dir)
+
+ def disable_text_checkbox_clicked(self):
+ self.settings.set(
+ "receive", "disable_text", self.disable_text_checkbox.isChecked()
+ )
+
+ def disable_files_checkbox_clicked(self):
+ self.settings.set(
+ "receive", "disable_files", self.disable_files_checkbox.isChecked()
+ )
+
+ def webhook_url_checkbox_clicked(self):
+ if self.webhook_url_checkbox.isChecked():
+ if self.settings.get("receive", "webhook_url"):
+ self.webhook_url_lineedit.setText(
+ self.settings.get("receive", "webhook_url")
+ )
+ self.show_webhook_url()
+ else:
+ self.settings.set("receive", "webhook_url", None)
+ self.hide_webhook_url()
+
+ def webhook_url_editing_finished(self):
+ self.settings.set("receive", "webhook_url", self.webhook_url_lineedit.text())
+
+ def hide_webhook_url(self):
+ self.webhook_url_lineedit.hide()
+
+ def show_webhook_url(self):
+ self.webhook_url_lineedit.show()
+
+ def get_stop_server_autostop_timer_text(self):
+ """
+ Return the string to put on the stop server button, if there's an auto-stop timer
+ """
+ return strings._("gui_receive_stop_server_autostop_timer")
+
+ def autostop_timer_finished_should_stop_server(self):
+ """
+ The auto-stop timer expired, should we stop the server? Returns a bool
+ """
+ # If there were no attempts to upload files, or all uploads are done, we can stop
+ if (
+ self.web.receive_mode.cur_history_id == 0
+ or not self.web.receive_mode.uploads_in_progress
+ ):
+ self.server_status.stop_server()
+ self.server_status_label.setText(strings._("close_on_autostop_timer"))
+ return True
+ # An upload is probably still running - hold off on stopping the share, but block new shares.
+ else:
+ self.server_status_label.setText(
+ strings._("gui_receive_mode_autostop_timer_waiting")
+ )
+ self.web.receive_mode.can_upload = False
+ return False
+
+ def start_server_custom(self):
+ """
+ Starting the server.
+ """
+ # Reset web counters
+ self.web.receive_mode.cur_history_id = 0
+
+ # Hide and reset the uploads if we have previously shared
+ self.reset_info_counters()
+
+ # Set proxies for webhook URL
+ if self.common.gui.local_only:
+ self.web.proxies = None
+ else:
+ (socks_address, socks_port) = self.common.gui.onion.get_tor_socks_port()
+ self.web.proxies = {
+ "http": f"socks5h://{socks_address}:{socks_port}",
+ "https": f"socks5h://{socks_address}:{socks_port}",
+ }
+
+ 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"),
+ strings._("systray_page_loaded_message"),
+ )
+
+ def handle_request_started(self, event):
+ """
+ Handle REQUEST_STARTED event.
+ """
+ item = ReceiveHistoryItem(
+ 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_receive_started_title"),
+ strings._("systray_receive_started_message"),
+ )
+
+ 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_upload_includes_message(self, event):
+ """
+ Handle REQUEST_UPLOAD_INCLUDES_MESSAGE event.
+ """
+ self.history.includes_message(event["data"]["id"], event["data"]["filename"])
+
+ 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_set_dir(self, event):
+ """
+ Handle REQUEST_UPLOAD_SET_DIR event.
+ """
+ self.history.update(
+ event["data"]["id"],
+ {
+ "action": "set_dir",
+ "filename": event["data"]["filename"],
+ "dir": event["data"]["dir"],
+ },
+ )
+
+ 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 handle_request_upload_canceled(self, event):
+ """
+ Handle REQUEST_UPLOAD_CANCELED event.
+ """
+ self.history.update(event["data"]["id"], {"action": "canceled"})
+ self.history.in_progress_count -= 1
+ 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()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
+
+ def update_primary_action(self):
+ self.common.log("ReceiveMode", "update_primary_action")
diff --git a/desktop/onionshare/tab/mode/share_mode/__init__.py b/desktop/onionshare/tab/mode/share_mode/__init__.py
new file mode 100644
index 00000000..ed7f6912
--- /dev/null
+++ b/desktop/onionshare/tab/mode/share_mode/__init__.py
@@ -0,0 +1,478 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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
+from PySide2 import QtCore, QtWidgets, QtGui
+
+from onionshare_cli.common import Common
+from onionshare_cli.web import Web
+
+from .threads import CompressThread
+from .. import Mode
+from ..file_selection import FileSelection
+from ..history import History, ToggleHistory, ShareHistoryItem
+from .... import strings
+from ....widgets import MinimumSizeWidget
+from ....gui_common import GuiCommon
+
+
+class ShareMode(Mode):
+ """
+ Parts of the main window UI for sharing files.
+ """
+
+ def init(self):
+ """
+ Custom initialization for ReceiveMode.
+ """
+ # Threads start out as None
+ self.compress_thread = None
+
+ # Create the Web object
+ self.web = Web(self.common, True, self.settings, "share")
+
+ # Settings
+ self.autostop_sharing_checkbox = QtWidgets.QCheckBox()
+ self.autostop_sharing_checkbox.clicked.connect(
+ self.autostop_sharing_checkbox_clicked
+ )
+ self.autostop_sharing_checkbox.setText(
+ strings._("mode_settings_share_autostop_sharing_checkbox")
+ )
+ if self.settings.get("share", "autostop_sharing"):
+ self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.autostop_sharing_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ self.mode_settings_widget.mode_specific_layout.addWidget(
+ self.autostop_sharing_checkbox
+ )
+
+ # File selection
+ self.file_selection = FileSelection(
+ self.common,
+ "images/{}_mode_share.png".format(self.common.gui.color_mode),
+ strings._("gui_new_tab_share_button"),
+ self,
+ )
+ if self.filenames:
+ for filename in self.filenames:
+ self.file_selection.file_list.add_file(filename)
+
+ # Set title placeholder
+ self.mode_settings_widget.title_lineedit.setPlaceholderText(
+ strings._("gui_tab_name_share")
+ )
+
+ # Server status
+ self.server_status.set_mode("share", self.file_selection)
+ self.server_status.server_started.connect(self.file_selection.server_started)
+ self.server_status.server_stopped.connect(self.file_selection.server_stopped)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.file_selection.server_stopped)
+ self.server_status.server_canceled.connect(self.update_primary_action)
+ self.file_selection.file_list.files_updated.connect(self.server_status.update)
+ self.file_selection.file_list.files_updated.connect(self.update_primary_action)
+ # Tell server_status about web, then update
+ self.server_status.web = self.web
+ self.server_status.update()
+
+ # Filesize warning
+ self.filesize_warning = QtWidgets.QLabel()
+ self.filesize_warning.setWordWrap(True)
+ self.filesize_warning.setStyleSheet(
+ self.common.gui.css["share_filesize_warning"]
+ )
+ self.filesize_warning.hide()
+
+ # Download history
+ self.history = History(
+ self.common,
+ QtGui.QPixmap.fromImage(
+ QtGui.QImage(
+ GuiCommon.get_resource_path("images/share_icon_transparent.png")
+ )
+ ),
+ strings._("gui_share_mode_no_files"),
+ strings._("gui_all_modes_history"),
+ )
+ self.history.hide()
+
+ # Info label
+ self.info_label = QtWidgets.QLabel()
+ self.info_label.hide()
+
+ # Delete all files button
+ self.remove_all_button = QtWidgets.QPushButton(
+ strings._("gui_file_selection_remove_all")
+ )
+ self.remove_all_button.setFlat(True)
+ self.remove_all_button.setStyleSheet(
+ self.common.gui.css["share_delete_all_files_button"]
+ )
+ self.remove_all_button.clicked.connect(self.delete_all)
+ self.remove_all_button.hide()
+
+ # Toggle history
+ self.toggle_history = ToggleHistory(
+ self.common,
+ self,
+ self.history,
+ QtGui.QIcon(GuiCommon.get_resource_path("images/share_icon_toggle.png")),
+ QtGui.QIcon(
+ GuiCommon.get_resource_path("images/share_icon_toggle_selected.png")
+ ),
+ )
+
+ # Top bar
+ top_bar_layout = QtWidgets.QHBoxLayout()
+ top_bar_layout.addWidget(self.info_label)
+ top_bar_layout.addStretch()
+ top_bar_layout.addWidget(self.remove_all_button)
+ top_bar_layout.addWidget(self.toggle_history)
+
+ # Primary action layout
+ self.primary_action_layout.addWidget(self.filesize_warning)
+ self.primary_action.hide()
+ self.update_primary_action()
+
+ # Status bar, zip progress bar
+ self._zip_progress_bar = None
+
+ # 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, stretch=1)
+ self.main_layout.addWidget(self.server_status)
+ self.main_layout.addWidget(MinimumSizeWidget(700, 0))
+
+ # Column layout
+ self.column_layout = QtWidgets.QHBoxLayout()
+ self.column_layout.addLayout(self.main_layout)
+ self.column_layout.addWidget(self.history, stretch=1)
+
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
+
+ # Always start with focus on file selection
+ self.file_selection.setFocus()
+
+ def get_type(self):
+ """
+ Returns the type of mode as a string (e.g. "share", "receive", etc.)
+ """
+ return "share"
+
+ def autostop_sharing_checkbox_clicked(self):
+ """
+ Save autostop sharing setting to the tab settings
+ """
+ self.settings.set(
+ "share", "autostop_sharing", self.autostop_sharing_checkbox.isChecked()
+ )
+
+ def get_stop_server_autostop_timer_text(self):
+ """
+ Return the string to put on the stop server button, if there's an auto-stop timer
+ """
+ return strings._("gui_share_stop_server_autostop_timer")
+
+ def autostop_timer_finished_should_stop_server(self):
+ """
+ The auto-stop 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.history.in_progress_count == 0 or self.web.done:
+ self.server_status.stop_server()
+ self.server_status_label.setText(strings._("close_on_autostop_timer"))
+ return True
+ # A download is probably still running - hold off on stopping the share
+ else:
+ self.server_status_label.setText(
+ strings._("gui_share_mode_autostop_timer_waiting")
+ )
+ return False
+
+ def start_server_custom(self):
+ """
+ Starting the server.
+ """
+ # Reset web counters
+ self.web.share_mode.cur_history_id = 0
+
+ # Hide and reset the downloads if we have previously shared
+ self.reset_info_counters()
+
+ self.remove_all_button.hide()
+
+ def start_server_step2_custom(self):
+ """
+ Step 2 in starting the server. Zipping up files.
+ """
+ # Add progress bar to the status bar, indicating the compressing of files.
+ self._zip_progress_bar = ZipProgressBar(self.common, 0)
+ self.filenames = self.file_selection.get_filenames()
+
+ 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
+ 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):
+ """
+ Step 3 in starting the server. Remove zip progress bar, and display large filesize
+ warning, if applicable.
+ """
+ # Remove zip progress bar
+ if self._zip_progress_bar is not None:
+ self.status_bar.removeWidget(self._zip_progress_bar)
+ self._zip_progress_bar = None
+
+ # Warn about sending large files over Tor
+ if self.web.share_mode.download_filesize >= 157286400: # 150mb
+ self.filesize_warning.setText(strings._("large_filesize"))
+ self.filesize_warning.show()
+
+ def start_server_error_custom(self):
+ """
+ Start server error.
+ """
+ if self._zip_progress_bar is not None:
+ self.status_bar.removeWidget(self._zip_progress_bar)
+ self._zip_progress_bar = None
+
+ def stop_server_custom(self):
+ """
+ Stop server.
+ """
+ # Remove the progress bar
+ if self._zip_progress_bar is not None:
+ self.status_bar.removeWidget(self._zip_progress_bar)
+ self._zip_progress_bar = None
+
+ self.filesize_warning.hide()
+ self.history.in_progress_count = 0
+ self.history.completed_count = 0
+ self.history.update_in_progress()
+ self.file_selection.file_list.adjustSize()
+
+ self.remove_all_button.show()
+
+ 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()
+
+ def handle_request_started(self, event):
+ """
+ Handle REQUEST_STARTED event.
+ """
+ if event["data"]["use_gzip"]:
+ filesize = self.web.share_mode.gzip_filesize
+ else:
+ filesize = self.web.share_mode.download_filesize
+
+ item = ShareHistoryItem(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_share_started_title"),
+ strings._("systray_share_started_message"),
+ )
+
+ def handle_request_progress(self, event):
+ """
+ Handle REQUEST_PROGRESS event.
+ """
+ self.history.update(event["data"]["id"], event["data"]["bytes"])
+
+ # Is the download complete?
+ if event["data"]["bytes"] == self.web.share_mode.filesize:
+ self.system_tray.showMessage(
+ strings._("systray_share_completed_title"),
+ strings._("systray_share_completed_message"),
+ )
+
+ # 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.settings.get("share", "autostop_sharing"):
+ self.server_status.stop_server()
+ self.status_bar.clearMessage()
+ 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"])
+ self.history.in_progress_count = 0
+ self.history.update_in_progress()
+
+ def handle_request_canceled(self, event):
+ """
+ Handle REQUEST_CANCELED event.
+ """
+ self.history.cancel(event["data"]["id"])
+
+ # Update in progress count
+ self.history.in_progress_count -= 1
+ self.history.update_in_progress()
+ self.system_tray.showMessage(
+ strings._("systray_share_canceled_title"),
+ strings._("systray_share_canceled_message"),
+ )
+
+ def on_reload_settings(self):
+ """
+ If there were some files listed for sharing, we should be ok to re-enable
+ the 'Start Sharing' button now.
+ """
+ if self.server_status.file_selection.get_num_files() > 0:
+ self.primary_action.show()
+ self.info_label.show()
+ self.remove_all_button.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_label.show()
+ self.remove_all_button.show()
+
+ # Update the file count in the info label
+ total_size_bytes = 0
+ for index in range(self.file_selection.file_list.count()):
+ item = self.file_selection.file_list.item(index)
+ total_size_bytes += item.size_bytes
+ total_size_readable = self.common.human_readable_filesize(total_size_bytes)
+
+ if file_count > 1:
+ self.info_label.setText(
+ strings._("gui_file_info").format(file_count, total_size_readable)
+ )
+ else:
+ self.info_label.setText(
+ strings._("gui_file_info_single").format(
+ file_count, total_size_readable
+ )
+ )
+
+ else:
+ self.primary_action.hide()
+ self.info_label.hide()
+ self.remove_all_button.hide()
+
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.history.reset()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
+
+ def delete_all(self):
+ """
+ Delete All button clicked
+ """
+ self.file_selection.file_list.clear()
+ self.file_selection.file_list.files_updated.emit()
+
+ self.file_selection.file_list.setCurrentItem(None)
+
+ @staticmethod
+ def _compute_total_size(filenames):
+ total_size = 0
+ for filename in filenames:
+ if os.path.isfile(filename):
+ total_size += os.path.getsize(filename)
+ if os.path.isdir(filename):
+ total_size += Common.dir_size(filename)
+ return total_size
+
+
+class ZipProgressBar(QtWidgets.QProgressBar):
+ update_processed_size_signal = QtCore.Signal(int)
+
+ def __init__(self, common, total_files_size):
+ super(ZipProgressBar, self).__init__()
+ self.common = common
+
+ self.setMaximumHeight(20)
+ self.setMinimumWidth(200)
+ self.setValue(0)
+ self.setFormat(strings._("zip_progress_bar_format"))
+ self.setStyleSheet(self.common.gui.css["share_zip_progess_bar"])
+
+ self._total_files_size = total_files_size
+ self._processed_size = 0
+
+ self.update_processed_size_signal.connect(self.update_processed_size)
+
+ @property
+ def total_files_size(self):
+ return self._total_files_size
+
+ @total_files_size.setter
+ def total_files_size(self, val):
+ self._total_files_size = val
+
+ @property
+ def processed_size(self):
+ return self._processed_size
+
+ @processed_size.setter
+ def processed_size(self, val):
+ self.update_processed_size(val)
+
+ 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:
+ self.setValue(100)
+ else:
+ self.setValue(0)
diff --git a/desktop/onionshare/tab/mode/share_mode/threads.py b/desktop/onionshare/tab/mode/share_mode/threads.py
new file mode 100644
index 00000000..839d30ea
--- /dev/null
+++ b/desktop/onionshare/tab/mode/share_mode/threads.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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 PySide2 import QtCore
+
+
+class CompressThread(QtCore.QThread):
+ """
+ Compresses files to be shared
+ """
+
+ success = QtCore.Signal()
+ error = QtCore.Signal(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 is not None:
+ self.mode._zip_progress_bar.update_processed_size_signal.emit(x)
+
+ def run(self):
+ self.mode.common.log("CompressThread", "run")
+
+ try:
+ self.mode.web.share_mode.set_file_info(
+ self.mode.filenames, processed_size_callback=self.set_processed_size
+ )
+ self.success.emit()
+ 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/desktop/onionshare/tab/mode/website_mode/__init__.py b/desktop/onionshare/tab/mode/website_mode/__init__.py
new file mode 100644
index 00000000..0acbc1a2
--- /dev/null
+++ b/desktop/onionshare/tab/mode/website_mode/__init__.py
@@ -0,0 +1,388 @@
+# -*- coding: utf-8 -*-
+"""
+OnionShare | https://onionshare.org/
+
+Copyright (C) 2014-2021 Micah Lee, et al. <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
+
+from PySide2 import QtCore, QtWidgets, QtGui
+
+from onionshare_cli.common import Common
+from onionshare_cli.web import Web
+
+from .. import Mode
+from ..file_selection import FileSelection
+from ..history import History, ToggleHistory
+from .... import strings
+from ....widgets import MinimumSizeWidget
+from ....gui_common import GuiCommon
+
+
+class WebsiteMode(Mode):
+ """
+ Parts of the main window UI for sharing files.
+ """
+
+ success = QtCore.Signal()
+ error = QtCore.Signal(str)
+
+ def init(self):
+ """
+ Custom initialization for ReceiveMode.
+ """
+ # Create the Web object
+ self.web = Web(self.common, True, self.settings, "website")
+
+ # Settings
+ # Disable CSP option
+ self.disable_csp_checkbox = QtWidgets.QCheckBox()
+ self.disable_csp_checkbox.clicked.connect(self.disable_csp_checkbox_clicked)
+ self.disable_csp_checkbox.setText(
+ strings._("mode_settings_website_disable_csp_checkbox")
+ )
+ if self.settings.get("website", "disable_csp"):
+ self.disable_csp_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
+ self.mode_settings_widget.mode_specific_layout.addWidget(
+ self.disable_csp_checkbox
+ )
+
+ # Custom CSP option
+ self.custom_csp_checkbox = QtWidgets.QCheckBox()
+ self.custom_csp_checkbox.clicked.connect(self.custom_csp_checkbox_clicked)
+ self.custom_csp_checkbox.setText(strings._("mode_settings_website_custom_csp_checkbox"))
+ if self.settings.get("website", "custom_csp") and not self.settings.get("website", "disable_csp"):
+ self.custom_csp_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.custom_csp_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.custom_csp = QtWidgets.QLineEdit()
+ self.custom_csp.setPlaceholderText(
+ "default-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; img-src 'self' data:;"
+ )
+ self.custom_csp.editingFinished.connect(self.custom_csp_editing_finished)
+
+ custom_csp_layout = QtWidgets.QHBoxLayout()
+ custom_csp_layout.setContentsMargins(0, 0, 0, 0)
+ custom_csp_layout.addWidget(self.custom_csp_checkbox)
+ custom_csp_layout.addWidget(self.custom_csp)
+ self.mode_settings_widget.mode_specific_layout.addLayout(custom_csp_layout)
+
+ # File selection
+ self.file_selection = FileSelection(
+ self.common,
+ "images/{}_mode_website.png".format(self.common.gui.color_mode),
+ strings._("gui_new_tab_website_button"),
+ self,
+ )
+ if self.filenames:
+ for filename in self.filenames:
+ self.file_selection.file_list.add_file(filename)
+
+ # Set title placeholder
+ self.mode_settings_widget.title_lineedit.setPlaceholderText(
+ strings._("gui_tab_name_website")
+ )
+
+ # Server status
+ self.server_status.set_mode("website", self.file_selection)
+ self.server_status.server_started.connect(self.file_selection.server_started)
+ self.server_status.server_stopped.connect(self.file_selection.server_stopped)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.file_selection.server_stopped)
+ self.server_status.server_canceled.connect(self.update_primary_action)
+ self.file_selection.file_list.files_updated.connect(self.server_status.update)
+ self.file_selection.file_list.files_updated.connect(self.update_primary_action)
+ # Tell server_status about web, then update
+ self.server_status.web = self.web
+ self.server_status.update()
+
+ # Filesize warning
+ self.filesize_warning = QtWidgets.QLabel()
+ self.filesize_warning.setWordWrap(True)
+ self.filesize_warning.setStyleSheet(
+ self.common.gui.css["share_filesize_warning"]
+ )
+ self.filesize_warning.hide()
+
+ # Download history
+ self.history = History(
+ self.common,
+ QtGui.QPixmap.fromImage(
+ QtGui.QImage(
+ GuiCommon.get_resource_path("images/share_icon_transparent.png")
+ )
+ ),
+ strings._("gui_website_mode_no_files"),
+ strings._("gui_all_modes_history"),
+ "website",
+ )
+ self.history.in_progress_label.hide()
+ self.history.completed_label.hide()
+ self.history.hide()
+
+ # Info label
+ self.info_label = QtWidgets.QLabel()
+ self.info_label.hide()
+
+ # Delete all files button
+ self.remove_all_button = QtWidgets.QPushButton(
+ strings._("gui_file_selection_remove_all")
+ )
+ self.remove_all_button.setFlat(True)
+ self.remove_all_button.setStyleSheet(
+ self.common.gui.css["share_delete_all_files_button"]
+ )
+ self.remove_all_button.clicked.connect(self.delete_all)
+ self.remove_all_button.hide()
+
+ # Toggle history
+ self.toggle_history = ToggleHistory(
+ self.common,
+ self,
+ self.history,
+ QtGui.QIcon(GuiCommon.get_resource_path("images/share_icon_toggle.png")),
+ QtGui.QIcon(
+ GuiCommon.get_resource_path("images/share_icon_toggle_selected.png")
+ ),
+ )
+
+ # Top bar
+ top_bar_layout = QtWidgets.QHBoxLayout()
+ top_bar_layout.addWidget(self.info_label)
+ top_bar_layout.addStretch()
+ top_bar_layout.addWidget(self.remove_all_button)
+ top_bar_layout.addWidget(self.toggle_history)
+
+ # Primary action layout
+ self.primary_action_layout.addWidget(self.filesize_warning)
+ self.primary_action.hide()
+ self.update_primary_action()
+
+ # 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, stretch=1)
+ self.main_layout.addWidget(self.server_status)
+ self.main_layout.addWidget(MinimumSizeWidget(700, 0))
+
+ # Column layout
+ self.column_layout = QtWidgets.QHBoxLayout()
+ self.column_layout.addLayout(self.main_layout)
+ self.column_layout.addWidget(self.history, stretch=1)
+
+ # Content layout
+ self.content_layout.addLayout(self.column_layout)
+
+ # Always start with focus on file selection
+ self.file_selection.setFocus()
+
+ def get_type(self):
+ """
+ Returns the type of mode as a string (e.g. "share", "receive", etc.)
+ """
+ return "website"
+
+ def disable_csp_checkbox_clicked(self):
+ """
+ Save disable CSP setting to the tab settings. Uncheck 'custom CSP'
+ setting if disabling CSP altogether.
+ """
+ self.settings.set(
+ "website", "disable_csp", self.disable_csp_checkbox.isChecked()
+ )
+ if self.disable_csp_checkbox.isChecked():
+ self.custom_csp_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.custom_csp_checkbox.setEnabled(False)
+ else:
+ self.custom_csp_checkbox.setEnabled(True)
+
+ def custom_csp_checkbox_clicked(self):
+ """
+ Uncheck 'disable CSP' setting if custom CSP is used.
+ """
+ if self.custom_csp_checkbox.isChecked():
+ self.disable_csp_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ self.disable_csp_checkbox.setEnabled(False)
+ self.settings.set(
+ "website", "custom_csp", self.custom_csp
+ )
+ else:
+ self.disable_csp_checkbox.setEnabled(True)
+ self.custom_csp.setText("")
+ self.settings.set(
+ "website", "custom_csp", None
+ )
+
+ def custom_csp_editing_finished(self):
+ if self.custom_csp.text().strip() == "":
+ self.custom_csp.setText("")
+ self.settings.set("website", "custom_csp", None)
+ else:
+ custom_csp = self.custom_csp.text()
+ self.settings.set("website", "custom_csp", custom_csp)
+
+ def get_stop_server_autostop_timer_text(self):
+ """
+ Return the string to put on the stop server button, if there's an auto-stop timer
+ """
+ return strings._("gui_share_stop_server_autostop_timer")
+
+ def autostop_timer_finished_should_stop_server(self):
+ """
+ The auto-stop timer expired, should we stop the server? Returns a bool
+ """
+
+ self.server_status.stop_server()
+ self.server_status_label.setText(strings._("close_on_autostop_timer"))
+ return True
+
+ def start_server_custom(self):
+ """
+ Starting the server.
+ """
+ # Reset web counters
+ self.web.website_mode.visit_count = 0
+
+ # Hide and reset the downloads if we have previously shared
+ self.reset_info_counters()
+
+ self.remove_all_button.hide()
+
+ def start_server_step2_custom(self):
+ """
+ Step 2 in starting the server. Zipping up files.
+ """
+ self.filenames = []
+ for index in range(self.file_selection.file_list.count()):
+ self.filenames.append(self.file_selection.file_list.item(index).filename)
+
+ # Continue
+ self.starting_server_step3.emit()
+ self.start_server_finished.emit()
+
+ def start_server_step3_custom(self):
+ """
+ Step 3 in starting the server. Display large filesize
+ warning, if applicable.
+ """
+ self.web.website_mode.set_file_info(self.filenames)
+ self.success.emit()
+
+ def start_server_error_custom(self):
+ """
+ Start server error.
+ """
+ if self._zip_progress_bar is not None:
+ self.status_bar.removeWidget(self._zip_progress_bar)
+ self._zip_progress_bar = None
+
+ def stop_server_custom(self):
+ """
+ Stop server.
+ """
+
+ self.filesize_warning.hide()
+ self.history.completed_count = 0
+ self.file_selection.file_list.adjustSize()
+
+ self.remove_all_button.show()
+
+ def cancel_server_custom(self):
+ """
+ Log that the server has been cancelled
+ """
+ self.common.log("WebsiteMode", "cancel_server")
+
+ def handle_tor_broke_custom(self):
+ """
+ Connection to Tor broke.
+ """
+ self.primary_action.hide()
+
+ def on_reload_settings(self):
+ """
+ If there were some files listed for sharing, we should be ok to re-enable
+ the 'Start Sharing' button now.
+ """
+ if self.server_status.file_selection.get_num_files() > 0:
+ self.primary_action.show()
+ self.info_label.show()
+ self.remove_all_button.show()
+
+ def update_primary_action(self):
+ self.common.log("WebsiteMode", "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_label.show()
+ self.remove_all_button.show()
+
+ # Update the file count in the info label
+ total_size_bytes = 0
+ for index in range(self.file_selection.file_list.count()):
+ item = self.file_selection.file_list.item(index)
+ total_size_bytes += item.size_bytes
+ total_size_readable = self.common.human_readable_filesize(total_size_bytes)
+
+ if file_count > 1:
+ self.info_label.setText(
+ strings._("gui_file_info").format(file_count, total_size_readable)
+ )
+ else:
+ self.info_label.setText(
+ strings._("gui_file_info_single").format(
+ file_count, total_size_readable
+ )
+ )
+
+ else:
+ self.primary_action.hide()
+ self.info_label.hide()
+ self.remove_all_button.hide()
+
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.history.reset()
+ self.toggle_history.indicator_count = 0
+ self.toggle_history.update_indicator()
+
+ def delete_all(self):
+ """
+ Delete All button clicked
+ """
+ self.file_selection.file_list.clear()
+ self.file_selection.file_list.files_updated.emit()
+
+ self.file_selection.file_list.setCurrentItem(None)
+
+ @staticmethod
+ def _compute_total_size(filenames):
+ total_size = 0
+ for filename in filenames:
+ if os.path.isfile(filename):
+ total_size += os.path.getsize(filename)
+ if os.path.isdir(filename):
+ total_size += Common.dir_size(filename)
+ return total_size