systemtray integration

This commit is contained in:
j 2016-01-31 20:15:44 +05:30
parent c1666978b2
commit 62f9daf16f
10 changed files with 683 additions and 0 deletions

View File

@ -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()

10
trayicon/README.md Normal file
View File

@ -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

BIN
trayicon/ico/oml.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

11
trayicon/index.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Open Media Library</title>
<meta charset="UTF-8"/>
<link href="/png/oml.png" rel="icon" type="image/png">
<script src="/js/install.js" type="text/javascript"></script>
<meta name="google" value="notranslate"/>
</head>
<body></body>
</html>

155
trayicon/install.py Normal file
View File

@ -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()

148
trayicon/js/install.js Normal file
View File

@ -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();
}
}());

BIN
trayicon/png/oml.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

47
trayicon/setup.py Normal file
View File

@ -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]
)

51
trayicon/svg/oml.svg Normal file
View File

@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" height="600" width="600">
<g transform="translate(134 -64) rotate(-45 300 300)">
<g transform="translate(115.47005383792516 66.66666666666667)">
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(140,131,114)"/>
</g>
<g transform="translate(230.94010767585033 133.33333333333334)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(156,163,142)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(131,140,114)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(105,112,91)"/>
</g>
<g transform="translate(346.4101615137755 200.0)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(142,163,142)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(114,140,114)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(91,112,91)"/>
</g>
<g transform="translate(230.94010767585033 266.6666666666667)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(142,163,156)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(114,140,131)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(91,112,105)"/>
</g>
<g transform="translate(115.47005383792516 333.33333333333337)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(142,156,163)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(114,131,140)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(91,105,112)"/>
</g>
<g transform="translate(0 400.0)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(142,142,163)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(114,114,140)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(91,91,112)"/>
</g>
<g transform="translate(0 266.6666666666667)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(156,142,163)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(131,114,140)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(105,91,112)"/>
</g>
<g transform="translate(0 133.33333333333334)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(163,142,156)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(140,114,131)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(112,91,105)"/>
</g>
<g transform="translate(0 0)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(163,142,142)"/>
<path d="M 86.60254037844388,50.0 L 173.20508075688775,0 173.20508075688775,100 86.60254037844388,150.0 86.60254037844388,50.0" fill="rgb(140,114,114)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(112,91,91)"/>
</g>
<g transform="translate(115.47005383792516 66.66666666666667)">
<path d="M 0,0 L 86.60254037844388,-50.0 173.20508075688775,0 86.60254037844388,50.0 0,0" fill="rgb(163,156,142)"/>
<path d="M 0,0 L 86.60254037844388,50.0 86.60254037844388,150.0 0,100 0,0" fill="rgb(112,105,91)"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256">
<g transform="translate(128, 128)" stroke="#808080" stroke-linecap="round" stroke-width="28">
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(0)" opacity="1"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(30)" opacity="0.083333"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(60)" opacity="0.166667"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(90)" opacity="0.25"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(120)" opacity="0.333333"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(150)" opacity="0.416667"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(180)" opacity="0.5"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(210)" opacity="0.583333"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(240)" opacity="0.666667"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(270)" opacity="0.75"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(300)" opacity="0.833333"/>
<line x1="0" y1="-114" x2="0" y2="-70" transform="rotate(330)" opacity="0.916667"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB