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