diff --git a/onionshare_gui/mode/history.py b/onionshare_gui/mode/history.py new file mode 100644 index 00000000..31b4a646 --- /dev/null +++ b/onionshare_gui/mode/history.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2018 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +import time +from PyQt5 import QtCore, QtWidgets, QtGui + +from onionshare import strings + + +class HistoryItem(QtWidgets.QWidget): + """ + The base history item + """ + def __init__(self): + super(HistoryItem, self).__init__() + + def update(self): + pass + + def cancel(self): + pass + + +class DownloadHistoryItem(HistoryItem): + """ + Download history item, for share mode + """ + def __init__(self, common, id, total_bytes): + super(DownloadHistoryItem, self).__init__() + self.common = common + + self.id = id + self.started = time.time() + self.total_bytes = total_bytes + self.downloaded_bytes = 0 + + self.setStyleSheet('QWidget { border: 1px solid red; }') + + # Progress bar + self.progress_bar = QtWidgets.QProgressBar() + self.progress_bar.setTextVisible(True) + self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) + self.progress_bar.setMinimum(0) + self.progress_bar.setMaximum(total_bytes) + self.progress_bar.setValue(0) + self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) + self.progress_bar.total_bytes = total_bytes + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.progress_bar) + self.setLayout(layout) + + # Start at 0 + self.update(0) + + def update(self, downloaded_bytes): + self.downloaded_bytes = downloaded_bytes + + self.progress_bar.setValue(downloaded_bytes) + if downloaded_bytes == self.progress_bar.total_bytes: + pb_fmt = strings._('gui_download_upload_progress_complete').format( + self.common.format_seconds(time.time() - self.started)) + else: + elapsed = time.time() - self.started + if elapsed < 10: + # Wait a couple of seconds for the download rate to stabilize. + # This prevents a "Windows copy dialog"-esque experience at + # the beginning of the download. + pb_fmt = strings._('gui_download_upload_progress_starting').format( + self.common.human_readable_filesize(downloaded_bytes)) + else: + pb_fmt = strings._('gui_download_upload_progress_eta').format( + self.common.human_readable_filesize(downloaded_bytes), + self.estimated_time_remaining) + + self.progress_bar.setFormat(pb_fmt) + + def cancel(self): + self.progress_bar.setFormat(strings._('gui_canceled')) + + @property + def estimated_time_remaining(self): + return self.common.estimated_time_remaining(self.downloaded_bytes, + self.total_bytes, + self.started) + + +class HistoryItemList(QtWidgets.QScrollArea): + """ + List of items + """ + def __init__(self, common): + super(HistoryItemList, self).__init__() + self.common = common + + self.items = {} + + # The layout that holds all of the items + self.items_layout = QtWidgets.QVBoxLayout() + self.items_layout.setContentsMargins(0, 0, 0, 0) + self.items_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + + # Wrapper layout that also contains a stretch + wrapper_layout = QtWidgets.QVBoxLayout() + wrapper_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize) + wrapper_layout.addLayout(self.items_layout) + wrapper_layout.addStretch() + + # The internal widget of the scroll area + widget = QtWidgets.QWidget() + widget.setLayout(wrapper_layout) + self.setWidget(widget) + self.setWidgetResizable(True) + + # Other scroll area settings + self.setBackgroundRole(QtGui.QPalette.Light) + self.verticalScrollBar().rangeChanged.connect(self.resizeScroll) + + def resizeScroll(self, minimum, maximum): + """ + Scroll to the bottom of the window when the range changes. + """ + self.verticalScrollBar().setValue(maximum) + + def add(self, id, item): + """ + Add a new item. Override this method. + """ + self.items[id] = item + self.items_layout.addWidget(item) + + def update(self, id, data): + """ + Update an item. Override this method. + """ + self.items[id].update(data) + + def cancel(self, id): + """ + Cancel an item. Override this method. + """ + self.items[id].cancel() + + def reset(self): + """ + Reset all items, emptying the list. Override this method. + """ + for item in self.items.values(): + self.items_layout.removeWidget(item) + self.items = {} + + +class History(QtWidgets.QWidget): + """ + A history of what's happened so far in this mode. This contains an internal + object full of a scrollable list of items. + """ + def __init__(self, common, empty_image, empty_text, header_text): + super(History, self).__init__() + self.common = common + + self.setMinimumWidth(350) + + # When there are no items + self.empty_image = QtWidgets.QLabel() + self.empty_image.setAlignment(QtCore.Qt.AlignCenter) + self.empty_image.setPixmap(empty_image) + self.empty_text = QtWidgets.QLabel(empty_text) + self.empty_text.setAlignment(QtCore.Qt.AlignCenter) + self.empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) + empty_layout = QtWidgets.QVBoxLayout() + empty_layout.addStretch() + empty_layout.addWidget(self.empty_image) + empty_layout.addWidget(self.empty_text) + empty_layout.addStretch() + self.empty = QtWidgets.QWidget() + self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) + self.empty.setLayout(empty_layout) + + # Header + self.header_label = QtWidgets.QLabel(header_text) + self.header_label.setStyleSheet(self.common.css['downloads_uploads_label']) + clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) + clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) + clear_button.setFlat(True) + clear_button.clicked.connect(self.reset) + header_layout = QtWidgets.QHBoxLayout() + header_layout.addWidget(self.header_label) + header_layout.addStretch() + header_layout.addWidget(clear_button) + + # When there are items + self.item_list = HistoryItemList(self.common) + self.not_empty_layout = QtWidgets.QVBoxLayout() + self.not_empty_layout.addLayout(header_layout) + self.not_empty_layout.addWidget(self.item_list) + self.not_empty = QtWidgets.QWidget() + self.not_empty.setLayout(self.not_empty_layout) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.empty) + layout.addWidget(self.not_empty) + self.setLayout(layout) + + # Reset once at the beginning + self.reset() + + def add(self, id, item): + """ + Add a new item. + """ + self.common.log('History', 'add', 'id: {}, item: {}'.format(id, item)) + + # Hide empty, show not empty + self.empty.hide() + self.not_empty.show() + + # Add it to the list + self.item_list.add(id, item) + + + def update(self, id, data): + """ + Update an item. + """ + self.item_list.update(id, data) + + def cancel(self, id): + """ + Cancel an item. + """ + self.item_list.cancel(id) + + def reset(self): + """ + Reset all items. + """ + self.item_list.reset() + + # Hide not empty, show empty + self.not_empty.hide() + self.empty.show() + + +class ToggleHistory(QtWidgets.QPushButton): + """ + Widget for toggling showing or hiding the history, as well as keeping track + of the indicator counter if it's hidden + """ + def __init__(self, common, current_mode, history_widget, icon, selected_icon): + super(ToggleHistory, self).__init__() + self.common = common + self.current_mode = current_mode + self.history_widget = history_widget + self.icon = icon + self.selected_icon = selected_icon + + # Toggle button + self.setDefault(False) + self.setFixedWidth(35) + self.setFixedHeight(30) + self.setFlat(True) + self.setIcon(icon) + self.clicked.connect(self.toggle_clicked) + + # Keep track of indicator + self.indicator_count = 0 + self.indicator_label = QtWidgets.QLabel(parent=self) + self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) + self.update_indicator() + + def update_indicator(self, increment=False): + """ + Update the display of the indicator count. If increment is True, then + only increment the counter if Downloads is hidden. + """ + if increment and not self.history_widget.isVisible(): + self.indicator_count += 1 + + self.indicator_label.setText("{}".format(self.indicator_count)) + + if self.indicator_count == 0: + self.indicator_label.hide() + else: + size = self.indicator_label.sizeHint() + self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) + self.indicator_label.show() + + def toggle_clicked(self): + """ + Toggle showing and hiding the history widget + """ + self.common.log('ToggleHistory', 'toggle_clicked') + + if self.history_widget.isVisible(): + self.history_widget.hide() + self.setIcon(self.icon) + self.setFlat(True) + else: + self.history_widget.show() + self.setIcon(self.selected_icon) + self.setFlat(False) + + # Reset the indicator count + self.indicator_count = 0 + self.update_indicator() + + self.current_mode.resize_window() diff --git a/onionshare_gui/mode/share_mode/__init__.py b/onionshare_gui/mode/share_mode/__init__.py index f7ba2760..bae4bec8 100644 --- a/onionshare_gui/mode/share_mode/__init__.py +++ b/onionshare_gui/mode/share_mode/__init__.py @@ -26,13 +26,12 @@ from onionshare.common import Common from onionshare.web import Web from .file_selection import FileSelection -from .downloads import Downloads from .threads import CompressThread -#from .info import ShareModeInfo from .. import Mode -from ..toggle_history import ToggleHistory +from ..history import History, ToggleHistory, DownloadHistoryItem from ...widgets import Alert + class ShareMode(Mode): """ Parts of the main window UI for sharing files. @@ -72,9 +71,14 @@ class ShareMode(Mode): self.filesize_warning.setStyleSheet(self.common.css['share_filesize_warning']) self.filesize_warning.hide() - # Downloads - self.downloads = Downloads(self.common) - self.downloads.hide() + # Download history + self.history = History( + self.common, + QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png'))), + strings._('gui_no_downloads'), + strings._('gui_downloads') + ) + self.history.hide() self.downloads_in_progress = 0 self.downloads_completed = 0 @@ -87,7 +91,7 @@ class ShareMode(Mode): # Toggle history self.toggle_history = ToggleHistory( - self.common, self, self.downloads, + self.common, self, self.history, QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle.png')), QtGui.QIcon(self.common.get_resource_path('images/downloads_toggle_selected.png')) ) @@ -116,7 +120,7 @@ class ShareMode(Mode): # Wrapper layout self.wrapper_layout = QtWidgets.QHBoxLayout() self.wrapper_layout.addLayout(self.main_layout) - self.wrapper_layout.addWidget(self.downloads) + self.wrapper_layout.addWidget(self.history) self.setLayout(self.wrapper_layout) # Always start with focus on file selection @@ -241,7 +245,9 @@ class ShareMode(Mode): filesize = self.web.share_mode.gzip_filesize else: filesize = self.web.share_mode.download_filesize - self.downloads.add(event["data"]["id"], filesize) + + item = DownloadHistoryItem(self.common, event["data"]["id"], filesize) + self.history.add(event["data"]["id"], item) self.toggle_history.update_indicator(True) self.downloads_in_progress += 1 #self.info.update_downloads_in_progress() @@ -252,7 +258,7 @@ class ShareMode(Mode): """ Handle REQUEST_PROGRESS event. """ - self.downloads.update(event["data"]["id"], event["data"]["bytes"]) + self.history.update(event["data"]["id"], event["data"]["bytes"]) # Is the download complete? if event["data"]["bytes"] == self.web.share_mode.filesize: @@ -272,7 +278,7 @@ class ShareMode(Mode): self.server_status_label.setText(strings._('closing_automatically', True)) else: if self.server_status.status == self.server_status.STATUS_STOPPED: - self.downloads.cancel(event["data"]["id"]) + self.history.cancel(event["data"]["id"]) self.downloads_in_progress = 0 #self.info.update_downloads_in_progress() @@ -280,7 +286,7 @@ class ShareMode(Mode): """ Handle REQUEST_CANCELED event. """ - self.downloads.cancel(event["data"]["id"]) + self.history.cancel(event["data"]["id"]) # Update the 'in progress downloads' info self.downloads_in_progress -= 1 @@ -332,11 +338,11 @@ class ShareMode(Mode): self.downloads_in_progress = 0 #self.info.update_downloads_completed() #self.info.update_downloads_in_progress() - self.downloads.reset() + self.history.reset() def resize_window(self): min_width = self.common.min_window_width - if self.downloads.isVisible(): + if self.history.isVisible(): min_width += 300 self.adjust_size.emit(min_width) diff --git a/onionshare_gui/mode/share_mode/downloads.py b/onionshare_gui/mode/share_mode/downloads.py deleted file mode 100644 index e78231ad..00000000 --- a/onionshare_gui/mode/share_mode/downloads.py +++ /dev/null @@ -1,248 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -import time -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class Download(QtWidgets.QWidget): - def __init__(self, common, download_id, total_bytes): - super(Download, self).__init__() - self.common = common - - self.download_id = download_id - self.started = time.time() - self.total_bytes = total_bytes - self.downloaded_bytes = 0 - - self.setStyleSheet('QWidget { border: 1px solid red; }') - - # Progress bar - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar.setTextVisible(True) - self.progress_bar.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.progress_bar.setAlignment(QtCore.Qt.AlignHCenter) - self.progress_bar.setMinimum(0) - self.progress_bar.setMaximum(total_bytes) - self.progress_bar.setValue(0) - self.progress_bar.setStyleSheet(self.common.css['downloads_uploads_progress_bar']) - self.progress_bar.total_bytes = total_bytes - - # Layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.progress_bar) - self.setLayout(layout) - - # Start at 0 - self.update(0) - - def update(self, downloaded_bytes): - self.downloaded_bytes = downloaded_bytes - - self.progress_bar.setValue(downloaded_bytes) - if downloaded_bytes == self.progress_bar.total_bytes: - pb_fmt = strings._('gui_download_upload_progress_complete').format( - self.common.format_seconds(time.time() - self.started)) - else: - elapsed = time.time() - self.started - if elapsed < 10: - # Wait a couple of seconds for the download rate to stabilize. - # This prevents a "Windows copy dialog"-esque experience at - # the beginning of the download. - pb_fmt = strings._('gui_download_upload_progress_starting').format( - self.common.human_readable_filesize(downloaded_bytes)) - else: - pb_fmt = strings._('gui_download_upload_progress_eta').format( - self.common.human_readable_filesize(downloaded_bytes), - self.estimated_time_remaining) - - self.progress_bar.setFormat(pb_fmt) - - def cancel(self): - self.progress_bar.setFormat(strings._('gui_canceled')) - - @property - def estimated_time_remaining(self): - return self.common.estimated_time_remaining(self.downloaded_bytes, - self.total_bytes, - self.started) - - -class DownloadList(QtWidgets.QScrollArea): - """ - List of download progress bars. - """ - def __init__(self, common): - super(DownloadList, self).__init__() - self.common = common - - self.downloads = {} - - # The layout that holds all of the downloads - self.downloads_layout = QtWidgets.QVBoxLayout() - self.downloads_layout.setContentsMargins(0, 0, 0, 0) - self.downloads_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.downloads_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, download_id, content_length): - """ - Add a new download progress bar. - """ - download = Download(self.common, download_id, content_length) - self.downloads[download_id] = download - self.downloads_layout.addWidget(download) - - def update(self, download_id, downloaded_bytes): - """ - Update the progress of a download progress bar. - """ - self.downloads[download_id].update(downloaded_bytes) - - def cancel(self, download_id): - """ - Update a download progress bar to show that it has been canceled. - """ - self.downloads[download_id].cancel() - - def reset(self): - """ - Reset the downloads back to zero - """ - for download in self.downloads.values(): - self.downloads_layout.removeWidget(download) - download.progress_bar.close() - self.downloads = {} - - -class Downloads(QtWidgets.QWidget): - """ - The downloads chunk of the GUI. This lists all of the active download - progress bars. - """ - def __init__(self, common): - super(Downloads, self).__init__() - self.common = common - - self.setMinimumWidth(350) - - # When there are no downloads - empty_image = QtWidgets.QLabel() - empty_image.setAlignment(QtCore.Qt.AlignCenter) - empty_image.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(self.common.get_resource_path('images/downloads_transparent.png')))) - empty_text = QtWidgets.QLabel(strings._('gui_no_downloads', True)) - empty_text.setAlignment(QtCore.Qt.AlignCenter) - empty_text.setStyleSheet(self.common.css['downloads_uploads_empty_text']) - empty_layout = QtWidgets.QVBoxLayout() - empty_layout.addStretch() - empty_layout.addWidget(empty_image) - empty_layout.addWidget(empty_text) - empty_layout.addStretch() - self.empty = QtWidgets.QWidget() - self.empty.setStyleSheet(self.common.css['downloads_uploads_empty']) - self.empty.setLayout(empty_layout) - - # When there are downloads - self.download_list = DownloadList(self.common) - - # Download header - downloads_label = QtWidgets.QLabel(strings._('gui_downloads', True)) - downloads_label.setStyleSheet(self.common.css['downloads_uploads_label']) - clear_button = QtWidgets.QPushButton(strings._('gui_clear_history', True)) - clear_button.setStyleSheet(self.common.css['downloads_uploads_clear']) - clear_button.setFlat(True) - clear_button.clicked.connect(self.reset) - download_header = QtWidgets.QHBoxLayout() - download_header.addWidget(downloads_label) - download_header.addStretch() - download_header.addWidget(clear_button) - - # Download layout - not_empty_layout = QtWidgets.QVBoxLayout() - not_empty_layout.addLayout(download_header) - not_empty_layout.addWidget(self.download_list) - self.not_empty = QtWidgets.QWidget() - self.not_empty.setLayout(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, download_id, content_length): - """ - Add a new download progress bar. - """ - self.common.log('Downloads', 'add', 'download_id: {}, content_length: {}'.format(download_id, content_length)) - - # Hide empty, show not empty - self.empty.hide() - self.not_empty.show() - - # Add it to the list - self.download_list.add(download_id, content_length) - - def update(self, download_id, downloaded_bytes): - """ - Update the progress of a download progress bar. - """ - self.download_list.update(download_id, downloaded_bytes) - - def cancel(self, download_id): - """ - Update a download progress bar to show that it has been canceled. - """ - self.download_list.cancel(download_id) - - def reset(self): - """ - Reset the downloads back to zero - """ - self.download_list.reset() - - # Hide not empty, show empty - self.not_empty.hide() - self.empty.show() diff --git a/onionshare_gui/mode/toggle_history.py b/onionshare_gui/mode/toggle_history.py deleted file mode 100644 index 81ecde86..00000000 --- a/onionshare_gui/mode/toggle_history.py +++ /dev/null @@ -1,88 +0,0 @@ -# -*- coding: utf-8 -*- -""" -OnionShare | https://onionshare.org/ - -Copyright (C) 2014-2018 Micah Lee - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" -from PyQt5 import QtCore, QtWidgets, QtGui - -from onionshare import strings - - -class ToggleHistory(QtWidgets.QPushButton): - """ - Widget for toggling download/upload history on or off, as well as keeping track - of the indicator counter - """ - def __init__(self, common, current_mode, history_widget, icon, selected_icon): - super(ToggleHistory, self).__init__() - self.common = common - self.current_mode = current_mode - self.history_widget = history_widget - self.icon = icon - self.selected_icon = selected_icon - - # Toggle button - self.setDefault(False) - self.setFixedWidth(35) - self.setFixedHeight(30) - self.setFlat(True) - self.setIcon(icon) - self.clicked.connect(self.toggle_clicked) - - # Keep track of indicator - self.indicator_count = 0 - self.indicator_label = QtWidgets.QLabel(parent=self) - self.indicator_label.setStyleSheet(self.common.css['download_uploads_indicator']) - self.update_indicator() - - def update_indicator(self, increment=False): - """ - Update the display of the indicator count. If increment is True, then - only increment the counter if Downloads is hidden. - """ - if increment and not self.history_widget.isVisible(): - self.indicator_count += 1 - - self.indicator_label.setText("{}".format(self.indicator_count)) - - if self.indicator_count == 0: - self.indicator_label.hide() - else: - size = self.indicator_label.sizeHint() - self.indicator_label.setGeometry(35-size.width(), 0, size.width(), size.height()) - self.indicator_label.show() - - def toggle_clicked(self): - """ - Toggle showing and hiding the history widget - """ - self.common.log('ToggleHistory', 'toggle_clicked') - - if self.history_widget.isVisible(): - self.history_widget.hide() - self.setIcon(self.icon) - self.setFlat(True) - else: - self.history_widget.show() - self.setIcon(self.selected_icon) - self.setFlat(False) - - # Reset the indicator count - self.indicator_count = 0 - self.update_indicator() - - self.current_mode.resize_window()