add keyboard shortcuts, icons, preloading
authorAndrew Lorimer <andrew@lorimer.id.au>
Tue, 27 Apr 2021 11:41:03 +0000 (21:41 +1000)
committerAndrew Lorimer <andrew@lorimer.id.au>
Tue, 27 Apr 2021 11:41:03 +0000 (21:41 +1000)
icons/first.svg [new file with mode: 0644]
icons/last.svg [new file with mode: 0644]
icons/left.svg [new file with mode: 0644]
icons/right.svg [new file with mode: 0644]
index.html
obs_ppt_server.py [deleted file]
ppt-control.js
ppt_control.py [new file with mode: 0755]
settings.js
style.css
diff --git a/icons/first.svg b/icons/first.svg
new file mode 100644 (file)
index 0000000..8d0f155
--- /dev/null
@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="step-backward" class="svg-inline--fa fa-step-backward fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M64 468V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12v176.4l195.5-181C352.1 22.3 384 36.6 384 64v384c0 27.4-31.9 41.7-52.5 24.6L136 292.7V468c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12z"></path></svg>
\ No newline at end of file
diff --git a/icons/last.svg b/icons/last.svg
new file mode 100644 (file)
index 0000000..7064515
--- /dev/null
@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="step-forward" class="svg-inline--fa fa-step-forward fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z"></path></svg>
\ No newline at end of file
diff --git a/icons/left.svg b/icons/left.svg
new file mode 100644 (file)
index 0000000..acb94c1
--- /dev/null
@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="chevron-circle-left" class="svg-inline--fa fa-chevron-circle-left fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 504C119 504 8 393 8 256S119 8 256 8s248 111 248 248-111 248-248 248zM142.1 273l135.5 135.5c9.4 9.4 24.6 9.4 33.9 0l17-17c9.4-9.4 9.4-24.6 0-33.9L226.9 256l101.6-101.6c9.4-9.4 9.4-24.6 0-33.9l-17-17c-9.4-9.4-24.6-9.4-33.9 0L142.1 239c-9.4 9.4-9.4 24.6 0 34z"></path></svg>
\ No newline at end of file
diff --git a/icons/right.svg b/icons/right.svg
new file mode 100644 (file)
index 0000000..a9e5aa7
--- /dev/null
@@ -0,0 +1 @@
+<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="chevron-circle-right" class="svg-inline--fa fa-chevron-circle-right fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8c137 0 248 111 248 248S393 504 256 504 8 393 8 256 119 8 256 8zm113.9 231L234.4 103.5c-9.4-9.4-24.6-9.4-33.9 0l-17 17c-9.4 9.4-9.4 24.6 0 33.9L285.1 256 183.5 357.6c-9.4 9.4-9.4 24.6 0 33.9l17 17c9.4 9.4 24.6 9.4 33.9 0L369.9 273c9.4-9.4 9.4-24.6 0-34z"></path></svg>
\ No newline at end of file
index a1a4c3ff1f419e85b2be66faf94c5b9b254a660e..5a0cec626fb85dda280d5abd531eaabedcc784c9 100755 (executable)
@@ -6,32 +6,39 @@
         <title>ppt-control</title>\r
     </head>\r
     <body onload="initSettings();">\r
         <title>ppt-control</title>\r
     </head>\r
     <body onload="initSettings();">\r
+\r
        <div id="img_container">\r
        <div id="img_container">\r
+\r
                <div id="current_div">\r
                        <h1>Current slide</h1>\r
                        <img id="current_img" src="/black.jpg" />\r
                </div>\r
                <div id="current_div">\r
                        <h1>Current slide</h1>\r
                        <img id="current_img" src="/black.jpg" />\r
                </div>\r
+\r
                <div id="next_div">\r
                        <h1>Next slide</h1>\r
                        <img id="next_img" src="/black.jpg" />\r
                </div>\r
                <div id="next_div">\r
                        <h1>Next slide</h1>\r
                        <img id="next_img" src="/black.jpg" />\r
                </div>\r
+\r
         </div>\r
 \r
                <div id="controls_container">\r
         </div>\r
 \r
                <div id="controls_container">\r
-               <p>\r
-                       <button id="prev">Prev</button>\r
-                       <button id="next">Next</button>\r
-                       <button id="first">First</button>\r
-                       <button id="last">Last</button>\r
-                       <button id="black">Black</button>\r
-                       <button id="white">White</button>\r
-                               <span id="count"><span id="slide_label">Current: </span><input type="text" id="current"></input>/<span id="total">?</span></span>\r
-               </p>\r
-\r
-               <input type="checkbox" checked="true" id="show_current">Show current slide</input>\r
-               <input type="checkbox" checked="true" id="show_next">Show next slide</input>\r
-\r
-               <p class="users">Not connected</p>\r
+                       <div id="controls_container_inner">\r
+                       <p>\r
+                               <img class="icon" id="first" src="icons/first.svg" />\r
+                               <img class="icon" id="prev" src="icons/left.svg" />\r
+                               <img class="icon" id="next" src="icons/right.svg" />\r
+                               <img class="icon" id="last" src="icons/last.svg" />\r
+                                       <span id="count"><span id="slide_label">Current: </span><input type="text" id="current"></input>/<span id="total">?</span></span>\r
+                               <button id="black">Black</button>\r
+                               <button id="white">White</button>\r
+                       </p>\r
+\r
+                       <input type="checkbox" checked="true" id="show_current">Show current slide</input>\r
+                       <input type="checkbox" checked="true" id="show_next">Show next slide</input>\r
+                       <input type="checkbox" checked="true" id="shortcuts">Keyboard shortcuts</input>\r
+\r
+                       <p class="users">Not connected</p>\r
+               </div>\r
         </div>\r
 \r
        <script src="ppt-control.js"></script>\r
         </div>\r
 \r
        <script src="ppt-control.js"></script>\r
diff --git a/obs_ppt_server.py b/obs_ppt_server.py
deleted file mode 100755 (executable)
index 4172d94..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-import sys\r
-sys.coinit_flags= 0\r
-import win32com.client\r
-import pywintypes\r
-import os\r
-import shutil\r
-import http_server_39 as server\r
-#import http.server as server\r
-import socketserver\r
-import threading\r
-import asyncio\r
-import websockets\r
-import logging, json\r
-import urllib\r
-import posixpath\r
-import time\r
-import pythoncom\r
-\r
-logging.basicConfig()\r
-\r
-global current_slideshow\r
-current_slideshow = None\r
-CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''\r
-CACHE_TIMEOUT = 2*60*60\r
-\r
-class Handler(server.SimpleHTTPRequestHandler):\r
-    def __init__(self, *args, **kwargs):\r
-        super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))\r
-        \r
-    def translate_path(self, path):\r
-        """Translate a /-separated PATH to the local filename syntax.\r
-\r
-        Components that mean special things to the local file system\r
-        (e.g. drive or directory names) are ignored.  (XXX They should\r
-        probably be diagnosed.)\r
-\r
-        """\r
-        # abandon query parameters\r
-        path = path.split('?',1)[0]\r
-        path = path.split('#',1)[0]\r
-        # Don't forget explicit trailing slash when normalizing. Issue17324\r
-        trailing_slash = path.rstrip().endswith('/')\r
-        try:\r
-            path = urllib.parse.unquote(path, errors='surrogatepass')\r
-        except UnicodeDecodeError:\r
-            path = urllib.parse.unquote(path)\r
-        path = posixpath.normpath(path)\r
-        words = path.split('/')\r
-        words = list(filter(None, words))\r
-        if len(words) > 0 and words[0] == "cache":\r
-            if current_slideshow:\r
-                path = CACHEDIR + "\\" + current_slideshow.name()\r
-            words.pop(0)\r
-        else:\r
-            path = self.directory\r
-        for word in words:\r
-            if os.path.dirname(word) or word in (os.curdir, os.pardir):\r
-                # Ignore components that are not a simple file/directory name\r
-                continue\r
-            path = os.path.join(path, word)\r
-        if trailing_slash:\r
-            path += '/'\r
-        print(path)\r
-        return path\r
-\r
-\r
-def run_http():\r
-    http_server = server.HTTPServer(("", 80), Handler)\r
-    http_server.serve_forever()\r
-\r
-STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0}\r
-USERS = set()\r
-\r
-\r
-def state_event():\r
-    return json.dumps({"type": "state", **STATE})\r
-\r
-\r
-def users_event():\r
-    return json.dumps({"type": "users", "count": len(USERS)})\r
-\r
-\r
-async def notify_state():\r
-    global current_slideshow\r
-    if current_slideshow:\r
-        STATE["current"] = current_slideshow.current_slide()\r
-        STATE["total"] = current_slideshow.total_slides()\r
-        STATE["visible"] = current_slideshow.visible()\r
-    if USERS:  # asyncio.wait doesn't accept an empty list\r
-        message = state_event()\r
-        await asyncio.wait([user.send(message) for user in USERS])\r
-\r
-\r
-async def notify_users():\r
-    if USERS:  # asyncio.wait doesn't accept an empty list\r
-        message = users_event()\r
-        await asyncio.wait([user.send(message) for user in USERS])\r
-\r
-\r
-async def register(websocket):\r
-    USERS.add(websocket)\r
-    await notify_users()\r
-\r
-\r
-async def unregister(websocket):\r
-    USERS.remove(websocket)\r
-    await notify_users()\r
-\r
-\r
-async def ws_handle(websocket, path):\r
-    global current_slideshow\r
-    # register(websocket) sends user_event() to websocket\r
-    await register(websocket)\r
-    try:\r
-        await websocket.send(state_event())\r
-        async for message in websocket:\r
-            data = json.loads(message)\r
-            if data["action"] == "prev":\r
-                if current_slideshow:\r
-                    current_slideshow.prev()\r
-                await notify_state()\r
-            elif data["action"] == "next":\r
-                if current_slideshow:\r
-                    current_slideshow.next()\r
-                await notify_state()\r
-            elif data["action"] == "first":\r
-                if current_slideshow:\r
-                    current_slideshow.first()\r
-                await notify_state()\r
-            elif data["action"] == "last":\r
-                if current_slideshow:\r
-                    current_slideshow.last()\r
-                await notify_state()\r
-            elif data["action"] == "black":\r
-                if current_slideshow:\r
-                    if current_slideshow.visible() == 3:\r
-                        current_slideshow.normal()\r
-                    else:\r
-                        current_slideshow.black()\r
-                await notify_state()\r
-            elif data["action"] == "white":\r
-                if current_slideshow:\r
-                    if current_slideshow.visible() == 4:\r
-                        current_slideshow.normal()\r
-                    else:\r
-                        current_slideshow.white()\r
-                await notify_state()\r
-            elif data["action"] == "goto":\r
-                if current_slideshow:\r
-                    current_slideshow.goto(int(data["value"]))\r
-                await notify_state()\r
-            elif data["action"] == "refresh":\r
-                if current_slideshow:\r
-                    current_slideshow.export_current_next()\r
-                    current_slideshow.refresh()\r
-                await notify_state()\r
-            else:\r
-                logging.error("unsupported event: {}", data)\r
-    finally:\r
-        await unregister(websocket)\r
-\r
-def run_ws():\r
-    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
-    asyncio.set_event_loop(asyncio.new_event_loop())\r
-    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)\r
-    print("Initialised websocket server")\r
-    asyncio.get_event_loop().run_until_complete(start_server)\r
-    print("Running websocket server until complete") \r
-    asyncio.get_event_loop().run_forever()\r
-\r
-def start_server():\r
-    #STATE["current"] = current_slide()\r
-    http_daemon = threading.Thread(name="http_daemon", target=run_http)\r
-    http_daemon.setDaemon(True)\r
-    http_daemon.start()\r
-    print("Started HTTP server")\r
-\r
-    #run_ws()\r
-    \r
-    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
-    ws_daemon.setDaemon(True)\r
-    ws_daemon.start()\r
-    print("Started websocket server")\r
-\r
-    #try:\r
-    #    ws_daemon.start()\r
-    #    http_daemon.start()\r
-    #except (KeyboardInterrupt, SystemExit):\r
-    #    cleanup_stop_thread()\r
-    #    sys.exit()\r
-\r
-class Slideshow:\r
-    def __init__(self, instance):\r
-        self.instance = instance\r
-        if self.instance is None:\r
-            raise ValueError("PPT instance cannot be None")\r
-\r
-        if self.instance.SlideShowWindows.Count == 0:\r
-            raise ValueError("PPT instance has no slideshow windows")\r
-        self.view = self.instance.SlideShowWindows[0].View\r
-\r
-        if self.instance.ActivePresentation is None:\r
-            raise ValueError("PPT instance has no  active presentation")\r
-        self.presentation = self.instance.ActivePresentation\r
-        \r
-        self.export_all()\r
-\r
-    def refresh(self):\r
-        if self.instance is None:\r
-            raise ValueError("PPT instance cannot be None")\r
-\r
-        #if self.instance.SlideShowWindows.Count == 0:\r
-        #    raise ValueError("PPT instance has no slideshow windows")\r
-        self.view = self.instance.SlideShowWindows[0].View\r
-\r
-        if self.instance.ActivePresentation is None:\r
-            raise ValueError("PPT instance has no  active presentation")\r
-\r
-    def total_slides(self):\r
-        return len(self.presentation.Slides)\r
-\r
-    def current_slide(self):\r
-        return self.view.CurrentShowPosition\r
-\r
-    def visible(self):\r
-        return self.view.State\r
-\r
-    def prev(self):\r
-        self.refresh()\r
-        self.view.Previous()\r
-\r
-    def next(self):\r
-        self.refresh()\r
-        self.view.Next()\r
-        self.export_current_next()\r
-\r
-    def first(self):\r
-        self.refresh()\r
-        self.view.First()\r
-        self.export_current_next()\r
-                \r
-    def last(self):\r
-        self.refresh()\r
-        self.view.Last()\r
-        self.export_current_next()\r
-\r
-    def goto(self, slide):\r
-        self.refresh()\r
-        if slide <= self.total_slides():\r
-            self.view.GotoSlide(slide)\r
-        else:\r
-            self.last()\r
-            self.next()\r
-        self.export_current_next()\r
-\r
-    def black(self):\r
-        self.refresh()\r
-        self.view.State = 3\r
-\r
-    def white(self):\r
-        self.refresh()\r
-        self.view.State = 4\r
-\r
-    def normal(self):\r
-        self.refresh()\r
-        self.view.State = 1\r
-\r
-    def name(self):\r
-        return self.presentation.Name\r
-\r
-    def export_current_next(self):\r
-        self.export(self.current_slide())\r
-        self.export(self.current_slide() + 1)\r
-\r
-    def export(self, slide):\r
-        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"\r
-        os.makedirs(os.path.dirname(destination), exist_ok=True)\r
-        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:\r
-            if slide <= self.total_slides():\r
-                attempts = 0\r
-                while attempts < 3:\r
-                    try:\r
-                        self.presentation.Slides(slide).Export(destination, "JPG")\r
-                        time.sleep(0.5)\r
-                        break\r
-                    except:\r
-                        pass\r
-                    attempts += 1\r
-            elif slide == self.total_slides() + 1:\r
-                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))\r
-            else:\r
-                pass\r
-\r
-    def export_all(self):\r
-        for i in range(1, self.total_slides()):\r
-            self.export(i)\r
-\r
-def get_ppt_instance():\r
-    instance = win32com.client.Dispatch('Powerpoint.Application')\r
-    if instance is None or instance.SlideShowWindows.Count == 0:\r
-        return None\r
-    return instance\r
-\r
-def get_current_slideshow():\r
-    print(str(current_slideshow))\r
-    return current_slideshow\r
-\r
-\r
-if __name__ == "__main__":\r
-\r
-    start_server()\r
-    \r
-    while True:\r
-        # Check if PowerPoint is running\r
-        instance = get_ppt_instance()\r
-        try:\r
-            current_slideshow = Slideshow(instance)\r
-            STATE["connected"] = 1\r
-            STATE["current"] = current_slideshow.current_slide()\r
-            STATE["total"] = current_slideshow.total_slides()\r
-            print("Connected to PowerPoint instance " + str(get_current_slideshow()))\r
-            break\r
-        except ValueError as e:\r
-            current_slideshow = None\r
-            pass\r
-        time.sleep(1)\r
index 7496f1b01c21dec38053d89a7f145e918d196350..7e4c3122bb26e4f1ada13075782dea4c51e44abb 100644 (file)
@@ -1,3 +1,5 @@
+var preloaded = false;
+
 function imageRefresh(id) {
     img = document.getElementById(id);
     var d = new Date;
 function imageRefresh(id) {
     img = document.getElementById(id);
     var d = new Date;
@@ -17,9 +19,6 @@ function startWebsocket() {
 
 var websocket = startWebsocket();
 
 
 var websocket = startWebsocket();
 
-//if (window.obssstudio) {
-//}
-
 var prev = document.querySelector('#prev'),
     next = document.querySelector('#next'),
     first = document.querySelector('#first'),
 var prev = document.querySelector('#prev'),
     next = document.querySelector('#next'),
     first = document.querySelector('#first'),
@@ -30,12 +29,15 @@ var prev = document.querySelector('#prev'),
     current = document.querySelector('#current'),
     total = document.querySelector('#total'),
     users = document.querySelector('.users'),
     current = document.querySelector('#current'),
     total = document.querySelector('#total'),
     users = document.querySelector('.users'),
-    prev_img = document.querySelector('#prev_img'),
+    current_img = document.querySelector('#current_img'),
     next_img = document.querySelector('#next_img'),
     current_div = document.querySelector('#current_div'),
     next_div = document.querySelector('#next_div'),
     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_current = document.querySelector('#show_current'),
-    show_next = document.querySelector('#show_next');
+    show_next = document.querySelector('#show_next'),
+    shortcuts = document.querySelector('#shortcuts');
 
 prev.onclick = function (event) {
     websocket.send(JSON.stringify({action: 'prev'}));
 
 prev.onclick = function (event) {
     websocket.send(JSON.stringify({action: 'prev'}));
@@ -69,34 +71,89 @@ current.addEventListener('keyup',function(e){
     if (e.which == 13) this.blur();
 });
 
     if (e.which == 13) this.blur();
 });
 
+current_img.onclick = function (event) {
+       next.click()
+}
+
+next_img.onclick = function (event) {
+       next.click()
+}
+
+
 function sync_current() {
 function sync_current() {
-    console.log("State of current checkbox changed");
     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";
     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 = "block";
-        next_div.style.width = "95%";
+        slide_label.style.display = "inline";
+        next_div.style.width = "calc(100% - 20px)";
     }
     }
+    set_control_width();
     saveSettings();
 }
 show_current.onclick = sync_current;
 
 function sync_next() {
     saveSettings();
 }
 show_current.onclick = sync_current;
 
 function sync_next() {
-    console.log("State of next checkbox changed");
     if (show_next.checked) {
         next_div.style.display = "block";
         current_div.style.width = "70%";
     } else {
         next_div.style.display = "none";
     if (show_next.checked) {
         next_div.style.display = "block";
         current_div.style.width = "70%";
     } else {
         next_div.style.display = "none";
-        current_div.style.width = "95%";
+        current_div.style.width = "calc(100% - 20px)";
     }
     }
+    set_control_width();
     saveSettings();
 }
 show_next.onclick = sync_next;
 
     saveSettings();
 }
 show_next.onclick = sync_next;
 
+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
+               }
+       }
+});
+
 websocket.onmessage = function (event) {
     data = JSON.parse(event.data);
     switch (data.type) {
 websocket.onmessage = function (event) {
     data = JSON.parse(event.data);
     switch (data.type) {
@@ -110,7 +167,8 @@ websocket.onmessage = function (event) {
                     current_img.src = "/white.jpg";
                     break;
                 default:
                     current_img.src = "/white.jpg";
                     break;
                 default:
-                    current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime();
+                    //current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime();
+                    current_img.src = "/cache/" + data.current + ".jpg";
                     break;
             }
             if (data.current == data.total + 1) { 
                     break;
             }
             if (data.current == data.total + 1) { 
@@ -125,6 +183,7 @@ websocket.onmessage = function (event) {
                current.value = data.current;
             }
             total.textContent = data.total;
                current.value = data.current;
             }
             total.textContent = data.total;
+            document.title = data.name;
             break;
         case 'users':
             users.textContent = (
             break;
         case 'users':
             users.textContent = (
@@ -135,6 +194,18 @@ websocket.onmessage = function (event) {
             console.error(
                 "unsupported event", data);
     }
             console.error(
                 "unsupported event", data);
     }
+       if (!preloaded) {
+               var i = 0
+               var preload = [];
+               for (let i=1; i<=Number(total.textContent); i++) {
+                       image = new Image();
+                       image.src = "/cache/" + i + ".jpg";
+                       preload.push(image);
+                       console.log("Preloaded image " + i);
+               }
+               preloaded = true;
+       }
+
 };
 
 var interval = setInterval(refresh, 5000);
 };
 
 var interval = setInterval(refresh, 5000);
@@ -142,3 +213,4 @@ var interval = setInterval(refresh, 5000);
 function refresh() {
     websocket.send(JSON.stringify({action: 'refresh'}));
 }
 function refresh() {
     websocket.send(JSON.stringify({action: 'refresh'}));
 }
+
diff --git a/ppt_control.py b/ppt_control.py
new file mode 100755 (executable)
index 0000000..75cf1b0
--- /dev/null
@@ -0,0 +1,323 @@
+import sys\r
+sys.coinit_flags= 0\r
+import win32com.client\r
+import pywintypes\r
+import os\r
+import shutil\r
+import http_server_39 as server\r
+#import http.server as server\r
+import socketserver\r
+import threading\r
+import asyncio\r
+import websockets\r
+import logging, json\r
+import urllib\r
+import posixpath\r
+import time\r
+import pythoncom\r
+\r
+logging.basicConfig()\r
+\r
+global current_slideshow\r
+current_slideshow = None\r
+CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''\r
+CACHE_TIMEOUT = 2*60*60\r
+\r
+class Handler(server.SimpleHTTPRequestHandler):\r
+    def __init__(self, *args, **kwargs):\r
+        super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))\r
+        \r
+    def translate_path(self, path):\r
+        """Translate a /-separated PATH to the local filename syntax.\r
+\r
+        Components that mean special things to the local file system\r
+        (e.g. drive or directory names) are ignored.  (XXX They should\r
+        probably be diagnosed.)\r
+\r
+        """\r
+        # abandon query parameters\r
+        path = path.split('?',1)[0]\r
+        path = path.split('#',1)[0]\r
+        # Don't forget explicit trailing slash when normalizing. Issue17324\r
+        trailing_slash = path.rstrip().endswith('/')\r
+        try:\r
+            path = urllib.parse.unquote(path, errors='surrogatepass')\r
+        except UnicodeDecodeError:\r
+            path = urllib.parse.unquote(path)\r
+        path = posixpath.normpath(path)\r
+        words = path.split('/')\r
+        words = list(filter(None, words))\r
+        if len(words) > 0 and words[0] == "cache":\r
+            if current_slideshow:\r
+                path = CACHEDIR + "\\" + current_slideshow.name()\r
+            words.pop(0)\r
+        else:\r
+            path = self.directory\r
+        for word in words:\r
+            if os.path.dirname(word) or word in (os.curdir, os.pardir):\r
+                # Ignore components that are not a simple file/directory name\r
+                continue\r
+            path = os.path.join(path, word)\r
+        if trailing_slash:\r
+            path += '/'\r
+        return path\r
+\r
+\r
+def run_http():\r
+    http_server = server.HTTPServer(("", 80), Handler)\r
+    http_server.serve_forever()\r
+\r
+STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""}\r
+USERS = set()\r
+\r
+\r
+def state_event():\r
+    return json.dumps({"type": "state", **STATE})\r
+\r
+\r
+def users_event():\r
+    return json.dumps({"type": "users", "count": len(USERS)})\r
+\r
+\r
+async def notify_state():\r
+    global current_slideshow\r
+    if current_slideshow:\r
+        STATE["current"] = current_slideshow.current_slide()\r
+        STATE["total"] = current_slideshow.total_slides()\r
+        STATE["visible"] = current_slideshow.visible()\r
+        STATE["name"] = current_slideshow.name()\r
+    if USERS:  # asyncio.wait doesn't accept an empty list\r
+        message = state_event()\r
+        await asyncio.wait([user.send(message) for user in USERS])\r
+\r
+\r
+async def notify_users():\r
+    if USERS:  # asyncio.wait doesn't accept an empty list\r
+        message = users_event()\r
+        await asyncio.wait([user.send(message) for user in USERS])\r
+\r
+\r
+async def register(websocket):\r
+    USERS.add(websocket)\r
+    await notify_users()\r
+\r
+\r
+async def unregister(websocket):\r
+    USERS.remove(websocket)\r
+    await notify_users()\r
+\r
+\r
+async def ws_handle(websocket, path):\r
+    global current_slideshow\r
+    # register(websocket) sends user_event() to websocket\r
+    await register(websocket)\r
+    try:\r
+        await websocket.send(state_event())\r
+        async for message in websocket:\r
+            data = json.loads(message)\r
+            if data["action"] == "prev":\r
+                if current_slideshow:\r
+                    current_slideshow.prev()\r
+                await notify_state()\r
+            elif data["action"] == "next":\r
+                if current_slideshow:\r
+                    current_slideshow.next()\r
+                await notify_state()\r
+            elif data["action"] == "first":\r
+                if current_slideshow:\r
+                    current_slideshow.first()\r
+                await notify_state()\r
+            elif data["action"] == "last":\r
+                if current_slideshow:\r
+                    current_slideshow.last()\r
+                await notify_state()\r
+            elif data["action"] == "black":\r
+                if current_slideshow:\r
+                    if current_slideshow.visible() == 3:\r
+                        current_slideshow.normal()\r
+                    else:\r
+                        current_slideshow.black()\r
+                await notify_state()\r
+            elif data["action"] == "white":\r
+                if current_slideshow:\r
+                    if current_slideshow.visible() == 4:\r
+                        current_slideshow.normal()\r
+                    else:\r
+                        current_slideshow.white()\r
+                await notify_state()\r
+            elif data["action"] == "goto":\r
+                if current_slideshow:\r
+                    current_slideshow.goto(int(data["value"]))\r
+                await notify_state()\r
+            elif data["action"] == "refresh":\r
+                if current_slideshow:\r
+                    current_slideshow.export_current_next()\r
+                    current_slideshow.refresh()\r
+                await notify_state()\r
+            else:\r
+                logging.error("unsupported event: {}", data)\r
+    finally:\r
+        await unregister(websocket)\r
+\r
+def run_ws():\r
+    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
+    asyncio.set_event_loop(asyncio.new_event_loop())\r
+    start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)\r
+    asyncio.get_event_loop().run_until_complete(start_server)\r
+    asyncio.get_event_loop().run_forever()\r
+\r
+def start_server():\r
+    #STATE["current"] = current_slide()\r
+    http_daemon = threading.Thread(name="http_daemon", target=run_http)\r
+    http_daemon.setDaemon(True)\r
+    http_daemon.start()\r
+    print("Started HTTP server")\r
+\r
+    #run_ws()\r
+    \r
+    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
+    ws_daemon.setDaemon(True)\r
+    ws_daemon.start()\r
+    print("Started websocket server")\r
+\r
+    #try:\r
+    #    ws_daemon.start()\r
+    #    http_daemon.start()\r
+    #except (KeyboardInterrupt, SystemExit):\r
+    #    cleanup_stop_thread()\r
+    #    sys.exit()\r
+\r
+class Slideshow:\r
+    def __init__(self, instance):\r
+        self.instance = instance\r
+        if self.instance is None:\r
+            raise ValueError("PPT instance cannot be None")\r
+\r
+        if self.instance.SlideShowWindows.Count == 0:\r
+            raise ValueError("PPT instance has no slideshow windows")\r
+        self.view = self.instance.SlideShowWindows[0].View\r
+\r
+        if self.instance.ActivePresentation is None:\r
+            raise ValueError("PPT instance has no  active presentation")\r
+        self.presentation = self.instance.ActivePresentation\r
+        \r
+        self.export_all()\r
+\r
+    def refresh(self):\r
+        if self.instance is None:\r
+            raise ValueError("PPT instance cannot be None")\r
+\r
+        #if self.instance.SlideShowWindows.Count == 0:\r
+        #    raise ValueError("PPT instance has no slideshow windows")\r
+        self.view = self.instance.SlideShowWindows[0].View\r
+\r
+        if self.instance.ActivePresentation is None:\r
+            raise ValueError("PPT instance has no  active presentation")\r
+\r
+    def total_slides(self):\r
+        return len(self.presentation.Slides)\r
+\r
+    def current_slide(self):\r
+        return self.view.CurrentShowPosition\r
+\r
+    def visible(self):\r
+        return self.view.State\r
+\r
+    def prev(self):\r
+        self.refresh()\r
+        self.view.Previous()\r
+\r
+    def next(self):\r
+        self.refresh()\r
+        self.view.Next()\r
+        self.export_current_next()\r
+\r
+    def first(self):\r
+        self.refresh()\r
+        self.view.First()\r
+        self.export_current_next()\r
+                \r
+    def last(self):\r
+        self.refresh()\r
+        self.view.Last()\r
+        self.export_current_next()\r
+\r
+    def goto(self, slide):\r
+        self.refresh()\r
+        if slide <= self.total_slides():\r
+            self.view.GotoSlide(slide)\r
+        else:\r
+            self.last()\r
+            self.next()\r
+        self.export_current_next()\r
+\r
+    def black(self):\r
+        self.refresh()\r
+        self.view.State = 3\r
+\r
+    def white(self):\r
+        self.refresh()\r
+        self.view.State = 4\r
+\r
+    def normal(self):\r
+        self.refresh()\r
+        self.view.State = 1\r
+\r
+    def name(self):\r
+        return self.presentation.Name\r
+\r
+    def export_current_next(self):\r
+        self.export(self.current_slide())\r
+        self.export(self.current_slide() + 1)\r
+\r
+    def export(self, slide):\r
+        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"\r
+        os.makedirs(os.path.dirname(destination), exist_ok=True)\r
+        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:\r
+            if slide <= self.total_slides():\r
+                attempts = 0\r
+                while attempts < 3:\r
+                    try:\r
+                        self.presentation.Slides(slide).Export(destination, "JPG")\r
+                        time.sleep(0.5)\r
+                        break\r
+                    except:\r
+                        pass\r
+                    attempts += 1\r
+            elif slide == self.total_slides() + 1:\r
+                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))\r
+            else:\r
+                pass\r
+\r
+    def export_all(self):\r
+        for i in range(1, self.total_slides()):\r
+            self.export(i)\r
+\r
+def get_ppt_instance():\r
+    instance = win32com.client.Dispatch('Powerpoint.Application')\r
+    if instance is None or instance.SlideShowWindows.Count == 0:\r
+        return None\r
+    return instance\r
+\r
+def get_current_slideshow():\r
+    return current_slideshow\r
+\r
+\r
+if __name__ == "__main__":\r
+\r
+    start_server()\r
+    \r
+    while True:\r
+        # Check if PowerPoint is running\r
+        instance = get_ppt_instance()\r
+        try:\r
+            current_slideshow = Slideshow(instance)\r
+            STATE["connected"] = 1\r
+            STATE["current"] = current_slideshow.current_slide()\r
+            STATE["total"] = current_slideshow.total_slides()\r
+            print("Connected to PowerPoint instance")\r
+            break\r
+        except ValueError as e:\r
+            current_slideshow = None\r
+            pass\r
+        time.sleep(1)\r
index 28078017adde2257831e2ca70ce8fbc7261c3e66..eb94f8ef49af8fc44b19e8917cd88a6ae06bb4ad 100644 (file)
@@ -25,21 +25,22 @@ function getCookie(cname) {
 }
 
 function saveSettings() {
 }
 
 function saveSettings() {
-    settingsString = JSON.stringify({showcurrent: show_current.checked, shownext: show_next.checked});
-    console.log("Saving cookie " + settingsString);
+    settingsString = JSON.stringify({showcurrent: show_current.checked, shownext: show_next.checked, enable_shortcuts: shortcuts.checked});
     setCookie(COOKIENAME, settingsString, COOKIEEXP);
 }
 
 function initSettings() {
     setCookie(COOKIENAME, settingsString, COOKIEEXP);
 }
 
 function initSettings() {
-    console.log("Retrieving cookie");
     if (getCookie(COOKIENAME) == 0) {
     if (getCookie(COOKIENAME) == 0) {
-        console.log("No cookie found - setting new cookie");
+               if (window.obssstudio) {
+                       shortcuts.checked = False;
+                       show_current.checked = False;
+               }
         saveSettings()
     } else {
         cookie = JSON.parse(getCookie(COOKIENAME));
         saveSettings()
     } else {
         cookie = JSON.parse(getCookie(COOKIENAME));
-        console.log("Found cookie " + cookie);
         show_current.checked = cookie.showcurrent;
         show_next.checked = cookie.shownext;
         show_current.checked = cookie.showcurrent;
         show_next.checked = cookie.shownext;
+        shortcuts.checked = cookie.enable_shortcuts;
         sync_current();
         sync_next();
     }
         sync_current();
         sync_next();
     }
index 5b2be5658714b633da7e930c80db9ec035296c18..01c57347f6a8fb9857a082e92c0ebb33a8c34929 100644 (file)
--- a/style.css
+++ b/style.css
@@ -30,10 +30,6 @@ p {
        clear: both;
 }
 
        clear: both;
 }
 
-::-webkit-scrollbar { 
-    //display: none; 
-}
-
 body {
     background: #3a393a;
     color: #efefef;
 body {
     background: #3a393a;
     color: #efefef;
@@ -52,6 +48,30 @@ input {
 
 @media only screen and (max-width: 800px) {
        #current_div, #next_div {
 
 @media only screen and (max-width: 800px) {
        #current_div, #next_div {
-               width: 95% !important;
+               width: calc(100% - 20px) !important;
        }
 }
        }
 }
+
+.icon {
+       width: 50px;
+       filter: invert(88%) sepia(4%) saturate(15%) hue-rotate(18deg) brightness(92%) contrast(97%);
+}
+
+.icon:hover {
+       cursor: pointer;
+       filter: invert(100%) sepia(24%) saturate(1720%) hue-rotate(187deg) brightness(123%) contrast(87%);
+}
+
+.icon#first, .icon#last {
+       width: 20px;
+       margin-bottom: 10px;
+}
+
+button {
+       float: right;
+       margin: 0 10px 0 0;
+}
+
+input[type='checkbox'] {
+       font-size: 15px;
+}