diff --git a/trayicon/Open Media Library.py b/trayicon/Open Media Library.py new file mode 100644 index 0000000..b5bc338 --- /dev/null +++ b/trayicon/Open Media Library.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +import os +import signal +import subprocess +import sys +import webbrowser + +import win32api +import win32con +import win32gui_struct + +try: + import winxpgui as win32gui +except ImportError: + import win32gui + +import install + +class OMLTrayIcon(object): + QUIT = 'QUIT' + SPECIAL_ACTIONS = [QUIT] + + FIRST_ID = 1023 + + def __init__(self): + launch() + name = "Open Media Library" + default_menu_index = 1 + self.icon = "ico/oml.ico" + self.hover_text = name + self.on_quit = quit + + menu_options = ( + ('Launch', None, launch), + ('Quit', None, self.QUIT) + ) + self._next_action_id = self.FIRST_ID + self.menu_actions_by_id = set() + self.menu_options = self._add_ids_to_menu_options(list(menu_options)) + self.menu_actions_by_id = dict(self.menu_actions_by_id) + del self._next_action_id + + self.default_menu_index = (default_menu_index or 0) + self.window_class_name = name + + message_map = {win32gui.RegisterWindowMessage("TaskbarCreated"): self.restart, + win32con.WM_DESTROY: self.destroy, + win32con.WM_COMMAND: self.command, + win32con.WM_USER+20 : self.notify,} + # Register the Window class. + window_class = win32gui.WNDCLASS() + hinst = window_class.hInstance = win32gui.GetModuleHandle(None) + window_class.lpszClassName = self.window_class_name + window_class.style = win32con.CS_VREDRAW | win32con.CS_HREDRAW; + window_class.hCursor = win32gui.LoadCursor(0, win32con.IDC_ARROW) + window_class.hbrBackground = win32con.COLOR_WINDOW + window_class.lpfnWndProc = message_map # could also specify a wndproc. + classAtom = win32gui.RegisterClass(window_class) + # Create the Window. + style = win32con.WS_OVERLAPPED | win32con.WS_SYSMENU + self.hwnd = win32gui.CreateWindow(classAtom, + self.window_class_name, + style, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + hinst, + None) + win32gui.UpdateWindow(self.hwnd) + self.notify_id = None + self.refresh_icon() + win32gui.PumpMessages() + + def _add_ids_to_menu_options(self, menu_options): + result = [] + for menu_option in menu_options: + option_text, option_icon, option_action = menu_option + if callable(option_action) or option_action in self.SPECIAL_ACTIONS: + self.menu_actions_by_id.add((self._next_action_id, option_action)) + result.append(menu_option + (self._next_action_id,)) + else: + print ('Unknown item', option_text, option_icon, option_action) + self._next_action_id += 1 + return result + + def refresh_icon(self): + # Try and find a custom icon + hinst = win32gui.GetModuleHandle(None) + if os.path.isfile(self.icon): + icon_flags = win32con.LR_LOADFROMFILE | win32con.LR_DEFAULTSIZE + hicon = win32gui.LoadImage(hinst, + self.icon, + win32con.IMAGE_ICON, + 0, + 0, + icon_flags) + else: + print ("Can't find icon file - using default.") + hicon = win32gui.LoadIcon(0, win32con.IDI_APPLICATION) + + if self.notify_id: message = win32gui.NIM_MODIFY + else: message = win32gui.NIM_ADD + self.notify_id = (self.hwnd, + 0, + win32gui.NIF_ICON | win32gui.NIF_MESSAGE | win32gui.NIF_TIP, + win32con.WM_USER+20, + hicon, + self.hover_text) + win32gui.Shell_NotifyIcon(message, self.notify_id) + + def restart(self, hwnd, msg, wparam, lparam): + self.refresh_icon() + + def destroy(self, hwnd, msg, wparam, lparam): + if self.on_quit: self.on_quit(self) + nid = (self.hwnd, 0) + win32gui.Shell_NotifyIcon(win32gui.NIM_DELETE, nid) + win32gui.PostQuitMessage(0) # Terminate the app. + + def notify(self, hwnd, msg, wparam, lparam): + if lparam==win32con.WM_LBUTTONDBLCLK: + self.execute_menu_option(self.default_menu_index + self.FIRST_ID) + elif lparam==win32con.WM_RBUTTONUP: + self.show_menu() + elif lparam==win32con.WM_LBUTTONUP: + self.show_menu() + return True + + def show_menu(self): + menu = win32gui.CreatePopupMenu() + self.create_menu(menu, self.menu_options) + #win32gui.SetMenuDefaultItem(menu, 1000, 0) + + pos = win32gui.GetCursorPos() + # See http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/menus_0hdi.asp + win32gui.SetForegroundWindow(self.hwnd) + win32gui.TrackPopupMenu(menu, + win32con.TPM_LEFTALIGN, + pos[0], + pos[1], + 0, + self.hwnd, + None) + win32gui.PostMessage(self.hwnd, win32con.WM_NULL, 0, 0) + + def create_menu(self, menu, menu_options): + for option_text, option_icon, option_action, option_id in menu_options[::-1]: + if option_icon: + option_icon = self.prep_menu_icon(option_icon) + + if option_id in self.menu_actions_by_id: + item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, + hbmpItem=option_icon, + wID=option_id) + win32gui.InsertMenuItem(menu, 0, 1, item) + else: + submenu = win32gui.CreatePopupMenu() + self.create_menu(submenu, option_action) + item, extras = win32gui_struct.PackMENUITEMINFO(text=option_text, + hbmpItem=option_icon, + hSubMenu=submenu) + win32gui.InsertMenuItem(menu, 0, 1, item) + + def prep_menu_icon(self, icon): + # First load the icon. + ico_x = win32api.GetSystemMetrics(win32con.SM_CXSMICON) + ico_y = win32api.GetSystemMetrics(win32con.SM_CYSMICON) + hicon = win32gui.LoadImage(0, icon, win32con.IMAGE_ICON, ico_x, ico_y, win32con.LR_LOADFROMFILE) + + hdcBitmap = win32gui.CreateCompatibleDC(0) + hdcScreen = win32gui.GetDC(0) + hbm = win32gui.CreateCompatibleBitmap(hdcScreen, ico_x, ico_y) + hbmOld = win32gui.SelectObject(hdcBitmap, hbm) + # Fill the background. + brush = win32gui.GetSysColorBrush(win32con.COLOR_MENU) + win32gui.FillRect(hdcBitmap, (0, 0, 16, 16), brush) + # unclear if brush needs to be feed. Best clue I can find is: + # "GetSysColorBrush returns a cached brush instead of allocating a new + # one." - implies no DeleteObject + # draw the icon + win32gui.DrawIconEx(hdcBitmap, 0, 0, hicon, ico_x, ico_y, 0, 0, win32con.DI_NORMAL) + win32gui.SelectObject(hdcBitmap, hbmOld) + win32gui.DeleteDC(hdcBitmap) + + return hbm + + def command(self, hwnd, msg, wparam, lparam): + id = win32gui.LOWORD(wparam) + self.execute_menu_option(id) + + def execute_menu_option(self, id): + menu_action = self.menu_actions_by_id[id] + if menu_action == self.QUIT: + win32gui.DestroyWindow(self.hwnd) + else: + menu_action(self) + +def check_pid(pid): + try: + with open(pid) as fd: + pid = int(fd.read()) + except: + return False + try: + os.kill(pid, 0) + except: + return False + else: + return True + +def launch(sysTrayIcon=None): + base = os.path.join(os.getenv('APPDATA'), 'Open Media Library') + pid = os.path.join(base, 'data', 'openmedialibrary.pid') + if os.path.exists(pid) and check_pid(pid): + webbrowser.open_new_tab(os.path.join(base, 'openmedialibrary', 'static', 'html', 'load.html')) + elif os.path.exists(base): + python = os.path.join(base, 'platform_win32', 'pythonw.exe') + oml = os.path.join(base, 'openmedialibrary') + subprocess.Popen([python, 'oml', 'server', pid], cwd=oml, start_new_session=True) + webbrowser.open_new_tab(os.path.join(base, 'openmedialibrary', 'static', 'html', 'load.html')) + else: + install.run() + +def quit(sysTrayIcon): + base = os.path.join(os.getenv('APPDATA'), 'Open Media Library') + pid = os.path.join(base, 'data', 'openmedialibrary.pid') + if os.path.exists(pid): + with open(pid) as fd: + data = fd.read().strip() + try: + os.kill(int(data), signal.SIGTERM) + except: + pass + +if __name__ == '__main__': + if getattr(sys, 'frozen', False): + base = os.path.dirname(sys.executable) + else: + base = os.path.dirname(os.path.realpath(__file__)) + base = os.path.abspath(base) + os.chdir(base) + OMLTrayIcon() diff --git a/trayicon/README.md b/trayicon/README.md new file mode 100644 index 0000000..ebcdbba --- /dev/null +++ b/trayicon/README.md @@ -0,0 +1,10 @@ +# Open Media Library Windows System Tray Integration + +To build a new version you need pywin32 + http://sourceforge.net/projects/pywin32/files/pywin32/Build%20220/pywin32-220.win32-py3.5.exe/download +and cx_Freeze from hg + hg clone https://bitbucket.org/anthony_tuininga/cx_freeze + +# Build + python setup.py bdist_msi + diff --git a/trayicon/ico/oml.ico b/trayicon/ico/oml.ico new file mode 100644 index 0000000..b7a6d51 Binary files /dev/null and b/trayicon/ico/oml.ico differ diff --git a/trayicon/index.html b/trayicon/index.html new file mode 100644 index 0000000..dcda6dc --- /dev/null +++ b/trayicon/index.html @@ -0,0 +1,11 @@ + + + + Open Media Library + + + + + + + diff --git a/trayicon/install.py b/trayicon/install.py new file mode 100644 index 0000000..32bca93 --- /dev/null +++ b/trayicon/install.py @@ -0,0 +1,155 @@ +from __future__ import division, with_statement + +from contextlib import closing +import json +import os +import sys +import time +import tarfile +from urllib.request import urlopen +import http.server +import socketserver +from threading import Thread +import subprocess +import webbrowser + + +PORT = 9841 +if getattr(sys, 'frozen', False): + static_dir = os.path.dirname(sys.executable) +else: + static_dir = os.path.dirname(os.path.realpath(__file__)) +static_dir = os.path.abspath(static_dir) + +def makedirs(dirname): + if not os.path.exists(dirname): + os.makedirs(dirname) + +def get_platform(): + name = sys.platform + if name.startswith('darwin'): + name = 'darwin64' + elif name.startswith('linux'): + import platform + if platform.architecture()[0] == '64bit': + name = 'linux64' + else: + name = 'linux32' + return name + +class Handler(http.server.SimpleHTTPRequestHandler): + def do_OPTIONS(self): + self.send_response(200, 'OK') + self.send_header('Allow', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Headers', 'X-Requested-With') + self.send_header('Content-Length', '0') + self.end_headers() + + def do_GET(self): + + if self.path == '/status': + content = json.dumps(self.server.install.status).encode() + self.send_response(200, 'OK') + else: + path = os.path.join(static_dir, 'index.html' if self.path == '/' else self.path[1:]) + if os.path.exists(path): + with open(path, 'rb') as fd: + content = fd.read() + self.send_response(200, 'OK') + content_type = { + 'html': 'text/html', + 'png': 'image/png', + 'svg': 'image/svg+xml', + 'txt': 'text/plain', + }.get(path.split('.')[-1], 'txt') + self.send_header('Content-Type', content_type) + else: + self.send_response(404, 'not found') + content = b'404 not found' + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Content-Length', str(len(content))) + self.end_headers() + self.wfile.write(content) + +class Install(Thread): + + release_url = "http://downloads.openmedialibrary.com/release.json" + status = { + 'step': 'Downloading...' + } + + def __init__(self, target, httpd): + target = os.path.normpath(os.path.join(os.path.abspath(target))) + self.target = target + self.httpd = httpd + Thread.__init__(self) + self.daemon = True + self.start() + + def run(self): + webbrowser.open('http://127.0.0.1:%s'%PORT) + target = self.target + makedirs(target) + os.chdir(target) + self.status["step"] = 'Downloading...' + release = self.get_release() + self.status["release"] = release + self.status["progress"] = 0 + platform = get_platform() + for module in sorted(release['modules']): + if release['modules'][module].get('platform', platform) == platform: + package_tar = release['modules'][module]['name'] + url = self.release_url.replace('release.json', package_tar) + self.download(url, package_tar) + self.status["step"] = 'Installing...' + for module in sorted(release['modules']): + if release['modules'][module].get('platform', platform) == platform: + package_tar = release['modules'][module]['name'] + tar = tarfile.open(package_tar) + tar.extractall() + tar.close() + os.unlink(package_tar) + makedirs('data') + with open('data/release.json', 'w') as fd: + json.dump(release, fd, indent=2) + self.status = {"relaunch": True} + open_oml(target) + time.sleep(5) + self.httpd.shutdown() + + def download(self, url, filename): + dirname = os.path.dirname(filename) + if dirname: + makedirs(dirname) + with open(filename, 'wb') as f: + with closing(urlopen(url)) as u: + size = int(u.headers.get('content-length', 0)) + self.status["size"] = size + available = 0 + data = u.read(4096) + while data: + if size: + available += len(data) + f.write(data) + data = u.read(4096) + + def get_release(self): + with closing(urlopen(self.release_url)) as u: + data = json.loads(u.read().decode()) + return data + +class Server(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + +def open_oml(base): + python = os.path.join(base, 'platform_win32', 'pythonw.exe') + pid = os.path.join(base, 'data', 'openmedialibrary.pid') + oml = os.path.join(base, 'openmedialibrary') + subprocess.Popen([python, 'oml', 'server', pid], cwd=oml, start_new_session=True) + +def run(target): + httpd = Server(("", PORT), Handler) + install = Install(target, httpd) + httpd.install = install + httpd.serve_forever() diff --git a/trayicon/js/install.js b/trayicon/js/install.js new file mode 100644 index 0000000..1a5f7e6 --- /dev/null +++ b/trayicon/js/install.js @@ -0,0 +1,148 @@ +'use strict'; + +(function() { + + loadImages(function(images) { + loadScreen(images); + initUpdate(); + }); + + function initUpdate(browserSupported) { + window.update = {}; + update.status = document.createElement('div'); + update.status.className = 'OxElement'; + update.status.style.position = 'absolute'; + update.status.style.left = '16px'; + update.status.style.top = '336px'; + update.status.style.right = 0; + update.status.style.bottom = 0; + update.status.style.width = '512px'; + update.status.style.height = '16px'; + update.status.style.margin = 'auto'; + update.status.style.textAlign = 'center'; + update.status.style.color = 'rgb(16, 16, 16)'; + update.status.style.fontFamily = 'Lucida Grande, Segoe UI, DejaVu Sans, Lucida Sans Unicode, Helvetica, Arial, sans-serif'; + update.status.style.fontSize = '11px'; + document.querySelector('#loadingScreen').appendChild(update.status); + update.status.innerHTML = ''; + updateStatus(); + } + + function load() { + var base = '//127.0.0.1:9842', + ws = new WebSocket('ws:' + base + '/ws'); + ws.onopen = function(event) { + document.location.href = 'http:' + base; + }; + ws.onerror = function(event) { + ws.close(); + setTimeout(load, 500); + }; + ws.onclose = function(event) { + setTimeout(load, 500); + }; + } + + function loadImages(callback) { + var images = {}; + images.logo = document.createElement('img'); + images.logo.onload = function() { + images.logo.style.position = 'absolute'; + images.logo.style.left = 0; + images.logo.style.top = 0; + images.logo.style.right = 0; + images.logo.style.bottom = '96px'; + images.logo.style.width = '256px'; + images.logo.style.height = '256px'; + images.logo.style.margin = 'auto'; + images.logo.style.MozUserSelect = 'none'; + images.logo.style.MSUserSelect = 'none'; + images.logo.style.OUserSelect = 'none'; + images.logo.style.WebkitUserSelect = 'none'; + images.loadingIcon = document.createElement('img'); + images.loadingIcon.setAttribute('id', 'loadingIcon'); + images.loadingIcon.style.position = 'absolute'; + images.loadingIcon.style.left = '16px'; + images.loadingIcon.style.top = '256px' + images.loadingIcon.style.right = 0; + images.loadingIcon.style.bottom = 0; + images.loadingIcon.style.width = '32px'; + images.loadingIcon.style.height = '32px'; + images.loadingIcon.style.margin = 'auto'; + images.loadingIcon.style.MozUserSelect = 'none'; + images.loadingIcon.style.MSUserSelect = 'none'; + images.loadingIcon.style.OUserSelect = 'none'; + images.loadingIcon.style.WebkitUserSelect = 'none'; + images.loadingIcon.src = '/svg/symbolLoading.svg'; + callback(images); + }; + images.logo.src = '/png/oml.png'; + } + + function loadScreen(images) { + var loadingScreen = document.createElement('div'); + loadingScreen.setAttribute('id', 'loadingScreen'); + loadingScreen.className = 'OxScreen'; + loadingScreen.style.position = 'absolute'; + loadingScreen.style.width = '100%'; + loadingScreen.style.height = '100%'; + loadingScreen.style.backgroundColor = 'rgb(224, 224, 224)'; + loadingScreen.style.zIndex = '1002'; + loadingScreen.appendChild(images.logo); + loadingScreen.appendChild(images.loadingIcon); + // FF3.6 document.body can be undefined here + window.onload = function() { + document.body.style.margin = 0; + document.body.appendChild(loadingScreen); + startAnimation(); + }; + // IE8 does not call onload if already loaded before set + document.body && window.onload(); + } + + + function startAnimation() { + var css, deg = 0, loadingIcon = document.getElementById('loadingIcon'), + previousTime = +new Date(); + var animationInterval = setInterval(function() { + var currentTime = +new Date(), + delta = (currentTime - previousTime) / 1000; + previousTime = currentTime; + deg = Math.round((deg + delta * 360) % 360 / 30) * 30; + css = 'rotate(' + deg + 'deg)'; + loadingIcon.style.MozTransform = css; + loadingIcon.style.MSTransform = css; + loadingIcon.style.OTransform = css; + loadingIcon.style.WebkitTransform = css; + loadingIcon.style.transform = css; + }, 83); + } + + function updateStatus() { + var xhr = new XMLHttpRequest(); + xhr.onload = function() { + var response = JSON.parse(this.responseText); + if (response.step) { + var status = response.step; + if (response.progress) { + status = parseInt(response.progress * 100) + '% ' + status; + } + update.status.innerHTML = status; + setTimeout(updateStatus, 1000); + } else { + update.status.innerHTML = 'Relaunching...'; + setTimeout(load, 500); + } + }; + xhr.onerror = function() { + var status = update.status.innerHTML; + if (['Relaunching...', ''].indexOf(status) == -1) { + update.status.innerHTML = 'Installation failed'; + } + load(); + } + xhr.open('get', '/status'); + xhr.send(); + } + +}()); diff --git a/trayicon/png/oml.png b/trayicon/png/oml.png new file mode 100644 index 0000000..9bf0dc7 Binary files /dev/null and b/trayicon/png/oml.png differ diff --git a/trayicon/setup.py b/trayicon/setup.py new file mode 100644 index 0000000..d7eb45e --- /dev/null +++ b/trayicon/setup.py @@ -0,0 +1,47 @@ +import sys +from cx_Freeze import setup, Executable + +''' +to build run: python.exe setup.py bdist_msi +''' + +build_exe_options = { + "packages": ["os"], + "excludes": ["tkinter"], + "include_msvcr": True, + "include_files": [ + "ico", + "index.html", + "js", + "png", + "svg", + ] +} + +bdist_msi_options = { + "upgrade_code": "{d2ff6dae-c817-11e5-bedb-08002781ab3d}" +} + +# GUI applications require a different base on Windows (the default is for a +# console application). +base = None +if sys.platform == "win32": + base = "Win32GUI" + +oml = Executable( + "Open Media Library.py", base=base, + shortcutName="Open Media Library", + shortcutDir="ProgramMenuFolder", + icon="ico/oml.ico" +) + +setup( + name = "Open Media Library", + version = "0.1", + description = "share media collectoins", + options = { + "build_exe": build_exe_options, + "bdist_msi": bdist_msi_options + }, + executables = [oml] +) diff --git a/trayicon/svg/oml.svg b/trayicon/svg/oml.svg new file mode 100644 index 0000000..d7b9e9b --- /dev/null +++ b/trayicon/svg/oml.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/trayicon/svg/symbolLoading.svg b/trayicon/svg/symbolLoading.svg new file mode 100644 index 0000000..eca747e --- /dev/null +++ b/trayicon/svg/symbolLoading.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file