summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Jacq <mig@mig5.net>2018-02-25 08:27:46 +1100
committerGitHub <noreply@github.com>2018-02-25 08:27:46 +1100
commitdface51dd0e00e4316d5384d14fe77568e51bf41 (patch)
tree38400a42f9d8db3cc19eecacaf046a2f76dcd9c2
parent6b7a6a9ec49ea9b33f9046526e8284f665eb6369 (diff)
parent560f27fc76f8b33c1d6e62c02db181b0c6ce8079 (diff)
downloadonionshare-dface51dd0e00e4316d5384d14fe77568e51bf41.tar.gz
onionshare-dface51dd0e00e4316d5384d14fe77568e51bf41.zip
Merge pull request #588 from micahflee/ux-update
Major user experience update
-rw-r--r--install/onionshare.nsi24
-rw-r--r--onionshare/onion.py22
-rw-r--r--onionshare/settings.py1
-rw-r--r--onionshare/web.py28
-rw-r--r--onionshare_gui/downloads.py8
-rw-r--r--onionshare_gui/file_selection.py238
-rw-r--r--onionshare_gui/onionshare_gui.py291
-rw-r--r--onionshare_gui/server_status.py208
-rw-r--r--onionshare_gui/settings_dialog.py18
-rw-r--r--share/html/404.html1
-rw-r--r--share/html/denied.html3
-rw-r--r--share/html/index.html263
-rw-r--r--share/images/download_completed.pngbin0 -> 646 bytes
-rw-r--r--share/images/download_completed_none.pngbin0 -> 437 bytes
-rw-r--r--share/images/download_in_progress.pngbin0 -> 638 bytes
-rw-r--r--share/images/download_in_progress_none.pngbin0 -> 412 bytes
-rw-r--r--share/images/drop_files.pngbin2035 -> 0 bytes
-rw-r--r--share/images/favicon.icobin0 -> 4286 bytes
-rw-r--r--share/images/file_delete.pngbin0 -> 182 bytes
-rw-r--r--share/images/info.pngbin0 -> 435 bytes
-rw-r--r--share/images/logo_transparent.pngbin0 -> 3740 bytes
-rw-r--r--share/images/server_started.pngbin346 -> 347 bytes
-rw-r--r--share/images/server_stopped.pngbin286 -> 342 bytes
-rw-r--r--share/images/server_working.pngbin338 -> 349 bytes
-rw-r--r--share/images/settings_inactive.pngbin513 -> 0 bytes
-rw-r--r--share/images/web_file.pngbin0 -> 251 bytes
-rw-r--r--share/images/web_folder.pngbin0 -> 338 bytes
-rw-r--r--share/locale/en.json52
-rw-r--r--test/conftest.py6
-rw-r--r--test/test_onionshare_settings.py1
30 files changed, 849 insertions, 315 deletions
diff --git a/install/onionshare.nsi b/install/onionshare.nsi
index 2bb89e57..c296d3a0 100644
--- a/install/onionshare.nsi
+++ b/install/onionshare.nsi
@@ -203,14 +203,22 @@ Section "install"
File "${BINPATH}\share\html\index.html"
SetOutPath "$INSTDIR\share\images"
- File "${BINPATH}\share\images\drop_files.png"
+ File "${BINPATH}\share\images\download_completed.png"
+ File "${BINPATH}\share\images\download_completed_none.png"
+ File "${BINPATH}\share\images\download_in_progress.png"
+ File "${BINPATH}\share\images\download_in_progress_none.png"
+ File "${BINPATH}\share\images\favicon.ico"
+ File "${BINPATH}\share\images\file_delete.png"
+ File "${BINPATH}\share\images\info.png"
File "${BINPATH}\share\images\logo.png"
+ File "${BINPATH}\share\images\logo_transparent.png"
File "${BINPATH}\share\images\logo_grayscale.png"
File "${BINPATH}\share\images\server_started.png"
File "${BINPATH}\share\images\server_stopped.png"
File "${BINPATH}\share\images\server_working.png"
File "${BINPATH}\share\images\settings.png"
- File "${BINPATH}\share\images\settings_inactive.png"
+ File "${BINPATH}\share\images\web_file.png"
+ File "${BINPATH}\share\images\web_folder.png"
SetOutPath "$INSTDIR\share\locale"
File "${BINPATH}\share\locale\cs.json"
@@ -381,14 +389,22 @@ FunctionEnd
Delete "$INSTDIR\share\html\404.html"
Delete "$INSTDIR\share\html\denied.html"
Delete "$INSTDIR\share\html\index.html"
- Delete "$INSTDIR\share\images\drop_files.png"
+ Delete "$INSTDIR\share\images\download_completed.png"
+ Delete "$INSTDIR\share\images\download_completed_none.png"
+ Delete "$INSTDIR\share\images\download_in_progress.png"
+ Delete "$INSTDIR\share\images\download_in_progress_none.png"
+ Delete "$INSTDIR\share\images\favicon.ico"
+ Delete "$INSTDIR\share\images\file_delete.png"
+ Delete "$INSTDIR\share\images\info.png"
Delete "$INSTDIR\share\images\logo.png"
+ Delete "$INSTDIR\share\images\logo_transparent.png"
Delete "$INSTDIR\share\images\logo_grayscale.png"
Delete "$INSTDIR\share\images\server_started.png"
Delete "$INSTDIR\share\images\server_stopped.png"
Delete "$INSTDIR\share\images\server_working.png"
Delete "$INSTDIR\share\images\settings.png"
- Delete "$INSTDIR\share\images\settings_inactive.png"
+ Delete "$INSTDIR\share\images\web_file.png"
+ Delete "$INSTDIR\share\images\web_folder.png"
Delete "$INSTDIR\share\license.txt"
Delete "$INSTDIR\share\locale\cs.json"
Delete "$INSTDIR\share\locale\de.json"
diff --git a/onionshare/onion.py b/onionshare/onion.py
index 887650c9..068648ba 100644
--- a/onionshare/onion.py
+++ b/onionshare/onion.py
@@ -488,8 +488,8 @@ class Onion(object):
auth_cookie = list(res.client_auth.values())[0]
self.auth_string = 'HidServAuth {} {}'.format(onion_host, auth_cookie)
- self.settings.save()
if onion_host is not None:
+ self.settings.save()
return onion_host
else:
raise TorErrorProtocolError(strings._('error_tor_protocol_error'))
@@ -500,13 +500,19 @@ class Onion(object):
"""
common.log('Onion', 'cleanup')
- # Cleanup the ephemeral onion service
- if self.service_id:
- try:
- self.c.remove_ephemeral_hidden_service(self.service_id)
- except:
- pass
- self.service_id = None
+ # Cleanup the ephemeral onion services, if we have any
+ try:
+ onions = self.c.list_ephemeral_hidden_services()
+ for onion in onions:
+ try:
+ common.log('Onion', 'cleanup', 'trying to remove onion {}'.format(onion))
+ self.c.remove_ephemeral_hidden_service(onion)
+ except:
+ common.log('Onion', 'cleanup', 'could not remove onion {}.. moving on anyway'.format(onion))
+ pass
+ except:
+ pass
+ self.service_id = None
if stop_tor:
# Stop tor process
diff --git a/onionshare/settings.py b/onionshare/settings.py
index cf3728cc..545915e8 100644
--- a/onionshare/settings.py
+++ b/onionshare/settings.py
@@ -58,6 +58,7 @@ class Settings(object):
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
+ 'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,
diff --git a/onionshare/web.py b/onionshare/web.py
index 103ddb1f..d16ca251 100644
--- a/onionshare/web.py
+++ b/onionshare/web.py
@@ -26,6 +26,7 @@ import queue
import socket
import sys
import tempfile
+import base64
from distutils.version import LooseVersion as Version
from urllib.request import urlopen
@@ -58,7 +59,7 @@ zip_filename = None
zip_filesize = None
security_headers = [
- ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; img-src \'self\' data:;'),
+ ('Content-Security-Policy', 'default-src \'self\'; style-src \'unsafe-inline\'; script-src \'unsafe-inline\'; img-src \'self\' data:;'),
('X-Frame-Options', 'DENY'),
('X-Xss-Protection', '1; mode=block'),
('X-Content-Type-Options', 'nosniff'),
@@ -125,6 +126,12 @@ def add_request(request_type, path, data=None):
})
+# Load and base64 encode images to pass into templates
+favicon_b64 = base64.b64encode(open(common.get_resource_path('images/favicon.ico'), 'rb').read()).decode()
+logo_b64 = base64.b64encode(open(common.get_resource_path('images/logo.png'), 'rb').read()).decode()
+folder_b64 = base64.b64encode(open(common.get_resource_path('images/web_folder.png'), 'rb').read()).decode()
+file_b64 = base64.b64encode(open(common.get_resource_path('images/web_file.png'), 'rb').read()).decode()
+
slug = None
@@ -206,7 +213,10 @@ def index(slug_candidate):
global stay_open, download_in_progress
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header, value in security_headers:
r.headers.set(header, value)
return r
@@ -215,6 +225,10 @@ def index(slug_candidate):
r = make_response(render_template_string(
open(common.get_resource_path('html/index.html')).read(),
+ favicon_b64=favicon_b64,
+ logo_b64=logo_b64,
+ folder_b64=folder_b64,
+ file_b64=file_b64,
slug=slug,
file_info=file_info,
filename=os.path.basename(zip_filename),
@@ -243,7 +257,10 @@ def download(slug_candidate):
global stay_open, download_in_progress, done
deny_download = not stay_open and download_in_progress
if deny_download:
- r = make_response(render_template_string(open(common.get_resource_path('html/denied.html')).read()))
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/denied.html')).read(),
+ favicon_b64=favicon_b64
+ ))
for header,value in security_headers:
r.headers.set(header, value)
return r
@@ -357,7 +374,10 @@ def page_not_found(e):
force_shutdown()
print(strings._('error_rate_limit'))
- r = make_response(render_template_string(open(common.get_resource_path('html/404.html')).read()), 404)
+ r = make_response(render_template_string(
+ open(common.get_resource_path('html/404.html')).read(),
+ favicon_b64=favicon_b64
+ ), 404)
for header, value in security_headers:
r.headers.set(header, value)
return r
diff --git a/onionshare_gui/downloads.py b/onionshare_gui/downloads.py
index 60bd59ac..166f14a4 100644
--- a/onionshare_gui/downloads.py
+++ b/onionshare_gui/downloads.py
@@ -34,13 +34,15 @@ class Download(object):
# make a new progress bar
cssStyleData ="""
QProgressBar {
- border: 2px solid grey;
- border-radius: 5px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
+ font-size: 12px;
}
QProgressBar::chunk {
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.progress_bar = QtWidgets.QProgressBar()
diff --git a/onionshare_gui/file_selection.py b/onionshare_gui/file_selection.py
index da03d24d..29bcc592 100644
--- a/onionshare_gui/file_selection.py
+++ b/onionshare_gui/file_selection.py
@@ -23,6 +23,50 @@ from .alert import Alert
from onionshare import strings, common
+class DropHereLabel(QtWidgets.QLabel):
+ """
+ When there are no files or folders in the FileList yet, display the
+ 'drop files here' message and graphic.
+ """
+ def __init__(self, parent, image=False):
+ self.parent = parent
+ super(DropHereLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+
+ if image:
+ self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/logo_transparent.png'))))
+ else:
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #999999;')
+
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.parent.drop_here_image.hide()
+ self.parent.drop_here_text.hide()
+ event.accept()
+
+
+class DropCountLabel(QtWidgets.QLabel):
+ """
+ While dragging files over the FileList, this counter displays the
+ number of files you're dragging.
+ """
+ def __init__(self, parent):
+ self.parent = parent
+ super(DropCountLabel, self).__init__(parent=parent)
+ self.setAcceptDrops(True)
+ self.setAlignment(QtCore.Qt.AlignCenter)
+ self.setText(strings._('gui_drag_and_drop', True))
+ self.setStyleSheet('color: #ffffff; background-color: #f44449; font-weight: bold; padding: 5px 10px; border-radius: 10px;')
+ self.hide()
+
+ def dragEnterEvent(self, event):
+ self.hide()
+ event.accept()
+
+
class FileList(QtWidgets.QListWidget):
"""
The list of files and folders in the GUI.
@@ -35,63 +79,82 @@ class FileList(QtWidgets.QListWidget):
self.setAcceptDrops(True)
self.setIconSize(QtCore.QSize(32, 32))
self.setSortingEnabled(True)
- self.setMinimumHeight(200)
+ self.setMinimumHeight(205)
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
-
- class DropHereLabel(QtWidgets.QLabel):
- """
- When there are no files or folders in the FileList yet, display the
- 'drop files here' message and graphic.
- """
- def __init__(self, parent, image=False):
- self.parent = parent
- super(DropHereLabel, self).__init__(parent=parent)
- self.setAcceptDrops(True)
- self.setAlignment(QtCore.Qt.AlignCenter)
-
- if image:
- self.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(common.get_resource_path('images/drop_files.png'))))
- else:
- self.setText(strings._('gui_drag_and_drop', True))
- self.setStyleSheet('color: #999999;')
-
- self.hide()
-
- def dragEnterEvent(self, event):
- self.parent.drop_here_image.hide()
- self.parent.drop_here_text.hide()
- event.ignore()
-
self.drop_here_image = DropHereLabel(self, True)
self.drop_here_text = DropHereLabel(self, False)
-
- self.filenames = []
- self.update()
+ self.drop_count = DropCountLabel(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 len(self.filenames) == 0:
+ if self.count() == 0:
self.drop_here_image.show()
self.drop_here_text.show()
else:
self.drop_here_image.hide()
self.drop_here_text.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_image.setGeometry(0, 0, self.width(), self.height())
- self.drop_here_text.setGeometry(0, 0, self.width(), self.height())
+ offset = 70
+ self.drop_here_image.setGeometry(0, 0, self.width(), self.height() - offset)
+ self.drop_here_text.setGeometry(0, offset, self.width(), self.height() - offset)
+
+ 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.
"""
if event.mimeData().hasUrls:
+ self.setStyleSheet('FileList { border: 3px solid #538ad0; }')
+ count = len(event.mimeData().urls())
+ self.drop_count.setText('+{}'.format(count))
+
+ size_hint = self.drop_count.sizeHint()
+ self.drop_count.setGeometry(self.width() - size_hint.width() - 10, self.height() - size_hint.height() - 10, size_hint.width(), size_hint.height())
+ self.drop_count.show()
event.accept()
else:
event.ignore()
@@ -100,6 +163,8 @@ class FileList(QtWidgets.QListWidget):
"""
dragLeaveEvent for dragging files and directories into the widget.
"""
+ self.setStyleSheet('FileList { border: none; }')
+ self.drop_count.hide()
event.accept()
self.update()
@@ -125,36 +190,84 @@ class FileList(QtWidgets.QListWidget):
self.add_file(filename)
else:
event.ignore()
+
+ self.setStyleSheet('border: none;')
+ self.drop_count.hide()
+
self.files_dropped.emit()
def add_file(self, filename):
"""
Add a file or directory to this widget.
"""
- if filename not in self.filenames:
+ 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(strings._("not_a_readable_file", True).format(filename))
return
- self.filenames.append(filename)
- # Re-sort the list internally
- self.filenames.sort()
-
fileinfo = QtCore.QFileInfo(filename)
- basename = os.path.basename(filename.rstrip('/'))
ip = QtWidgets.QFileIconProvider()
icon = ip.icon(fileinfo)
if os.path.isfile(filename):
- size = common.human_readable_filesize(fileinfo.size())
+ size_bytes = fileinfo.size()
+ size_readable = common.human_readable_filesize(size_bytes)
else:
- size = common.human_readable_filesize(common.dir_size(filename))
- item_name = '{0:s} ({1:s})'.format(basename, size)
- item = QtWidgets.QListWidgetItem(item_name)
- item.setToolTip(size)
+ size_bytes = common.dir_size(filename)
+ size_readable = 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('QLabel { color: #666666; font-size: 11px; }')
+
+ 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(common.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.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()
@@ -168,21 +281,23 @@ class FileSelection(QtWidgets.QVBoxLayout):
super(FileSelection, self).__init__()
self.server_on = False
- # file list
+ # File list
self.file_list = FileList()
- self.file_list.currentItemChanged.connect(self.update)
+ self.file_list.itemSelectionChanged.connect(self.update)
self.file_list.files_dropped.connect(self.update)
+ self.file_list.files_updated.connect(self.update)
- # buttons
+ # Buttons
self.add_button = QtWidgets.QPushButton(strings._('gui_add', True))
self.add_button.clicked.connect(self.add)
self.delete_button = QtWidgets.QPushButton(strings._('gui_delete', True))
self.delete_button.clicked.connect(self.delete)
button_layout = QtWidgets.QHBoxLayout()
+ button_layout.addStretch()
button_layout.addWidget(self.add_button)
button_layout.addWidget(self.delete_button)
- # add the widgets
+ # Add the widgets
self.addWidget(self.file_list)
self.addLayout(button_layout)
@@ -192,21 +307,20 @@ class FileSelection(QtWidgets.QVBoxLayout):
"""
Update the GUI elements based on the current state.
"""
- # all buttons should be disabled if the server is on
+ # All buttons should be hidden if the server is on
if self.server_on:
- self.add_button.setEnabled(False)
- self.delete_button.setEnabled(False)
+ self.add_button.hide()
+ self.delete_button.hide()
else:
- self.add_button.setEnabled(True)
+ self.add_button.show()
- # delete button should be disabled if item isn't selected
- current_item = self.file_list.currentItem()
- if not current_item:
- self.delete_button.setEnabled(False)
+ # Delete button should be hidden if item isn't selected
+ if len(self.file_list.selectedItems()) == 0:
+ self.delete_button.hide()
else:
- self.delete_button.setEnabled(True)
+ self.delete_button.show()
- # update the file list
+ # Update the file list
self.file_list.update()
def add(self):
@@ -218,6 +332,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
for filename in file_dialog.selectedFiles():
self.file_list.add_file(filename)
+ self.file_list.setCurrentItem(None)
self.update()
def delete(self):
@@ -227,9 +342,10 @@ class FileSelection(QtWidgets.QVBoxLayout):
selected = self.file_list.selectedItems()
for item in selected:
itemrow = self.file_list.row(item)
- self.file_list.filenames.pop(itemrow)
self.file_list.takeItem(itemrow)
self.file_list.files_updated.emit()
+
+ self.file_list.setCurrentItem(None)
self.update()
def server_started(self):
@@ -237,7 +353,7 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server starts.
"""
self.server_on = True
- self.file_list.setAcceptDrops(False)
+ self.file_list.server_started()
self.update()
def server_stopped(self):
@@ -245,14 +361,14 @@ class FileSelection(QtWidgets.QVBoxLayout):
Gets called when the server stops.
"""
self.server_on = False
- self.file_list.setAcceptDrops(True)
+ 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(self.file_list.filenames)
+ return len(range(self.file_list.count()))
def setFocus(self):
"""
diff --git a/onionshare_gui/onionshare_gui.py b/onionshare_gui/onionshare_gui.py
index 582ebdb3..e6987cfb 100644
--- a/onionshare_gui/onionshare_gui.py
+++ b/onionshare_gui/onionshare_gui.py
@@ -56,6 +56,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.setWindowTitle('OnionShare')
self.setWindowIcon(QtGui.QIcon(common.get_resource_path('images/logo.png')))
+ self.setMinimumWidth(430)
# Load settings
self.config = config
@@ -72,20 +73,31 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.server_status = ServerStatus(self.qtapp, self.app, web, self.file_selection, self.settings)
self.server_status.server_started.connect(self.file_selection.server_started)
self.server_status.server_started.connect(self.start_server)
+ self.server_status.server_started.connect(self.update_server_status_indicator)
self.server_status.server_stopped.connect(self.file_selection.server_stopped)
self.server_status.server_stopped.connect(self.stop_server)
+ self.server_status.server_stopped.connect(self.update_server_status_indicator)
+ self.server_status.server_stopped.connect(self.update_primary_action)
+ self.server_status.server_canceled.connect(self.cancel_server)
+ self.server_status.server_canceled.connect(self.file_selection.server_stopped)
+ self.server_status.server_canceled.connect(self.update_primary_action)
self.start_server_finished.connect(self.clear_message)
self.start_server_finished.connect(self.server_status.start_server_finished)
+ self.start_server_finished.connect(self.update_server_status_indicator)
self.stop_server_finished.connect(self.server_status.stop_server_finished)
+ self.stop_server_finished.connect(self.update_server_status_indicator)
self.file_selection.file_list.files_updated.connect(self.server_status.update)
+ self.file_selection.file_list.files_updated.connect(self.update_primary_action)
self.server_status.url_copied.connect(self.copy_url)
self.server_status.hidservauth_copied.connect(self.copy_hidservauth)
self.starting_server_step2.connect(self.start_server_step2)
self.starting_server_step3.connect(self.start_server_step3)
self.starting_server_error.connect(self.start_server_error)
+ self.server_status.button_clicked.connect(self.clear_message)
# Filesize warning
self.filesize_warning = QtWidgets.QLabel()
+ self.filesize_warning.setWordWrap(True)
self.filesize_warning.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
self.filesize_warning.hide()
@@ -99,38 +111,95 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.vbar = self.downloads_container.verticalScrollBar()
self.downloads_container.hide() # downloads start out hidden
self.new_download = False
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
- # Status bar
- self.status_bar = QtWidgets.QStatusBar()
- self.status_bar.setSizeGripEnabled(False)
- self.status_bar.setStyleSheet(
- "QStatusBar::item { border: 0px; }")
- version_label = QtWidgets.QLabel('v{0:s}'.format(common.get_version()))
- version_label.setStyleSheet('color: #666666')
+ # Info label along top of screen
+ self.info_layout = QtWidgets.QHBoxLayout()
+ self.info_label = QtWidgets.QLabel()
+ self.info_label.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_in_progress_download_count = QtWidgets.QLabel()
+ self.info_in_progress_download_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.info_completed_downloads_count = QtWidgets.QLabel()
+ self.info_completed_downloads_count.setStyleSheet('QLabel { font-size: 12px; color: #666666; }')
+
+ self.update_downloads_completed(self.downloads_in_progress)
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
+ self.info_layout.addWidget(self.info_label)
+ self.info_layout.addStretch()
+ self.info_layout.addWidget(self.info_in_progress_download_count)
+ self.info_layout.addWidget(self.info_completed_downloads_count)
+
+ self.info_widget = QtWidgets.QWidget()
+ self.info_widget.setLayout(self.info_layout)
+ self.info_widget.hide()
+
+ # Settings button on the status bar
self.settings_button = QtWidgets.QPushButton()
self.settings_button.setDefault(False)
self.settings_button.setFlat(True)
+ self.settings_button.setFixedWidth(40)
self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
self.settings_button.clicked.connect(self.open_settings)
- self.status_bar.addPermanentWidget(version_label)
+
+ # Server status indicator on the status bar
+ self.server_status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
+ self.server_status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
+ self.server_status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
+ self.server_status_image_label = QtWidgets.QLabel()
+ self.server_status_image_label.setFixedWidth(20)
+ self.server_status_label = QtWidgets.QLabel()
+ self.server_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; }')
+ server_status_indicator_layout = QtWidgets.QHBoxLayout()
+ server_status_indicator_layout.addWidget(self.server_status_image_label)
+ server_status_indicator_layout.addWidget(self.server_status_label)
+ self.server_status_indicator = QtWidgets.QWidget()
+ self.server_status_indicator.setLayout(server_status_indicator_layout)
+ self.update_server_status_indicator()
+
+ # Status bar
+ self.status_bar = QtWidgets.QStatusBar()
+ self.status_bar.setSizeGripEnabled(False)
+ statusBar_cssStyleData ="""
+ QStatusBar {
+ font-style: italic;
+ color: #666666;
+ }
+
+ QStatusBar::item {
+ border: 0px;
+ }"""
+
+ self.status_bar.setStyleSheet(statusBar_cssStyleData)
+ self.status_bar.addPermanentWidget(self.server_status_indicator)
self.status_bar.addPermanentWidget(self.settings_button)
self.setStatusBar(self.status_bar)
# Status bar, zip progress bar
self._zip_progress_bar = None
-
- # Persistent URL notification
- self.persistent_url_label = QtWidgets.QLabel(strings._('persistent_url_in_use', True))
- self.persistent_url_label.setStyleSheet('padding: 10px 0; font-weight: bold; color: #333333;')
- self.persistent_url_label.hide()
+ # Status bar, sharing messages
+ self.server_share_status_label = QtWidgets.QLabel('')
+ self.server_share_status_label.setStyleSheet('QLabel { font-style: italic; color: #666666; padding: 2px; }')
+ self.status_bar.insertWidget(0, self.server_share_status_label)
+
+ # Primary action layout
+ primary_action_layout = QtWidgets.QVBoxLayout()
+ primary_action_layout.addWidget(self.server_status)
+ primary_action_layout.addWidget(self.filesize_warning)
+ primary_action_layout.addWidget(self.downloads_container)
+ self.primary_action = QtWidgets.QWidget()
+ self.primary_action.setLayout(primary_action_layout)
+ self.primary_action.hide()
+ self.update_primary_action()
# Main layout
self.layout = QtWidgets.QVBoxLayout()
+ self.layout.addWidget(self.info_widget)
self.layout.addLayout(self.file_selection)
- self.layout.addLayout(self.server_status)
- self.layout.addWidget(self.filesize_warning)
- self.layout.addWidget(self.persistent_url_label)
- self.layout.addWidget(self.downloads_container)
+ self.layout.addWidget(self.primary_action)
central_widget = QtWidgets.QWidget()
central_widget.setLayout(self.layout)
self.setCentralWidget(central_widget)
@@ -158,6 +227,46 @@ class OnionShareGui(QtWidgets.QMainWindow):
# After connecting to Tor, check for updates
self.check_for_updates()
+ def update_primary_action(self):
+ # Show or hide primary action layout
+ file_count = self.file_selection.file_list.count()
+ if file_count > 0:
+ self.primary_action.show()
+ self.info_widget.show()
+
+ # 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 = common.human_readable_filesize(total_size_bytes)
+
+ if file_count > 1:
+ self.info_label.setText(strings._('gui_file_info', True).format(file_count, total_size_readable))
+ else:
+ self.info_label.setText(strings._('gui_file_info_single', True).format(file_count, total_size_readable))
+
+ else:
+ self.primary_action.hide()
+ self.info_widget.hide()
+
+ # Resize window
+ self.adjustSize()
+
+ def update_server_status_indicator(self):
+ common.log('OnionShareGui', 'update_server_status_indicator')
+
+ # Set the status image
+ if self.server_status.status == self.server_status.STATUS_STOPPED:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_stopped))
+ self.server_status_label.setText(strings._('gui_status_indicator_stopped', True))
+ elif self.server_status.status == self.server_status.STATUS_WORKING:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_working))
+ self.server_status_label.setText(strings._('gui_status_indicator_working', True))
+ elif self.server_status.status == self.server_status.STATUS_STARTED:
+ self.server_status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.server_status_image_started))
+ self.server_status_label.setText(strings._('gui_status_indicator_started', True))
+
def _initSystemTray(self):
system = common.get_platform()
@@ -238,11 +347,17 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self.server_status.file_selection.get_num_files() > 0:
self.server_status.server_button.setEnabled(True)
self.status_bar.clearMessage()
+ # If we switched off the shutdown timeout setting, ensure the widget is hidden.
+ if not self.settings.get('shutdown_timeout'):
+ self.server_status.shutdown_timeout_container.hide()
d = SettingsDialog(self.onion, self.qtapp, self.config)
d.settings_saved.connect(reload_settings)
d.exec_()
+ # When settings close, refresh the server status UI
+ self.server_status.update()
+
def start_server(self):
"""
Start the onionshare server. This uses multiple threads to start the Tor onion
@@ -257,7 +372,9 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Hide and reset the downloads if we have previously shared
self.downloads_container.hide()
self.downloads.reset_downloads()
+ self.reset_info_counters()
self.status_bar.clearMessage()
+ self.server_share_status_label.setText('')
# Reset web counters
web.download_count = 0
@@ -284,9 +401,10 @@ class OnionShareGui(QtWidgets.QMainWindow):
# wait for modules in thread to load, preventing a thread-related cx_Freeze crash
time.sleep(0.2)
- t = threading.Thread(target=start_onion_service, kwargs={'self': self})
- t.daemon = True
- t.start()
+ common.log('OnionshareGui', 'start_server', 'Starting an onion thread')
+ self.t = OnionThread(function=start_onion_service, kwargs={'self': self})
+ self.t.daemon = True
+ self.t.start()
def start_server_step2(self):
"""
@@ -296,8 +414,11 @@ class OnionShareGui(QtWidgets.QMainWindow):
# add progress bar to the status bar, indicating the crunching of files.
self._zip_progress_bar = ZipProgressBar(0)
- self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(
- self.file_selection.file_list.filenames)
+ self.filenames = []
+ for index in range(self.file_selection.file_list.count()):
+ self.filenames.append(self.file_selection.file_list.item(index).filename)
+
+ self._zip_progress_bar.total_files_size = OnionShareGui._compute_total_size(self.filenames)
self.status_bar.insertWidget(0, self._zip_progress_bar)
# prepare the files for sending in a new thread
@@ -307,7 +428,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
if self._zip_progress_bar != None:
self._zip_progress_bar.update_processed_size_signal.emit(x)
try:
- web.set_file_info(self.file_selection.file_list.filenames, processed_size_callback=_set_processed_size)
+ web.set_file_info(self.filenames, processed_size_callback=_set_processed_size)
self.app.cleanup_filenames.append(web.zip_filename)
self.starting_server_step3.emit()
@@ -317,7 +438,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.starting_server_error.emit(e.strerror)
return
- #self.status_bar.showMessage(strings._('gui_starting_server2', True))
t = threading.Thread(target=finish_starting_server, kwargs={'self': self})
t.daemon = True
t.start()
@@ -339,7 +459,7 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.filesize_warning.setText(strings._("large_filesize", True))
self.filesize_warning.show()
- if self.server_status.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Convert the date value to seconds between now and then
now = QtCore.QDateTime.currentDateTime()
self.timeout = now.secsTo(self.server_status.timeout)
@@ -352,9 +472,6 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.stop_server()
self.start_server_error(strings._('gui_server_started_after_timeout'))
- if self.settings.get('save_private_key'):
- self.persistent_url_label.show()
-
def start_server_error(self, error):
"""
If there's an error when trying to start the onion service
@@ -370,6 +487,14 @@ class OnionShareGui(QtWidgets.QMainWindow):
self._zip_progress_bar = None
self.status_bar.clearMessage()
+ def cancel_server(self):
+ """
+ Cancel the server while it is preparing to start
+ """
+ if self.t:
+ self.t.terminate()
+ self.stop_server()
+
def stop_server(self):
"""
Stop the onionshare server.
@@ -386,10 +511,13 @@ class OnionShareGui(QtWidgets.QMainWindow):
# Remove ephemeral service, but don't disconnect from Tor
self.onion.cleanup(stop_tor=False)
self.filesize_warning.hide()
- self.persistent_url_label.hide()
- self.stop_server_finished.emit()
+ self.downloads_in_progress = 0
+ self.downloads_completed = 0
+ self.update_downloads_in_progress(0)
+ self.file_selection.file_list.adjustSize()
self.set_server_active(False)
+ self.stop_server_finished.emit()
def check_for_updates(self):
"""
@@ -455,6 +583,8 @@ class OnionShareGui(QtWidgets.QMainWindow):
self.downloads_container.show() # show the downloads layout
self.downloads.add_download(event["data"]["id"], web.zip_filesize)
self.new_download = True
+ self.downloads_in_progress += 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_started_title', True), strings._('systray_download_started_message', True))
@@ -469,16 +599,30 @@ class OnionShareGui(QtWidgets.QMainWindow):
if event["data"]["bytes"] == web.zip_filesize:
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_completed_title', True), strings._('systray_download_completed_message', True))
+ # Update the total 'completed downloads' info
+ self.downloads_completed += 1
+ self.update_downloads_completed(self.downloads_completed)
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
# close on finish?
if not web.get_stay_open():
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('closing_automatically', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('closing_automatically', True))
else:
if self.server_status.status == self.server_status.STATUS_STOPPED:
self.downloads.cancel_download(event["data"]["id"])
+ self.downloads_in_progress = 0
+ self.update_downloads_in_progress(self.downloads_in_progress)
+
elif event["type"] == web.REQUEST_CANCELED:
self.downloads.cancel_download(event["data"]["id"])
+ # Update the 'in progress downloads' info
+ self.downloads_in_progress -= 1
+ self.update_downloads_in_progress(self.downloads_in_progress)
if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
self.systemTray.showMessage(strings._('systray_download_canceled_title', True), strings._('systray_download_canceled_message', True))
@@ -487,30 +631,37 @@ class OnionShareGui(QtWidgets.QMainWindow):
# If the auto-shutdown timer has stopped, stop the server
if self.server_status.status == self.server_status.STATUS_STARTED:
- if self.app.shutdown_timer and self.server_status.timer_enabled:
+ if self.app.shutdown_timer and self.settings.get('shutdown_timeout'):
if self.timeout > 0:
+ now = QtCore.QDateTime.currentDateTime()
+ seconds_remaining = now.secsTo(self.server_status.timeout)
+ self.server_status.server_button.setText(strings._('gui_stop_server_shutdown_timeout', True).format(seconds_remaining))
if not self.app.shutdown_timer.is_alive():
# If there were no attempts to download the share, or all downloads are done, we can stop
if web.download_count == 0 or web.done:
self.server_status.stop_server()
- self.status_bar.showMessage(strings._('close_on_timeout', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('close_on_timeout', True))
# A download is probably still running - hold off on stopping the share
else:
- self.status_bar.showMessage(strings._('timeout_download_still_running', True))
+ self.status_bar.clearMessage()
+ self.server_share_status_label.setText(strings._('timeout_download_still_running', True))
def copy_url(self):
"""
When the URL gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_url')
- self.status_bar.showMessage(strings._('gui_copied_url', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_url_title', True), strings._('gui_copied_url', True))
def copy_hidservauth(self):
"""
When the stealth onion service HidServAuth gets copied to the clipboard, display this in the status bar.
"""
common.log('OnionShareGui', 'copy_hidservauth')
- self.status_bar.showMessage(strings._('gui_copied_hidservauth', True), 2000)
+ if self.systemTray.supportsMessages() and self.settings.get('systray_notifications'):
+ self.systemTray.showMessage(strings._('gui_copied_hidservauth_title', True), strings._('gui_copied_hidservauth', True))
def clear_message(self):
"""
@@ -522,22 +673,52 @@ class OnionShareGui(QtWidgets.QMainWindow):
"""
Disable the Settings button while an OnionShare server is active.
"""
- self.settings_button.setEnabled(not active)
if active:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings_inactive.png')) )
+ self.settings_button.hide()
else:
- self.settings_button.setIcon( QtGui.QIcon(common.get_resource_path('images/settings.png')) )
+ self.settings_button.show()
# Disable settings menu action when server is active
self.settingsAction.setEnabled(not active)
+ def reset_info_counters(self):
+ """
+ Set the info counters back to zero.
+ """
+ self.update_downloads_completed(0)
+ self.update_downloads_in_progress(0)
+
+ def update_downloads_completed(self, count):
+ """
+ Update the 'Downloads completed' info widget.
+ """
+ if count == 0:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed_none.png')
+ else:
+ self.info_completed_downloads_image = common.get_resource_path('images/download_completed.png')
+ self.info_completed_downloads_count.setText('<img src={0:s} /> {1:d}'.format(self.info_completed_downloads_image, count))
+ self.info_completed_downloads_count.setToolTip(strings._('info_completed_downloads_tooltip', True).format(count))
+
+ def update_downloads_in_progress(self, count):
+ """
+ Update the 'Downloads in progress' info widget.
+ """
+ if count == 0:
+ self.info_in_progress_download_image = common.get_resource_path('images/download_in_progress_none.png')
+ else:
+ self.info_in_progress_download_image = common.get_resource_path('images/download_in_progress.png')
+ self.info_in_progress_download_count.setText('<img src={0:s} /> {1:d}'.format(self.info_in_progress_download_image, count))
+ self.info_in_progress_download_count.setToolTip(strings._('info_in_progress_downloads_tooltip', True).format(count))
+
def closeEvent(self, e):
common.log('OnionShareGui', 'closeEvent')
try:
if self.server_status.status != self.server_status.STATUS_STOPPED:
+ common.log('OnionShareGui', 'closeEvent, opening warning dialog')
dialog = QtWidgets.QMessageBox()
- dialog.setWindowTitle("OnionShare")
+ dialog.setWindowTitle(strings._('gui_quit_title', True))
dialog.setText(strings._('gui_quit_warning', True))
+ dialog.setIcon(QtWidgets.QMessageBox.Critical)
quit_button = dialog.addButton(strings._('gui_quit_warning_quit', True), QtWidgets.QMessageBox.YesRole)
dont_quit_button = dialog.addButton(strings._('gui_quit_warning_dont_quit', True), QtWidgets.QMessageBox.NoRole)
dialog.setDefaultButton(dont_quit_button)
@@ -566,14 +747,15 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setFormat(strings._('zip_progress_bar_format'))
cssStyleData ="""
QProgressBar {
- background-color: rgba(255, 255, 255, 0.0) !important;
- border: 0px;
+ border: 1px solid #4e064f;
+ background-color: #ffffff !important;
text-align: center;
+ color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
- background: qlineargradient(x1: 0.5, y1: 0, x2: 0.5, y2: 1, stop: 0 #b366ff, stop: 1 #d9b3ff);
+ background-color: #4e064f;
width: 10px;
}"""
self.setStyleSheet(cssStyleData)
@@ -607,3 +789,26 @@ class ZipProgressBar(QtWidgets.QProgressBar):
self.setValue(100)
else:
self.setValue(0)
+
+
+class OnionThread(QtCore.QThread):
+ """
+ A QThread for starting our Onion Service.
+ By using QThread rather than threading.Thread, we are able
+ to call quit() or terminate() on the startup if the user
+ decided to cancel (in which case do not proceed with obtaining
+ the Onion address and starting the web server).
+ """
+ def __init__(self, function, kwargs=None):
+ super(OnionThread, self).__init__()
+ common.log('OnionThread', '__init__')
+ self.function = function
+ if not kwargs:
+ self.kwargs = {}
+ else:
+ self.kwargs = kwargs
+
+ def run(self):
+ common.log('OnionThread', 'run')
+
+ self.function(**self.kwargs)
diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py
index 442ae440..03540415 100644
--- a/onionshare_gui/server_status.py
+++ b/onionshare_gui/server_status.py
@@ -23,12 +23,14 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings, common, settings
-class ServerStatus(QtWidgets.QVBoxLayout):
+class ServerStatus(QtWidgets.QWidget):
"""
The server status chunk of the GUI.
"""
server_started = QtCore.pyqtSignal()
server_stopped = QtCore.pyqtSignal()
+ server_canceled = QtCore.pyqtSignal()
+ button_clicked = QtCore.pyqtSignal()
url_copied = QtCore.pyqtSignal()
hidservauth_copied = QtCore.pyqtSignal()
@@ -47,100 +49,103 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings = settings
- # Helper boolean as this is used in a few places
- self.timer_enabled = False
# Shutdown timeout layout
- self.server_shutdown_timeout_checkbox = QtWidgets.QCheckBox()
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout_checkbox.toggled.connect(self.shutdown_timeout_toggled)
- self.server_shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_choice", True))
- self.server_shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
- self.server_shutdown_timeout = QtWidgets.QDateTimeEdit()
+ self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True))
+ self.shutdown_timeout = QtWidgets.QDateTimeEdit()
# Set proposed timeout to be 5 minutes into the future
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setDisplayFormat("hh:mm A MMM d, yy")
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
# Onion services can take a little while to start, so reduce the risk of it expiring too soon by setting the minimum to 2 min from now
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
- self.server_shutdown_timeout.setCurrentSectionIndex(4)
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
- shutdown_timeout_layout_group = QtWidgets.QHBoxLayout()
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_checkbox)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout_label)
- shutdown_timeout_layout_group.addWidget(self.server_shutdown_timeout)
- # server layout
- self.status_image_stopped = QtGui.QImage(common.get_resource_path('images/server_stopped.png'))
- self.status_image_working = QtGui.QImage(common.get_resource_path('images/server_working.png'))
- self.status_image_started = QtGui.QImage(common.get_resource_path('images/server_started.png'))
- self.status_image_label = QtWidgets.QLabel()
- self.status_image_label.setFixedWidth(30)
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ self.shutdown_timeout.setCurrentSection(QtWidgets.QDateTimeEdit.MinuteSection)
+ shutdown_timeout_layout = QtWidgets.QHBoxLayout()
+ shutdown_timeout_layout.addWidget(self.shutdown_timeout_label)
+ shutdown_timeout_layout.addWidget(self.shutdown_timeout)
+
+ # Shutdown timeout container, so it can all be hidden and shown as a group
+ shutdown_timeout_container_layout = QtWidgets.QVBoxLayout()
+ shutdown_timeout_container_layout.addLayout(shutdown_timeout_layout)
+ self.shutdown_timeout_container = QtWidgets.QWidget()
+ self.shutdown_timeout_container.setLayout(shutdown_timeout_container_layout)
+ self.shutdown_timeout_container.hide()
+
+
+ # Server layout
self.server_button = QtWidgets.QPushButton()
self.server_button.clicked.connect(self.server_button_clicked)
- server_layout = QtWidgets.QHBoxLayout()
- server_layout.addWidget(self.status_image_label)
- server_layout.addWidget(self.server_button)
- # url layout
+ # URL layout
url_font = QtGui.QFont()
- self.url_label = QtWidgets.QLabel()
- self.url_label.setFont(url_font)
- self.url_label.setWordWrap(False)
- self.url_label.setAlignment(QtCore.Qt.AlignCenter)
+ self.url_description = QtWidgets.QLabel(strings._('gui_url_description', True))
+ self.url_description.setWordWrap(True)
+ self.url_description.setMinimumHeight(50)
+ self.url = QtWidgets.QLabel()
+ self.url.setFont(url_font)
+ self.url.setWordWrap(True)
+ self.url.setMinimumHeight(60)
+ self.url.setMinimumSize(self.url.sizeHint())
+ self.url.setStyleSheet('QLabel { background-color: #ffffff; color: #000000; padding: 10px; border: 1px solid #666666; }')
+
+ url_buttons_style = 'QPushButton { color: #3f7fcf; }'
self.copy_url_button = QtWidgets.QPushButton(strings._('gui_copy_url', True))
+ self.copy_url_button.setFlat(True)
+ self.copy_url_button.setStyleSheet(url_buttons_style)
self.copy_url_button.clicked.connect(self.copy_url)
self.copy_hidservauth_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
+ self.copy_hidservauth_button.setFlat(True)
+ self.copy_hidservauth_button.setStyleSheet(url_buttons_style)
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
- url_layout = QtWidgets.QHBoxLayout()
- url_layout.addWidget(self.url_label)
- url_layout.addWidget(self.copy_url_button)
- url_layout.addWidget(self.copy_hidservauth_button)
-
- # add the widgets
- self.addLayout(shutdown_timeout_layout_group)
- self.addLayout(server_layout)
- self.addLayout(url_layout)
+ url_buttons_layout = QtWidgets.QHBoxLayout()
+ url_buttons_layout.addWidget(self.copy_url_button)
+ url_buttons_layout.addWidget(self.copy_hidservauth_button)
+ url_buttons_layout.addStretch()
+
+ url_layout = QtWidgets.QVBoxLayout()
+ url_layout.addWidget(self.url_description)
+ url_layout.addWidget(self.url)
+ url_layout.addLayout(url_buttons_layout)
+
+ # Add the widgets
+ layout = QtWidgets.QVBoxLayout()
+ layout.addWidget(self.server_button)
+ layout.addLayout(url_layout)
+ layout.addWidget(self.shutdown_timeout_container)
+ self.setLayout(layout)
self.update()
- def shutdown_timeout_toggled(self, checked):
- """
- Shutdown timer option was toggled. If checked, show the timer settings.
- """
- if checked:
- self.timer_enabled = True
- # Hide the checkbox, show the options
- self.server_shutdown_timeout_label.show()
- # Reset the default timer to 5 minutes into the future after toggling the option on
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.show()
- else:
- self.timer_enabled = False
- self.server_shutdown_timeout_label.hide()
- self.server_shutdown_timeout.hide()
-
def shutdown_timeout_reset(self):
"""
Reset the timeout in the UI after stopping a share
"""
- self.server_shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
- self.server_shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
- self.server_shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
+ self.shutdown_timeout.setDateTime(QtCore.QDateTime.currentDateTime().addSecs(300))
+ self.shutdown_timeout.setMinimumDateTime(QtCore.QDateTime.currentDateTime().addSecs(120))
def update(self):
"""
Update the GUI elements based on the current state.
"""
- # set the status image
- if self.status == self.STATUS_STOPPED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_stopped))
- elif self.status == self.STATUS_WORKING:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_working))
- elif self.status == self.STATUS_STARTED:
- self.status_image_label.setPixmap(QtGui.QPixmap.fromImage(self.status_image_started))
-
- # set the URL fields
+ # Set the URL fields
if self.status == self.STATUS_STARTED:
- self.url_label.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
- self.url_label.show()
+ self.url_description.show()
+
+ info_image = common.get_resource_path('images/info.png')
+ self.url_description.setText(strings._('gui_url_description', True).format(info_image))
+ # Show a Tool Tip explaining the lifecycle of this URL
+ if self.settings.get('save_private_key'):
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime_and_persistent', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_persistent', True))
+ else:
+ if self.settings.get('close_after_first_download'):
+ self.url_description.setToolTip(strings._('gui_url_label_onetime', True))
+ else:
+ self.url_description.setToolTip(strings._('gui_url_label_stay_open', True))
+
+ self.url.setText('http://{0:s}/{1:s}'.format(self.app.onion_host, self.web.slug))
+ self.url.show()
+
self.copy_url_button.show()
if self.settings.get('save_private_key'):
@@ -148,53 +153,63 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.settings.set('slug', self.web.slug)
self.settings.save()
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+
if self.app.stealth:
self.copy_hidservauth_button.show()
else:
self.copy_hidservauth_button.hide()
-
- # resize parent widget
- p = self.parentWidget()
- p.resize(p.sizeHint())
else:
- self.url_label.hide()
+ self.url_description.hide()
+ self.url.hide()
self.copy_url_button.hide()
self.copy_hidservauth_button.hide()
- # button
+ # Button
+ button_stopped_style = 'QPushButton { background-color: #5fa416; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
+ button_working_style = 'QPushButton { background-color: #4c8211; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; font-style: italic; }'
+ button_started_style = 'QPushButton { background-color: #d0011b; color: #ffffff; padding: 10px; border: 0; border-radius: 5px; }'
if self.file_selection.get_num_files() == 0:
- self.server_button.setEnabled(False)
- self.server_button.setText(strings._('gui_start_server', True))
+ self.server_button.hide()
else:
+ self.server_button.show()
+
if self.status == self.STATUS_STOPPED:
+ self.server_button.setStyleSheet(button_stopped_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_start_server', True))
- self.server_shutdown_timeout.setEnabled(True)
- self.server_shutdown_timeout_checkbox.setEnabled(True)
+ self.server_button.setToolTip('')
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.show()
elif self.status == self.STATUS_STARTED:
+ self.server_button.setStyleSheet(button_started_style)
self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_stop_server', True))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
+ self.server_button.setToolTip(strings._('gui_stop_server_shutdown_timeout_tooltip', True).format(self.timeout))
elif self.status == self.STATUS_WORKING:
- self.server_button.setEnabled(False)
+ self.server_button.setStyleSheet(button_working_style)
+ self.server_button.setEnabled(True)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
else:
+ self.server_button.setStyleSheet(button_working_style)
self.server_button.setEnabled(False)
self.server_button.setText(strings._('gui_please_wait'))
- self.server_shutdown_timeout.setEnabled(False)
- self.server_shutdown_timeout_checkbox.setEnabled(False)
+ if self.settings.get('shutdown_timeout'):
+ self.shutdown_timeout_container.hide()
def server_button_clicked(self):
"""
Toggle starting or stopping the server.
"""
if self.status == self.STATUS_STOPPED:
- if self.timer_enabled:
+ if self.settings.get('shutdown_timeout'):
# Get the timeout chosen, stripped of its seconds. This prevents confusion if the share stops at (say) 37 seconds past the minute chosen
- self.timeout = self.server_shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
+ self.timeout = self.shutdown_timeout.dateTime().toPyDateTime().replace(second=0, microsecond=0)
# If the timeout has actually passed already before the user hit Start, refuse to start the server.
if QtCore.QDateTime.currentDateTime().toPyDateTime() > self.timeout:
Alert(strings._('gui_server_timeout_expired', QtWidgets.QMessageBox.Warning))
@@ -204,6 +219,9 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.start_server()
elif self.status == self.STATUS_STARTED:
self.stop_server()
+ elif self.status == self.STATUS_WORKING:
+ self.cancel_server()
+ self.button_clicked.emit()
def start_server(self):
"""
@@ -230,6 +248,16 @@ class ServerStatus(QtWidgets.QVBoxLayout):
self.update()
self.server_stopped.emit()
+ def cancel_server(self):
+ """
+ Cancel the server.
+ """
+ common.log('ServerStatus', 'cancel_server', 'Canceling the server mid-startup')
+ self.status = self.STATUS_WORKING
+ self.shutdown_timeout_reset()
+ self.update()
+ self.server_canceled.emit()
+
def stop_server_finished(self):
"""
The server has finished stopping.
diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py
index 424f2589..a279723e 100644
--- a/onionshare_gui/settings_dialog.py
+++ b/onionshare_gui/settings_dialog.py
@@ -60,6 +60,11 @@ class SettingsDialog(QtWidgets.QDialog):
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Checked)
self.systray_notifications_checkbox.setText(strings._("gui_settings_systray_notifications", True))
+ # Whether or not to use a shutdown timer
+ self.shutdown_timeout_checkbox = QtWidgets.QCheckBox()
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True))
+
# Whether or not to save the Onion private key for reuse
self.save_private_key_checkbox = QtWidgets.QCheckBox()
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked)
@@ -69,6 +74,7 @@ class SettingsDialog(QtWidgets.QDialog):
sharing_group_layout = QtWidgets.QVBoxLayout()
sharing_group_layout.addWidget(self.close_after_first_download_checkbox)
sharing_group_layout.addWidget(self.systray_notifications_checkbox)
+ sharing_group_layout.addWidget(self.shutdown_timeout_checkbox)
sharing_group_layout.addWidget(self.save_private_key_checkbox)
sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True))
sharing_group.setLayout(sharing_group_layout)
@@ -80,12 +86,14 @@ class SettingsDialog(QtWidgets.QDialog):
stealth_details.setWordWrap(True)
stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
stealth_details.setOpenExternalLinks(True)
+ stealth_details.setMinimumSize(stealth_details.sizeHint())
self.stealth_checkbox = QtWidgets.QCheckBox()
self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked)
self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True))
hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True))
hidservauth_details.setWordWrap(True)
+ hidservauth_details.setMinimumSize(hidservauth_details.sizeHint())
hidservauth_details.hide()
self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True))
@@ -317,9 +325,12 @@ class SettingsDialog(QtWidgets.QDialog):
self.save_button.clicked.connect(self.save_clicked)
self.cancel_button = QtWidgets.QPushButton(strings._('gui_settings_button_cancel', True))
self.cancel_button.clicked.connect(self.cancel_clicked)
+ version_label = QtWidgets.QLabel('OnionShare {0:s}'.format(common.get_version()))
+ version_label.setStyleSheet('color: #666666')
self.help_button = QtWidgets.QPushButton(strings._('gui_settings_button_help', True))
self.help_button.clicked.connect(self.help_clicked)
buttons_layout = QtWidgets.QHBoxLayout()
+ buttons_layout.addWidget(version_label)
buttons_layout.addWidget(self.help_button)
buttons_layout.addStretch()
buttons_layout.addWidget(self.save_button)
@@ -371,6 +382,12 @@ class SettingsDialog(QtWidgets.QDialog):
else:
self.systray_notifications_checkbox.setCheckState(QtCore.Qt.Unchecked)
+ shutdown_timeout = self.old_settings.get('shutdown_timeout')
+ if shutdown_timeout:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked)
+ else:
+ self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked)
+
save_private_key = self.old_settings.get('save_private_key')
if save_private_key:
self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked)
@@ -723,6 +740,7 @@ class SettingsDialog(QtWidgets.QDialog):
settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked())
settings.set('systray_notifications', self.systray_notifications_checkbox.isChecked())
+ settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked())
if self.save_private_key_checkbox.isChecked():
settings.set('save_private_key', True)
settings.set('private_key', self.old_settings.get('private_key'))
diff --git a/share/html/404.html b/share/html/404.html
index 2e2b06f0..09d0fc3c 100644
--- a/share/html/404.html
+++ b/share/html/404.html
@@ -2,6 +2,7 @@
<html>
<head>
<title>Error 404</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
body {
background-color: #FFC4D5;
diff --git a/share/html/denied.html b/share/html/denied.html
index ef77642e..a82ac027 100644
--- a/share/html/denied.html
+++ b/share/html/denied.html
@@ -2,6 +2,7 @@
<html>
<head>
<title>OnionShare</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style>
body {
background-color: #222222;
@@ -15,4 +16,4 @@
<body>
<p>OnionShare download in progress</p>
</body>
-</html>
+</html>
diff --git a/share/html/index.html b/share/html/index.html
index aa91174e..57711e02 100644
--- a/share/html/index.html
+++ b/share/html/index.html
@@ -2,106 +2,207 @@
<html>
<head>
<title>OnionShare</title>
+ <link href="data:image/x-icon;base64,{{favicon_b64}}" rel="icon" type="image/x-icon" />
<style type="text/css">
- body {
- background-color: #222222;
- color: #ffffff;
- text-align: center;
- font-family: sans-serif;
- padding: 5em 1em;
- }
- .button {
- -moz-box-shadow:inset 0px 1px 0px 0px #cae3fc;
- -webkit-box-shadow:inset 0px 1px 0px 0px #cae3fc;
- box-shadow:inset 0px 1px 0px 0px #cae3fc;
- background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #79bbff), color-stop(1, #4197ee) );
- background:-moz-linear-gradient( center top, #79bbff 5%, #4197ee 100% );
- filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#79bbff', endColorstr='#4197ee');
- background-color:#79bbff;
- -webkit-border-top-left-radius:12px;
- -moz-border-radius-topleft:12px;
- border-top-left-radius:12px;
- -webkit-border-top-right-radius:12px;
- -moz-border-radius-topright:12px;
- border-top-right-radius:12px;
- -webkit-border-bottom-right-radius:12px;
- -moz-border-radius-bottomright:12px;
- border-bottom-right-radius:12px;
- -webkit-border-bottom-left-radius:12px;
- -moz-border-radius-bottomleft:12px;
- border-bottom-left-radius:12px;
- text-indent:0;
- border:1px solid #469df5;
- display:inline-block;
- color:#ffffff;
- font-size:29px;
- font-weight:bold;
- font-style:normal;
- height:50px;
- line-height:50px;
- text-decoration:none;
- text-align:center;
- text-shadow:1px 1px 0px #287ace;
- padding: 0 20px;
- }
- .button:hover {
- background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #4197ee), color-stop(1, #79bbff) );
- background:-moz-linear-gradient( center top, #4197ee 5%, #79bbff 100% );
- filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4197ee', endColorstr='#79bbff');
- background-color:#4197ee;
- }.button:active {
- position:relative;
- top:1px;
- }
+ .clearfix:after {
+ content: ".";
+ display: block;
+ clear: both;
+ visibility: hidden;
+ line-height: 0;
+ height: 0;
+ }
- .download-size {
- color: #999999;
- }
- .download-description {
- padding: 10px;
- }
- .file-list {
- margin: 50px auto 0 auto;
- padding: 10px;
- text-align: left;
- background-color: #333333;
- }
- .file-list th {
- padding: 5px;
- font-weight: bold;
- color: #999999;
- }
- .file-list td {
- padding: 5px;
- }
+ body {
+ margin: 0;
+ font-family: Helvetica;
+ }
+
+ header {
+ border-bottom: 1px solid rgba(0, 0, 0, 0.1);
+ background: #fcfcfc;
+ background: -webkit-linear-gradient(top, #fcfcfc 0%, #f2f2f2 100%);
+ padding: 0.8rem;
+ }
+
+ header .logo {
+ vertical-align: middle;
+ width: 45px;
+ height: 45px;
+ }
+
+ header h1 {
+ display: inline-block;
+ margin: 0 0 0 0.5rem;
+ vertical-align: middle;
+ font-weight: normal;
+ font-size: 1.5rem;
+ color: #666666;
+ }
+
+ header .right {
+ float: right;
+ font-size: .75rem;
+ }
+
+ header .right ul li {
+ display: inline;
+ margin: 0 0 0 .5rem;
+ font-size: 1rem;
+ }
+
+ header .button {
+ color: #ffffff;
+ background-color: #4e064f;
+ padding: 10px;
+ border-radius: 5px;
+ text-decoration: none;
+ margin-left: 1rem;
+ cursor: pointer;
+ }
+
+ table.file-list {
+ width: 100%;
+ margin: 0 auto;
+ border-collapse: collapse;
+ }
+
+ table.file-list th {
+ text-align: left;
+ text-transform: uppercase;
+ font-weight: normal;
+ color: #666666;
+ padding: 0.5rem;
+ }
+
+ table.file-list tr {
+ border-bottom: 1px solid #e0e0e0;
+ }
+
+ table.file-list td {
+ white-space: nowrap;
+ padding: 0.5rem 10rem 0.5rem 0.8rem;
+ }
+
+ table.file-list td img {
+ vertical-align: middle;
+ margin-right: 0.5rem;
+ }
+
+ table.file-list td:last-child {
+ width: 100%;
+ }
</style>
<meta name="onionshare-filename" content="{{ filename }}">
<meta name="onionshare-filesize" content="{{ filesize }}">
</head>
<body>
- <p><a class="button" href='/{{ slug }}/download'>{{ filename }} &#x25BC;</a></p>
- <p class="download-size"><strong title="{{ filesize }} bytes">{{ filesize_human }} (compressed)</strong></p>
- <p class="download-description">This zip file contains the following contents:</p>
- <table class="file-list">
+
+ <header class="clearfix">
+ <div class="right">
+ <ul>
+ <li>Total size: <strong>{{ filesize_human }}</strong> (compressed)</li>
+ <li><a class="button" href='/{{ slug }}/download'>Download Files</a></li>
+ </ul>
+ </div>
+ <img class="logo" src="data:image/png;base64,{{logo_b64}}" title="OnionShare">
+ <h1>OnionShare</h1>
+ </header>
+
+ <table class="file-list" id="file-list">
<tr>
- <th>Type</th>
- <th>Name</th>
- <th>Size</th>
+ <th onclick="sortTable(0)">Filename</th>
+ <th onclick="sortTable(1)">Size</th>
+ <th></th>
</tr>
{% for info in file_info.dirs %}
<tr>
- <td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQXOpwjbQAAAM5JREFUSMftlksOwyAMRDFCbNjkDL1kz9K75Cq9AZF3kYIM7oqoP/KpcFK1mRUrnsbW2Fbq0N+JiPhZRMS1OZAfzLzocwCAGmC9V2Vhjduarj8CzynGqIwxsDl4SXV267E4uFTN33X8deCxDyV1XXeRYJo1UUDEa9M0pxoRK+b4HiqRcz3nVAJKRK+TSxqaGbrkNKUkBn0oNSK2+T0MA1dau9NzO4QwuvPen1lAxQtEsq/vNpSWhvZ9v/3IRMTWOQezkyuEoKy14tfHoU11A6Mr5AxrpuMVAAAAAElFTkSuQmCC" /></td>
- <td>{{ info.basename }}</td>
+ <td>
+ <img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ folder_b64 }}" />
+ {{ info.basename }}
+ </td>
<td>{{ info.size_human }}</td>
+ <td></td>
</tr>
{% endfor %}
{% for info in file_info.files %}
<tr>
- <td><img width="30" height="30" title="" alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH3ggbFCQqYvRvfAAAAJFJREFUSMftlUkOwCAIAEWJ//+uIUivtYmtG7VN8KYHRhlA5zYtuB6klCTG2BcEAHrBeN4ws3jvX3lxQZmFiogMgVesVrhKXlvgakKf4KqVdAdXL+Ea3O8aILh0GnUMEhjtw9mLbUv1tx33fgItynBVoN+k2hybY3Nsjg08Bs45q4GYuQ4OIQARLYcSkUPEoiUP4j5IFasKOnUAAAAASUVORK5CYII=" /></td>
- <td>{{ info.basename }}</td>
+ <td>
+ <img width="30" height="30" title="" alt="" src="data:image/png;base64,{{ file_b64 }}" />
+ {{ info.basename }}
+ </td>
<td>{{ info.size_human }}</td>
+ <td></td>
</tr>
{% endfor %}
</table>
+ <script>
+ // Function to convert human-readable sizes back to bytes, for sorting
+ function unhumanize(text) {
+ var powers = {'b': 0, 'k': 1, 'm': 2, 'g': 3, 't': 4};
+ var regex = /(\d+(?:\.\d+)?)\s?(B|K|M|G|T)?/i;
+ var res = regex.exec(text);
+ if(res[2] === undefined) {
+ // Account for alphabetical words (file/dir names)
+ return text;
+ } else {
+ return res[1] * Math.pow(1024, powers[res[2].toLowerCase()]);
+ }
+ }
+ function sortTable(n) {
+ var table, rows, switching, i, x, y, shouldSwitch, dir, switchcount = 0;
+ table = document.getElementById("file-list");
+ switching = true;
+ // Set the sorting direction to ascending:
+ dir = "asc";
+ /* Make a loop that will continue until
+ no switching has been done: */
+ while (switching) {
+ // Start by saying: no switching is done:
+ switching = false;
+ rows = table.getElementsByTagName("TR");
+ /* Loop through all table rows (except the
+ first, which contains table headers): */
+ for (i = 1; i < (rows.length - 1); i++) {
+ // Start by saying there should be no switching:
+ shouldSwitch = false;
+ /* Get the two elements you want to compare,
+ one from current row and one from the next: */
+ x = rows[i].getElementsByTagName("TD")[n];
+ y = rows[i + 1].getElementsByTagName("TD")[n];
+ /* Check if the two rows should switch place,
+ based on the direction, asc or desc: */
+ if (dir == "asc") {
+ if (unhumanize(x.innerHTML.toLowerCase()) > unhumanize(y.innerHTML.toLowerCase())) {
+ // If so, mark as a switch and break the loop:
+ shouldSwitch= true;
+ break;
+ }
+ } else if (dir == "desc") {
+ if (unhumanize(x.innerHTML.toLowerCase()) < unhumanize(y.innerHTML.toLowerCase())) {
+ // If so, mark as a switch and break the loop:
+ shouldSwitch= true;
+ break;
+ }
+ }
+ }
+ if (shouldSwitch) {
+ /* If a switch has been marked, make the switch
+ and mark that a switch has been done: */
+ rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
+ switching = true;
+ // Each time a switch is done, increase this count by 1:
+ switchcount ++;
+ } else {
+ /* If no switching has been done AND the direction is "asc",
+ set the direction to "desc" and run the while loop again. */
+ if (switchcount == 0 && dir == "asc") {
+ dir = "desc";
+ switching = true;
+ }
+ }
+ }
+ }
+ </script>
</body>
</html>
diff --git a/share/images/download_completed.png b/share/images/download_completed.png
new file mode 100644
index 00000000..e68fe5a2
--- /dev/null
+++ b/share/images/download_completed.png
Binary files differ
diff --git a/share/images/download_completed_none.png b/share/images/download_completed_none.png
new file mode 100644
index 00000000..8dbd6939
--- /dev/null
+++ b/share/images/download_completed_none.png
Binary files differ
diff --git a/share/images/download_in_progress.png b/share/images/download_in_progress.png
new file mode 100644
index 00000000..19694659
--- /dev/null
+++ b/share/images/download_in_progress.png
Binary files differ
diff --git a/share/images/download_in_progress_none.png b/share/images/download_in_progress_none.png
new file mode 100644
index 00000000..2d61dba4
--- /dev/null
+++ b/share/images/download_in_progress_none.png
Binary files differ
diff --git a/share/images/drop_files.png b/share/images/drop_files.png
deleted file mode 100644
index 1e2ea7d6..00000000
--- a/share/images/drop_files.png
+++ /dev/null
Binary files differ
diff --git a/share/images/favicon.ico b/share/images/favicon.ico
new file mode 100644
index 00000000..63e65d8b
--- /dev/null
+++ b/share/images/favicon.ico
Binary files differ
diff --git a/share/images/file_delete.png b/share/images/file_delete.png
new file mode 100644
index 00000000..b9057df5
--- /dev/null
+++ b/share/images/file_delete.png
Binary files differ
diff --git a/share/images/info.png b/share/images/info.png
new file mode 100644
index 00000000..4be4e65e
--- /dev/null
+++ b/share/images/info.png
Binary files differ
diff --git a/share/images/logo_transparent.png b/share/images/logo_transparent.png
new file mode 100644
index 00000000..1e8ed196
--- /dev/null
+++ b/share/images/logo_transparent.png
Binary files differ
diff --git a/share/images/server_started.png b/share/images/server_started.png
index 833387e1..9c0c3176 100644
--- a/share/images/server_started.png
+++ b/share/images/server_started.png
Binary files differ
diff --git a/share/images/server_stopped.png b/share/images/server_stopped.png
index 414ce81e..5c5b2ec0 100644
--- a/share/images/server_stopped.png
+++ b/share/images/server_stopped.png
Binary files differ
diff --git a/share/images/server_working.png b/share/images/server_working.png
index c6db94ff..e5c8b318 100644
--- a/share/images/server_working.png
+++ b/share/images/server_working.png
Binary files differ
diff --git a/share/images/settings_inactive.png b/share/images/settings_inactive.png
deleted file mode 100644
index 1b35201b..00000000
--- a/share/images/settings_inactive.png
+++ /dev/null
Binary files differ
diff --git a/share/images/web_file.png b/share/images/web_file.png
new file mode 100644
index 00000000..1931aff0
--- /dev/null
+++ b/share/images/web_file.png
Binary files differ
diff --git a/share/images/web_folder.png b/share/images/web_folder.png
new file mode 100644
index 00000000..3ca5df21
--- /dev/null
+++ b/share/images/web_folder.png
Binary files differ
diff --git a/share/locale/en.json b/share/locale/en.json
index 5bc2dc8d..6cd5b4e2 100644
--- a/share/locale/en.json
+++ b/share/locale/en.json
@@ -5,17 +5,17 @@
"wait_for_hs_trying": "Trying...",
"wait_for_hs_nope": "Not ready yet.",
"wait_for_hs_yup": "Ready!",
- "give_this_url": "Give this URL to the person you're sending the file to:",
- "give_this_url_stealth": "Give this URL and HidServAuth line to the person you're sending the file to:",
- "ctrlc_to_stop": "Press Ctrl-C to stop server",
+ "give_this_url": "Give this address to the person you're sending the file to:",
+ "give_this_url_stealth": "Give this address and HidServAuth line to the person you're sending the file to:",
+ "ctrlc_to_stop": "Press Ctrl-C to stop server",
"not_a_file": "{0:s} is not a valid file.",
"not_a_readable_file": "{0:s} is not a readable file.",
"no_available_port": "Could not start the Onion service as there was no available port.",
"download_page_loaded": "Download page loaded",
- "other_page_loaded": "URL loaded",
- "close_on_timeout": "Closing automatically because timeout was reached",
- "closing_automatically": "Closing automatically because download finished",
- "timeout_download_still_running": "Waiting for download to complete before auto-stopping",
+ "other_page_loaded": "Address loaded",
+ "close_on_timeout": "Stopped because timer expired",
+ "closing_automatically": "Stopped because download finished",
+ "timeout_download_still_running": "Waiting for download to complete",
"large_filesize": "Warning: Sending large files could take hours",
"error_tails_invalid_port": "Invalid value, port must be an integer",
"error_tails_unknown_root": "Unknown error with Tails root process",
@@ -34,21 +34,25 @@
"help_debug": "Log application errors to stdout, and log web errors to disk",
"help_filename": "List of files or folders to share",
"help_config": "Path to a custom JSON config file (optional)",
- "gui_drag_and_drop": "Drag and drop\nfiles here",
+ "gui_drag_and_drop": "Drag and drop files and folders\nto start sharing",
"gui_add": "Add",
"gui_delete": "Delete",
"gui_choose_items": "Choose",
"gui_start_server": "Start Sharing",
"gui_stop_server": "Stop Sharing",
- "gui_copy_url": "Copy URL",
+ "gui_stop_server_shutdown_timeout": "Stop Sharing ({}s remaining)",
+ "gui_stop_server_shutdown_timeout_tooltip": "Share will stop automatically at {}",
+ "gui_copy_url": "Copy Address",
"gui_copy_hidservauth": "Copy HidServAuth",
"gui_downloads": "Downloads:",
"gui_canceled": "Canceled",
- "gui_copied_url": "Copied URL to clipboard",
- "gui_copied_hidservauth": "Copied HidServAuth line to clipboard",
+ "gui_copied_url_title": "Copied OnionShare address",
+ "gui_copied_url": "The OnionShare address has been copied to clipboard",
+ "gui_copied_hidservauth_title": "Copied HidServAuth",
+ "gui_copied_hidservauth": "The HidServAuth line has been copied to clipboard",
"gui_starting_server1": "Starting Tor onion service...",
"gui_starting_server2": "Crunching files...",
- "gui_please_wait": "Please wait...",
+ "gui_please_wait": "Starting... Click to cancel",
"error_hs_dir_cannot_create": "Cannot create onion service dir {0:s}",
"error_hs_dir_not_writable": "onion service dir {0:s} is not writable",
"using_ephemeral": "Starting ephemeral Tor onion service and awaiting publication",
@@ -56,10 +60,11 @@
"gui_download_progress_starting": "{0:s}, %p% (Computing ETA)",
"gui_download_progress_eta": "{0:s}, ETA: {1:s}, %p%",
"version_string": "Onionshare {0:s} | https://onionshare.org/",
- "gui_quit_warning": "Are you sure you want to quit?\nThe URL you are sharing won't exist anymore.",
+ "gui_quit_title": "Transfer in Progress",
+ "gui_quit_warning": "You're in the process of sending files. Are you sure you want to quit OnionShare?",
"gui_quit_warning_quit": "Quit",
- "gui_quit_warning_dont_quit": "Don't Quit",
- "error_rate_limit": "An attacker might be trying to guess your URL. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new URL.",
+ "gui_quit_warning_dont_quit": "Cancel",
+ "error_rate_limit": "An attacker might be trying to guess your address. To prevent this, OnionShare has automatically stopped the server. To share the files you must start it again and share the new address.",
"zip_progress_bar_format": "Crunching files: %p%",
"error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.",
"error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.",
@@ -105,7 +110,7 @@
"gui_settings_button_save": "Save",
"gui_settings_button_cancel": "Cancel",
"gui_settings_button_help": "Help",
- "gui_settings_shutdown_timeout_choice": "Set auto-stop timer?",
+ "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer",
"gui_settings_shutdown_timeout": "Stop the share at:",
"settings_saved": "Settings saved to {}",
"settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.",
@@ -135,6 +140,17 @@
"gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.",
"gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.",
"share_via_onionshare": "Share via OnionShare",
- "gui_save_private_key_checkbox": "Use a persistent URL\n(unchecking will delete any saved URL)",
- "persistent_url_in_use": "This share is using a persistent URL"
+ "gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved address)",
+ "gui_url_description": "<b>Anyone</b> with this link can <b>download</b> your files using <b>Tor Browser</b>: <img src={} />",
+ "gui_url_label_persistent": "This share will not stop automatically unless a timer is set.<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
+ "gui_url_label_stay_open": "This share will not stop automatically unless a timer is set.",
+ "gui_url_label_onetime": "This share will stop after the first download",
+ "gui_url_label_onetime_and_persistent": "This share will stop after the first download<br><br>Every share will have the same address (to use one-time addresses, disable persistence in the Settings)",
+ "gui_status_indicator_stopped": "Ready to Share",
+ "gui_status_indicator_working": "Starting...",
+ "gui_status_indicator_started": "Sharing",
+ "gui_file_info": "{} Files, {}",
+ "gui_file_info_single": "{} File, {}",
+ "info_in_progress_downloads_tooltip": "{} download(s) in progress",
+ "info_completed_downloads_tooltip": "{} download(s) completed"
}
diff --git a/test/conftest.py b/test/conftest.py
index 0a3bc806..8f10162b 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -1,13 +1,15 @@
+import sys
+# Force tests to look for resources in the source code tree
+sys.onionshare_dev_mode = True
+
import os
import shutil
-import sys
import tempfile
import pytest
from onionshare import common
-
@pytest.fixture
def temp_dir_1024():
""" Create a temporary directory that has a single file of a
diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py
index 24c46042..e50eee41 100644
--- a/test/test_onionshare_settings.py
+++ b/test/test_onionshare_settings.py
@@ -55,6 +55,7 @@ class TestSettings:
'auth_password': '',
'close_after_first_download': True,
'systray_notifications': True,
+ 'shutdown_timeout': False,
'use_stealth': False,
'use_autoupdate': True,
'autoupdate_timestamp': None,