From 8b3313fa3b36ea31bc2f486b3fa3d98013ccc9ad Mon Sep 17 00:00:00 2001 From: Andrew Lorimer Date: Fri, 7 May 2021 23:26:00 +1000 Subject: [PATCH] remove lint --- black.jpg | Bin 12015 -> 0 bytes http_server_39.py | 1294 -------------------------------------------- icons/first.svg | 1 - icons/last.svg | 1 - icons/left.svg | 1 - icons/ppt.ico | Bin 170879 -> 0 bytes icons/ppt.png | Bin 12417 -> 0 bytes icons/right.svg | 1 - index.html | 49 -- ppt-control.js | 240 -------- ppt_control.py | 431 --------------- ppt_control_obs.py | 175 ------ settings.js | 50 -- style.css | 77 --- white.jpg | Bin 11325 -> 0 bytes 15 files changed, 2320 deletions(-) delete mode 100755 black.jpg delete mode 100755 http_server_39.py delete mode 100644 icons/first.svg delete mode 100644 icons/last.svg delete mode 100644 icons/left.svg delete mode 100755 icons/ppt.ico delete mode 100755 icons/ppt.png delete mode 100644 icons/right.svg delete mode 100755 index.html delete mode 100644 ppt-control.js delete mode 100755 ppt_control.py delete mode 100755 ppt_control_obs.py delete mode 100644 settings.js delete mode 100644 style.css delete mode 100644 white.jpg diff --git a/black.jpg b/black.jpg deleted file mode 100755 index 7b0593596f60936ee4f72e03ba70cecfb56a40c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12015 zcmeI0PiPZS5XQgvwy{xCOfZ6q;0m#)6huY5MH5YlL=3S)tAcJeOE#N5N~r zgMThoJ$UipNv|Tk3IRb|6%oOkCq*w(p7S=L+e$Y*^rkbA?3bBu-rM)v>}AxF`i%J5 zd?61(krrt{eUZ-!x!l!K`Fy@mJVOOQE;MY%8#)X)&9++}&zh4{m(1a>7{Xp`hk;Sc z_FAV(r6NM(*IUp^z?(6>R;X>`!bo-6vjJgJ`J~-)+oZ3NK7PI3()upxq&;I*Nk1i> zcIgI5FX?=xr@v`k_q8O6q&rd0(b>e-O~eSUw5x+>H)3!6Gl zV-5~1zaRP;kcT2*>(!1P@lzs}1tOo6`f&&tz74E&l=}Tpsg*~7@ez304EpC5fD=C{ z-wAT52f)so!1Ki*SGfbcxCQK4ZdtD7?_HN+NfC(@GCcp%k025#WOn?b6f&YuZCezyZrqkNKW56s{ HJC^?d_~1Cr diff --git a/http_server_39.py b/http_server_39.py deleted file mode 100755 index def05f4..0000000 --- a/http_server_39.py +++ /dev/null @@ -1,1294 +0,0 @@ -"""HTTP server classes. - -Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see -SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, -and CGIHTTPRequestHandler for CGI scripts. - -It does, however, optionally implement HTTP/1.1 persistent connections, -as of version 0.3. - -Notes on CGIHTTPRequestHandler ------------------------------- - -This class implements GET and POST requests to cgi-bin scripts. - -If the os.fork() function is not present (e.g. on Windows), -subprocess.Popen() is used as a fallback, with slightly altered semantics. - -In all cases, the implementation is intentionally naive -- all -requests are executed synchronously. - -SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL --- it may execute arbitrary Python code or external programs. - -Note that status code 200 is sent prior to execution of a CGI script, so -scripts cannot send other status codes such as 302 (redirect). - -XXX To do: - -- log requests even later (to capture byte count) -- log user-agent header and other interesting goodies -- send error log to separate file -""" - - -# See also: -# -# HTTP Working Group T. Berners-Lee -# INTERNET-DRAFT R. T. Fielding -# H. Frystyk Nielsen -# Expires September 8, 1995 March 8, 1995 -# -# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt -# -# and -# -# Network Working Group R. Fielding -# Request for Comments: 2616 et al -# Obsoletes: 2068 June 1999 -# Category: Standards Track -# -# URL: http://www.faqs.org/rfcs/rfc2616.html - -# Log files -# --------- -# -# Here's a quote from the NCSA httpd docs about log file format. -# -# | The logfile format is as follows. Each line consists of: -# | -# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb -# | -# | host: Either the DNS name or the IP number of the remote client -# | rfc931: Any information returned by identd for this person, -# | - otherwise. -# | authuser: If user sent a userid for authentication, the user name, -# | - otherwise. -# | DD: Day -# | Mon: Month (calendar name) -# | YYYY: Year -# | hh: hour (24-hour format, the machine's timezone) -# | mm: minutes -# | ss: seconds -# | request: The first line of the HTTP request as sent by the client. -# | ddd: the status code returned by the server, - if not available. -# | bbbb: the total number of bytes sent, -# | *not including the HTTP/1.0 header*, - if not available -# | -# | You can determine the name of the file accessed through request. -# -# (Actually, the latter is only true if you know the server configuration -# at the time the request was made!) - -__version__ = "0.6" - -__all__ = [ - "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", - "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler", -] - -import copy -import datetime -import email.utils -import html -import http.client -import io -import mimetypes -import os -import posixpath -import select -import shutil -import socket # For gethostbyaddr() -import socketserver -import sys -import time -import urllib.parse -import contextlib -from functools import partial - -from http import HTTPStatus - - -# Default error message template -DEFAULT_ERROR_MESSAGE = """\ - - - - - Error response - - -

Error response

-

Error code: %(code)d

-

Message: %(message)s.

-

Error code explanation: %(code)s - %(explain)s.

- - -""" - -DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" - -class HTTPServer(socketserver.TCPServer): - - allow_reuse_address = 1 # Seems to make sense in testing environment - - def server_bind(self): - """Override server_bind to store the server name.""" - socketserver.TCPServer.server_bind(self) - host, port = self.server_address[:2] - self.server_name = socket.getfqdn(host) - self.server_port = port - - -class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): - daemon_threads = True - - -class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): - - """HTTP request handler base class. - - The following explanation of HTTP serves to guide you through the - code as well as to expose any misunderstandings I may have about - HTTP (so you don't need to read the code to figure out I'm wrong - :-). - - HTTP (HyperText Transfer Protocol) is an extensible protocol on - top of a reliable stream transport (e.g. TCP/IP). The protocol - recognizes three parts to a request: - - 1. One line identifying the request type and path - 2. An optional set of RFC-822-style headers - 3. An optional data part - - The headers and data are separated by a blank line. - - The first line of the request has the form - - - - where is a (case-sensitive) keyword such as GET or POST, - is a string containing path information for the request, - and should be the string "HTTP/1.0" or "HTTP/1.1". - is encoded using the URL encoding scheme (using %xx to signify - the ASCII character with hex code xx). - - The specification specifies that lines are separated by CRLF but - for compatibility with the widest range of clients recommends - servers also handle LF. Similarly, whitespace in the request line - is treated sensibly (allowing multiple spaces between components - and allowing trailing whitespace). - - Similarly, for output, lines ought to be separated by CRLF pairs - but most clients grok LF characters just fine. - - If the first line of the request has the form - - - - (i.e. is left out) then this is assumed to be an HTTP - 0.9 request; this form has no optional headers and data part and - the reply consists of just the data. - - The reply form of the HTTP 1.x protocol again has three parts: - - 1. One line giving the response code - 2. An optional set of RFC-822-style headers - 3. The data - - Again, the headers and data are separated by a blank line. - - The response code line has the form - - - - where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), - is a 3-digit response code indicating success or - failure of the request, and is an optional - human-readable string explaining what the response code means. - - This server parses the request and the headers, and then calls a - function specific to the request type (). Specifically, - a request SPAM will be handled by a method do_SPAM(). If no - such method exists the server sends an error response to the - client. If it exists, it is called with no arguments: - - do_SPAM() - - Note that the request name is case sensitive (i.e. SPAM and spam - are different requests). - - The various request details are stored in instance variables: - - - client_address is the client IP address in the form (host, - port); - - - command, path and version are the broken-down request line; - - - headers is an instance of email.message.Message (or a derived - class) containing the header information; - - - rfile is a file object open for reading positioned at the - start of the optional input data part; - - - wfile is a file object open for writing. - - IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! - - The first thing to be written must be the response line. Then - follow 0 or more header lines, then a blank line, and then the - actual data (if any). The meaning of the header lines depends on - the command executed by the server; in most cases, when data is - returned, there should be at least one header line of the form - - Content-type: / - - where and should be registered MIME types, - e.g. "text/html" or "text/plain". - - """ - - # The Python system version, truncated to its first component. - sys_version = "Python/" + sys.version.split()[0] - - # The server software version. You may want to override this. - # The format is multiple whitespace-separated strings, - # where each string is of the form name[/version]. - server_version = "BaseHTTP/" + __version__ - - error_message_format = DEFAULT_ERROR_MESSAGE - error_content_type = DEFAULT_ERROR_CONTENT_TYPE - - # The default request version. This only affects responses up until - # the point where the request line is parsed, so it mainly decides what - # the client gets back when sending a malformed request line. - # Most web servers default to HTTP 0.9, i.e. don't send a status line. - default_request_version = "HTTP/0.9" - - def parse_request(self): - """Parse a request (internal). - - The request should be stored in self.raw_requestline; the results - are in self.command, self.path, self.request_version and - self.headers. - - Return True for success, False for failure; on failure, any relevant - error response has already been sent back. - - """ - self.command = None # set in case of error on the first line - self.request_version = version = self.default_request_version - self.close_connection = True - requestline = str(self.raw_requestline, 'iso-8859-1') - requestline = requestline.rstrip('\r\n') - self.requestline = requestline - words = requestline.split() - if len(words) == 0: - return False - - if len(words) >= 3: # Enough to determine protocol version - version = words[-1] - try: - if not version.startswith('HTTP/'): - raise ValueError - base_version_number = version.split('/', 1)[1] - version_number = base_version_number.split(".") - # RFC 2145 section 3.1 says there can be only one "." and - # - major and minor numbers MUST be treated as - # separate integers; - # - HTTP/2.4 is a lower version than HTTP/2.13, which in - # turn is lower than HTTP/12.3; - # - Leading zeros MUST be ignored by recipients. - if len(version_number) != 2: - raise ValueError - version_number = int(version_number[0]), int(version_number[1]) - except (ValueError, IndexError): - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad request version (%r)" % version) - return False - if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": - self.close_connection = False - if version_number >= (2, 0): - self.send_error( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - "Invalid HTTP version (%s)" % base_version_number) - return False - self.request_version = version - - if not 2 <= len(words) <= 3: - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad request syntax (%r)" % requestline) - return False - command, path = words[:2] - if len(words) == 2: - self.close_connection = True - if command != 'GET': - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad HTTP/0.9 request type (%r)" % command) - return False - self.command, self.path = command, path - - # Examine the headers and look for a Connection directive. - try: - self.headers = http.client.parse_headers(self.rfile, - _class=self.MessageClass) - except http.client.LineTooLong as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Line too long", - str(err)) - return False - except http.client.HTTPException as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Too many headers", - str(err) - ) - return False - - conntype = self.headers.get('Connection', "") - if conntype.lower() == 'close': - self.close_connection = True - elif (conntype.lower() == 'keep-alive' and - self.protocol_version >= "HTTP/1.1"): - self.close_connection = False - # Examine the headers and look for an Expect directive - expect = self.headers.get('Expect', "") - if (expect.lower() == "100-continue" and - self.protocol_version >= "HTTP/1.1" and - self.request_version >= "HTTP/1.1"): - if not self.handle_expect_100(): - return False - return True - - def handle_expect_100(self): - """Decide what to do with an "Expect: 100-continue" header. - - If the client is expecting a 100 Continue response, we must - respond with either a 100 Continue or a final response before - waiting for the request body. The default is to always respond - with a 100 Continue. You can behave differently (for example, - reject unauthorized requests) by overriding this method. - - This method should either return True (possibly after sending - a 100 Continue response) or send an error response and return - False. - - """ - self.send_response_only(HTTPStatus.CONTINUE) - self.end_headers() - return True - - def handle_one_request(self): - """Handle a single HTTP request. - - You normally don't need to override this method; see the class - __doc__ string for information on how to handle specific HTTP - commands such as GET and POST. - - """ - try: - self.raw_requestline = self.rfile.readline(65537) - if len(self.raw_requestline) > 65536: - self.requestline = '' - self.request_version = '' - self.command = '' - self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) - return - if not self.raw_requestline: - self.close_connection = True - return - if not self.parse_request(): - # An error code has been sent, just exit - return - mname = 'do_' + self.command - if not hasattr(self, mname): - self.send_error( - HTTPStatus.NOT_IMPLEMENTED, - "Unsupported method (%r)" % self.command) - return - method = getattr(self, mname) - method() - self.wfile.flush() #actually send the response if not already done. - except socket.timeout as e: - #a read or a write timed out. Discard this connection - self.log_error("Request timed out: %r", e) - self.close_connection = True - return - - def handle(self): - """Handle multiple requests if necessary.""" - self.close_connection = True - - self.handle_one_request() - while not self.close_connection: - self.handle_one_request() - - def send_error(self, code, message=None, explain=None): - """Send and log an error reply. - - Arguments are - * code: an HTTP error code - 3 digits - * message: a simple optional 1 line reason phrase. - *( HTAB / SP / VCHAR / %x80-FF ) - defaults to short entry matching the response code - * explain: a detailed message defaults to the long entry - matching the response code. - - This sends an error response (so it must be called before any - output has been generated), logs the error, and finally sends - a piece of HTML explaining the error to the user. - - """ - - try: - shortmsg, longmsg = self.responses[code] - except KeyError: - shortmsg, longmsg = '???', '???' - if message is None: - message = shortmsg - if explain is None: - explain = longmsg - self.log_error("code %d, message %s", code, message) - self.send_response(code, message) - self.send_header('Connection', 'close') - - # Message body is omitted for cases described in: - # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) - # - RFC7231: 6.3.6. 205(Reset Content) - body = None - if (code >= 200 and - code not in (HTTPStatus.NO_CONTENT, - HTTPStatus.RESET_CONTENT, - HTTPStatus.NOT_MODIFIED)): - # HTML encode to prevent Cross Site Scripting attacks - # (see bug #1100201) - content = (self.error_message_format % { - 'code': code, - 'message': html.escape(message, quote=False), - 'explain': html.escape(explain, quote=False) - }) - body = content.encode('UTF-8', 'replace') - self.send_header("Content-Type", self.error_content_type) - self.send_header('Content-Length', str(len(body))) - self.end_headers() - - if self.command != 'HEAD' and body: - self.wfile.write(body) - - def send_response(self, code, message=None): - """Add the response header to the headers buffer and log the - response code. - - Also send two standard headers with the server software - version and the current date. - - """ - self.log_request(code) - self.send_response_only(code, message) - self.send_header('Server', self.version_string()) - self.send_header('Date', self.date_time_string()) - - def send_response_only(self, code, message=None): - """Send the response header only.""" - if self.request_version != 'HTTP/0.9': - if message is None: - if code in self.responses: - message = self.responses[code][0] - else: - message = '' - if not hasattr(self, '_headers_buffer'): - self._headers_buffer = [] - self._headers_buffer.append(("%s %d %s\r\n" % - (self.protocol_version, code, message)).encode( - 'latin-1', 'strict')) - - def send_header(self, keyword, value): - """Send a MIME header to the headers buffer.""" - if self.request_version != 'HTTP/0.9': - if not hasattr(self, '_headers_buffer'): - self._headers_buffer = [] - self._headers_buffer.append( - ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) - - if keyword.lower() == 'connection': - if value.lower() == 'close': - self.close_connection = True - elif value.lower() == 'keep-alive': - self.close_connection = False - - def end_headers(self): - """Send the blank line ending the MIME headers.""" - if self.request_version != 'HTTP/0.9': - self._headers_buffer.append(b"\r\n") - self.flush_headers() - - def flush_headers(self): - if hasattr(self, '_headers_buffer'): - self.wfile.write(b"".join(self._headers_buffer)) - self._headers_buffer = [] - - def log_request(self, code='-', size='-'): - """Log an accepted request. - - This is called by send_response(). - - """ - if isinstance(code, HTTPStatus): - code = code.value - self.log_message('"%s" %s %s', - self.requestline, str(code), str(size)) - - def log_error(self, format, *args): - """Log an error. - - This is called when a request cannot be fulfilled. By - default it passes the message on to log_message(). - - Arguments are the same as for log_message(). - - XXX This should go to the separate error log. - - """ - - self.log_message(format, *args) - - def log_message(self, format, *args): - """Log an arbitrary message. - - This is used by all other logging functions. Override - it if you have specific logging wishes. - - The first argument, FORMAT, is a format string for the - message to be logged. If the format string contains - any % escapes requiring parameters, they should be - specified as subsequent arguments (it's just like - printf!). - - The client ip and current date/time are prefixed to - every message. - - """ - - sys.stderr.write("%s - - [%s] %s\n" % - (self.address_string(), - self.log_date_time_string(), - format%args)) - - def version_string(self): - """Return the server software version string.""" - return self.server_version + ' ' + self.sys_version - - def date_time_string(self, timestamp=None): - """Return the current date and time formatted for a message header.""" - if timestamp is None: - timestamp = time.time() - return email.utils.formatdate(timestamp, usegmt=True) - - def log_date_time_string(self): - """Return the current time formatted for logging.""" - now = time.time() - year, month, day, hh, mm, ss, x, y, z = time.localtime(now) - s = "%02d/%3s/%04d %02d:%02d:%02d" % ( - day, self.monthname[month], year, hh, mm, ss) - return s - - weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - - monthname = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - def address_string(self): - """Return the client address.""" - - return self.client_address[0] - - # Essentially static class variables - - # The version of the HTTP protocol we support. - # Set this to HTTP/1.1 to enable automatic keepalive - protocol_version = "HTTP/1.0" - - # MessageClass used to parse headers - MessageClass = http.client.HTTPMessage - - # hack to maintain backwards compatibility - responses = { - v: (v.phrase, v.description) - for v in HTTPStatus.__members__.values() - } - - -class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): - - """Simple HTTP request handler with GET and HEAD commands. - - This serves files from the current directory and any of its - subdirectories. The MIME type for files is determined by - calling the .guess_type() method. - - The GET and HEAD requests are identical except that the HEAD - request omits the actual contents of the file. - - """ - - server_version = "SimpleHTTP/" + __version__ - extensions_map = _encodings_map_default = { - '.gz': 'application/gzip', - '.Z': 'application/octet-stream', - '.bz2': 'application/x-bzip2', - '.xz': 'application/x-xz', - } - - def __init__(self, *args, directory=None, **kwargs): - if directory is None: - directory = os.getcwd() - self.directory = os.fspath(directory) - super().__init__(*args, **kwargs) - - def do_GET(self): - """Serve a GET request.""" - f = self.send_head() - if f: - try: - self.copyfile(f, self.wfile) - finally: - f.close() - - def do_HEAD(self): - """Serve a HEAD request.""" - f = self.send_head() - if f: - f.close() - - def send_head(self): - """Common code for GET and HEAD commands. - - This sends the response code and MIME headers. - - Return value is either a file object (which has to be copied - to the outputfile by the caller unless the command was HEAD, - and must be closed by the caller under all circumstances), or - None, in which case the caller has nothing further to do. - - """ - path = self.translate_path(self.path) - f = None - if os.path.isdir(path): - parts = urllib.parse.urlsplit(self.path) - if not parts.path.endswith('/'): - # redirect browser - doing basically what apache does - self.send_response(HTTPStatus.MOVED_PERMANENTLY) - new_parts = (parts[0], parts[1], parts[2] + '/', - parts[3], parts[4]) - new_url = urllib.parse.urlunsplit(new_parts) - self.send_header("Location", new_url) - self.end_headers() - return None - for index in "index.html", "index.htm": - index = os.path.join(path, index) - if os.path.exists(index): - path = index - break - else: - return self.list_directory(path) - ctype = self.guess_type(path) - # check for trailing "/" which should return 404. See Issue17324 - # The test for this was added in test_httpserver.py - # However, some OS platforms accept a trailingSlash as a filename - # See discussion on python-dev and Issue34711 regarding - # parseing and rejection of filenames with a trailing slash - if path.endswith("/"): - self.send_error(HTTPStatus.NOT_FOUND, "File not found") - return None - try: - f = open(path, 'rb') - except OSError: - self.send_error(HTTPStatus.NOT_FOUND, "File not found") - return None - - try: - fs = os.fstat(f.fileno()) - # Use browser cache if possible - if ("If-Modified-Since" in self.headers - and "If-None-Match" not in self.headers): - # compare If-Modified-Since and time of last file modification - try: - ims = email.utils.parsedate_to_datetime( - self.headers["If-Modified-Since"]) - except (TypeError, IndexError, OverflowError, ValueError): - # ignore ill-formed values - pass - else: - if ims.tzinfo is None: - # obsolete format with no timezone, cf. - # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 - ims = ims.replace(tzinfo=datetime.timezone.utc) - if ims.tzinfo is datetime.timezone.utc: - # compare to UTC datetime of last modification - last_modif = datetime.datetime.fromtimestamp( - fs.st_mtime, datetime.timezone.utc) - # remove microseconds, like in If-Modified-Since - last_modif = last_modif.replace(microsecond=0) - - if last_modif <= ims: - self.send_response(HTTPStatus.NOT_MODIFIED) - self.end_headers() - f.close() - return None - - self.send_response(HTTPStatus.OK) - self.send_header("Content-type", ctype) - self.send_header("Content-Length", str(fs[6])) - self.send_header("Last-Modified", - self.date_time_string(fs.st_mtime)) - self.end_headers() - return f - except: - f.close() - raise - - def list_directory(self, path): - """Helper to produce a directory listing (absent index.html). - - Return value is either a file object, or None (indicating an - error). In either case, the headers are sent, making the - interface the same as for send_head(). - - """ - try: - list = os.listdir(path) - except OSError: - self.send_error( - HTTPStatus.NOT_FOUND, - "No permission to list directory") - return None - list.sort(key=lambda a: a.lower()) - r = [] - try: - displaypath = urllib.parse.unquote(self.path, - errors='surrogatepass') - except UnicodeDecodeError: - displaypath = urllib.parse.unquote(path) - displaypath = html.escape(displaypath, quote=False) - enc = sys.getfilesystemencoding() - title = 'Directory listing for %s' % displaypath - r.append('') - r.append('\n') - r.append('' % enc) - r.append('%s\n' % title) - r.append('\n

%s

' % title) - r.append('
\n
    ') - for name in list: - fullname = os.path.join(path, name) - displayname = linkname = name - # Append / for directories or @ for symbolic links - if os.path.isdir(fullname): - displayname = name + "/" - linkname = name + "/" - if os.path.islink(fullname): - displayname = name + "@" - # Note: a link to a directory displays with @ and links with / - r.append('
  • %s
  • ' - % (urllib.parse.quote(linkname, - errors='surrogatepass'), - html.escape(displayname, quote=False))) - r.append('
\n
\n\n\n') - encoded = '\n'.join(r).encode(enc, 'surrogateescape') - f = io.BytesIO() - f.write(encoded) - f.seek(0) - self.send_response(HTTPStatus.OK) - self.send_header("Content-type", "text/html; charset=%s" % enc) - self.send_header("Content-Length", str(len(encoded))) - self.end_headers() - return f - - def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - - """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - # Don't forget explicit trailing slash when normalizing. Issue17324 - trailing_slash = path.rstrip().endswith('/') - try: - path = urllib.parse.unquote(path, errors='surrogatepass') - except UnicodeDecodeError: - path = urllib.parse.unquote(path) - path = posixpath.normpath(path) - words = path.split('/') - words = filter(None, words) - path = self.directory - for word in words: - if os.path.dirname(word) or word in (os.curdir, os.pardir): - # Ignore components that are not a simple file/directory name - continue - path = os.path.join(path, word) - if trailing_slash: - path += '/' - return path - - def copyfile(self, source, outputfile): - """Copy all data between two file objects. - - The SOURCE argument is a file object open for reading - (or anything with a read() method) and the DESTINATION - argument is a file object open for writing (or - anything with a write() method). - - The only reason for overriding this would be to change - the block size or perhaps to replace newlines by CRLF - -- note however that this the default server uses this - to copy binary data as well. - - """ - shutil.copyfileobj(source, outputfile) - - def guess_type(self, path): - """Guess the type of a file. - - Argument is a PATH (a filename). - - Return value is a string of the form type/subtype, - usable for a MIME Content-type header. - - The default implementation looks the file's extension - up in the table self.extensions_map, using application/octet-stream - as a default; however it would be permissible (if - slow) to look inside the data to make a better guess. - - """ - base, ext = posixpath.splitext(path) - if ext in self.extensions_map: - return self.extensions_map[ext] - ext = ext.lower() - if ext in self.extensions_map: - return self.extensions_map[ext] - guess, _ = mimetypes.guess_type(path) - if guess: - return guess - return 'application/octet-stream' - - -# Utilities for CGIHTTPRequestHandler - -def _url_collapse_path(path): - """ - Given a URL path, remove extra '/'s and '.' path elements and collapse - any '..' references and returns a collapsed path. - - Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. - The utility of this function is limited to is_cgi method and helps - preventing some security attacks. - - Returns: The reconstituted URL, which will always start with a '/'. - - Raises: IndexError if too many '..' occur within the path. - - """ - # Query component should not be involved. - path, _, query = path.partition('?') - path = urllib.parse.unquote(path) - - # Similar to os.path.split(os.path.normpath(path)) but specific to URL - # path semantics rather than local operating system semantics. - path_parts = path.split('/') - head_parts = [] - for part in path_parts[:-1]: - if part == '..': - head_parts.pop() # IndexError if more '..' than prior parts - elif part and part != '.': - head_parts.append( part ) - if path_parts: - tail_part = path_parts.pop() - if tail_part: - if tail_part == '..': - head_parts.pop() - tail_part = '' - elif tail_part == '.': - tail_part = '' - else: - tail_part = '' - - if query: - tail_part = '?'.join((tail_part, query)) - - splitpath = ('/' + '/'.join(head_parts), tail_part) - collapsed_path = "/".join(splitpath) - - return collapsed_path - - - -nobody = None - -def nobody_uid(): - """Internal routine to get nobody's uid""" - global nobody - if nobody: - return nobody - try: - import pwd - except ImportError: - return -1 - try: - nobody = pwd.getpwnam('nobody')[2] - except KeyError: - nobody = 1 + max(x[2] for x in pwd.getpwall()) - return nobody - - -def executable(path): - """Test for executable file.""" - return os.access(path, os.X_OK) - - -class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): - - """Complete HTTP server with GET, HEAD and POST commands. - - GET and HEAD also support running CGI scripts. - - The POST command is *only* implemented for CGI scripts. - - """ - - # Determine platform specifics - have_fork = hasattr(os, 'fork') - - # Make rfile unbuffered -- we need to read one line and then pass - # the rest to a subprocess, so we can't use buffered input. - rbufsize = 0 - - def do_POST(self): - """Serve a POST request. - - This is only implemented for CGI scripts. - - """ - - if self.is_cgi(): - self.run_cgi() - else: - self.send_error( - HTTPStatus.NOT_IMPLEMENTED, - "Can only POST to CGI scripts") - - def send_head(self): - """Version of send_head that support CGI scripts""" - if self.is_cgi(): - return self.run_cgi() - else: - return SimpleHTTPRequestHandler.send_head(self) - - def is_cgi(self): - """Test whether self.path corresponds to a CGI script. - - Returns True and updates the cgi_info attribute to the tuple - (dir, rest) if self.path requires running a CGI script. - Returns False otherwise. - - If any exception is raised, the caller should assume that - self.path was rejected as invalid and act accordingly. - - The default implementation tests whether the normalized url - path begins with one of the strings in self.cgi_directories - (and the next character is a '/' or the end of the string). - - """ - collapsed_path = _url_collapse_path(self.path) - dir_sep = collapsed_path.find('/', 1) - while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories: - dir_sep = collapsed_path.find('/', dir_sep+1) - if dir_sep > 0: - head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] - self.cgi_info = head, tail - return True - return False - - - cgi_directories = ['/cgi-bin', '/htbin'] - - def is_executable(self, path): - """Test whether argument path is an executable file.""" - return executable(path) - - def is_python(self, path): - """Test whether argument path is a Python script.""" - head, tail = os.path.splitext(path) - return tail.lower() in (".py", ".pyw") - - def run_cgi(self): - """Execute a CGI script.""" - dir, rest = self.cgi_info - path = dir + '/' + rest - i = path.find('/', len(dir)+1) - while i >= 0: - nextdir = path[:i] - nextrest = path[i+1:] - - scriptdir = self.translate_path(nextdir) - if os.path.isdir(scriptdir): - dir, rest = nextdir, nextrest - i = path.find('/', len(dir)+1) - else: - break - - # find an explicit query string, if present. - rest, _, query = rest.partition('?') - - # dissect the part after the directory name into a script name & - # a possible additional path, to be stored in PATH_INFO. - i = rest.find('/') - if i >= 0: - script, rest = rest[:i], rest[i:] - else: - script, rest = rest, '' - - scriptname = dir + '/' + script - scriptfile = self.translate_path(scriptname) - if not os.path.exists(scriptfile): - self.send_error( - HTTPStatus.NOT_FOUND, - "No such CGI script (%r)" % scriptname) - return - if not os.path.isfile(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not a plain file (%r)" % scriptname) - return - ispy = self.is_python(scriptname) - if self.have_fork or not ispy: - if not self.is_executable(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not executable (%r)" % scriptname) - return - - # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html - # XXX Much of the following could be prepared ahead of time! - env = copy.deepcopy(os.environ) - env['SERVER_SOFTWARE'] = self.version_string() - env['SERVER_NAME'] = self.server.server_name - env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['SERVER_PROTOCOL'] = self.protocol_version - env['SERVER_PORT'] = str(self.server.server_port) - env['REQUEST_METHOD'] = self.command - uqrest = urllib.parse.unquote(rest) - env['PATH_INFO'] = uqrest - env['PATH_TRANSLATED'] = self.translate_path(uqrest) - env['SCRIPT_NAME'] = scriptname - if query: - env['QUERY_STRING'] = query - env['REMOTE_ADDR'] = self.client_address[0] - authorization = self.headers.get("authorization") - if authorization: - authorization = authorization.split() - if len(authorization) == 2: - import base64, binascii - env['AUTH_TYPE'] = authorization[0] - if authorization[0].lower() == "basic": - try: - authorization = authorization[1].encode('ascii') - authorization = base64.decodebytes(authorization).\ - decode('ascii') - except (binascii.Error, UnicodeError): - pass - else: - authorization = authorization.split(':') - if len(authorization) == 2: - env['REMOTE_USER'] = authorization[0] - # XXX REMOTE_IDENT - if self.headers.get('content-type') is None: - env['CONTENT_TYPE'] = self.headers.get_content_type() - else: - env['CONTENT_TYPE'] = self.headers['content-type'] - length = self.headers.get('content-length') - if length: - env['CONTENT_LENGTH'] = length - referer = self.headers.get('referer') - if referer: - env['HTTP_REFERER'] = referer - accept = self.headers.get_all('accept', ()) - env['HTTP_ACCEPT'] = ','.join(accept) - ua = self.headers.get('user-agent') - if ua: - env['HTTP_USER_AGENT'] = ua - co = filter(None, self.headers.get_all('cookie', [])) - cookie_str = ', '.join(co) - if cookie_str: - env['HTTP_COOKIE'] = cookie_str - # XXX Other HTTP_* headers - # Since we're setting the env in the parent, provide empty - # values to override previously set values - for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', - 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): - env.setdefault(k, "") - - self.send_response(HTTPStatus.OK, "Script output follows") - self.flush_headers() - - decoded_query = query.replace('+', ' ') - - if self.have_fork: - # Unix -- fork as we should - args = [script] - if '=' not in decoded_query: - args.append(decoded_query) - nobody = nobody_uid() - self.wfile.flush() # Always flush before forking - pid = os.fork() - if pid != 0: - # Parent - pid, sts = os.waitpid(pid, 0) - # throw away additional data [see bug #427345] - while select.select([self.rfile], [], [], 0)[0]: - if not self.rfile.read(1): - break - exitcode = os.waitstatus_to_exitcode(sts) - if exitcode: - self.log_error(f"CGI script exit code {exitcode}") - return - # Child - try: - try: - os.setuid(nobody) - except OSError: - pass - os.dup2(self.rfile.fileno(), 0) - os.dup2(self.wfile.fileno(), 1) - os.execve(scriptfile, args, env) - except: - self.server.handle_error(self.request, self.client_address) - os._exit(127) - - else: - # Non-Unix -- use subprocess - import subprocess - cmdline = [scriptfile] - if self.is_python(scriptfile): - interp = sys.executable - if interp.lower().endswith("w.exe"): - # On Windows, use python.exe, not pythonw.exe - interp = interp[:-5] + interp[-4:] - cmdline = [interp, '-u'] + cmdline - if '=' not in query: - cmdline.append(query) - self.log_message("command: %s", subprocess.list2cmdline(cmdline)) - try: - nbytes = int(length) - except (TypeError, ValueError): - nbytes = 0 - p = subprocess.Popen(cmdline, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env = env - ) - if self.command.lower() == "post" and nbytes > 0: - data = self.rfile.read(nbytes) - else: - data = None - # throw away additional data [see bug #427345] - while select.select([self.rfile._sock], [], [], 0)[0]: - if not self.rfile._sock.recv(1): - break - stdout, stderr = p.communicate(data) - self.wfile.write(stdout) - if stderr: - self.log_error('%s', stderr) - p.stderr.close() - p.stdout.close() - status = p.returncode - if status: - self.log_error("CGI script exit status %#x", status) - else: - self.log_message("CGI script exited OK") - - -def _get_best_family(*address): - infos = socket.getaddrinfo( - *address, - type=socket.SOCK_STREAM, - flags=socket.AI_PASSIVE, - ) - family, type, proto, canonname, sockaddr = next(iter(infos)) - return family, sockaddr - - -def test(HandlerClass=BaseHTTPRequestHandler, - ServerClass=ThreadingHTTPServer, - protocol="HTTP/1.0", port=8000, bind=None): - """Test the HTTP request handler class. - - This runs an HTTP server on port 8000 (or the port argument). - - """ - ServerClass.address_family, addr = _get_best_family(bind, port) - - HandlerClass.protocol_version = protocol - with ServerClass(addr, HandlerClass) as httpd: - host, port = httpd.socket.getsockname()[:2] - url_host = f'[{host}]' if ':' in host else host - print( - f"Serving HTTP on {host} port {port} " - f"(http://{url_host}:{port}/) ..." - ) - try: - httpd.serve_forever() - except KeyboardInterrupt: - print("\nKeyboard interrupt received, exiting.") - sys.exit(0) - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument('--cgi', action='store_true', - help='Run as CGI Server') - parser.add_argument('--bind', '-b', metavar='ADDRESS', - help='Specify alternate bind address ' - '[default: all interfaces]') - parser.add_argument('--directory', '-d', default=os.getcwd(), - help='Specify alternative directory ' - '[default:current directory]') - parser.add_argument('port', action='store', - default=8000, type=int, - nargs='?', - help='Specify alternate port [default: 8000]') - args = parser.parse_args() - if args.cgi: - handler_class = CGIHTTPRequestHandler - else: - handler_class = partial(SimpleHTTPRequestHandler, - directory=args.directory) - - # ensure dual-stack is not disabled; ref #38907 - class DualStackServer(ThreadingHTTPServer): - def server_bind(self): - # suppress exception when protocol is IPv4 - with contextlib.suppress(Exception): - self.socket.setsockopt( - socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - return super().server_bind() - - test( - HandlerClass=handler_class, - ServerClass=DualStackServer, - port=args.port, - bind=args.bind, - ) diff --git a/icons/first.svg b/icons/first.svg deleted file mode 100644 index 8d0f155..0000000 --- a/icons/first.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/last.svg b/icons/last.svg deleted file mode 100644 index 7064515..0000000 --- a/icons/last.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/left.svg b/icons/left.svg deleted file mode 100644 index acb94c1..0000000 --- a/icons/left.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/ppt.ico b/icons/ppt.ico deleted file mode 100755 index e3fb47b626724ff19755ce57a4ff89adb53cfecf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 170879 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 diff --git a/icons/ppt.png b/icons/ppt.png deleted file mode 100755 index 841b67b8fcb3cb6238776a4b0115c847b6af4358..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12417 zcmb7rWmuDM*#0weARW?-P#J>KpyZ?lq(ljUfr5mTbc`{Ol2!@n62Tx8DS?5cQX`~e zG)RqRumSIW{}2E7^ZQ}P{T#=$JI?dE?yHXLd2MW{Lr=?13jhH9eO*mc005C5K>(PF zG+29=Jtqw`Ub>b(0Km}x-w)L7S>^x$0>FLEduD+-+qpsSXF4*wuy22c7-H~k@?CaS z^q8#n8!4~af8DUYdvzk}T6RBG)t_)Fb8w0{Q>*?R7VvW#9(9&hLwP|OL23t9b%v^p z_G@;n&)t0{sFq%IC8#Xz^#vyFRGb$-nJqEBfAdpGx|L1K+P}Z6nF*~44T zDxJN0pi}A_dQ)&`G5uZ}@Ow~0)J8>FxyjO~6yCr3ZzDH^+BP@%AUVWXlD0?EjM))z zh2L~#Ab^{F9gd&F7ZRtd@(YW@a6#?)VK<9WIN8y4iVRpA~zY z?Rgf`g^Q4Ozxw@Od@1j6){w*?xIv(2*Eae+H;RRABBysP>_>ptvd**0uz7drv-vn_ zV5q?TN#Imdf?>rV_^#MS*Kd{p^47PdhkFaw7kM9F?7$h)geHpUU`GN_gRTbH`0ocV zPmYCL{{WJ1{C`$1g-KY<;NgHH&=&L6M$)qn*`F!qtA1OD&L_mu^)JAAxV(R;MB;zv zuHD{u52ywJ<3br*HrmZiTmxeZy692&jFN_NzDL`QPQ!`%n{!N1awalQ#Kp4IGoBIn z7>f|%RU{|yDo?otJ2|R`&u8@rEK}O((u|S@ewpO_@R=f}ew?+4zl-LfkpTc&Ve0nD znf}D_nHD;j>c+7dk0TIqZ~Eu%-=IX%(EEE8KQ-TKlH0*VA2T{u!(GlcPV%++N+^2( zGpZnh{(qc};VMcNlv}P%0-Pjj5xDu?wBpJy$w3rWf9cEYfADeSey_jv&gv24VkL*H z`hcsatA(7`bX2^_piWV)Ly?-bZc2cJ;!$Y7%X#6o7=qA-Aemhu+ZRHN&TZZ!oLV3y zO(K;U#XP4dZT!G_}Zd!qUG z9q9kPGR;jnDDsFcLJ!J$BabVz7zLw)<85B4t798ONP5pEzHbr1sLe>3VafJ$# zp9ULk`bXc5@X&ycXR(s|RFRR#J6ffA)Bnv9O>{XApYufil!z(oruXt80cG> zFPe8`jYS`l^C?K&xyn4iX^;gH2)lElvD78!c%dHdzf@}X?#%SBJcV<<|?@Z?^Naa<^(h;KW?!=un?kJ;YS0? z)nfHuVO_-D-Gysy;S4`%fCZ}=vn5wOfGfcA=b)_*n!psvfZD#%Tla#L{5|NcyhfQ%F(zHkUle22bC=JhMGR7N;VZ*kzZ5PTVwUwrKdzy^puN57_N+^eln7xNv`8Xbxs1-a;Y(8#l>A&l5 zs^5++_R!@OC{Y7M(SjeU^u&S#Bfi%u7%B=%-Xb51McJ5&!u9bT)IV{K=8NHB+LpGA*>xSFZ6sqLtJtYep9|O$P{_2K8ulupe*DylxH&e$zYR9aA7>j+)HhzV>6R zyzB<6j+`2*O@@*j(R8&;7#S{rDtBEKfe&@)(jq|Pu9_F?(l*xkWp5L>p=y><4!Vh*hW6^wT z@l73kl!7VbR}iJSNF&q}`gM9%?Oj7`@0wH3k*~kd3p#ewRbClwm=!S)CS)hjL;k~9>*~%W*1#fQ}KL~1sEGUJ9F3Iq$cu$I- z9UlrY#wp+GyADm0oSWtZX&ni|2tQuB2Nc=?Wcrk{Y|&?&*P`I<3rL3`5&8CQ$YVj8Y6U?*VJJBg-EI2(>cY zqc147IZsd6P4vQwktC02LM|k3=}1<3NpL)<$C+6m>>lGw{7#j>2gUK;`UuhzHr}DG zI;MseX^A*I#%;rMl2K=5vM`~UfJi+~aQ3Wgpan)8aqrYPQex?dvTbc@;BBwW2|b&U zW8U2o1hoLc{WDa6+lps+T%zSLzl)@cuz2a23^AO<45KwZ2k%H9svUJ^pVCx5u0#8k)qsQ)1pnmKaKw6OZwUsb4kDb`2Fa%pEibg(*?8^ zeqaGTJK%`J%&F-1x`$740;bq1AqQ^Lvy!l;^}aaB4Oe+U`u0fWZaeqbv!~Wv)7Mvy zqS|8WziP;>YzsybBD_5o(Ce>O%HEK%d1}ddX8h5~*3zWf&81`l2={Cg*sL`F*m z>)C8f`!-n~eL&nV$|0v$Zl!v)u!%=P5t&}tFT1z6-zcz+ZSUlFx9s*W2F%$w59z(<=U4At{h_RsayR`S z!GucC6+DQXvZuC9qK&3>k7m)MJ!bG>Zpb(iCbscp7IZrPe-%Ct3~h|qdVa4ScN6h# z+{rwv2`x%g;3!>DF`Ix0OH*VB+#zoTV`IiE4fEhSBLS2v?#{YbE!93}$rAfGM?|3mYnZ zKm<|(&Fy}7+|_pkRi5ol2H8DTl}L?9}k!zVas7Kwh})A6)1BQ z{PnQcpSYQNKbD+Zkhe_mz95cRrp zr@PJ4HoyrynW-1AIa>Bbmx4gUCa4?`)E4Y|Rw47U|3ItFX}T{iPwwsNK_MF?;JG2M z55Kaa-&$M3?vh;Sc=qm#ZFw6)?ly;(Upgi~wC3X+@*{of7j5RStl>|XLgi~vcFGAJ zK%$Kr0dCptdJy)s5@(xq9#d_U7D8qM>q7p zy&x+Ma;>|^ti=8BOvHH5|LW})(RFD$3(I<(^}tIlH<#>CUItrV@}$#0GItg$@(bO^ zUUuF>wuKV3Ome?!oUraR);&mNg1XYH>k`H~4=@x5tjYe4om2u0BLOc^wUgZovDekw zne~y;wUd+@^I{kdq{y$8({xwop!*J=MNhDFCZ|Yx$=Zem1KpV$;+#~6Vni>Gk#b7f z==rrffkTP9H9sa(KU?D$D@*FN(3U2t=S!zGf#us_ zJ5hI@{=p1My3kfX*2Y&=q%xfFUGd7Q?DTxJKy4#TjrJWaO^)tu23c8`;>K>?l40oH zf)GKZF$SlMeKc-+fS|eD`7!Cu&tB8|SE?MMJ^ANKB8r$uPI{9WuN}BOZpYC2#?}2l zL`h~Zr1-$nm;jm+tswL>d#$G~e?F|WmOx+^9<3?up*@vfU(#yNbAVP(JIkemYyTYF zdejiW0}e14;L=k~6g}58a{%Hv9)76%HzxVtQNj-@9meqe2hGt0L&|sf2V$B`>Qnj6 zMBZ9R>r=2>G-yEv!%<F!JMIUDPwob3e%YhSg_yzsuikR3{mrN(1UfHbMG!Osy z>3Fs)enCrBljB{3vQS!mpQhB+@EkPT2O9 z(#%$cMRP)ze^^#+QqXm5UWsCSX&K|%Gp)hYGT*wxP-BfG;rBOz_#neQ`|-d1);>XM*Drn+N#!cC$ZLf6^}~oCfhf3prNo`8oZYCPgin1tYk7yW zva`>fhrXUllGdux!yoBrk6V!2Nt1~hwQ=0;YF&E5Anu&Ym@Hw{K(M@|Do7y%yB^Q) ztw{06TqmAs34frEVuO*NL<$^Kl2gA~yV8?gcR!d2R9L75p_<=VD6ig2fI5FZgFfNnnyB zkp??0#dg1_M9`0dLL)rg1huhtTC;j2$}PgT{zYrDIRZ`IVvFrX_H2%;a0_lT^^rrh zfIqY+v#DD~cp$~qWr_bx)aWw0=HmvDHve?(XS_ZpzVs^8m0zAxQX6zl5uXFL!aw>4 z_C<)zm0A>RuP=ovdM)}ZaLPX*%nWWZpN2*Jetk-CugX-Y)tQ^F!2Yy(Zw)qrbxG0%~%Ts0@GIQn5e) zoPPln0tg?e%LU~7MUt9T^9FCT{OG&W77(wPk1sj+wvuH(sTD<_Wdv)3p1xh97Dnvt z-DF07uAz^yZR`kGX9PlQEni>MjnURZ7sNg%^%}rlWzn8&C#|T>7}eC;Z{^5%{dSBO zL#pqtmgrk2SGI*}u~!kT>+NoOu}4XKYjko@`S3sl-kZ=qbb(%y@*m&|SLQ}w#KgX> zrKOE6abUG?A{+%a=wl39CzemsY|^OV!SC8wsnK%3BQXpYRL8@ZeClyth41C314Fs? zuJn$sQvTe`RD0ULk}nRn(k6Cdc6fI(>Vku#Z?cEh++H(#$_RuQ2}*LHS&_$bS4 z>*2!i$|Aix29&inU>p3)W?cf-Dt*v3@&Q&KI4a2r?Qz75hE2TcAKeX6W&QY3^<{yttTEKbK#?4-837eHI>)*5DIWMll{^wL^xr z1h&>lM>c_<%4+jft(CjYTTe!n;Z0pFyJ^JDxHh%|(;ZK{JQ^^v+VRVF(UZI8g%nWY zVEegM&q3yF+U{mX-SHMdse>7o8iYi(IWdTtJDx9}rotDDn}sr+Bdbos?%xnyicTTJ__MwbHOtK;-z~j|JOURn; z>z19st=ZX4yF98sO(6Y>5LFQJI)@8g6H@#DLAU{B>GR6-fPX8lqb6l{Cm_xa0ZazF z=k+o#Y(tZf$swJSd121wG#4tiJyc?DP(q64s2j_CYuLO$OkwkVR&Z!iN0!AjMWKq1SZfRF8P1BXt>&1oSm(=B{NV>-FJypvl17<8a3@C3uEcb#J`C~|G zEYXr^LK03s3F5KM8S!IF(F_R6OGd$q+Pt1TCzg$Vv`_m|y&+JoVf z;SR%c?`@L2@t9KJhK8LK@bb^WfA%;p!~7?&#>y#6qf+Xl8v?Jt;<)D9!+IGbI8*6h zU-uE_ON$TjweH{>GnH0(o|j`aEZ2z+?}~rmzNdMrMSTY`1tVYWYZlZmpH_75;*K`& zV(K_W9LZV!guI$$helqyUEr8Nk)j9*LNpYyIkzaccjY7$jP+O8Fy)Rxrhw21a*Y3L)pJ3O4A4eCc zb|`F}6iQ5PfU(cxH7Dz12#dj~_;?VV9clP@vk zgNRsvvXt=U1HfYA7G`ZjUoQ_bGy6D>6s0E3o$nsQ^;Z9`vY#B1Tz{%K6?` zg482zZT~5u4-=Q=#StD$IAi`Z8QY4wi?~5RyzXm>;q#8BDAQz8x#@3&@Q^o;!-j0# zxav#*TYNpuqMU78Xpe;Vk#33bqQvCbrc!A7Fj;3o#Vgc%0x0$$q<%Cm_H;IVCuD*5 zOg|X_D9Lv%T<$}SuU;;oQ+E@;FWwsaXwih$a_{qBQ`|CtP5Gj_QUC6@U7JqaoEu}C zMBOvVwCIn)pcm06R62Rh+oR=S8bLE~*#ivg)XZ9~1bL^@PTR@Ck-JJm;;tiUA?1O# zjH9=UkAv9I1uXF|_k|EU`swWGPh9ek?IN8ye>)U#xd6K%nEPj%axtIp;fT>=&?~hUt5i>(t^^M$<(Ag)l21%j*ndK5fj~w6oj(%G{$uW z$)MVx56*O{8%tVn`MQM>qvkygi%yC2bqOWerT(V1ItlVB*z!?H3Om~5@yC*@dlQYz zuqaYh(!KX}6eM=M?oyF*Itt4vze&%j>y$Dix8A`!$9j8tErtL#sTHJ6Z*Dm7p9mlZ zZ5%EQO+*zj-{FED)BHAb+5F@FUvcx+;>)Y(uNiE0J;l3}X%-)@{@A7LmQ7=C3>&$v zArPqHsTro2sGTdI73)G#klM?EP3@4_;xRUh_fsM7MPCN5Sw+sDgJKTO4)+KcYOk@B z#M=PCB=TP`fIKRQsh<^4ruokrSr$%NM_#Xv``PSQ-XSD2iLqQbw7F?rg?wfSs04{QAOqP|iU%B2_zOjJ8XllQ>ROz>y)RUI2|zfceRz_> zh&5M$Lvl{dU*IZ7UWsxdpG8kyaxq!?e>QS~}Ql)z)DWVBV5BTFRT0tu#LYM#+(VeSb6@ zmbxEY{sUs)7Ut7T{I)&yp)3dPw()bJW|q9s?rMm@JEUgED8N}23i`?{sR)(>rf_w> zm+pG{t1TZBU7KXTr$;B;J-LnT)DdR}^zxoK(S*PAk0^To zBl~=H!#zyn*8xv1f_)Z;)1JY6SV*B8s{RBM?sK^};#XZdBUvHkr>*Hu&%5wPuw$`M zKiB`m)==Uu{22v?0hdMxa^mcH^Qz$l9(B3!y(SF*Ct0OohX0Cxwb{FXv?P9AbMis7 zZ}OO+uCtfzuYDCc)Ar_RIbK{1G3MWp2_|fNU%)yT|8@n*P5q9(=qf`E2|71reXNx? zs6Q+F!??+P-=MUh94zxYc zMrD2~Wj>J13o>i{nZz{rUh?Q*+xRnMqDt-QXa5}>@RT`D!%&D1g-nGZ{Zm2(2*!o)v?;`q0_znm9iJbhK=ZWh<^Dpef4 zV6Ks4v-p*rF=^b4$X8Pm|INs{FBm2o0UhhpA#dU`*8Zg?=6^W%a#lY(Kcc=HJ3yf3 z2mPJ8>p&k|mv#e(nEBai$FdZYDO{o5dl2U&E+?F25;y>F>*mUg&Pvi-p zA;9xJ?rF<~>~uk~3w1|5V%oQouyvyZJpDH(d%m4#VQUg*d*n5tww~{?Ko-059P(6}p-<^L67C?nO*l`_ds1;hPzsB9L=?h6dLF2LzxD^Hw<$&V z8i4Gyba3fzd1%BIe(ar}y7Y~gnnywdmBYmi6r24)YA?jyl75A%bOldJJTkeV?}BZ9 z5nz43JzjgsN4ZzcKexls^7#lKuRC?>SoyL}6?AO!j9x}ZA(1dgGqU6QU{lUlOiCF3 zw5~H}>0sJn)A-Ja!rN(ll#_yOO>Kys0xciXf3fn+qJ{N-956F449K(m4xoQd4d2?h z#4_%TE2352W%1YT+7hKhyhm(Iiq;Db3oVsgj?T6+ioKjj3YLExetO*h;!3~~S045~ z1`~ob8GvxzSHTqNq98PqMd57q1X`q|)0va1B>HF(Jhe?LVP?nv9{n4%S{e!s!JtD1^#A6UoDZ6ia?tl%Nb6RWJg z>e3!}gC4=X!q}@U#deRYQcdgMkRrW>M4NHsQD?uKQ@$>-eWupf6L+LHjA__AO?a)Y zosW7$ICz8l7XW5}%qp*LkHO&*3&A3lrIb= zHnx*X@Zo27E)BI&+q|-4mWm8vcbvLPRqNqIQ?g87SUtR|zfA1n*cN*Zhei^KOFy@~ zk&lVrzy!;;d1Rci6+xM-#s)P0Evl)Q7BAA_+j)H54VGO{4>)m$3*~=MZC65cyt8+n zU|(*BHGOxy+ov}02R-peb?Z);cqilE@uSuNI_?vO*pIY4Z-B7ImrF)Qs%{r1&$2LI zf;TDuj{J##OR}~XgleOf2DY(opK3w1%R3Sm;r!od2$mby$?U#uGA;LVcD1dG_mLO0 zAf3ju3au}qgu7)iZi6-7q;tLPo7on=2VAS$Lm%;thlY+ehD9`F`TeQMl=u2;x<60+5q(*HT+H<#JT1@v-MW$OX2ztJZ*|q14e>WvvO{9iE_zG4r~) z!~W6f$E^sQsswWKQ}Z`=HRF6{;0jivb&a^yYk|4AZSV4Xccb_Q#Ao2w(rx~Bb z4d_iLoUX@+=iFo{Hn-9}gzKqEwJrAF-mIK362=9+i)Uc-n4tn%7iU(8lUV=1~}W2ZH!u2djK}OHCoPS zf$M>L#S~i-tZ70q!dE`#+fKjqQEUNr2MUU-0Byu zJ@OtPwo0F70u54?wj)qlyh_pj#gyZ%gV^Hb z*!K^hN!YG|`QLv8hjyh(tr@uLLEz<#W=6hSJeH@+V>|FLLd|w1 z457gw7OXnH7M5>xxSN#4p;kbLq9al$%U_>QL_x0)+N=G89(@yib|cKA3+co=HW!!` zM|<;`WCjl5gPG3{`@YaDl1~Z*$j356f=KBO_hK*Y@e*X-WTZ{OmWt!}br2>W zgH^TIx)Gh$Sk?j4I`m}$BSfWkrQ=V3CaY=IhFFSVcIo*vb##81U%;i$c+26A3{!}^ zJbbMa6B3MM2rE)Y@;d@ef^k&%d=b_XaFKIsMVKHZtYu~5l_3Iq$B|Bxvnb5b{L%*w zKictO3URA!!%Y5tqr1QZC7o}u5cW5d6U^s(e(;L zw%7V=R-OL5si`|52o2ZArp2$^@{R-O9mm~vc&yK@*XBm`u`>;zW818wzfE|bh;x!{ z(hLc`>ImHIt_Xu&v%6671Y3~Lv)QN4Vbpxv67G_g%ley>sroHiIP#Y@y4nZ(J1Z%C zArZ4JPGh^=yiFmok0;h64R=55|A&l;E|MUzzlbP>OyB#Bf&P%8Kt$TTzD6>NvK!B+ zlQQ=hw;zlgdkz>t(#Aw;OOF1%e9iVfXbP+Jc6vhY&Ucp+1mXSYyXzblr zKW51$z?wBYBF*hWi4sWYu)B@Do6;XZ*6Njaygj4v{WWm7ViWoTEOK2ULPfI)FY@1rO=EglN z$Cly~P-U;GDxy3|jm`K8E^^hIOe=W*YTE4%am73v9!*F5{`cL8nh;FPq{oU@^V!z| zt;LMjHz(x(%8@#7soRGg1ct(@?a606{?HlIdS@;d&oI(tpa zX98BMFIif~CG$@f{hwyg;L_HwlRB|&e{++QHvKW6uNIz>1S_@1-pBh>!L-*6?>4ib zbw~|w68>`&x9jR;7(DXS^vDGWETfnfb392_Q@=#bGo%=ZiW6IFzT#IE_INI}sKVj3 zVi<_dkFRB;%1WN_!QWYIQpIytDQyRT4Y^u;cUvgVZ z7fPODoikAWb+GxVh_%Q)v2QxD(!kOE8cZWg!~eGEYho_R>2C*9(W&0}Li(lvxUXfX JS)pzh@jvp^P)7g& diff --git a/icons/right.svg b/icons/right.svg deleted file mode 100644 index a9e5aa7..0000000 --- a/icons/right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100755 index b19415f..0000000 --- a/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - ppt-control - - - -
- -
-

Current slide

- -
- -
-

Next slide

- -
- -
- -
-
-

- - - - - Current: /? - - -

- - Show current slide - Show next slide - Keyboard shortcuts - -

Not connected

-
-
- - - - - - diff --git a/ppt-control.js b/ppt-control.js deleted file mode 100644 index e1b8841..0000000 --- a/ppt-control.js +++ /dev/null @@ -1,240 +0,0 @@ -var DEFAULT_TITLE = "ppt-control" -var preloaded = false; -var preload = []; - -function imageRefresh(id) { - img = document.getElementById(id); - var d = new Date; - var http = img.src; - if (http.indexOf("?t=") != -1) { http = http.split("?t=")[0]; } - img.src = http + '?t=' + d.getTime(); -} - -function startWebsocket() { - ws = new WebSocket("ws://" + window.location.host + ":5678/"); - ws.onclose = function(){ - //websocket = null; - setTimeout(function(){startWebsocket()}, 10000); - } - return ws; -} - -var websocket = startWebsocket(); - -var prev = document.querySelector('#prev'), - next = document.querySelector('#next'), - first = document.querySelector('#first'), - last = document.querySelector('#last'), - black = document.querySelector('#black'), - white = document.querySelector('#white'), - slide_label = document.querySelector('#slide_label'), - current = document.querySelector('#current'), - total = document.querySelector('#total'), - users = document.querySelector('.users'), - current_img = document.querySelector('#current_img'), - next_img = document.querySelector('#next_img'), - current_div = document.querySelector('#current_div'), - next_div = document.querySelector('#next_div'), - controls_container = document.querySelector('#controls_container'), - controls_container_inner = document.querySelector('#controls_container_inner'), - show_current = document.querySelector('#show_current'), - show_next = document.querySelector('#show_next'), - shortcuts = document.querySelector('#shortcuts'); - -prev.onclick = function (event) { - websocket.send(JSON.stringify({action: 'prev'})); -} - -next.onclick = function (event) { - websocket.send(JSON.stringify({action: 'next'})); -} - -first.onclick = function (event) { - websocket.send(JSON.stringify({action: 'first'})); -} - -last.onclick = function (event) { - websocket.send(JSON.stringify({action: 'last'})); -} - -black.onclick = function (event) { - websocket.send(JSON.stringify({action: 'black'})); -} - -white.onclick = function (event) { - websocket.send(JSON.stringify({action: 'white'})); -} - -current.onblur = function (event) { - websocket.send(JSON.stringify({action: 'goto', value: current.value})); -} - -current.addEventListener('keyup',function(e){ - if (e.which == 13) this.blur(); -}); - -current_img.onclick = function (event) { - next.click() -} - -next_img.onclick = function (event) { - next.click() -} - - -function sync_current() { - if (show_current.checked) { - current_div.style.display = "block"; - slide_label.style.display = "none"; - next_div.style.width = "25%"; - } else { - current_div.style.display = "none"; - slide_label.style.display = "inline"; - next_div.style.width = "calc(100% - 20px)"; - } - set_control_width(); - saveSettings(); -} -show_current.onclick = sync_current; - -function sync_next() { - if (show_next.checked) { - next_div.style.display = "block"; - current_div.style.width = "70%"; - } else { - next_div.style.display = "none"; - current_div.style.width = "calc(100% - 20px)"; - } - set_control_width(); - saveSettings(); -} -show_next.onclick = sync_next; - -function sync_shortcuts() { - saveSettings(); -} -shortcuts.onclick = sync_shortcuts; - -function set_control_width() { - var width = window.innerWidth - || document.documentElement.clientWidth - || document.body.clientWidth; - if (show_current.checked && show_next.checked && width > 800) { - controls_container_inner.style.width = "70%" - } else { - controls_container_inner.style.width = "100%" - } -} - - -document.addEventListener('keydown', function (e) { - if (shortcuts.checked) { - switch (e.key) { - case "Left": - case "ArrowLeft": - case "Up": - case "ArrowUp": - case "k": - case "K": - prev.click(); - break; - case " ": - case "Spacebar": - case "Enter": - case "Right": - case "ArrowRight": - case "Down": - case "ArrowDown": - case "j": - case "J": - next.click(); - break; - case "b": - case "B": - black.click(); - case "w": - case "W": - white.click(); - default: - return - } - } -}); - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -function disconnect() { - document.title = DEFAULT_TITLE; - current_img.src = "/black.jpg"; - next_img.src = "/black.jpg"; - users.textContent = "Connection to PowerPoint failed"; -} - -websocket.onmessage = function (event) { - console.log("Received data"); - data = JSON.parse(event.data); - switch (data.type) { - case 'state': - if (data.connected == "0" || data.connected == 0) { - console.log("Disconnected"); - disconnect(); - break; - } - var d = new Date; - switch (data.visible) { - case 3: - current_img.src = "/black.jpg"; - break; - case 4: - current_img.src = "/white.jpg"; - break; - default: - //current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime(); - current_img.src = "/cache/" + data.current + ".jpg"; - break; - } - if (data.current == data.total + 1) { - //next_img.src = "/cache/" + (data.total + 1) + ".jpg?t=" + d.getTime(); - next_img.src = "/cache/" + (data.total + 1) + ".jpg"; - } else { - //next_img.src = "/cache/" + (data.current + 1) + ".jpg?t=" + d.getTime(); - next_img.src = "/cache/" + (data.current + 1) + ".jpg"; - } - - if (document.activeElement != current) { - current.value = data.current; - } - total.textContent = data.total; - document.title = data.name; - break; - case 'users': - users.textContent = ( - data.count.toString() + " client" + - (data.count == 1 ? "" : "s")); - break; - default: - console.error( - "unsupported event", data); - } - if (preloaded == false && ! isNaN(total.textContent)) { - image = document.getElementById("preload_img"); - for (let i=1; i<=Number(total.textContent); i++) { - image.src = "/cache/" + i + ".jpg"; - preload.push(image); - console.log("Preloaded " + total.textContent); - //sleep(0.5) - } - preloaded = true; - } - -}; - -var interval = setInterval(refresh, 5000); - -function refresh() { - console.log("Refreshing") - websocket.send(JSON.stringify({action: 'refresh'})); -} - diff --git a/ppt_control.py b/ppt_control.py deleted file mode 100755 index 5e8bdfa..0000000 --- a/ppt_control.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -sys.coinit_flags= 0 -import win32com.client -import pywintypes -import os -import shutil -import http_server_39 as server -#import http.server as server -import socketserver -import threading -import asyncio -import websockets -import logging, json -import urllib -import posixpath -import time -import pythoncom -import pystray -import tkinter as tk -from tkinter import ttk -from PIL import Image, ImageDraw - -logging.basicConfig() - -global STATE -global STATE_DEFAULT -global current_slideshow -current_slideshow = None -CACHEDIR = r'''C:\Windows\Temp\ppt-cache''' -CACHE_TIMEOUT = 2*60*60 - -class Handler(server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__))) - - def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - - """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - # Don't forget explicit trailing slash when normalizing. Issue17324 - trailing_slash = path.rstrip().endswith('/') - try: - path = urllib.parse.unquote(path, errors='surrogatepass') - except UnicodeDecodeError: - path = urllib.parse.unquote(path) - path = posixpath.normpath(path) - words = path.split('/') - words = list(filter(None, words)) - if len(words) > 0 and words[0] == "cache": - if current_slideshow: - path = CACHEDIR + "\\" + current_slideshow.name() - else: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/' - return path - words.pop(0) - else: - path = self.directory - for word in words: - if os.path.dirname(word) or word in (os.curdir, os.pardir): - # Ignore components that are not a simple file/directory name - continue - path = os.path.join(path, word) - if trailing_slash: - path += '/' - return path - - -def run_http(): - http_server = server.HTTPServer(("", 80), Handler) - http_server.serve_forever() - -STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""} -STATE = STATE_DEFAULT -USERS = set() - - -def state_event(): - print("Running state event") - return json.dumps({"type": "state", **STATE}) - - -def users_event(): - return json.dumps({"type": "users", "count": len(USERS)}) - - -async def notify_state(): - print("Notifying state to " + str(len(USERS)) + " users") - global STATE - if current_slideshow and STATE["connected"] == 1: - STATE["current"] = current_slideshow.current_slide() - STATE["total"] = current_slideshow.total_slides() - STATE["visible"] = current_slideshow.visible() - STATE["name"] = current_slideshow.name() - else: - STATE = STATE_DEFAULT - if USERS: # asyncio.wait doesn't accept an empty list - message = state_event() - await asyncio.wait([user.send(message) for user in USERS]) - - -async def notify_users(): - if USERS: # asyncio.wait doesn't accept an empty list - message = users_event() - await asyncio.wait([user.send(message) for user in USERS]) - - -async def register(websocket): - USERS.add(websocket) - await notify_users() - - -async def unregister(websocket): - USERS.remove(websocket) - await notify_users() - - -async def ws_handle(websocket, path): - print("Received command") - global current_slideshow - # register(websocket) sends user_event() to websocket - await register(websocket) - try: - await websocket.send(state_event()) - async for message in websocket: - data = json.loads(message) - if data["action"] == "prev": - if current_slideshow: - current_slideshow.prev() - await notify_state() - elif data["action"] == "next": - if current_slideshow: - current_slideshow.next() - await notify_state() - elif data["action"] == "first": - if current_slideshow: - current_slideshow.first() - await notify_state() - elif data["action"] == "last": - if current_slideshow: - current_slideshow.last() - await notify_state() - elif data["action"] == "black": - if current_slideshow: - if current_slideshow.visible() == 3: - current_slideshow.normal() - else: - current_slideshow.black() - await notify_state() - elif data["action"] == "white": - if current_slideshow: - if current_slideshow.visible() == 4: - current_slideshow.normal() - else: - current_slideshow.white() - await notify_state() - elif data["action"] == "goto": - if current_slideshow: - current_slideshow.goto(int(data["value"])) - await notify_state() - elif data["action"] == "refresh": - print("Received refresh command") - await notify_state() - if current_slideshow: - current_slideshow.export_current_next() - current_slideshow.refresh() - else: - logging.error("unsupported event: {}", data) - finally: - await unregister(websocket) - -def run_ws(): - # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084 - # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/ - pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) - asyncio.set_event_loop(asyncio.new_event_loop()) - start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None) - asyncio.get_event_loop().run_until_complete(start_server) - asyncio.get_event_loop().run_forever() - -def start_server(): - http_daemon = threading.Thread(name="http_daemon", target=run_http) - http_daemon.setDaemon(True) - http_daemon.start() - print("Started HTTP server") - - ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) - ws_daemon.setDaemon(True) - ws_daemon.start() - print("Started websocket server") - -class Slideshow: - def __init__(self, instance): - self.instance = instance - if self.instance is None: - raise ValueError("PPT instance cannot be None") - - if self.instance.SlideShowWindows.Count == 0: - raise ValueError("PPT instance has no slideshow windows") - self.view = self.instance.SlideShowWindows[0].View - - if self.instance.ActivePresentation is None: - raise ValueError("PPT instance has no active presentation") - self.presentation = self.instance.ActivePresentation - - def unload(self): - connect_ppt() - - def refresh(self): - try: - if self.instance is None: - raise ValueError("PPT instance cannot be None") - - if self.instance.SlideShowWindows.Count == 0: - raise ValueError("PPT instance has no slideshow windows") - self.view = self.instance.SlideShowWindows[0].View - - if self.instance.ActivePresentation is None: - raise ValueError("PPT instance has no active presentation") - except: - self.unload() - - def total_slides(self): - try: - self.refresh() - return len(self.presentation.Slides) - except ValueError or pywintypes.com_error: - self.unload() - - def current_slide(self): - try: - self.refresh() - return self.view.CurrentShowPosition - except ValueError or pywintypes.com_error: - self.unload() - - def visible(self): - try: - self.refresh() - return self.view.State - except ValueError or pywintypes.com_error: - self.unload() - - def prev(self): - try: - self.refresh() - self.view.Previous() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def next(self): - try: - self.refresh() - self.view.Next() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def first(self): - try: - self.refresh() - self.view.First() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def last(self): - try: - self.refresh() - self.view.Last() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def goto(self, slide): - try: - self.refresh() - if slide <= self.total_slides(): - self.view.GotoSlide(slide) - else: - self.last() - self.next() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def black(self): - try: - self.refresh() - self.view.State = 3 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def white(self): - try: - self.refresh() - self.view.State = 4 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def normal(self): - try: - self.refresh() - self.view.State = 1 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def name(self): - try: - self.refresh() - return self.presentation.Name - except ValueError or pywintypes.com_error: - self.unload() - - - def export_current_next(self): - self.export(self.current_slide()) - self.export(self.current_slide() + 1) - - def export(self, slide): - destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg" - os.makedirs(os.path.dirname(destination), exist_ok=True) - if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT: - if slide <= self.total_slides(): - attempts = 0 - while attempts < 3: - try: - self.presentation.Slides(slide).Export(destination, "JPG") - break - except: - pass - attempts += 1 - elif slide == self.total_slides() + 1: - shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb')) - else: - pass - - def export_all(self): - for i in range(1, self.total_slides()): - self.export(i) - -def get_ppt_instance(): - instance = win32com.client.Dispatch('Powerpoint.Application') - if instance is None or instance.SlideShowWindows.Count == 0: - return None - return instance - -def get_current_slideshow(): - return current_slideshow - -def connect_ppt(): - global STATE - if STATE["connected"] == 1: - print("Disconnected from PowerPoint instance") - STATE = STATE_DEFAULT - while True: - try: - instance = get_ppt_instance() - global current_slideshow - current_slideshow = Slideshow(instance) - STATE["connected"] = 1 - STATE["current"] = current_slideshow.current_slide() - STATE["total"] = current_slideshow.total_slides() - print("Connected to PowerPoint instance") - current_slideshow.export_all() - break - except ValueError as e: - current_slideshow = None - pass - time.sleep(1) - -def start(_=None): - #root = tk.Tk() - #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png")) - #root.geometry("250x150+300+300") - #app = Interface(root) - #interface_thread = threading.Thread(target=root.mainloop()) - #interface_thread.setDaemon(True) - #interface_thread.start() - start_server() - connect_ppt() - - -def null_action(): - pass - -class Interface(ttk.Frame): - - def __init__(self, parent): - ttk.Frame.__init__(self, parent) - - self.parent = parent - - self.initUI() - - def initUI(self): - - self.parent.title("ppt-control") - self.style = ttk.Style() - #self.style.theme_use("default") - - self.pack(fill=tk.BOTH, expand=1) - - quitButton = ttk.Button(self, text="Close", - command=self.quit) - quitButton.place(x=50, y=50) - status_label = ttk.Label(self, text="PowerPoint status: not detected") - status_label.place(x=10,y=10) - - - -def show_icon(): - menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False), - pystray.MenuItem("Restart", lambda: start()), - pystray.MenuItem("Settings", lambda: open_settings())) - icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu) - icon.visible = True - icon.run(setup=start) - -if __name__ == "__main__": - show_icon() diff --git a/ppt_control_obs.py b/ppt_control_obs.py deleted file mode 100755 index e36957e..0000000 --- a/ppt_control_obs.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- - -import obspython as obs -import asyncio -import websockets -import threading -from time import sleep - -PORT_DEFAULT = 5678 -HOSTNAME_DEFAULT = "localhost" - -hotkey_id_first = None -hotkey_id_prev = None -hotkey_id_next = None -hotkey_id_last = None -hotkey_id_black = None -hotkey_id_white = None - -HOTKEY_NAME_FIRST = 'powerpoint_slides.first' -HOTKEY_NAME_PREV = 'powerpoint_slides.previous' -HOTKEY_NAME_NEXT = 'powerpoint_slides.next' -HOTKEY_NAME_LAST = 'powerpoint_slides.last' -HOTKEY_NAME_BLACK = 'powerpoint_slides.black' -HOTKEY_NAME_WHITE = 'powerpoint_slides.white' - -HOTKEY_DESC_FIRST = 'First PowerPoint slide' -HOTKEY_DESC_PREV = 'Previous PowerPoint slide' -HOTKEY_DESC_NEXT = 'Next PowerPoint slide' -HOTKEY_DESC_LAST = 'Last PowerPoint slide' -HOTKEY_DESC_BLACK = 'Black PowerPoint slide' -HOTKEY_DESC_WHITE = 'White PowerPoint slide' - -global cmd -global attempts -cmd = "" -hostname = HOSTNAME_DEFAULT -port = PORT_DEFAULT -attempts = 0 - -async def communicate(): - async with websockets.connect("ws://%s:%s" % (hostname, port), ping_interval=None) as websocket: - global cmd - global attempts - while True: - if cmd: - try: - await websocket.send('{"action": "%s"}' % cmd) - cmd = "" - except websockets.ConnectionClosed as exc: - attempts += 1 - if attempts == 4: - print("Failed to send command after {} attempts - aborting connection".format(attempts)) - attempts = 0 - cmd = "" - raise websockets.exceptions.ConnectionClosedError(1006, "Sending command failed after {} attempts".format(attempts)) - await asyncio.sleep(0.05 + 0.5*attempts**2) - -def run_ws(): - while True: - try: - asyncio.set_event_loop(asyncio.new_event_loop()) - asyncio.get_event_loop().run_until_complete(communicate()) - except (OSError, websockets.exceptions.ConnectionClosedError): - # No server available - just keep trying - pass - except Exception as e: - print("Failed to connect to websocket: {} - {}".format(type(e), e)) - finally: - sleep(1) - -#------------------------------------------------------------ -# global functions for script plugins - -def script_load(settings): - global hotkey_id_first - global hotkey_id_prev - global hotkey_id_next - global hotkey_id_last - global hotkey_id_black - global hotkey_id_white - - hotkey_id_first = register_and_load_hotkey(settings, HOTKEY_NAME_FIRST, HOTKEY_DESC_FIRST, first_slide) - hotkey_id_prev = register_and_load_hotkey(settings, HOTKEY_NAME_PREV, HOTKEY_DESC_PREV, prev_slide) - hotkey_id_next = register_and_load_hotkey(settings, HOTKEY_NAME_NEXT, HOTKEY_DESC_NEXT, next_slide) - hotkey_id_last = register_and_load_hotkey(settings, HOTKEY_NAME_LAST, HOTKEY_DESC_LAST, last_slide) - hotkey_id_black = register_and_load_hotkey(settings, HOTKEY_NAME_BLACK, HOTKEY_DESC_BLACK, black) - hotkey_id_white = register_and_load_hotkey(settings, HOTKEY_NAME_WHITE, HOTKEY_DESC_WHITE, white) - - ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) - ws_daemon.setDaemon(True) - ws_daemon.start() - print("Started websocket client") - -def script_unload(): - obs.obs_hotkey_unregister(first_slide) - obs.obs_hotkey_unregister(prev_slide) - obs.obs_hotkey_unregister(next_slide) - obs.obs_hotkey_unregister(last_slide) - obs.obs_hotkey_unregister(black) - obs.obs_hotkey_unregister(white) - -def script_save(settings): - save_hotkey(settings, HOTKEY_NAME_FIRST, hotkey_id_first) - save_hotkey(settings, HOTKEY_NAME_PREV, hotkey_id_prev) - save_hotkey(settings, HOTKEY_NAME_NEXT, hotkey_id_next) - save_hotkey(settings, HOTKEY_NAME_LAST, hotkey_id_last) - save_hotkey(settings, HOTKEY_NAME_BLACK, hotkey_id_black) - save_hotkey(settings, HOTKEY_NAME_WHITE, hotkey_id_white) - -def script_description(): - return """ppt-control client - - Provides hotkeys for controlling PowerPoint slides using websockets. - Go to OBS settings -> Hotkeys to change hotkeys (none set by default).""" - -def script_defaults(settings): - obs.obs_data_set_default_string(settings, 'hostname', HOSTNAME_DEFAULT) - obs.obs_data_set_default_int(settings, 'port', PORT_DEFAULT) - -def script_properties(): - props = obs.obs_properties_create() - - obs.obs_properties_add_text(props, "hostname", "Hostname: ", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_int(props, "port", "Port: ", 0, 9999, 1) - return props - -def script_update(settings): - global port - port = obs.obs_data_get_int(settings, "port") - hostname = obs.obs_data_get_string(settings, "hostname") - -def register_and_load_hotkey(settings, name, description, callback): - hotkey_id = obs.obs_hotkey_register_frontend(name, description, callback) - hotkey_save_array = obs.obs_data_get_array(settings, name) - obs.obs_hotkey_load(hotkey_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - return hotkey_id - -def save_hotkey(settings, name, hotkey_id): - hotkey_save_array = obs.obs_hotkey_save(hotkey_id) - obs.obs_data_set_array(settings, name, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - -#------------------------------------- - -def first_slide(pressed): - if pressed: - global cmd - cmd = "first" - -def prev_slide(pressed): - if pressed: - global cmd - cmd = "prev" - -def next_slide(pressed): - if pressed: - global cmd - cmd = "next" - -def last_slide(pressed): - if pressed: - global cmd - cmd = "last" - -def black(pressed): - if pressed: - global cmd - cmd = "black" - -def white(pressed): - if pressed: - global cmd - cmd = "white" diff --git a/settings.js b/settings.js deleted file mode 100644 index 901af0b..0000000 --- a/settings.js +++ /dev/null @@ -1,50 +0,0 @@ -const COOKIENAME = "settings"; -const COOKIEEXP = 365; - -function setCookie(cname, cvalue, exdays) { - var d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = "expires="+ d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; -} - -function getCookie(cname) { - var name = cname + "="; - var decodedCookie = decodeURIComponent(document.cookie); - var ca = decodedCookie.split(';'); - for(var i = 0; i h2oPg|RM+uAnK(Zx22(+#>dz-|Bl -- 2.47.1