diff --git a/pype/ftrack/actions/action_application_loader.py b/pype/ftrack/actions/action_application_loader.py index 67a158db14..bc126fc691 100644 --- a/pype/ftrack/actions/action_application_loader.py +++ b/pype/ftrack/actions/action_application_loader.py @@ -37,6 +37,9 @@ def registerApp(app, session): description = apptoml.get('description', None) preactions = apptoml.get('preactions', []) + if icon: + icon = icon.format(os.environ.get('PYPE_STATICS_SERVER', '')) + # register action AppAction( session, label, name, executable, variant, diff --git a/pype/ftrack/actions/action_component_open.py b/pype/ftrack/actions/action_component_open.py index c40a04b2fd..d3213c555a 100644 --- a/pype/ftrack/actions/action_component_open.py +++ b/pype/ftrack/actions/action_component_open.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import subprocess -import os from pype.vendor import ftrack_api from pype.ftrack import BaseAction @@ -15,9 +15,8 @@ class ComponentOpen(BaseAction): # Action label label = 'Open File' # Action icon - icon = ( - 'https://cdn4.iconfinder.com/data/icons/rcons-application/32/' - 'application_go_run-256.png' + icon = '{}/ftrack/action_icons/ComponentOpen.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_cust_attrs.py b/pype/ftrack/actions/action_create_cust_attrs.py index 9f2564406a..7dd8335ecc 100644 --- a/pype/ftrack/actions/action_create_cust_attrs.py +++ b/pype/ftrack/actions/action_create_cust_attrs.py @@ -114,10 +114,8 @@ class CustomAttributes(BaseAction): description = 'Creates Avalon/Mongo ID for double check' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-4/512/' - 'Bullet_list_menu_lines_points_items_options-512.png' + icon = '{}/ftrack/action_icons/CustomAttributes.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def __init__(self, session): diff --git a/pype/ftrack/actions/action_create_folders.py b/pype/ftrack/actions/action_create_folders.py index 9c8f576e3b..2a777911b4 100644 --- a/pype/ftrack/actions/action_create_folders.py +++ b/pype/ftrack/actions/action_create_folders.py @@ -22,10 +22,10 @@ class CreateFolders(BaseAction): label = 'Create Folders' #: Action Icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '698620-icon-105-folder-add-512.png' + icon = '{}/ftrack/action_icons/CreateFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) + db = DbConnector() def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_create_project_folders.py b/pype/ftrack/actions/action_create_project_folders.py index dba2a88f02..3ccdb08714 100644 --- a/pype/ftrack/actions/action_create_project_folders.py +++ b/pype/ftrack/actions/action_create_project_folders.py @@ -20,9 +20,8 @@ class CreateProjectFolders(BaseAction): description = 'Creates folder structure' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'buttons-9/512/Button_Add-01.png' + icon = '{}/ftrack/action_icons/CreateProjectFolders.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) pattern_array = re.compile('\[.*\]') diff --git a/pype/ftrack/actions/action_delete_asset.py b/pype/ftrack/actions/action_delete_asset.py index 838a77570f..eabadecee6 100644 --- a/pype/ftrack/actions/action_delete_asset.py +++ b/pype/ftrack/actions/action_delete_asset.py @@ -1,3 +1,4 @@ +import os import sys import logging from bson.objectid import ObjectId @@ -16,10 +17,8 @@ class DeleteAsset(BaseAction): label = 'Delete Asset/Subsets' #: Action description. description = 'Removes from Avalon with all childs and asset from Ftrack' - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Delete_dustbin_empty_recycle_recycling_remove_trash-512.png' + icon = '{}/ftrack/action_icons/DeleteAsset.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] diff --git a/pype/ftrack/actions/action_delete_asset_byname.py b/pype/ftrack/actions/action_delete_asset_byname.py index 9da60ce763..fa966096a8 100644 --- a/pype/ftrack/actions/action_delete_asset_byname.py +++ b/pype/ftrack/actions/action_delete_asset_byname.py @@ -1,3 +1,4 @@ +import os import sys import logging import argparse @@ -17,10 +18,8 @@ class AssetsRemover(BaseAction): description = 'Removes assets from Ftrack and Avalon db with all childs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/' - 'ios-web-user-interface-multi-circle-flat-vol-5/512/' - 'Clipboard_copy_delete_minus_paste_remove-512.png' + icon = '{}/ftrack/action_icons/AssetsRemover.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: Db db = DbConnector() diff --git a/pype/ftrack/actions/action_djvview.py b/pype/ftrack/actions/action_djvview.py index 29d4604968..e0c0334e5f 100644 --- a/pype/ftrack/actions/action_djvview.py +++ b/pype/ftrack/actions/action_djvview.py @@ -16,7 +16,9 @@ class DJVViewAction(BaseAction): identifier = "djvview-launch-action" label = "DJV View" description = "DJV View Launcher" - icon = "http://a.fsdn.com/allura/p/djv/icon" + icon = '{}/app_icons/djvView.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): diff --git a/pype/ftrack/actions/action_job_killer.py b/pype/ftrack/actions/action_job_killer.py index 8cbddecb1c..44acb24d55 100644 --- a/pype/ftrack/actions/action_job_killer.py +++ b/pype/ftrack/actions/action_job_killer.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -18,9 +19,8 @@ class JobKiller(BaseAction): description = 'Killing selected running jobs' #: roles that are allowed to register this action role_list = ['Pypeclub', 'Administrator'] - icon = ( - 'https://cdn2.iconfinder.com/data/icons/new-year-resolutions/64/' - 'resolutions-23-512.png' + icon = '{}/ftrack/action_icons/JobKiller.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_multiple_notes.py b/pype/ftrack/actions/action_multiple_notes.py index 2d93f64242..338083fe47 100644 --- a/pype/ftrack/actions/action_multiple_notes.py +++ b/pype/ftrack/actions/action_multiple_notes.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class MultipleNotes(BaseAction): label = 'Multiple Notes' #: Action description. description = 'Add same note to multiple Asset Versions' - icon = ( - 'https://cdn2.iconfinder.com/data/icons/' - 'mixed-rounded-flat-icon/512/note_1-512.png' + icon = '{}/ftrack/action_icons/MultipleNotes.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_rv.py b/pype/ftrack/actions/action_rv.py index f07d339e5c..3832dffae4 100644 --- a/pype/ftrack/actions/action_rv.py +++ b/pype/ftrack/actions/action_rv.py @@ -17,7 +17,9 @@ class RVAction(BaseAction): identifier = "rv.launch.action" label = "rv" description = "rv Launcher" - icon = "https://img.icons8.com/color/48/000000/circled-play.png" + icon = '{}/ftrack/action_icons/RV.png'.format( + os.environ.get('PYPE_STATICS_SERVER', '') + ) type = 'Application' def __init__(self, session): diff --git a/pype/ftrack/actions/action_sync_to_avalon_local.py b/pype/ftrack/actions/action_sync_to_avalon_local.py index 1056b5ee55..54fd0b47f8 100644 --- a/pype/ftrack/actions/action_sync_to_avalon_local.py +++ b/pype/ftrack/actions/action_sync_to_avalon_local.py @@ -50,9 +50,8 @@ class SyncToAvalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon-local.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) #: roles that are allowed to register this action role_list = ['Pypeclub'] diff --git a/pype/ftrack/actions/action_test.py b/pype/ftrack/actions/action_test.py index 36adb99074..dcb9dd32d0 100644 --- a/pype/ftrack/actions/action_test.py +++ b/pype/ftrack/actions/action_test.py @@ -1,8 +1,8 @@ +import os import sys import argparse import logging import collections -import os import json import re @@ -27,9 +27,8 @@ class TestAction(BaseAction): priority = 10000 #: roles that are allowed to register this action role_list = ['Pypeclub'] - icon = ( - 'https://cdn4.iconfinder.com/data/icons/hospital-19/512/' - '8_hospital-512.png' + icon = '{}/ftrack/action_icons/TestAction.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToChildern.py b/pype/ftrack/actions/action_thumbToChildern.py index 5b63ec264f..4e7f1298f5 100644 --- a/pype/ftrack/actions/action_thumbToChildern.py +++ b/pype/ftrack/actions/action_thumbToChildern.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -15,9 +16,8 @@ class ThumbToChildren(BaseAction): # Action label label = 'Thumbnail to Children' # Action icon - icon = ( - 'https://cdn3.iconfinder.com/data/icons/transfers/100/' - '239322-download_transfer-128.png' + icon = '{}/ftrack/action_icons/thumbToChildren.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/actions/action_thumbToParent.py b/pype/ftrack/actions/action_thumbToParent.py index eb5623328e..632d2a50b2 100644 --- a/pype/ftrack/actions/action_thumbToParent.py +++ b/pype/ftrack/actions/action_thumbToParent.py @@ -1,3 +1,4 @@ +import os import sys import argparse import logging @@ -14,9 +15,8 @@ class ThumbToParent(BaseAction): # Action label label = 'Thumbnail to Parent' # Action icon - icon = ( - "https://cdn3.iconfinder.com/data/icons/transfers/100/" - "239419-upload_transfer-512.png" + icon = '{}/ftrack/action_icons/thumbToParent.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def discover(self, session, entities, event): diff --git a/pype/ftrack/events/action_sync_to_avalon.py b/pype/ftrack/events/action_sync_to_avalon.py index 22358cd775..dd9534a764 100644 --- a/pype/ftrack/events/action_sync_to_avalon.py +++ b/pype/ftrack/events/action_sync_to_avalon.py @@ -49,9 +49,8 @@ class Sync_To_Avalon(BaseAction): #: Action description. description = 'Send data from Ftrack to Avalon' #: Action icon. - icon = ( - 'https://cdn1.iconfinder.com/data/icons/hawcons/32/' - '699650-icon-92-inbox-download-512.png' + icon = '{}/ftrack/action_icons/SyncToAvalon.svg'.format( + os.environ.get('PYPE_STATICS_SERVER', '') ) def register(self): diff --git a/pype/services/statics_server/__init__.py b/pype/services/statics_server/__init__.py new file mode 100644 index 0000000000..4b2721b18b --- /dev/null +++ b/pype/services/statics_server/__init__.py @@ -0,0 +1,5 @@ +from .statics_server import StaticsServer + + +def tray_init(tray_widget, main_widget): + return StaticsServer() diff --git a/pype/services/statics_server/statics_server.py b/pype/services/statics_server/statics_server.py new file mode 100644 index 0000000000..90048d2ee2 --- /dev/null +++ b/pype/services/statics_server/statics_server.py @@ -0,0 +1,190 @@ +import os +import socket +import http.server +import urllib +import posixpath +import socketserver + +from Qt import QtCore +from pypeapp import config, Logger + + +DIRECTORY = os.path.sep.join([os.environ['PYPE_MODULE_ROOT'], 'res']) + + +class Handler(http.server.SimpleHTTPRequestHandler): + directory=DIRECTORY + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + parts = urllib.parse.urlsplit(self.path) + if not parts.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(HTTPStatus.MOVED_PERMANENTLY) + new_parts = (parts[0], parts[1], parts[2] + '/', + parts[3], parts[4]) + new_url = urllib.parse.urlunsplit(new_parts) + self.send_header("Location", new_url) + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + f = open(path, 'rb') + except OSError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return None + + try: + fs = os.fstat(f.fileno()) + # Use browser cache if possible + if ("If-Modified-Since" in self.headers + and "If-None-Match" not in self.headers): + # compare If-Modified-Since and time of last file modification + try: + ims = email.utils.parsedate_to_datetime( + self.headers["If-Modified-Since"]) + except (TypeError, IndexError, OverflowError, ValueError): + # ignore ill-formed values + pass + else: + if ims.tzinfo is None: + # obsolete format with no timezone, cf. + # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 + ims = ims.replace(tzinfo=datetime.timezone.utc) + if ims.tzinfo is datetime.timezone.utc: + # compare to UTC datetime of last modification + last_modif = datetime.datetime.fromtimestamp( + fs.st_mtime, datetime.timezone.utc) + # remove microseconds, like in If-Modified-Since + last_modif = last_modif.replace(microsecond=0) + + if last_modif <= ims: + self.send_response(HTTPStatus.NOT_MODIFIED) + self.end_headers() + f.close() + return None + + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", ctype) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", + self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + # Don't forget explicit trailing slash when normalizing. Issue17324 + trailing_slash = path.rstrip().endswith('/') + try: + path = urllib.parse.unquote(path, errors='surrogatepass') + except UnicodeDecodeError: + path = urllib.parse.unquote(path) + path = posixpath.normpath(path) + words = path.split('/') + words = filter(None, words) + path = self.directory + for word in words: + if os.path.dirname(word) or word in (os.curdir, os.pardir): + # Ignore components that are not a simple file/directory name + continue + path = os.path.join(path, word) + if trailing_slash: + path += '/' + return path + + +class StaticsServer(QtCore.QThread): + """ Measure user's idle time in seconds. + Idle time resets on keyboard/mouse input. + Is able to emit signals at specific time idle. + """ + + def __init__(self): + super(StaticsServer, self).__init__() + self._is_running = False + self._failed = False + self.log = Logger().get_logger(self.__class__.__name__) + try: + self.presets = config.get_presets().get( + 'services', {}).get('statics_server') + except Exception: + self.presets = {'default_port': 8010, 'exclude_ports': []} + + self.port = self.find_port() + + def tray_start(self): + self.start() + + @property + def is_running(self): + return self._is_running + + @property + def failed(self): + return self._failed + + def stop(self): + self._is_running = False + + def run(self): + self._is_running = True + try: + with socketserver.TCPServer(("", self.port), Handler) as httpd: + while self._is_running: + httpd.handle_request() + except Exception: + self._failed = True + self._is_running = False + + def find_port(self): + start_port = self.presets['default_port'] + exclude_ports = self.presets['exclude_ports'] + found_port = None + # port check takes time so it's lowered to 100 ports + for port in range(start_port, start_port+100): + if port in exclude_ports: + continue + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + result = sock.connect_ex(('localhost', port)) + if result != 0: + found_port = port + if found_port is not None: + break + if found_port is None: + return None + os.environ['PYPE_STATICS_SERVER'] = 'http://localhost:{}'.format(found_port) + return found_port diff --git a/res/app_icons/Aport.png b/res/app_icons/Aport.png new file mode 100644 index 0000000000..0a6816513a Binary files /dev/null and b/res/app_icons/Aport.png differ diff --git a/res/app_icons/clockify-white.png b/res/app_icons/clockify-white.png new file mode 100644 index 0000000000..2803049fbe Binary files /dev/null and b/res/app_icons/clockify-white.png differ diff --git a/res/app_icons/clockify.png b/res/app_icons/clockify.png new file mode 100644 index 0000000000..ac4c44c763 Binary files /dev/null and b/res/app_icons/clockify.png differ diff --git a/res/app_icons/djvView.png b/res/app_icons/djvView.png new file mode 100644 index 0000000000..854604d57f Binary files /dev/null and b/res/app_icons/djvView.png differ diff --git a/res/app_icons/houdini.png b/res/app_icons/houdini.png new file mode 100644 index 0000000000..11cfa46dce Binary files /dev/null and b/res/app_icons/houdini.png differ diff --git a/res/app_icons/maya.png b/res/app_icons/maya.png new file mode 100644 index 0000000000..e84a6a3742 Binary files /dev/null and b/res/app_icons/maya.png differ diff --git a/res/app_icons/nuke.png b/res/app_icons/nuke.png new file mode 100644 index 0000000000..4234454096 Binary files /dev/null and b/res/app_icons/nuke.png differ diff --git a/res/app_icons/premiere.png b/res/app_icons/premiere.png new file mode 100644 index 0000000000..eb5b3d1ba2 Binary files /dev/null and b/res/app_icons/premiere.png differ diff --git a/res/app_icons/python.png b/res/app_icons/python.png new file mode 100644 index 0000000000..b3b5b2220a Binary files /dev/null and b/res/app_icons/python.png differ diff --git a/res/ftrack/action_icons/AssetsRemover.svg b/res/ftrack/action_icons/AssetsRemover.svg new file mode 100644 index 0000000000..e838ee9f28 --- /dev/null +++ b/res/ftrack/action_icons/AssetsRemover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/ComponentOpen.svg b/res/ftrack/action_icons/ComponentOpen.svg new file mode 100644 index 0000000000..6d4eba6839 --- /dev/null +++ b/res/ftrack/action_icons/ComponentOpen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/CreateFolders.svg b/res/ftrack/action_icons/CreateFolders.svg new file mode 100644 index 0000000000..c07e474e5c --- /dev/null +++ b/res/ftrack/action_icons/CreateFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CreateProjectFolders.svg b/res/ftrack/action_icons/CreateProjectFolders.svg new file mode 100644 index 0000000000..5fa653361e --- /dev/null +++ b/res/ftrack/action_icons/CreateProjectFolders.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/CustomAttributes.svg b/res/ftrack/action_icons/CustomAttributes.svg new file mode 100644 index 0000000000..6d73746ed0 --- /dev/null +++ b/res/ftrack/action_icons/CustomAttributes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/DeleteAsset.svg b/res/ftrack/action_icons/DeleteAsset.svg new file mode 100644 index 0000000000..a41ae31d12 --- /dev/null +++ b/res/ftrack/action_icons/DeleteAsset.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/JobKiller.svg b/res/ftrack/action_icons/JobKiller.svg new file mode 100644 index 0000000000..595c780a9b --- /dev/null +++ b/res/ftrack/action_icons/JobKiller.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/MultipleNotes.svg b/res/ftrack/action_icons/MultipleNotes.svg new file mode 100644 index 0000000000..6ed916f1aa --- /dev/null +++ b/res/ftrack/action_icons/MultipleNotes.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/RV.png b/res/ftrack/action_icons/RV.png new file mode 100644 index 0000000000..741e7a9772 Binary files /dev/null and b/res/ftrack/action_icons/RV.png differ diff --git a/res/ftrack/action_icons/SyncToAvalon-local.svg b/res/ftrack/action_icons/SyncToAvalon-local.svg new file mode 100644 index 0000000000..bf4708e8a5 --- /dev/null +++ b/res/ftrack/action_icons/SyncToAvalon-local.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/ftrack/action_icons/SyncToAvalon.svg b/res/ftrack/action_icons/SyncToAvalon.svg new file mode 100644 index 0000000000..48071b2430 --- /dev/null +++ b/res/ftrack/action_icons/SyncToAvalon.svg @@ -0,0 +1,67 @@ + + + + + + diff --git a/res/ftrack/action_icons/TestAction.svg b/res/ftrack/action_icons/TestAction.svg new file mode 100644 index 0000000000..771644340e --- /dev/null +++ b/res/ftrack/action_icons/TestAction.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/res/ftrack/action_icons/thumbToChildren.svg b/res/ftrack/action_icons/thumbToChildren.svg new file mode 100644 index 0000000000..30b146803e --- /dev/null +++ b/res/ftrack/action_icons/thumbToChildren.svg @@ -0,0 +1,88 @@ + + + + + + diff --git a/res/ftrack/action_icons/thumbToParent.svg b/res/ftrack/action_icons/thumbToParent.svg new file mode 100644 index 0000000000..254b650306 --- /dev/null +++ b/res/ftrack/action_icons/thumbToParent.svg @@ -0,0 +1,95 @@ + + + + + +