mirror of
https://github.com/onionshare/onionshare.git
synced 2025-06-19 20:14:13 -04:00
Merge pull request #104 from lazlolazlolazlo/nativeqt
Qt interface using native widgets
This commit is contained in:
commit
af4fe2338c
8 changed files with 267 additions and 612 deletions
Binary file not shown.
Before Width: | Height: | Size: 3.7 KiB |
|
@ -1,7 +1,6 @@
|
||||||
import os, sys, subprocess, inspect, platform, argparse
|
from __future__ import division
|
||||||
from PyQt4.QtCore import *
|
import os, sys, subprocess, inspect, platform, argparse, threading, time, math
|
||||||
from PyQt4.QtGui import *
|
from PyQt4 import QtCore, QtGui
|
||||||
from PyQt4.QtWebKit import *
|
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
onionshare_gui_dir = os.path.dirname(__file__)
|
onionshare_gui_dir = os.path.dirname(__file__)
|
||||||
|
@ -14,40 +13,274 @@ except ImportError:
|
||||||
sys.path.append(os.path.abspath(onionshare_gui_dir+"/.."))
|
sys.path.append(os.path.abspath(onionshare_gui_dir+"/.."))
|
||||||
import onionshare
|
import onionshare
|
||||||
from onionshare import translated
|
from onionshare import translated
|
||||||
import webapp
|
|
||||||
|
|
||||||
|
app = None
|
||||||
window_icon = None
|
window_icon = None
|
||||||
|
onion_host = None
|
||||||
|
onionshare_port = None
|
||||||
|
progress = None
|
||||||
|
|
||||||
class Application(QApplication):
|
# request types
|
||||||
|
REQUEST_LOAD = 0
|
||||||
|
REQUEST_DOWNLOAD = 1
|
||||||
|
REQUEST_PROGRESS = 2
|
||||||
|
REQUEST_OTHER = 3
|
||||||
|
|
||||||
|
class Application(QtGui.QApplication):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
platform = onionshare.get_platform()
|
platform = onionshare.get_platform()
|
||||||
if platform == 'Tails' or platform == 'Linux':
|
if platform == 'Tails' or platform == 'Linux':
|
||||||
self.setAttribute(Qt.AA_X11InitThreads, True)
|
self.setAttribute(QtCore.Qt.AA_X11InitThreads, True)
|
||||||
|
QtGui.QApplication.__init__(self, sys.argv)
|
||||||
|
|
||||||
QApplication.__init__(self, sys.argv)
|
class OnionShareGui(QtGui.QWidget):
|
||||||
|
def __init__(self, filename, basename):
|
||||||
|
super(OnionShareGui, self).__init__()
|
||||||
|
# initialize ui
|
||||||
|
self.init_ui(filename, basename)
|
||||||
|
# check for requests every 1000ms
|
||||||
|
self.timer = QtCore.QTimer()
|
||||||
|
QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.check_for_requests)
|
||||||
|
self.timer.start(1000)
|
||||||
|
# copy url to clipboard
|
||||||
|
self.copy_to_clipboard()
|
||||||
|
|
||||||
class WebAppThread(QThread):
|
def init_ui(self, filename, basename):
|
||||||
def __init__(self, webapp_port):
|
# window
|
||||||
QThread.__init__(self)
|
|
||||||
self.webapp_port = webapp_port
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
webapp.app.run(port=self.webapp_port)
|
|
||||||
|
|
||||||
class Window(QWebView):
|
|
||||||
def __init__(self, basename, webapp_port):
|
|
||||||
global window_icon
|
|
||||||
QWebView.__init__(self)
|
|
||||||
self.setWindowTitle("{0} | OnionShare".format(basename))
|
self.setWindowTitle("{0} | OnionShare".format(basename))
|
||||||
self.resize(580, 400)
|
self.resize(580, 400)
|
||||||
self.setMinimumSize(580, 400)
|
self.setMinimumSize(580, 400)
|
||||||
self.setMaximumSize(580, 400)
|
self.setMaximumSize(580, 400)
|
||||||
self.setWindowIcon(window_icon)
|
palette = QtGui.QPalette()
|
||||||
self.load(QUrl("http://127.0.0.1:{0}".format(webapp_port)))
|
palette.setColor(QtGui.QPalette.Background, QtCore.Qt.white)
|
||||||
|
self.setPalette(palette)
|
||||||
|
|
||||||
def alert(msg, icon=QMessageBox.NoIcon):
|
# icon
|
||||||
|
self.setWindowIcon(window_icon)
|
||||||
|
|
||||||
|
# widget
|
||||||
|
self.widget = QtGui.QWidget(self)
|
||||||
|
self.widget.setGeometry(QtCore.QRect(5, 5, 570, 390))
|
||||||
|
|
||||||
|
# wrapper
|
||||||
|
self.wrapper = QtGui.QVBoxLayout(self.widget)
|
||||||
|
self.wrapper.setMargin(0)
|
||||||
|
self.wrapper.setObjectName("wrapper")
|
||||||
|
|
||||||
|
# header
|
||||||
|
self.header = QtGui.QHBoxLayout()
|
||||||
|
|
||||||
|
# logo
|
||||||
|
self.logoLabel = QtGui.QLabel(self.widget)
|
||||||
|
self.logo = QtGui.QPixmap("{0}/static/logo.png".format(onionshare_gui_dir))
|
||||||
|
self.logoLabel.setPixmap(self.logo)
|
||||||
|
self.header.addWidget(self.logoLabel)
|
||||||
|
|
||||||
|
# fileinfo
|
||||||
|
self.fileinfo = QtGui.QVBoxLayout()
|
||||||
|
|
||||||
|
# filename
|
||||||
|
self.filenameLabel = QtGui.QLabel(self.widget)
|
||||||
|
self.filenameLabel.setStyleSheet("font-family: sans-serif; font-size: 22px; font-weight: bold; color: #000000; white-space: nowrap")
|
||||||
|
self.filenameLabel.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.fileinfo.addWidget(self.filenameLabel)
|
||||||
|
|
||||||
|
# checksum
|
||||||
|
self.checksumLabel = QtGui.QLabel(self.widget)
|
||||||
|
self.checksumLabel.setStyleSheet("font-family: arial; text-align: left; color: #666666")
|
||||||
|
self.checksumLabel.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.fileinfo.addWidget(self.checksumLabel)
|
||||||
|
|
||||||
|
# filesize
|
||||||
|
self.filesizeLabel = QtGui.QLabel(self.widget)
|
||||||
|
self.filesizeLabel.setStyleSheet("font-family: arial; text-align: left; color: #666666")
|
||||||
|
self.filesizeLabel.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.fileinfo.addWidget(self.filesizeLabel)
|
||||||
|
self.header.addLayout(self.fileinfo)
|
||||||
|
|
||||||
|
fileinfoSpacer = QtGui.QSpacerItem(20, 50, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)
|
||||||
|
self.header.addItem(fileinfoSpacer)
|
||||||
|
self.wrapper.addLayout(self.header)
|
||||||
|
|
||||||
|
# header seperator
|
||||||
|
self.headerSeperator = QtGui.QFrame(self.widget)
|
||||||
|
self.headerSeperator.setFrameShape(QtGui.QFrame.HLine)
|
||||||
|
self.headerSeperator.setFrameShadow(QtGui.QFrame.Plain)
|
||||||
|
self.wrapper.addWidget(self.headerSeperator)
|
||||||
|
|
||||||
|
# log
|
||||||
|
self.log = QtGui.QVBoxLayout()
|
||||||
|
self.log.setAlignment(QtCore.Qt.AlignTop)
|
||||||
|
self.wrapper.addLayout(self.log)
|
||||||
|
spacerItem2 = QtGui.QSpacerItem(1, 400, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Maximum)
|
||||||
|
self.wrapper.addItem(spacerItem2)
|
||||||
|
|
||||||
|
# footer seperator
|
||||||
|
self.footerSeperator = QtGui.QFrame(self.widget)
|
||||||
|
self.footerSeperator.setFrameShape(QtGui.QFrame.HLine)
|
||||||
|
self.footerSeperator.setFrameShadow(QtGui.QFrame.Plain)
|
||||||
|
self.wrapper.addWidget(self.footerSeperator)
|
||||||
|
|
||||||
|
# footer
|
||||||
|
self.footer = QtGui.QHBoxLayout()
|
||||||
|
|
||||||
|
# close automatically checkbox
|
||||||
|
self.closeAutomatically = QtGui.QCheckBox(self.widget)
|
||||||
|
self.closeAutomatically.setCheckState(QtCore.Qt.Checked)
|
||||||
|
if onionshare.stay_open:
|
||||||
|
self.closeAutomatically.setCheckState(QtCore.Qt.Unchecked)
|
||||||
|
|
||||||
|
self.closeAutomatically.setStyleSheet("font-size: 12px")
|
||||||
|
self.connect(self.closeAutomatically, QtCore.SIGNAL('stateChanged(int)'), self.stay_open_changed)
|
||||||
|
self.footer.addWidget(self.closeAutomatically)
|
||||||
|
|
||||||
|
# footer spacer
|
||||||
|
spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||||
|
self.footer.addItem(spacerItem1)
|
||||||
|
|
||||||
|
# copy url button
|
||||||
|
self.copyURL = QtGui.QPushButton(self.widget)
|
||||||
|
self.connect(self.copyURL, QtCore.SIGNAL("clicked()"), self.copy_to_clipboard)
|
||||||
|
|
||||||
|
self.footer.addWidget(self.copyURL)
|
||||||
|
self.wrapper.addLayout(self.footer)
|
||||||
|
|
||||||
|
url = 'http://{0}/{1}'.format(onion_host, onionshare.slug)
|
||||||
|
|
||||||
|
filehash, filesize = onionshare.file_crunching(filename)
|
||||||
|
onionshare.set_file_info(filename, filehash, filesize)
|
||||||
|
onionshare.filesize = filesize
|
||||||
|
|
||||||
|
# start onionshare service in new thread
|
||||||
|
t = threading.Thread(target=onionshare.app.run, kwargs={'port': onionshare_port})
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
# show url to share
|
||||||
|
loaded = QtGui.QLabel(translated("give_this_url") + "<br /><strong>" + url + "</strong>")
|
||||||
|
loaded.setStyleSheet("color: #000000; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
loaded.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(loaded)
|
||||||
|
|
||||||
|
# translate
|
||||||
|
self.filenameLabel.setText(basename)
|
||||||
|
self.checksumLabel.setText(translated("sha1_checksum") + ": <strong>" + filehash + "</strong>")
|
||||||
|
self.filesizeLabel.setText(translated("filesize") + ": <strong>" + onionshare.human_readable_filesize(filesize) + "</strong>")
|
||||||
|
self.closeAutomatically.setText(translated("close_on_finish"))
|
||||||
|
self.copyURL.setText(translated("copy_url"))
|
||||||
|
|
||||||
|
# show dialog
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def update_log(self, event, msg):
|
||||||
|
global progress
|
||||||
|
if event["type"] == REQUEST_LOAD:
|
||||||
|
label = QtGui.QLabel(msg)
|
||||||
|
label.setStyleSheet("color: #009900; font-weight: bold; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
label.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(label)
|
||||||
|
elif event["type"] == REQUEST_DOWNLOAD:
|
||||||
|
download = QtGui.QLabel(msg)
|
||||||
|
download.setStyleSheet("color: #009900; font-weight: bold; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
download.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(download)
|
||||||
|
progress = QtGui.QLabel()
|
||||||
|
progress.setStyleSheet("color: #0000cc; font-weight: bold; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
progress.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(progress)
|
||||||
|
elif event["type"] == REQUEST_PROGRESS:
|
||||||
|
progress.setText(msg)
|
||||||
|
elif event["path"] != '/favicon.ico':
|
||||||
|
other = QtGui.QLabel(msg)
|
||||||
|
other.setStyleSheet("color: #009900; font-weight: bold; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
other.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(other)
|
||||||
|
return
|
||||||
|
|
||||||
|
def check_for_requests(self):
|
||||||
|
events = []
|
||||||
|
|
||||||
|
done = False
|
||||||
|
while not done:
|
||||||
|
try:
|
||||||
|
r = onionshare.q.get(False)
|
||||||
|
events.append(r)
|
||||||
|
except onionshare.Queue.Empty:
|
||||||
|
done = True
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
if event["type"] == REQUEST_LOAD:
|
||||||
|
self.update_log(event, translated("download_page_loaded"))
|
||||||
|
elif event["type"] == REQUEST_DOWNLOAD:
|
||||||
|
self.update_log(event, translated("download_started"))
|
||||||
|
elif event["type"] == REQUEST_PROGRESS:
|
||||||
|
# is the download complete?
|
||||||
|
if event["data"]["bytes"] == onionshare.filesize:
|
||||||
|
self.update_log(event, translated("download_finished"))
|
||||||
|
# close on finish?
|
||||||
|
if not onionshare.stay_open:
|
||||||
|
time.sleep(1)
|
||||||
|
def close_countdown(i):
|
||||||
|
if i > 0:
|
||||||
|
QtGui.QApplication.quit()
|
||||||
|
else:
|
||||||
|
time.sleep(1)
|
||||||
|
i -= 1
|
||||||
|
closing.setText(translated("close_countdown").format(str(i)))
|
||||||
|
print translated("close_countdown").format(str(i))
|
||||||
|
close_countdown(i)
|
||||||
|
|
||||||
|
closing = QtGui.QLabel(self.widget)
|
||||||
|
closing.setStyleSheet("font-weight: bold; font-style: italic; font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
closing.setText(translated("close_countdown").format("3"))
|
||||||
|
closing.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(closing)
|
||||||
|
close_countdown(3)
|
||||||
|
|
||||||
|
# still in progress
|
||||||
|
else:
|
||||||
|
percent = math.floor((event["data"]["bytes"] / onionshare.filesize) * 100)
|
||||||
|
self.update_log(event, " " + onionshare.human_readable_filesize(event["data"]["bytes"]) + ', ' + str(percent) +'%')
|
||||||
|
|
||||||
|
elif event["path"] != '/favicon.ico':
|
||||||
|
self.update_log(event, translated("other_page_loaded"))
|
||||||
|
|
||||||
|
def copy_to_clipboard(self):
|
||||||
|
global onion_host
|
||||||
|
url = 'http://{0}/{1}'.format(onion_host, onionshare.slug)
|
||||||
|
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
# Qt's QClipboard isn't working in Windows
|
||||||
|
# https://github.com/micahflee/onionshare/issues/46
|
||||||
|
import ctypes
|
||||||
|
GMEM_DDESHARE = 0x2000
|
||||||
|
ctypes.windll.user32.OpenClipboard(None)
|
||||||
|
ctypes.windll.user32.EmptyClipboard()
|
||||||
|
hcd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(url))+1)
|
||||||
|
pch_data = ctypes.windll.kernel32.GlobalLock(hcd)
|
||||||
|
ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pch_data), bytes(url))
|
||||||
|
ctypes.windll.kernel32.GlobalUnlock(hcd)
|
||||||
|
ctypes.windll.user32.SetClipboardData(1, hcd)
|
||||||
|
ctypes.windll.user32.CloseClipboard()
|
||||||
|
else:
|
||||||
|
clipboard = app.clipboard()
|
||||||
|
clipboard.setText(url)
|
||||||
|
|
||||||
|
copied = QtGui.QLabel(translated("copied_url"))
|
||||||
|
copied.setStyleSheet("font-size: 14px; padding: 5px 10px; border-bottom: 1px solid #cccccc;")
|
||||||
|
copied.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse)
|
||||||
|
self.log.addWidget(copied)
|
||||||
|
return
|
||||||
|
|
||||||
|
def stay_open_changed(self, state):
|
||||||
|
if state > 0:
|
||||||
|
onionshare.set_stay_open(False)
|
||||||
|
onionshare.set_stay_open(True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def alert(msg, icon=QtGui.QMessageBox.NoIcon):
|
||||||
global window_icon
|
global window_icon
|
||||||
dialog = QMessageBox()
|
dialog = QtGui.QMessageBox()
|
||||||
dialog.setWindowTitle("OnionShare")
|
dialog.setWindowTitle("OnionShare")
|
||||||
dialog.setWindowIcon(window_icon)
|
dialog.setWindowIcon(window_icon)
|
||||||
dialog.setText(msg)
|
dialog.setText(msg)
|
||||||
|
@ -61,7 +294,7 @@ def select_file(strings, filename=None):
|
||||||
if onionshare.get_platform() == 'Tails':
|
if onionshare.get_platform() == 'Tails':
|
||||||
args['directory'] = '/home/amnesia'
|
args['directory'] = '/home/amnesia'
|
||||||
|
|
||||||
filename = QFileDialog.getOpenFileName(caption=translated('choose_file'), options=QFileDialog.ReadOnly, **args)
|
filename = QtGui.QFileDialog.getOpenFileName(caption=translated('choose_file'), options=QtGui.QFileDialog.ReadOnly, **args)
|
||||||
if not filename:
|
if not filename:
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
|
@ -69,7 +302,7 @@ def select_file(strings, filename=None):
|
||||||
|
|
||||||
# validate filename
|
# validate filename
|
||||||
if not os.path.isfile(filename):
|
if not os.path.isfile(filename):
|
||||||
alert(translated("not_a_file").format(filename), QMessageBox.Warning)
|
alert(translated("not_a_file").format(filename), QtGui.QMessageBox.Warning)
|
||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
filename = os.path.abspath(filename)
|
filename = os.path.abspath(filename)
|
||||||
|
@ -77,9 +310,11 @@ def select_file(strings, filename=None):
|
||||||
return filename, basename
|
return filename, basename
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global onionshare_port
|
||||||
onionshare.strings = onionshare.load_strings()
|
onionshare.strings = onionshare.load_strings()
|
||||||
|
|
||||||
# start the Qt app
|
# start the Qt app
|
||||||
|
global app
|
||||||
app = Application()
|
app = Application()
|
||||||
|
|
||||||
# check for root in Tails
|
# check for root in Tails
|
||||||
|
@ -104,16 +339,17 @@ def main():
|
||||||
|
|
||||||
# create the onionshare icon
|
# create the onionshare icon
|
||||||
global window_icon, onionshare_gui_dir
|
global window_icon, onionshare_gui_dir
|
||||||
window_icon = QIcon("{0}/onionshare-icon.png".format(onionshare_gui_dir))
|
window_icon = QtGui.QIcon("{0}/static/logo.png".format(onionshare_gui_dir))
|
||||||
|
|
||||||
# try starting hidden service
|
# try starting hidden service
|
||||||
|
global onion_host
|
||||||
onionshare_port = onionshare.choose_port()
|
onionshare_port = onionshare.choose_port()
|
||||||
local_host = "127.0.0.1:{0}".format(onionshare_port)
|
local_host = "127.0.0.1:{0}".format(onionshare_port)
|
||||||
if not local_only:
|
if not local_only:
|
||||||
try:
|
try:
|
||||||
onion_host = onionshare.start_hidden_service(onionshare_port)
|
onion_host = onionshare.start_hidden_service(onionshare_port)
|
||||||
except onionshare.NoTor as e:
|
except onionshare.NoTor as e:
|
||||||
alert(e.args[0], QMessageBox.Warning)
|
alert(e.args[0], QtGui.QMessageBox.Warning)
|
||||||
return
|
return
|
||||||
onionshare.tails_open_port(onionshare_port)
|
onionshare.tails_open_port(onionshare_port)
|
||||||
|
|
||||||
|
@ -122,36 +358,13 @@ def main():
|
||||||
if not filename:
|
if not filename:
|
||||||
return
|
return
|
||||||
|
|
||||||
# initialize the web app
|
|
||||||
webapp.onionshare = onionshare
|
|
||||||
webapp.onionshare_port = onionshare_port
|
|
||||||
webapp.filename = filename
|
|
||||||
webapp.qtapp = app
|
|
||||||
webapp.clipboard = app.clipboard()
|
|
||||||
webapp.stay_open = stay_open
|
|
||||||
if not local_only:
|
|
||||||
webapp.onion_host = onion_host
|
|
||||||
else:
|
|
||||||
webapp.onion_host = local_host
|
|
||||||
if debug:
|
|
||||||
onionshare.debug_mode()
|
|
||||||
webapp.debug_mode()
|
|
||||||
|
|
||||||
# run the web app in a new thread
|
|
||||||
webapp_port = onionshare.choose_port()
|
|
||||||
onionshare.tails_open_port(webapp_port)
|
|
||||||
webapp_thread = WebAppThread(webapp_port)
|
|
||||||
webapp_thread.start()
|
|
||||||
|
|
||||||
# clean up when app quits
|
# clean up when app quits
|
||||||
def shutdown():
|
def shutdown():
|
||||||
onionshare.tails_close_port(onionshare_port)
|
onionshare.tails_close_port(onionshare_port)
|
||||||
onionshare.tails_close_port(webapp_port)
|
app.connect(app, QtCore.SIGNAL("aboutToQuit()"), shutdown)
|
||||||
app.connect(app, SIGNAL("aboutToQuit()"), shutdown)
|
|
||||||
|
|
||||||
# launch the window
|
# launch the gui
|
||||||
web = Window(basename, webapp_port)
|
gui = OnionShareGui(filename, basename)
|
||||||
web.show()
|
|
||||||
|
|
||||||
# all done
|
# all done
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
|
|
|
@ -1,60 +0,0 @@
|
||||||
function human_readable_filesize(bytes, si) {
|
|
||||||
var thresh = si ? 1000 : 1024;
|
|
||||||
if(bytes < thresh) return bytes + ' B';
|
|
||||||
var units = si ? ['kB','MB','GB','TB','PB','EB','ZB','YB'] : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
|
|
||||||
var u = -1;
|
|
||||||
do {
|
|
||||||
bytes /= thresh;
|
|
||||||
++u;
|
|
||||||
} while(bytes >= thresh);
|
|
||||||
return bytes.toFixed(1)+' '+units[u];
|
|
||||||
};
|
|
||||||
|
|
||||||
function htmlspecialchars(string, quote_style, charset, double_encode) {
|
|
||||||
var optTemp = 0,
|
|
||||||
i = 0,
|
|
||||||
noquotes = false;
|
|
||||||
if (typeof quote_style === 'undefined' || quote_style === null) {
|
|
||||||
quote_style = 2;
|
|
||||||
}
|
|
||||||
string = string.toString();
|
|
||||||
if (double_encode !== false) {
|
|
||||||
// Put this first to avoid double-encoding
|
|
||||||
string = string.replace(/&/g, '&');
|
|
||||||
}
|
|
||||||
string = string.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>');
|
|
||||||
|
|
||||||
var OPTS = {
|
|
||||||
'ENT_NOQUOTES': 0,
|
|
||||||
'ENT_HTML_QUOTE_SINGLE': 1,
|
|
||||||
'ENT_HTML_QUOTE_DOUBLE': 2,
|
|
||||||
'ENT_COMPAT': 2,
|
|
||||||
'ENT_QUOTES': 3,
|
|
||||||
'ENT_IGNORE': 4
|
|
||||||
};
|
|
||||||
if (quote_style === 0) {
|
|
||||||
noquotes = true;
|
|
||||||
}
|
|
||||||
if (typeof quote_style !== 'number') {
|
|
||||||
// Allow for a single string or an array of string flags
|
|
||||||
quote_style = [].concat(quote_style);
|
|
||||||
for (i = 0; i < quote_style.length; i++) {
|
|
||||||
// Resolve string input to bitwise e.g. 'ENT_IGNORE' becomes 4
|
|
||||||
if (OPTS[quote_style[i]] === 0) {
|
|
||||||
noquotes = true;
|
|
||||||
} else if (OPTS[quote_style[i]]) {
|
|
||||||
optTemp = optTemp | OPTS[quote_style[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
quote_style = optTemp;
|
|
||||||
}
|
|
||||||
if (quote_style & OPTS.ENT_HTML_QUOTE_SINGLE) {
|
|
||||||
string = string.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
if (!noquotes) {
|
|
||||||
string = string.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return string;
|
|
||||||
}
|
|
4
onionshare_gui/static/jquery-1.11.1.min.js
vendored
4
onionshare_gui/static/jquery-1.11.1.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,128 +0,0 @@
|
||||||
$(function(){
|
|
||||||
var onionshare = {}
|
|
||||||
|
|
||||||
function update($msg) {
|
|
||||||
var $line = $('<li>').append($msg);
|
|
||||||
$('#log').prepend($line);
|
|
||||||
}
|
|
||||||
|
|
||||||
function copy_to_clipboard() {
|
|
||||||
$.ajax({
|
|
||||||
url: '/copy_url',
|
|
||||||
success: function(data, textStatus, jqXHR){
|
|
||||||
update(onionshare.strings['copied_url']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
$('#copy-button').click(copy_to_clipboard);
|
|
||||||
|
|
||||||
var REQUEST_LOAD = 0;
|
|
||||||
var REQUEST_DOWNLOAD = 1;
|
|
||||||
var REQUEST_PROGRESS = 2;
|
|
||||||
var REQUEST_OTHER = 3;
|
|
||||||
function check_for_requests() {
|
|
||||||
$.ajax({
|
|
||||||
url: '/heartbeat',
|
|
||||||
success: function(data, textStatus, jqXHR){
|
|
||||||
if(data != '') {
|
|
||||||
var events = JSON.parse(data);
|
|
||||||
for(var i=0; i<events.length; i++) {
|
|
||||||
var r = events[i];
|
|
||||||
|
|
||||||
if(r.type == REQUEST_LOAD) {
|
|
||||||
update($('<span>').addClass('weblog').html(onionshare.strings['download_page_loaded']));
|
|
||||||
} else if(r.type == REQUEST_DOWNLOAD) {
|
|
||||||
var $download = $('<span>')
|
|
||||||
.attr('id', 'download-'+r.data.id)
|
|
||||||
.addClass('weblog').html(onionshare.strings['download_started'])
|
|
||||||
.append($('<span>').addClass('progress'));
|
|
||||||
update($download);
|
|
||||||
} else if(r.type == REQUEST_PROGRESS) {
|
|
||||||
// is the download complete?
|
|
||||||
if(r.data.bytes == onionshare.filesize) {
|
|
||||||
$('#download-'+r.data.id).html(onionshare.strings['download_finished']);
|
|
||||||
|
|
||||||
// close on finish?
|
|
||||||
if($('#close-on-finish').is(':checked')) {
|
|
||||||
function close_countdown(i) {
|
|
||||||
$('#close-countdown').html(onionshare.strings['close_countdown'].replace('{0}', i));
|
|
||||||
if(i == 0) {
|
|
||||||
// close program
|
|
||||||
$.ajax({ url: '/close' });
|
|
||||||
} else {
|
|
||||||
// continue countdown
|
|
||||||
setTimeout(function(){ close_countdown(i-1) }, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update($('<span>').attr('id', 'close-countdown'));
|
|
||||||
close_countdown(3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// still in progress
|
|
||||||
else {
|
|
||||||
var percent = Math.floor((r.data.bytes / onionshare.filesize) * 100);
|
|
||||||
$('#download-'+r.data.id+' .progress').html(' '+human_readable_filesize(r.data.bytes)+', '+percent+'%');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(r.path != '/favicon.ico')
|
|
||||||
update($('<span>').addClass('weblog-error').html(onionshare.strings['other_page_loaded']+': '+htmlspecialchars(r.path)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(check_for_requests, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#close-on-finish').change(function(){
|
|
||||||
if($('#close-on-finish').is(':checked')) {
|
|
||||||
$.ajax({ url: '/stay_open_false' });
|
|
||||||
} else {
|
|
||||||
$.ajax({ url: '/stay_open_true' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// initialize
|
|
||||||
$.ajax({
|
|
||||||
url: '/init_info',
|
|
||||||
success: function(data, textStatus, jqXHR){
|
|
||||||
onionshare = JSON.parse(data);
|
|
||||||
|
|
||||||
$('#basename').html(onionshare.basename);
|
|
||||||
$('#filesize .label').html(onionshare.strings['filesize']+':');
|
|
||||||
$('#filehash .label').html(onionshare.strings['sha1_checksum']+':');
|
|
||||||
$('#close-on-finish-wrapper label').html(onionshare.strings['close_on_finish']);
|
|
||||||
$('#loading .calculating').html(onionshare.strings['calculating_sha1']);
|
|
||||||
$('#copy-button').html(onionshare.strings['copy_url']);
|
|
||||||
|
|
||||||
if(onionshare.stay_open) {
|
|
||||||
$('#close-on-finish').removeAttr('checked');
|
|
||||||
}
|
|
||||||
|
|
||||||
// after getting the initial info, start the onionshare server
|
|
||||||
$.ajax({
|
|
||||||
url: '/start_onionshare',
|
|
||||||
success: function(data, textStatus, jqXHR){
|
|
||||||
var data_obj = JSON.parse(data);
|
|
||||||
onionshare.filehash = data_obj.filehash;
|
|
||||||
onionshare.filesize = data_obj.filesize;
|
|
||||||
onionshare.url = data_obj.url;
|
|
||||||
|
|
||||||
$('#loading').remove();
|
|
||||||
|
|
||||||
$('#filesize .value').html(human_readable_filesize(onionshare.filesize));
|
|
||||||
$('#filehash .value').html(onionshare.filehash);
|
|
||||||
$('#filesize').show(500);
|
|
||||||
$('#filehash').show(500);
|
|
||||||
|
|
||||||
update('<span>'+onionshare.strings['give_this_url']+'</span><br/><strong>'+onionshare.url+'</strong>');
|
|
||||||
copy_to_clipboard();
|
|
||||||
$('#copy-button').show();
|
|
||||||
|
|
||||||
setTimeout(check_for_requests, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,173 +0,0 @@
|
||||||
body {
|
|
||||||
color: #000000;
|
|
||||||
font-family: arial;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
min-width: 550px;
|
|
||||||
min-height: 300px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wrapper {
|
|
||||||
}
|
|
||||||
|
|
||||||
#header {
|
|
||||||
height: 80px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
#header #logo {
|
|
||||||
float: left;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
#header #header-content {
|
|
||||||
float: left;
|
|
||||||
width: 440px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
#header #header-content #basename {
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #000000;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
#header #header-content #filesize,
|
|
||||||
#header #header-content #filehash {
|
|
||||||
margin-top: 3px;
|
|
||||||
word-break: break-all;
|
|
||||||
text-align: left;
|
|
||||||
color: #666666;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
#header #header-content .label {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
#header #header-content .value {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#log {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
height: 258px;
|
|
||||||
overflow: auto;
|
|
||||||
word-wrap: break-word;
|
|
||||||
font-family: arial;
|
|
||||||
font-size: 14px;
|
|
||||||
border-top: 1px solid #000000;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
ul#log li {
|
|
||||||
margin: 0 10px;
|
|
||||||
padding: 10px;
|
|
||||||
border-bottom: 1px solid #cccccc;
|
|
||||||
}
|
|
||||||
ul#log .weblog {
|
|
||||||
color: #009900;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
ul#log .weblog .progress {
|
|
||||||
color: #0000cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#log .weblog-error {
|
|
||||||
color: #ff0000;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul#log li#loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 60px 30px;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
ul#log li#loading .calculating {
|
|
||||||
color: #666666;
|
|
||||||
font-style: italic;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
ul#log #close-countdown {
|
|
||||||
font-weight: bold;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
height: 20px;
|
|
||||||
background-color: #ffffff;
|
|
||||||
padding: 10px 0;
|
|
||||||
width: 100%;
|
|
||||||
border-top: 1px solid #000000;
|
|
||||||
}
|
|
||||||
#footer #close-on-finish-wrapper {
|
|
||||||
float: left;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
#footer #close-on-finish-wrapper input {
|
|
||||||
cursor: pointer;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
#footer #close-on-finish-wrapper label {
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer #copy-button-wrapper {
|
|
||||||
float: right;
|
|
||||||
margin-right: 5px;
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
#footer #copy-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
-moz-box-shadow:inset 0px 1px 0px 0px #d197fe;
|
|
||||||
-webkit-box-shadow:inset 0px 1px 0px 0px #d197fe;
|
|
||||||
box-shadow:inset 0px 1px 0px 0px #d197fe;
|
|
||||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #a53df6), color-stop(1, #7c16cb) );
|
|
||||||
background:-moz-linear-gradient( center top, #a53df6 5%, #7c16cb 100% );
|
|
||||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#a53df6', endColorstr='#7c16cb');
|
|
||||||
background-color:#a53df6;
|
|
||||||
-webkit-border-top-left-radius:0px;
|
|
||||||
-moz-border-radius-topleft:0px;
|
|
||||||
border-top-left-radius:0px;
|
|
||||||
-webkit-border-top-right-radius:0px;
|
|
||||||
-moz-border-radius-topright:0px;
|
|
||||||
border-top-right-radius:0px;
|
|
||||||
-webkit-border-bottom-right-radius:0px;
|
|
||||||
-moz-border-radius-bottomright:0px;
|
|
||||||
border-bottom-right-radius:0px;
|
|
||||||
-webkit-border-bottom-left-radius:0px;
|
|
||||||
-moz-border-radius-bottomleft:0px;
|
|
||||||
border-bottom-left-radius:0px;
|
|
||||||
text-indent:0;
|
|
||||||
border:1px solid #9c33ed;
|
|
||||||
display:inline-block;
|
|
||||||
color:#ffffff;
|
|
||||||
font-family:Arial;
|
|
||||||
font-size:13px;
|
|
||||||
font-weight:bold;
|
|
||||||
font-style:normal;
|
|
||||||
text-decoration:none;
|
|
||||||
text-align:center;
|
|
||||||
text-shadow:1px 1px 0px #7d15cd;
|
|
||||||
}
|
|
||||||
.button:hover {
|
|
||||||
background:-webkit-gradient( linear, left top, left bottom, color-stop(0.05, #7c16cb), color-stop(1, #a53df6) );
|
|
||||||
background:-moz-linear-gradient( center top, #7c16cb 5%, #a53df6 100% );
|
|
||||||
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7c16cb', endColorstr='#a53df6');
|
|
||||||
background-color:#7c16cb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" type="text/css" media="all" href="static/style.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="wrapper">
|
|
||||||
<div id="header">
|
|
||||||
<img id="logo" src="static/logo.png" />
|
|
||||||
<div id="header-content">
|
|
||||||
<div id="basename"></div>
|
|
||||||
<div id="filehash">
|
|
||||||
<span class="label"></span>
|
|
||||||
<span class="value"></span>
|
|
||||||
</div>
|
|
||||||
<div id="filesize">
|
|
||||||
<span class="label"></span>
|
|
||||||
<span class="value"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul id="log">
|
|
||||||
<li id="loading">
|
|
||||||
<img src="static/loader.gif" />
|
|
||||||
<p class="calculating"></p>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div id="footer">
|
|
||||||
<div id="close-on-finish-wrapper">
|
|
||||||
<input type="checkbox" id="close-on-finish" name="close-on-finish" checked />
|
|
||||||
<label for="close-on-finish"></span>
|
|
||||||
</div>
|
|
||||||
<div id="copy-button-wrapper">
|
|
||||||
<button class="button" id="copy-button">Copy URL</button>
|
|
||||||
</div>
|
|
||||||
<div class="clear"></div>
|
|
||||||
</div>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<script src="static/jquery-1.11.1.min.js"></script>
|
|
||||||
<script src="static/helpers.js"></script>
|
|
||||||
<script src="static/onionshare.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,147 +0,0 @@
|
||||||
from flask import Flask, render_template, make_response
|
|
||||||
from functools import wraps
|
|
||||||
import threading, json, os, time, platform, sys
|
|
||||||
|
|
||||||
onionshare = None
|
|
||||||
onionshare_port = None
|
|
||||||
filename = None
|
|
||||||
onion_host = None
|
|
||||||
qtapp = None
|
|
||||||
clipboard = None
|
|
||||||
stay_open = None
|
|
||||||
|
|
||||||
url = None
|
|
||||||
|
|
||||||
app = Flask(__name__, template_folder='./templates')
|
|
||||||
|
|
||||||
def debug_mode():
|
|
||||||
import logging
|
|
||||||
global app
|
|
||||||
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
temp_dir = os.environ['Temp'].replace('\\', '/')
|
|
||||||
else:
|
|
||||||
temp_dir = '/tmp/'
|
|
||||||
|
|
||||||
log_handler = logging.FileHandler('{0}/onionshare_gui.log'.format(temp_dir))
|
|
||||||
log_handler.setLevel(logging.WARNING)
|
|
||||||
app.logger.addHandler(log_handler)
|
|
||||||
|
|
||||||
def add_response_headers(headers={}):
|
|
||||||
def decorator(f):
|
|
||||||
@wraps(f)
|
|
||||||
def decorated_function(*args, **kwargs):
|
|
||||||
resp = make_response(f(*args, **kwargs))
|
|
||||||
h = resp.headers
|
|
||||||
for header, value in headers.items():
|
|
||||||
h[header] = value
|
|
||||||
return resp
|
|
||||||
return decorated_function
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def csp(f):
|
|
||||||
@wraps(f)
|
|
||||||
# disable inline js, external js
|
|
||||||
@add_response_headers({'Content-Security-Policy': "default-src 'self'; connect-src 'self'"})
|
|
||||||
# ugh, webkit embedded in Qt4 is stupid old
|
|
||||||
# TODO: remove webkit, build GUI with normal Qt widgets
|
|
||||||
@add_response_headers({'X-WebKit-CSP': "default-src 'self'; connect-src 'self'"})
|
|
||||||
def decorated_function(*args, **kwargs):
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
return decorated_function
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
@csp
|
|
||||||
def index():
|
|
||||||
return render_template('index.html')
|
|
||||||
|
|
||||||
@app.route("/init_info")
|
|
||||||
@csp
|
|
||||||
def init_info():
|
|
||||||
global onionshare, filename, stay_open
|
|
||||||
basename = os.path.basename(filename)
|
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
'strings': onionshare.strings,
|
|
||||||
'basename': basename,
|
|
||||||
'stay_open': stay_open
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route("/start_onionshare")
|
|
||||||
@csp
|
|
||||||
def start_onionshare():
|
|
||||||
global onionshare, onionshare_port, filename, onion_host, url
|
|
||||||
|
|
||||||
url = 'http://{0}/{1}'.format(onion_host, onionshare.slug)
|
|
||||||
|
|
||||||
filehash, filesize = onionshare.file_crunching(filename)
|
|
||||||
onionshare.set_file_info(filename, filehash, filesize)
|
|
||||||
|
|
||||||
# start onionshare service in new thread
|
|
||||||
t = threading.Thread(target=onionshare.app.run, kwargs={'port': onionshare_port})
|
|
||||||
t.daemon = True
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
return json.dumps({
|
|
||||||
'filehash': filehash,
|
|
||||||
'filesize': filesize,
|
|
||||||
'url': url
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.route("/copy_url")
|
|
||||||
@csp
|
|
||||||
def copy_url():
|
|
||||||
if platform.system() == 'Windows':
|
|
||||||
# Qt's QClipboard isn't working in Windows
|
|
||||||
# https://github.com/micahflee/onionshare/issues/46
|
|
||||||
import ctypes
|
|
||||||
GMEM_DDESHARE = 0x2000
|
|
||||||
ctypes.windll.user32.OpenClipboard(None)
|
|
||||||
ctypes.windll.user32.EmptyClipboard()
|
|
||||||
hcd = ctypes.windll.kernel32.GlobalAlloc(GMEM_DDESHARE, len(bytes(url))+1)
|
|
||||||
pch_data = ctypes.windll.kernel32.GlobalLock(hcd)
|
|
||||||
ctypes.cdll.msvcrt.strcpy(ctypes.c_char_p(pch_data), bytes(url))
|
|
||||||
ctypes.windll.kernel32.GlobalUnlock(hcd)
|
|
||||||
ctypes.windll.user32.SetClipboardData(1, hcd)
|
|
||||||
ctypes.windll.user32.CloseClipboard()
|
|
||||||
else:
|
|
||||||
global clipboard
|
|
||||||
clipboard.setText(url)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
@app.route("/stay_open_true")
|
|
||||||
@csp
|
|
||||||
def stay_open_true():
|
|
||||||
global onionshare
|
|
||||||
onionshare.set_stay_open(True)
|
|
||||||
|
|
||||||
@app.route("/stay_open_false")
|
|
||||||
@csp
|
|
||||||
def stay_open_false():
|
|
||||||
global onionshare
|
|
||||||
onionshare.set_stay_open(False)
|
|
||||||
|
|
||||||
@app.route("/heartbeat")
|
|
||||||
@csp
|
|
||||||
def check_for_requests():
|
|
||||||
global onionshare
|
|
||||||
events = []
|
|
||||||
|
|
||||||
done = False
|
|
||||||
while not done:
|
|
||||||
try:
|
|
||||||
r = onionshare.q.get(False)
|
|
||||||
events.append(r)
|
|
||||||
except onionshare.Queue.Empty:
|
|
||||||
done = True
|
|
||||||
|
|
||||||
return json.dumps(events)
|
|
||||||
|
|
||||||
@app.route("/close")
|
|
||||||
@csp
|
|
||||||
def close():
|
|
||||||
global qtapp
|
|
||||||
time.sleep(1)
|
|
||||||
qtapp.closeAllWindows()
|
|
||||||
return ''
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue