├── app ├── config │ └── params.py ├── setup.bat ├── requirements.txt ├── templates │ ├── error.html │ ├── new_user.html │ ├── update_config.html │ ├── dll_error.html │ ├── new_user_manual.html │ ├── base.html │ └── home.html ├── modules │ ├── func_timer.py │ ├── view_logic.py │ ├── img_proc.py │ ├── registration.py │ ├── startup.py │ ├── hue_interface.py │ ├── sb_controller.py │ ├── presets.py │ ├── icon_names.py │ └── utility.py ├── setup.py ├── static │ └── js │ │ ├── colorWave.js │ │ ├── screenBloomPresets.js │ │ └── zoneselect.js └── screenbloom.py ├── website ├── scripts │ └── create_db.py ├── static │ ├── images │ │ ├── ip.png │ │ ├── large.jpg │ │ ├── small.jpg │ │ ├── console.png │ │ ├── favicon.png │ │ └── batthern.png │ ├── js │ │ ├── screenbloom.js │ │ └── screenBloomAnalytics.js │ └── css │ │ └── screenbloom.css ├── models.py ├── routes.py └── templates │ ├── analytics.html │ └── welcome.html ├── .gitignore └── readme.md /app/config/params.py: -------------------------------------------------------------------------------- 1 | ENV = 'dev' 2 | BUILD = 'win' 3 | VERSION = 2.2 4 | -------------------------------------------------------------------------------- /website/scripts/create_db.py: -------------------------------------------------------------------------------- 1 | from website import db 2 | 3 | 4 | db.create_all(bind=['sb_db']) 5 | -------------------------------------------------------------------------------- /website/static/images/ip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/ip.png -------------------------------------------------------------------------------- /website/static/images/large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/large.jpg -------------------------------------------------------------------------------- /website/static/images/small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/small.jpg -------------------------------------------------------------------------------- /app/setup.bat: -------------------------------------------------------------------------------- 1 | C:\Programming\Python\screenbloom\app\venv\scripts\python C:\Programming\Python\screenbloom\app\setup.py build -------------------------------------------------------------------------------- /website/static/images/console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/console.png -------------------------------------------------------------------------------- /website/static/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/favicon.png -------------------------------------------------------------------------------- /website/static/images/batthern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kershner/screenBloom/HEAD/website/static/images/batthern.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | 6 | app/static/images/* 7 | app/backup/* 8 | app/tests/* 9 | website/* 10 | 11 | config.cfg 12 | 13 | randomColor.js 14 | *.txt 15 | color_converter.py 16 | rgb_cie.py 17 | ssdp.py 18 | setup.py 19 | __init__.py -------------------------------------------------------------------------------- /app/requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.2 2 | backports-abc==0.5 3 | certifi==2017.1.23 4 | click==6.7 5 | Desktopmagic==14.3.11 6 | Flask==0.12 7 | itsdangerous==0.24 8 | Jinja2==2.9.5 9 | MarkupSafe==0.23 10 | olefile==0.44 11 | packaging==16.8 12 | Pillow==4.0.0 13 | pyparsing==2.1.10 14 | requests==2.13.0 15 | singledispatch==3.4.0.3 16 | six==1.10.0 17 | tornado==4.4.2 18 | Werkzeug==0.11.15 19 | -------------------------------------------------------------------------------- /website/models.py: -------------------------------------------------------------------------------- 1 | from website import db 2 | 3 | 4 | class Download(db.Model): 5 | __bind_key__ = 'sb_db' 6 | 7 | id = db.Column(db.Integer, primary_key=True) 8 | date = db.Column(db.DateTime()) 9 | version = db.Column(db.String(64)) 10 | build = db.Column(db.String(64)) 11 | location_info = db.Column(db.String(256)) 12 | user_agent = db.Column(db.String(256)) 13 | -------------------------------------------------------------------------------- /app/templates/error.html: -------------------------------------------------------------------------------- 1 | {% extends "/base.html" %} 2 | {% block content %} 3 |
4 |

Error - {{ code }}

5 |

{{ name }}

6 | 7 |
8 | Sorry about that! 9 |
10 | 11 | {% if code == 500 %} 12 |

Error Text:

13 | 14 |
{{ error }}
15 | {% endif %} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /app/modules/func_timer.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | 3 | 4 | # Execution time decorator 5 | def func_timer(func): 6 | # Nested function for timing other functions 7 | def function_timer(*args, **kwargs): 8 | start = time() 9 | value = func(*args, **kwargs) # Nested function execution 10 | end = time() 11 | runtime = end - start 12 | print '|| [Execution Time] {func}() {time:02.4f} seconds'.format(func=func.__name__, time=runtime) 13 | return value 14 | return function_timer 15 | -------------------------------------------------------------------------------- /app/templates/new_user.html: -------------------------------------------------------------------------------- 1 | {% extends "/base.html" %} 2 | {% block content %} 3 |
4 |

5 | Hello!

6 | 7 |
8 | ScreenBloom needs to register itself with your Philips Hue Bridge.

9 | 1Press the link button on your bridge.
10 | 2Click the button below within 30 seconds to register. 11 |
12 | 13 |
Register ScreenBloom
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/update_config.html: -------------------------------------------------------------------------------- 1 | {% extends "/base.html" %} 2 | {% block content %} 3 | 20 | 21 |
22 |

{{ code }}

23 |

{{ name }}

24 | 25 |
26 | 27 | You're probably upgrading from a previous version of ScreenBloom. 28 |
29 |
30 | To be sure everything runs correctly you'll need to 31 | re-create your config file by syncing with your Hue Bridge. 32 | Your presets will remain intact. 33 |
34 |
35 |
36 | Hit the button to delete and re-create your config file. 37 | 38 |
Regen Config File
39 |
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /app/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | 7 | from cx_Freeze import setup, Executable 8 | 9 | 10 | def files_under_dir(dir_name): 11 | file_list = [] 12 | for root, dirs, files in os.walk(dir_name): 13 | for name in files: 14 | file_list.append(os.path.join(root, name)) 15 | return file_list 16 | 17 | 18 | includefiles = [] 19 | for directory in ("static", "templates", "modules", "config"): 20 | includefiles.extend(files_under_dir(directory)) 21 | 22 | # base = None 23 | base = "Win32GUI" 24 | 25 | build_options = { 26 | "build_exe": { 27 | "packages": ["requests", 28 | "PIL", 29 | "tornado", 30 | "desktopmagic", 31 | "jinja2"], 32 | "excludes": ["jinja2.asyncfilters", "jinja2.asyncsupport", 33 | "tkinter", "collections.sys", 34 | "collections._weakref"], 35 | "include_files": includefiles, 36 | "include_msvcr": True}} 37 | 38 | main_executable = Executable("ScreenBloom.py", base=base, icon="static/images/icon.ico") 39 | setup(name="ScreenBloom", 40 | version="2.2", 41 | description="ScreenBloom", 42 | options=build_options, 43 | executables=[main_executable]) 44 | -------------------------------------------------------------------------------- /app/templates/dll_error.html: -------------------------------------------------------------------------------- 1 | {% extends "/base.html" %} 2 | {% block content %} 3 | 32 | 33 |
34 |

Error - {{ code }}

35 |

{{ name }}

36 | 37 |
38 | Some Windows installations don't have a few DLL files that ScreenBloom relies on to take screenshots. 39 | Grab this 1.7 MB C++ Redistributable Package, restart ScreenBloom, and you'll be all set. 40 | 41 |
42 | Microsoft Visual C++ 2008 Redistributable Package 43 |
44 |
45 |
46 |
47 | {% endblock %} -------------------------------------------------------------------------------- /app/static/js/colorWave.js: -------------------------------------------------------------------------------- 1 | // My colorWave code 2 | // http://codepen.io/kershner/pen/Yyyzjz 3 | (function ($) { 4 | $.fn.colorWave = function (colors) { 5 | function _colorWave(colors, element) { 6 | var finalHtml = '', 7 | text = $(element).text(), 8 | defaultColor = $(element).css('color'), 9 | wait = text.length * 350, 10 | tempHtml = ''; 11 | 12 | // Placing around each letter with class="colorwave" 13 | for (var i = 0; i < text.length; i++) { 14 | tempHtml = '' + text[i] + ''; 15 | finalHtml += tempHtml; 16 | } 17 | $(element).empty().append(finalHtml); 18 | _colorLetters(colors, element, wait, defaultColor); 19 | } 20 | 21 | // Iterates through given color array, applies color to a colorwave span 22 | function _colorLetters(colors, element, wait, defaultColor) { 23 | var counter = (Math.random() * (colors.length + 1)) << 0, 24 | delay = 100, 25 | adjustedWait = wait / 5; 26 | $(element).find('.colorwave').each(function () { 27 | if (counter >= colors.length) { 28 | counter = 0; 29 | } 30 | $(this).animate({ 31 | 'color': colors[counter], 32 | 'bottom': '+=6px' 33 | }, delay); 34 | delay += 75; 35 | counter += 1; 36 | }); 37 | setTimeout(function () { 38 | _removeColor(element, defaultColor); 39 | }, adjustedWait); 40 | } 41 | 42 | // Iterates through color wave spans, returns each to default color 43 | function _removeColor(element, defaultColor) { 44 | var delay = 100; 45 | $(element).find('.colorwave').each(function () { 46 | $(this).animate({ 47 | 'color': defaultColor, 48 | 'bottom': '-=6px' 49 | }, delay); 50 | delay += 75; 51 | }); 52 | } 53 | 54 | return this.each(function () { 55 | _colorWave(colors, this); 56 | }); 57 | } 58 | }(jQuery)); -------------------------------------------------------------------------------- /app/templates/new_user_manual.html: -------------------------------------------------------------------------------- 1 | {% extends "/base.html" %} 2 | {% block content %} 3 |
4 |

Hello!

5 | 6 |
7 |
8 | 9 | 10 |
11 |

Your bridge's IP can be found in a few ways

12 | 13 | 1 14 |

If you're currently connected to the same network as your bridge, you should be able to 15 | find your IP in the result returned by this URL:
16 | Click Here 17 |

18 | 19 | 2 20 |

21 | Log in to your account at meethue.com.
22 | Click Settings > My bridge > More bridge details
23 | Your IP address is listed as Internal IP Address 24 |

25 | 26 | 3 27 |

28 | Use an IP scanning program to locate the bridge's IP 29 |

30 |
31 |
32 | 33 | It looks like there was a problem grabbing your Hue Bridge's IP
34 | 1Enter your Hue Bridge's IP Address 35 | Where can I find my Hue Bridge's IP? 36 |
37 | 2Press the link button on your bridge.
38 | 3Click the button below within 30 seconds to register. 39 |
40 | 41 |
Register ScreenBloom
42 |
43 | {% endblock %} -------------------------------------------------------------------------------- /website/routes.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, jsonify 2 | from flask.ext.cors import cross_origin 3 | from screenbloom import models, db 4 | from datetime import datetime 5 | from sqlalchemy import desc 6 | import json 7 | import os 8 | 9 | app = Flask(__name__) 10 | app.secret_key = os.urandom(24) 11 | 12 | 13 | @app.route('/') 14 | def screenbloom(): 15 | current_version = '2.0' 16 | return render_template('/welcome.html', 17 | version=current_version) 18 | 19 | 20 | @app.route('/version-check', methods=['POST', 'OPTIONS']) 21 | @cross_origin(headers='Content-Type') 22 | def version_check(): 23 | if request.method == 'POST': 24 | current_version = 2.0 25 | app_version = float(request.json) 26 | 27 | message = '' 28 | if app_version < current_version: 29 | message = 'A new version is available!' 30 | 31 | data = { 32 | 'message': message 33 | } 34 | return jsonify(data) 35 | else: 36 | return 'You got it, dude' 37 | 38 | 39 | @app.route('/view-download-analytics') 40 | def view_download_analytics(): 41 | return render_template('/analytics.html') 42 | 43 | 44 | @app.route('/get-analytics-data', methods=['POST']) 45 | def get_analytics_data(): 46 | if request.method == 'POST': 47 | dates = request.json 48 | date1 = datetime.strptime(dates['date1'], '%Y-%m-%d') 49 | date2 = datetime.strptime(dates['date2'], '%Y-%m-%d') 50 | 51 | Download = models.Download 52 | downloads = Download.query.filter(Download.date.between(date1, date2)).order_by(desc(Download.id)).all() 53 | new_downloads = [] 54 | for download in downloads: 55 | tmp = { 56 | 'id': download.id, 57 | 'date': download.date, 58 | 'version': download.version, 59 | 'build': download.build, 60 | 'location_info': download.location_info, 61 | 'user_agent': download.user_agent 62 | } 63 | new_downloads.append(tmp) 64 | 65 | response = { 66 | 'downloads': new_downloads 67 | } 68 | 69 | return jsonify(response) 70 | 71 | 72 | # Record download in DB 73 | @app.route('/download-analytics', methods=['POST']) 74 | def download_analytics(): 75 | if request.method == 'POST': 76 | data = request.json 77 | build = data['build'] 78 | version = data['version'] 79 | user_agent = str(request.headers.get('User-Agent')) 80 | 81 | try: 82 | location_info = json.dumps(data['locationInfo']) 83 | except KeyError: 84 | location_info = '' 85 | 86 | new_download = models.Download(date=datetime.now(), 87 | version=version, 88 | build=build, 89 | user_agent=user_agent, 90 | location_info=location_info) 91 | db.session.add(new_download) 92 | db.session.commit() 93 | return 'Hello world!' -------------------------------------------------------------------------------- /app/modules/view_logic.py: -------------------------------------------------------------------------------- 1 | import sb_controller 2 | import hue_interface 3 | import utility 4 | import json 5 | import ast 6 | 7 | 8 | def get_index_data(): 9 | config_dict = utility.get_config_dict() 10 | 11 | hue_ip = config_dict['ip'] 12 | username = config_dict['username'] 13 | auto_start = config_dict['autostart'] 14 | current_preset = config_dict['current_preset'] 15 | 16 | update = config_dict['update'] 17 | update_buffer = config_dict['update_buffer'] 18 | max_bri = config_dict['max_bri'] 19 | min_bri = config_dict['min_bri'] 20 | bulb_settings = json.loads(config_dict['bulb_settings']) 21 | zones = ast.literal_eval(config_dict['zones']) 22 | zone_state = config_dict['zone_state'] 23 | display_index = config_dict['display_index'] 24 | sat = config_dict['sat'] 25 | 26 | party_mode = config_dict['party_mode'] 27 | 28 | state = config_dict['app_state'] 29 | 30 | lights = hue_interface.get_lights_data(hue_ip, username) 31 | for light in lights: 32 | light.append(int(bulb_settings[unicode(light[0])]['max_bri'])) 33 | light.append(int(bulb_settings[unicode(light[0])]['min_bri'])) 34 | light[2] = str(light[2]) 35 | 36 | lights.sort(key=lambda x: x[2]) 37 | presets = utility.get_all_presets() 38 | 39 | icon_size = 10 40 | if len(lights) > 3: 41 | icon_size = 4 42 | 43 | data = { 44 | 'auto_start_state': auto_start, 45 | 'update': update, 46 | 'update_buffer': update_buffer, 47 | 'max_bri': max_bri, 48 | 'min_bri': min_bri, 49 | 'default': config_dict['default'], 50 | 'lights': lights, 51 | 'lights_number': len(lights), 52 | 'icon_size': icon_size, 53 | 'username': username, 54 | 'party_mode': party_mode, 55 | 'zones': zones, 56 | 'zone_state': zone_state, 57 | 'display_index': display_index, 58 | 'sat': sat, 59 | 'presets': presets, 60 | 'current_preset': current_preset, 61 | 'state': state, 62 | } 63 | return data 64 | 65 | 66 | def start_screenbloom(): 67 | config = utility.get_config_dict() 68 | state = config['app_state'] 69 | sb_controller.get_screen_object().bulb_state = 'on' 70 | 71 | if state: 72 | message = 'ScreenBloom already running' 73 | else: 74 | sb_controller.re_initialize() 75 | sb_controller.start() 76 | 77 | message = 'ScreenBloom Started!' 78 | 79 | data = { 80 | 'message': message 81 | } 82 | return data 83 | 84 | 85 | def stop_screenbloom(): 86 | sb_controller.stop() 87 | sb_controller.re_initialize() 88 | sb_controller.update_bulb_default() 89 | 90 | data = { 91 | 'message': 'ScreenBloom stopped' 92 | } 93 | return data 94 | 95 | 96 | def restart_check(): 97 | global t 98 | 99 | try: 100 | if t.isAlive(): 101 | t.join() 102 | sb_controller.start() 103 | else: 104 | sb_controller.re_initialize() 105 | except NameError: 106 | sb_controller.re_initialize() 107 | -------------------------------------------------------------------------------- /app/modules/img_proc.py: -------------------------------------------------------------------------------- 1 | from PIL import ImageEnhance 2 | from config import params 3 | import utility 4 | 5 | if params.BUILD == 'win': 6 | from desktopmagic.screengrab_win32 import getDisplaysAsImages, getRectAsImage 7 | else: 8 | from PIL import ImageGrab 9 | 10 | 11 | LOW_THRESHOLD = 10 12 | MID_THRESHOLD = 40 13 | HIGH_THRESHOLD = 240 14 | 15 | 16 | # Return avg color of all pixels and ratio of dark pixels for a given image 17 | def img_avg(img): 18 | dark_pixels = 1 19 | mid_range_pixels = 1 20 | total_pixels = 1 21 | r = 1 22 | g = 1 23 | b = 1 24 | 25 | # Win version of imgGrab does not contain alpha channel 26 | if img.mode == 'RGB': 27 | img.putalpha(0) 28 | 29 | # Create list of pixels 30 | pixels = list(img.getdata()) 31 | 32 | for red, green, blue, alpha in pixels: 33 | # Don't count pixels that are too dark 34 | if red < LOW_THRESHOLD and green < LOW_THRESHOLD and blue < LOW_THRESHOLD: 35 | dark_pixels += 1 36 | # Or too light 37 | elif red > HIGH_THRESHOLD and green > HIGH_THRESHOLD and blue > HIGH_THRESHOLD: 38 | pass 39 | else: 40 | if red < MID_THRESHOLD and green < MID_THRESHOLD and blue < MID_THRESHOLD: 41 | mid_range_pixels += 1 42 | dark_pixels += 1 43 | r += red 44 | g += green 45 | b += blue 46 | total_pixels += 1 47 | 48 | n = len(pixels) 49 | r_avg = r / n 50 | g_avg = g / n 51 | b_avg = b / n 52 | rgb = [r_avg, g_avg, b_avg] 53 | 54 | # If computed average below darkness threshold, set to the threshold 55 | for index, item in enumerate(rgb): 56 | if item <= LOW_THRESHOLD: 57 | rgb[index] = LOW_THRESHOLD 58 | 59 | rgb = (rgb[0], rgb[1], rgb[2]) 60 | 61 | data = { 62 | 'rgb': rgb, 63 | 'dark_ratio': float(dark_pixels) / float(total_pixels) * 100 64 | } 65 | return data 66 | 67 | 68 | # Grabs screenshot of current window, calls img_avg (including on zones if present) 69 | def screen_avg(_screen): 70 | screen_data = {} 71 | 72 | # Win version uses DesktopMagic for multiple displays 73 | if params.BUILD == 'win': 74 | try: 75 | img = getRectAsImage(_screen.bbox) 76 | except IndexError: 77 | utility.display_check(_screen) 78 | img = getRectAsImage(_screen.bbox) 79 | # Mac version uses standard PIL ImageGrab 80 | else: 81 | img = ImageGrab.grab() 82 | 83 | # Resize for performance - this could be a user editable setting 84 | size = (16, 9) 85 | img = img.resize(size) 86 | 87 | # Enhance saturation according to user settings 88 | sat_scale_factor = float(_screen.sat) 89 | if sat_scale_factor > 1.0: 90 | sat_converter = ImageEnhance.Color(img) 91 | img = sat_converter.enhance(sat_scale_factor) 92 | 93 | zone_result = [] 94 | if _screen.zone_state: 95 | for zone in _screen.zones: 96 | box = (int(zone['x1']), int(zone['y1']), int(zone['x2']), int(zone['y2'])) 97 | zone_img = img.copy().crop(box) 98 | zone_data = img_avg(zone_img) 99 | zone_data['bulbs'] = zone['bulbs'] 100 | zone_result.append(zone_data) 101 | 102 | screen_data['zones'] = zone_result 103 | else: 104 | screen_data = img_avg(img) 105 | 106 | return screen_data 107 | 108 | 109 | def get_monitor_screenshots(): 110 | return getDisplaysAsImages() 111 | -------------------------------------------------------------------------------- /website/templates/analytics.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ScreenBloom Analytics 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 87 | -------------------------------------------------------------------------------- /app/modules/registration.py: -------------------------------------------------------------------------------- 1 | import sb_controller 2 | import hue_interface 3 | import ConfigParser 4 | import requests 5 | import utility 6 | import json 7 | import os 8 | 9 | 10 | # Create config file on first run 11 | def create_config(hue_ip, username): 12 | config = ConfigParser.RawConfigParser() 13 | lights = hue_interface.get_lights_list(hue_ip, username) 14 | active = ','.join([str(0) for light in lights]) 15 | 16 | default_bulb_settings = {} 17 | for light in lights: 18 | settings = { 19 | 'max_bri': 254, 20 | 'min_bri': 1 21 | } 22 | default_bulb_settings[light] = settings 23 | 24 | config.add_section('Configuration') 25 | config.set('Configuration', 'hue_ip', hue_ip) 26 | config.set('Configuration', 'username', username) 27 | config.set('Configuration', 'auto_start', 0) 28 | config.set('Configuration', 'current_preset', '') 29 | 30 | config.add_section('Light Settings') 31 | config.set('Light Settings', 'all_lights', ','.join(lights)) 32 | config.set('Light Settings', 'active', active) 33 | config.set('Light Settings', 'bulb_settings', json.dumps(default_bulb_settings)) 34 | config.set('Light Settings', 'update', '0.7') 35 | config.set('Light Settings', 'update_buffer', '0') 36 | config.set('Light Settings', 'default', '') 37 | config.set('Light Settings', 'max_bri', '254') 38 | config.set('Light Settings', 'min_bri', '1') 39 | config.set('Light Settings', 'zones', '[]') 40 | config.set('Light Settings', 'zone_state', 0) 41 | config.set('Light Settings', 'display_index', 0) 42 | config.set('Light Settings', 'sat', 1.0) 43 | 44 | config.add_section('Party Mode') 45 | config.set('Party Mode', 'running', 0) 46 | 47 | config.add_section('App State') 48 | config.set('App State', 'running', False) 49 | 50 | directory = os.getenv('APPDATA') + '\\screenBloom' 51 | if not os.path.exists(directory): 52 | os.makedirs(directory) 53 | 54 | with open(utility.get_config_path(), 'wb') as config_file: 55 | config.write(config_file) 56 | 57 | # Now that the config is created, set initial light setting 58 | utility.write_config('Light Settings', 'default', json.dumps(utility.get_hue_initial_state(hue_ip, username))) 59 | 60 | 61 | def remove_config(): 62 | file_path = utility.get_config_path() 63 | success = True 64 | 65 | try: 66 | os.remove(file_path) 67 | except Exception: 68 | success = False 69 | 70 | return success 71 | 72 | 73 | def register_logic(ip, host): 74 | if not ip: 75 | # Attempting to grab IP from Philips uPNP app 76 | try: 77 | requests.packages.urllib3.disable_warnings() 78 | url = 'https://www.meethue.com/api/nupnp' 79 | r = requests.get(url, verify=False).json() 80 | ip = str(r[0]['internalipaddress']) 81 | except Exception: 82 | # utility.write_traceback() 83 | error_type = 'manual' 84 | error_description = 'Error grabbing Hue IP, redirecting to manual entry...' 85 | data = { 86 | 'success': False, 87 | 'error_type': error_type, 88 | 'error_description': error_description, 89 | 'host': host 90 | } 91 | return data 92 | try: 93 | # Send post request to Hue bridge to register new username, return response as JSON 94 | result = register_device(ip) 95 | temp_result = result[0] 96 | result_type = '' 97 | for k, v in temp_result.items(): 98 | result_type = str(k) 99 | if result_type == 'error': 100 | error_type = result[0]['error']['type'] 101 | error_description = result[0]['error']['description'] 102 | data = { 103 | 'success': False, 104 | 'error_type': str(error_type), 105 | 'error_description': str(error_description) 106 | } 107 | return data 108 | else: 109 | # Successfully paired with bridge, create config file 110 | username = temp_result[result_type]['username'] 111 | create_config(ip, username) 112 | data = { 113 | 'success': True, 114 | 'message': 'Success!' 115 | } 116 | return data 117 | except requests.exceptions.ConnectionError: 118 | data = { 119 | 'success': False, 120 | 'error_type': 'Invalid URL', 121 | 'error_description': 'Something went wrong with the connection, please try again...' 122 | } 123 | return data 124 | except IOError: 125 | data = { 126 | 'success': False, 127 | 'error_type': 'permission', 128 | 'error_description': 'Permission denied, administrator rights needed..' 129 | } 130 | return data 131 | 132 | 133 | # Add username to bridge whitelist 134 | def register_device(hue_ip): 135 | url = 'http://%s/api/' % hue_ip 136 | data = { 137 | 'devicetype': 'ScreenBloom' 138 | } 139 | body = json.dumps(data) 140 | r = requests.post(url, data=body, timeout=5) 141 | return r.json() 142 | -------------------------------------------------------------------------------- /app/modules/startup.py: -------------------------------------------------------------------------------- 1 | from tornado.httpserver import HTTPServer 2 | from tornado.wsgi import WSGIContainer 3 | from tornado.ioloop import IOLoop 4 | from config import params 5 | import sb_controller 6 | import webbrowser 7 | import view_logic 8 | import threading 9 | import utility 10 | import presets 11 | import socket 12 | import json 13 | import os 14 | 15 | 16 | # Class for the start-up process 17 | class StartupThread(threading.Thread): 18 | def __init__(self, host, port, args, app): 19 | super(StartupThread, self).__init__() 20 | self.stoprequest = threading.Event() 21 | self.host = host 22 | self.port = port 23 | self.args = args 24 | self.app = app 25 | self.new_user = False 26 | self.needs_update = False 27 | self.error = False 28 | self.base_url = 'http://%s:%d/' % (self.host, self.port) 29 | self.url = None 30 | 31 | def run(self): 32 | if not self.stoprequest.isSet(): 33 | if params.BUILD == 'win': 34 | # Check if config needs to be moved 35 | utility.move_files_check() 36 | 37 | # Initialize system tray menu 38 | SysTrayMenu(self) 39 | 40 | self.startup_checks() 41 | self.start_server() 42 | 43 | def join(self, timeout=None): 44 | self.stoprequest.set() 45 | super(StartupThread, self).join(timeout) 46 | 47 | def startup_checks(self): 48 | # Check For DLL error 49 | if params.BUILD == 'win': 50 | if not utility.dll_check(): 51 | self.url = self.base_url + 'dll-error' 52 | self.error = True 53 | return 54 | 55 | # Check if config file has been created yet 56 | if os.path.isfile(utility.get_config_path()): 57 | # Check to see if config needs to be updated 58 | if not utility.config_check(): 59 | self.url = self.base_url + 'update-config' 60 | self.needs_update = True 61 | return 62 | else: 63 | presets.update_presets_if_necessary() 64 | config = utility.get_config_dict() 65 | lights_initial_state = json.dumps(utility.get_hue_initial_state(config['ip'], config['username'])) 66 | 67 | # Init Screen object with some first-run defaults 68 | utility.write_config('App State', 'running', False) 69 | utility.write_config('Light Settings', 'default', lights_initial_state) 70 | sb_controller.init() 71 | 72 | self.url = self.base_url 73 | return 74 | else: 75 | # Config file doesn't exist, open New User interface 76 | self.url = self.base_url + 'new-user' 77 | self.new_user = True 78 | return 79 | 80 | def start_server(self): 81 | try: 82 | http_server = HTTPServer(WSGIContainer(self.app)) 83 | http_server.listen(self.port) 84 | 85 | if not self.needs_update and not self.error and not self.new_user: 86 | # Autostart check 87 | if not self.args.silent: 88 | webbrowser.open(self.url) 89 | else: 90 | config = utility.get_config_dict() 91 | auto_start = config['autostart'] 92 | if auto_start: 93 | sb_controller.start() 94 | 95 | # New User / Error / Needs Update - skip autostart 96 | else: 97 | webbrowser.open(self.url) 98 | 99 | IOLoop.instance().start() 100 | 101 | # Handle port collision 102 | except socket.error: 103 | self.port += 1 104 | self.start_server() 105 | 106 | 107 | # System Tray Menu 108 | class SysTrayMenu(object): 109 | def __init__(self, startup_thread, interval=1): 110 | self.interval = interval 111 | self.startup_thread = startup_thread 112 | thread = threading.Thread(target=self.run, args=()) 113 | thread.daemon = True 114 | thread.start() 115 | 116 | def run(self): 117 | from modules.vendor import sys_tray_icon as sys_tray 118 | 119 | while True: 120 | base_path = os.path.dirname(os.path.abspath(__file__)) 121 | if params.ENV == 'dev': 122 | icon_path = os.path.dirname(base_path) + '\\static\\images\\' 123 | else: 124 | icon_path = os.path.dirname(os.path.dirname(base_path)) + '\\' 125 | icon = icon_path + 'icon.ico' 126 | 127 | def open_ui(sys_tray_icon): 128 | url = 'http://%s:%d/' % (self.startup_thread.host, self.startup_thread.port) 129 | webbrowser.open(url) 130 | 131 | def start_sb_thread(sys_tray_icon): 132 | view_logic.start_screenbloom() 133 | 134 | def stop_sb_thread(sys_tray_icon): 135 | view_logic.stop_screenbloom() 136 | 137 | # Small helper to make dynamic 'apply preset' functions 138 | def make_func(preset_number): 139 | def _function(sys_tray_icon): 140 | presets.apply_preset(preset_number) 141 | return _function 142 | 143 | all_presets = utility.get_all_presets() 144 | presets_buffer = [] 145 | for index in all_presets: 146 | preset = all_presets[index] 147 | new_tray_entry = [preset['preset_name'], None, make_func(preset['preset_number'])] 148 | presets_buffer.append(new_tray_entry) 149 | 150 | presets_buffer.sort(key=lambda x: x[0]) 151 | presets_tuple = tuple(tuple(x) for x in presets_buffer) 152 | 153 | hover_text = 'ScreenBloom' 154 | menu_options = (('Home', None, open_ui), 155 | ('Start ScreenBloom', None, start_sb_thread), 156 | ('Stop ScreenBloom', None, stop_sb_thread), 157 | ('Presets', None, presets_tuple)) 158 | 159 | def bye(sys_tray_icon): 160 | os._exit(1) 161 | 162 | sys_tray.SysTrayIcon(icon, hover_text, menu_options, on_quit=bye, default_menu_index=0) 163 | -------------------------------------------------------------------------------- /app/modules/hue_interface.py: -------------------------------------------------------------------------------- 1 | import vendor.rgb_xy as rgb_xy 2 | import sb_controller 3 | import requests 4 | import utility 5 | import json 6 | 7 | 8 | # Return more detailed information about specified lights 9 | def get_lights_data(hue_ip, username): 10 | config = utility.get_config_dict() 11 | 12 | all_lights = [int(i) for i in config['all_lights'].split(',')] 13 | active_bulbs = [int(i) for i in config['active'].split(',')] 14 | lights = [] 15 | 16 | for counter, light in enumerate(all_lights): 17 | result = get_light(hue_ip, username, light) 18 | 19 | if type(result) is dict: # Skip unavailable lights 20 | state = result['state']['on'] 21 | light_name = result['name'] 22 | model_id = result['modelid'] 23 | bri = result['state']['bri'] 24 | 25 | # Setting defaults for non-color bulbs 26 | try: 27 | colormode = result['state']['colormode'] 28 | except KeyError: 29 | colormode = None 30 | 31 | try: 32 | xy = result['state']['xy'] 33 | except KeyError: 34 | xy = [] 35 | 36 | active = light if int(light) in active_bulbs else 0 37 | light_data = [light, state, light_name, active, model_id, bri, xy, colormode] 38 | 39 | lights.append(light_data) 40 | 41 | return lights 42 | 43 | 44 | # Return list of current Hue addressable light IDs 45 | def get_lights_list(hue_ip, username): 46 | lights = get_all_lights(hue_ip, username) 47 | 48 | lights_list = [] 49 | for light in lights: 50 | # Skip "lights" that don't have a bri property 51 | # Probably a Hue light switch or a non-Hue brand product 52 | try: 53 | bri = lights[light]['state']['bri'] 54 | lights_list.append(light) 55 | except KeyError: 56 | continue 57 | 58 | return lights_list 59 | 60 | 61 | # Send on/off Hue API command to bulbs 62 | def lights_on_off(state): 63 | _screen = sb_controller.get_screen_object() 64 | 65 | active_lights = _screen.bulbs 66 | on = True if state == 'on' else False 67 | 68 | for light in active_lights: 69 | state = { 70 | 'on': on, 71 | 'bri': int(_screen.max_bri), 72 | 'transitiontime': _screen.update 73 | } 74 | update_light(_screen.ip, _screen.devicename, light, json.dumps(state)) 75 | 76 | 77 | # Constructs Hue data structure for bulb state change 78 | def get_bulb_state(bulb_settings, rgb_or_xy, bri, update): 79 | bulb_gamut = bulb_settings['gamut'] 80 | gamut = get_rgb_xy_gamut(bulb_gamut) 81 | converter = rgb_xy.Converter(gamut) 82 | 83 | state = { 84 | 'bri': int(bri), 85 | 'transitiontime': utility.get_transition_time(update) 86 | } 87 | 88 | if rgb_or_xy: 89 | if len(rgb_or_xy) > 2: # [R, G, B] vs [X, Y] 90 | try: 91 | hue_color = converter.rgb_to_xy(rgb_or_xy[0], rgb_or_xy[1], rgb_or_xy[2]) 92 | except ZeroDivisionError: 93 | return 94 | else: 95 | hue_color = (rgb_or_xy[0], rgb_or_xy[1]) 96 | 97 | state['xy'] = hue_color 98 | 99 | return json.dumps(state) 100 | 101 | 102 | def get_rgb_xy_gamut(bulb_gamut): 103 | if bulb_gamut == 'A': 104 | return rgb_xy.GamutA 105 | elif bulb_gamut == 'B': 106 | return rgb_xy.GamutB 107 | elif bulb_gamut == 'C': 108 | return rgb_xy.GamutC 109 | 110 | 111 | def get_light(hue_ip, username, light_id): 112 | lights = get_all_lights(hue_ip, username) 113 | return lights[unicode(light_id)] 114 | 115 | 116 | def get_all_lights(hue_ip, username): 117 | url = _get_hue_url(hue_ip, username) 118 | r = requests.get(url) 119 | return r.json() 120 | 121 | 122 | # @func_timer 123 | def update_light(hue_ip, username, light_id, state): 124 | if light_id: 125 | url = _get_hue_url(hue_ip, username, light_id) 126 | try: 127 | r = requests.put(url, data=state) 128 | return r.json() 129 | except Exception as e: 130 | return 131 | 132 | 133 | def _get_hue_url(hue_ip, username, light_id=None): 134 | url = 'http://{bridge_ip}/api/{username}/lights' 135 | if light_id: 136 | url += '/{light_id}/state' 137 | return url.format(bridge_ip=hue_ip, 138 | username=username, 139 | light_id=light_id) 140 | 141 | return url.format(bridge_ip=hue_ip, 142 | username=username) 143 | 144 | 145 | def get_gamut(model_id): 146 | try: 147 | gamut = GAMUTS[model_id]['gamut'] 148 | except KeyError: 149 | gamut = 'B' 150 | return gamut 151 | 152 | # https://developers.meethue.com/documentation/supported-lights 153 | GAMUTS = { 154 | 'LCT001': { 155 | 'name': 'Hue bulb A19', 156 | 'gamut': 'B' 157 | }, 158 | 'LCT007': { 159 | 'name': 'Hue bulb A19', 160 | 'gamut': 'B' 161 | }, 162 | 'LCT010': { 163 | 'name': 'Hue bulb A19', 164 | 'gamut': 'C' 165 | }, 166 | 'LCT014': { 167 | 'name': 'Hue bulb A19', 168 | 'gamut': 'C' 169 | }, 170 | 'LCT002': { 171 | 'name': 'Hue Spot BR30', 172 | 'gamut': 'B' 173 | }, 174 | 'LCT003': { 175 | 'name': 'Hue Spot GU10', 176 | 'gamut': 'B' 177 | }, 178 | 'LCT011': { 179 | 'name': 'Hue BR30', 180 | 'gamut': 'C' 181 | }, 182 | 'LST001': { 183 | 'name': 'Hue LightStrips', 184 | 'gamut': 'A' 185 | }, 186 | 'LLC010': { 187 | 'name': 'Hue Living Colors Iris', 188 | 'gamut': 'A' 189 | }, 190 | 'LLC011': { 191 | 'name': 'Hue Living Colors Bloom', 192 | 'gamut': 'A' 193 | }, 194 | 'LLC012': { 195 | 'name': 'Hue Living Colors Bloom', 196 | 'gamut': 'A' 197 | }, 198 | 'LLC006': { 199 | 'name': 'Living Colors Gen3 Iris*', 200 | 'gamut': 'A' 201 | }, 202 | 'LLC007': { 203 | 'name': 'Living Colors Gen3 Bloom, Aura*', 204 | 'gamut': 'A' 205 | }, 206 | 'LLC013': { 207 | 'name': 'Disney Living Colors', 208 | 'gamut': 'A' 209 | }, 210 | 'LLM001': { 211 | 'name': 'Color Light Module', 212 | 'gamut': 'A' 213 | }, 214 | 'LLC020': { 215 | 'name': 'Hue Go', 216 | 'gamut': 'C' 217 | }, 218 | 'LST002': { 219 | 'name': 'Hue LightStrips Plus', 220 | 'gamut': 'C' 221 | }, 222 | } 223 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## ScreenBloom 2 | 3 | A Flask application to parse a screen's average color and send the value to connected [Philips Hue Bulbs](http://www2.meethue.com/en-us/) 4 | 5 | ![No Man's Sky with ScreenBloom](https://thumbs.gfycat.com/MixedPertinentAtlanticbluetang-size_restricted.gif) 6 | 7 | :information_source: More info at [screenbloom.com](http://screenbloom.com) 8 | 9 | --- 10 | 11 | 12 | * [Settings](#settings) 13 | + [Global Brightness](#global-brightness) 14 | + [Update Speed](#update-speed) 15 | - [Update Buffer](#update-buffer) 16 | - [Transition Speed](#transition-speed) 17 | + [Party Mode](#party-mode) 18 | + [Screen Zones](#screen-zones) 19 | + [Bulbs](#bulbs) 20 | + [Saturation](#saturation) 21 | + [Auto Start](#auto-start) 22 | * [Presets](#presets) 23 | * [Performance Tips](#performance-tips) 24 | + [Hardware](#hardware) 25 | + [Network](#network) 26 | + [Number of displays](#number-of-displays) 27 | + [Number of lights](#number-of-lights) 28 | + [Update Buffer](#update-buffer-1) 29 | * [Command Line Args (Windows version)](#command-line-args-windows-version) 30 | + [Silent Mode](#silent-mode) 31 | * [API](#api) 32 | * [Developers](#developers) 33 | 34 | 35 | 36 | ## Settings 37 | 38 | Basic explanations of the various editable settings within ScreenBloom. 39 | 40 | ### Global Brightness 41 | 42 | Sets a hard limit for how bright or how dim ScreenBloom will be able to tune your lights. Each light has its own min/max brightness settings, but the global value will always take priority. Dynamic brightness can effectively be turned off by setting the min and max values equal to each other. 43 | 44 | ### Update Speed 45 | 46 | Contains two settings: **Update Buffer** and **Transition Speed**. 47 | 48 | #### Update Buffer 49 | 50 | Sets a small delay in between update loops. This feature was introduced to address a problem with various CPUs running the ScreenBloom update loop inconsistently, potentially leading to large delays as the Hue bridge becomes overwhelmed with commands. This setting can provide a huge speedup on older/slower hardware. 51 | 52 | #### Transition Speed 53 | 54 | maps to the Hue API value for the speed of the color transition animation. Lower values will seem more responsive while higher values will be smoother. 55 | 56 | ### Choose Display 57 | 58 | Is a Windows-only feature allowing you to set which display ScreenBloom will parse. 59 | 60 | ### Party Mode 61 | 62 | Sends a random RGB color to each of your selected bulbs using your chosen transition speed. Kind of outside the scope of ScreenBloom but I wanted the functionality and added it on a whim a few years ago. 63 | 64 | ### Screen Zones 65 | 66 | Will divide up the screen into discrete ScreenBloom-parsable zones. A common use case is to split the screen in half and assign each to a light on either side of the room/TV/monitor. 67 | 68 | ### Bulbs 69 | 70 | Is where you select or de-select lights to be included in the ScreenBloom update loop. 71 | 72 | ### Saturation 73 | 74 | Arbitrarily enhances the the color ScreenBloom parses to be more vibrant and saturated. Uses [this method](http://pillow.readthedocs.io/en/3.1.x/reference/ImageEnhance.html#PIL.ImageEnhance.Color) of [PIL/Pillow](http://pillow.readthedocs.io/en/3.1.x/) 75 | 76 | ### Auto Start 77 | 78 | Determines if the ScreenBloom update loop starts automatically after the program is launched. 79 | 80 | 81 | 82 | ## Presets 83 | 84 | ![ScreenBloom Presets Button](http://www.screenbloom.com/static/images/presets.png) 85 | 86 | Saving a preset gathers up all your current settings, including selected bulbs and their individual settings, and saves them as a preset. Presets can be updated by expanding their options menu and clicking **Update**, which overrides the preset with the current ScreenBloom settings. 87 | 88 | ## Performance Tips 89 | 90 | ScreenBloom can be extremely responsive but there are a number of factors that will contribute to how well it performs. 91 | 92 | ### Hardware 93 | 94 | ScreenBloom will run on pretty much anything but you're going to have the best results on a relatively modern quad-core system. There's a pretty wide difference in performance between my beefy desktop gaming PC and my 2014 Macbook Pro, for instance. 95 | 96 | ### Network 97 | 98 | You'll get the best results on a PC with a stable, wired connection. Router configurations and firewalls can also play a role, but I don't have much data about that to say definitively. 99 | 100 | ### Number of lights 101 | 102 | Each light that ScreenBloom addresses during its update loop adds another 2-4 commands that must be processed by the Hue bridge before continuing on to the next set of commands (i.e. the next light). 103 | 104 | Philips recommends a budget of ~10 commands per second to prevent bridge congestion, meaning the more lights being addressed the higher potential for congestion and slowdown. I think the sweet spot is around 5 lights, with 1 light giving the best possible performance and anything under 10 giving pretty acceptable performance. 105 | 106 | ### Update Buffer 107 | 108 | If you're on older hardware or are generally experiencing large delays between ScreenBloom light updates, consider experimenting with the [Update Buffer](#update-buffer) setting (located in the [Update Speed](#update-speed) section). 109 | 110 | ## Command Line Args (Windows version) 111 | 112 | On Windows, ScreenBloom can be launched with command line arguments. This functionality is limited to just silent mode at the moment, I hope to expand it in the future. 113 | 114 | ### Silent Mode 115 | 116 | Use the `-q` or `--silent` args to launch ScreenBloom without opening a browser to the web interface. If you have autostart enabled the ScreenBloom update loop will begin. 117 | 118 | ## API 119 | 120 | Though it wasn't really designed for it from the outset, ScreenBloom is fully addressable and scriptable as a RESTful API. 121 | 122 | Endpoints should be pretty easy to discern from the main [screenbloom.py](https://github.com/kershner/screenBloom/blob/master/app/screenbloom.py) file, starting after the `index()` function. 123 | 124 | Requests can be sent to the ScreenBloom web server: 125 | 126 | ` 127 | http://:/endpoint 128 | ` 129 | 130 | Example to **start** ScreenBloom update loop: 131 | 132 | ` 133 | [GET] http://192.168.0.69:5000/start 134 | ` 135 | 136 | **POST** endpoints accept their parameters in JSON format and will return a JSON response. Take a look at the individual endpoint functions to figure out the exact format it expects. 137 | 138 | ## Developers 139 | 140 | Forks and pull requests very welcome! Don't hesitate to contact me or raise an issue if you have any questions. 141 | 142 | ### Quickstart Guide: 143 | 1. Clone the repo 144 | 2. [Grab the static files](http://www.screenbloom.com/static/distribute/screenbloom_2.2_static_files.zip) 145 | 3. Setup your **[virtualenv](http://python-guide-pt-br.readthedocs.io/en/latest/dev/virtualenvs/)** with Python 2 146 | 4. Install the dependencies with `pip install -r requirements.txt` 147 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {% if title %} 7 | ScreenBloom - {{ title }} 8 | {% else %} 9 | ScreenBloom 10 | {% endif %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 | 27 | 32 | 33 |
34 |
35 | 36 | {% block content %}{% endblock %} 37 | 38 |
39 |
40 | 41 |
42 | 43 | 58 |
59 | 60 |
61 | Secret Goldblum! 62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 125 | 126 | -------------------------------------------------------------------------------- /website/static/js/screenbloom.js: -------------------------------------------------------------------------------- 1 | var siteConfig = { 2 | 'analyticsUrl' : '', 3 | 'gfyGroups' : [], 4 | 'gfyIndex' : 0 5 | }; 6 | 7 | function screenBloom() { 8 | callFakeScreenBloom(); 9 | clickScroll(); 10 | hiddenMenu(); 11 | crappyAnalytics(); 12 | buttonColors(); 13 | moreGifsBtn(); 14 | prevVersionsBtn(); 15 | colorMedia(); 16 | } 17 | 18 | function colorMedia() { 19 | $('.gif-wrapper, .video, .screenshot').each(function () { 20 | var color = randomColor({luminosity: 'dark'}); 21 | $(this).css('border-color', color); 22 | }); 23 | } 24 | 25 | function moreGifsBtn() { 26 | var gfyNames = [ 27 | 'EachCalmArmednylonshrimp', 28 | 'AmusedFantasticFireant', 29 | 'DefiantThickHalicore', 30 | 'DownrightEnormousHypacrosaurus', 31 | 'CompetentWindingAfricangoldencat', 32 | 'CommonNaughtyArkshell', 33 | 'WindyExcellentEasteuropeanshepherd', 34 | 'UnselfishHollowAntipodesgreenparakeet', 35 | 'IckyLargeAnt', 36 | 'NippyImpeccableFreshwatereel', 37 | 'LimpingOrdinaryArcticseal', 38 | 'ActualInfiniteIraniangroundjay', 39 | 'HeartyVigilantCaudata', 40 | 'AptWavyDachshund', 41 | 'SpectacularJoyfulBighorn', 42 | 'AthleticBelatedAoudad', 43 | 'EminentRadiantGelding', 44 | 'GlossyOptimisticItalianbrownbear', 45 | 'ZestyAnchoredCurassow', 46 | 'ScornfulWarpedAztecant' 47 | ]; 48 | 49 | var gifsPerGroup = 4, 50 | counter = 1, 51 | group = [], 52 | groups = []; 53 | 54 | // Sort gfycats into groups to stagger loading 55 | for (var i=0; i' + 78 | ''; 79 | $('.gifs-container').append(html); 80 | } 81 | 82 | colorMedia(); 83 | siteConfig.gfyIndex += 1; 84 | }); 85 | } 86 | 87 | function prevVersionsBtn() { 88 | $('.prev-versions-button').on('click', function () { 89 | $('.prev-versions-wrapper').toggleClass('hidden'); 90 | }); 91 | } 92 | 93 | function buttonColors() { 94 | $('#buttons-container .button').on({ 95 | mouseenter: function () { 96 | var color = randomColor({luminosity: 'dark'}); 97 | $(this).css({ 98 | 'background-color': color 99 | }); 100 | }, 101 | mouseleave: function () { 102 | $(this).css({ 103 | 'background-color': '#FFFFFF' 104 | }); 105 | } 106 | }); 107 | } 108 | 109 | function crappyAnalytics() { 110 | $('.download-btn').on('click', function () { 111 | var version = $(this).data('version'), 112 | build = $(this).data('build'), 113 | downloadData = { 114 | 'build': build, 115 | 'version': version 116 | }; 117 | 118 | // Ajax call for some location info 119 | $.ajax({ 120 | url: 'http://ipinfo.io', 121 | dataType: 'jsonp', 122 | success: function (response) { 123 | downloadData.locationInfo = response; 124 | }, 125 | error: function (stuff, stuff1, stuff2) { 126 | console.log(stuff); 127 | console.log(stuff1); 128 | console.log(stuff2); 129 | }, 130 | complete: function () { 131 | // Ajax call to server to record download in DB 132 | $.ajax({ 133 | url: siteConfig.analyticsUrl, 134 | method: 'POST', 135 | contentType: 'application/json;charset=UTF-8', 136 | data: JSON.stringify(downloadData), 137 | success: function (result) { 138 | console.log(result); 139 | }, 140 | error: function (result) { 141 | console.log(result); 142 | } 143 | }); 144 | } 145 | }); 146 | }); 147 | } 148 | 149 | function callFakeScreenBloom() { 150 | fakeScreenBloom(); 151 | setInterval(fakeScreenBloom, 3000); 152 | } 153 | 154 | function fakeScreenBloom() { 155 | var color = randomColor(), 156 | newBoxShadow = '0 0 10vw 1vw ' + color, 157 | borderBottom = '.5vh solid ' + color, 158 | elements = [ 159 | '#bloom', '#hidden-bloom', '#download-section-title', 160 | '#about-section-title', '#media-section-title', 161 | '#support-section-title', '.poop' 162 | ]; 163 | 164 | for (var i=0; i offset) { 215 | $('#hidden-menu').css('opacity', '1'); 216 | } else { 217 | $('#hidden-menu').css('opacity', '0'); 218 | } 219 | }); 220 | } -------------------------------------------------------------------------------- /app/modules/sb_controller.py: -------------------------------------------------------------------------------- 1 | from func_timer import func_timer 2 | from config import params 3 | from time import sleep 4 | import hue_interface 5 | import threading 6 | import urllib2 7 | import utility 8 | import random 9 | import json 10 | import ast 11 | 12 | if utility.dll_check(): 13 | import img_proc 14 | 15 | 16 | # Class for running ScreenBloom thread 17 | class ScreenBloom(threading.Thread): 18 | def __init__(self, update): 19 | super(ScreenBloom, self).__init__() 20 | self.stoprequest = threading.Event() 21 | self.update = float(update) 22 | 23 | def run(self): 24 | while not self.stoprequest.isSet(): 25 | run() 26 | sleep(.1) 27 | 28 | def join(self, timeout=None): 29 | self.stoprequest.set() 30 | super(ScreenBloom, self).join(timeout) 31 | 32 | 33 | # Class for Screen object to hold values during runtime 34 | class Screen(object): 35 | def __init__(self, ip, devicename, bulbs, bulb_settings, default, 36 | rgb, update, update_buffer, max_bri, min_bri, zones, zone_state, 37 | display_index, party_mode, sat, bbox): 38 | self.ip = ip 39 | self.devicename = devicename 40 | self.bulbs = bulbs 41 | self.bulb_settings = bulb_settings 42 | self.default = default 43 | self.rgb = rgb 44 | self.update = update 45 | self.update_buffer = update_buffer 46 | self.max_bri = max_bri 47 | self.min_bri = min_bri 48 | self.zones = zones 49 | self.zone_state = zone_state 50 | self.display_index = display_index 51 | self.party_mode = party_mode 52 | self.sat = sat 53 | self.bbox = bbox 54 | 55 | 56 | def init(): 57 | atr = initialize() 58 | global _screen 59 | _screen = Screen(*atr) 60 | 61 | 62 | def start(): 63 | global t 64 | screen = get_screen_object() 65 | t = ScreenBloom(screen.update) 66 | t.start() 67 | 68 | hue_interface.lights_on_off('on') 69 | utility.write_config('App State', 'running', True) 70 | 71 | 72 | def stop(): 73 | global t 74 | 75 | try: 76 | t.join() 77 | except NameError: 78 | pass 79 | 80 | utility.write_config('App State', 'running', False) 81 | 82 | 83 | def get_screen_object(): 84 | try: 85 | global _screen 86 | return _screen 87 | except NameError: 88 | init() 89 | return _screen 90 | 91 | 92 | # Grab attributes for screen instance 93 | def initialize(): 94 | config_dict = utility.get_config_dict() 95 | 96 | ip = config_dict['ip'] 97 | username = config_dict['username'] 98 | 99 | max_bri = config_dict['max_bri'] 100 | min_bri = config_dict['min_bri'] 101 | 102 | active_lights = [int(i) for i in config_dict['active'].split(',')] 103 | all_lights = [int(i) for i in config_dict['all_lights'].split(',')] 104 | 105 | # Check selected bulbs vs all known bulbs 106 | bulb_list = [] 107 | for counter, bulb in enumerate(all_lights): 108 | if active_lights[counter]: 109 | bulb_list.append(active_lights[counter]) 110 | else: 111 | bulb_list.append(0) 112 | 113 | bulb_settings = json.loads(config_dict['bulb_settings']) 114 | 115 | update = config_dict['update'] 116 | update_buffer = config_dict['update_buffer'] 117 | 118 | default = config_dict['default'] 119 | 120 | zones = ast.literal_eval(config_dict['zones']) 121 | zone_state = bool(config_dict['zone_state']) 122 | 123 | party_mode = bool(config_dict['party_mode']) 124 | display_index = config_dict['display_index'] 125 | 126 | sat = config_dict['sat'] 127 | 128 | bbox = None 129 | if params.BUILD == 'win': 130 | from desktopmagic.screengrab_win32 import getDisplayRects 131 | bbox = getDisplayRects()[int(display_index)] 132 | 133 | return ip, username, bulb_list, bulb_settings, default, [], \ 134 | update, update_buffer, max_bri, min_bri, zones, zone_state, \ 135 | display_index, party_mode, sat, bbox 136 | 137 | 138 | # Get updated attributes, re-initialize screen object 139 | def re_initialize(): 140 | # Attributes 141 | at = initialize() 142 | 143 | global _screen 144 | _screen = Screen(*at) 145 | 146 | update_bulb_default() 147 | 148 | 149 | # Updates Hue bulbs to specified RGB/brightness value 150 | def update_bulbs(new_rgb, dark_ratio): 151 | screen = get_screen_object() 152 | screen.rgb = new_rgb 153 | active_bulbs = [bulb for bulb in screen.bulbs if bulb] 154 | send_light_commands(active_bulbs, new_rgb, dark_ratio) 155 | 156 | 157 | # Set bulbs to initial color/brightness 158 | def update_bulb_default(): 159 | screen = get_screen_object() 160 | active_bulbs = [bulb for bulb in screen.bulbs if bulb] 161 | 162 | for bulb in active_bulbs: 163 | bulb_settings = screen.bulb_settings[unicode(bulb)] 164 | bulb_initial_state = json.loads(screen.default)[str(bulb)] 165 | 166 | xy = None 167 | if bulb_initial_state['colormode']: 168 | xy = bulb_initial_state['xy'] 169 | 170 | bulb_state = hue_interface.get_bulb_state(bulb_settings, xy, bulb_initial_state['bri'], .8) 171 | hue_interface.update_light(screen.ip, screen.devicename, bulb, bulb_state) 172 | 173 | 174 | # Set bulbs to random RGB 175 | def update_bulb_party(): 176 | screen = get_screen_object() 177 | active_bulbs = [bulb for bulb in screen.bulbs if bulb] 178 | party_color = utility.party_rgb() 179 | send_light_commands(active_bulbs, party_color, 0.0, party=True) 180 | 181 | 182 | def send_light_commands(bulbs, rgb, dark_ratio, party=False): 183 | screen = get_screen_object() 184 | 185 | bulb_states = {} 186 | for bulb in bulbs: 187 | bulb_settings = screen.bulb_settings[unicode(bulb)] 188 | bulb_max_bri = bulb_settings['max_bri'] 189 | bulb_min_bri = bulb_settings['min_bri'] 190 | bri = utility.get_brightness(screen, bulb_max_bri, bulb_min_bri, dark_ratio) 191 | 192 | if party: 193 | rgb = utility.party_rgb() 194 | try: 195 | bri = random.randrange(int(screen.min_bri), int(bri) + 1) 196 | except ValueError: 197 | continue 198 | 199 | try: 200 | bulb_settings = screen.bulb_settings[str(bulb)] 201 | bulb_state = hue_interface.get_bulb_state(bulb_settings, rgb, bri, screen.update) 202 | bulb_states[bulb] = bulb_state 203 | except Exception as e: 204 | print e.message 205 | continue 206 | 207 | for bulb in bulb_states: 208 | hue_interface.update_light(screen.ip, screen.devicename, bulb, bulb_states[bulb]) 209 | 210 | 211 | # Main loop 212 | # @func_timer 213 | def run(): 214 | screen = get_screen_object() 215 | sleep(float(screen.update_buffer)) 216 | 217 | if screen.party_mode: 218 | update_bulb_party() 219 | sleep(float(screen.update)) 220 | else: 221 | results = img_proc.screen_avg(screen) 222 | screenbloom_control_flow(results) 223 | 224 | 225 | def screenbloom_control_flow(screen_avg_results): 226 | try: 227 | # Zone Mode 228 | if 'zones' in screen_avg_results: 229 | for zone in screen_avg_results['zones']: 230 | zone_bulbs = [int(bulb) for bulb in zone['bulbs']] 231 | send_light_commands(zone_bulbs, zone['rgb'], zone['dark_ratio']) 232 | 233 | # Standard Mode 234 | else: 235 | rgb = screen_avg_results['rgb'] 236 | dark_ratio = screen_avg_results['dark_ratio'] 237 | update_bulbs(rgb, dark_ratio) 238 | except urllib2.URLError: 239 | pass 240 | -------------------------------------------------------------------------------- /app/modules/presets.py: -------------------------------------------------------------------------------- 1 | import hue_interface 2 | import ConfigParser 3 | import view_logic 4 | import utility 5 | import random 6 | import json 7 | import os 8 | 9 | 10 | def save_new_preset(): 11 | json_to_write = utility.get_config_dict() 12 | fa_icons = utility.get_fa_class_names() 13 | icon = random.choice(fa_icons) 14 | 15 | if os.path.isfile(utility.get_json_filepath()): 16 | with open(utility.get_json_filepath()) as data_file: 17 | presets = json.load(data_file) 18 | 19 | preset_number = 0 20 | for key in presets: 21 | new_preset_number = int(key[key.find('_') + 1:]) 22 | if new_preset_number > preset_number: 23 | preset_number = new_preset_number 24 | preset_number = str(preset_number + 1) 25 | new_key = 'preset_%s' % preset_number 26 | presets[new_key] = json_to_write 27 | presets[new_key]['preset_name'] = 'Preset %s' % preset_number 28 | presets[new_key]['preset_number'] = int(preset_number) 29 | presets[new_key]['icon_class'] = icon 30 | else: 31 | preset_name = 'preset_1' 32 | preset_number = 1 33 | json_to_write['preset_name'] = 'Preset 1' 34 | json_to_write['preset_number'] = preset_number 35 | json_to_write['icon_class'] = icon 36 | presets = { 37 | preset_name: json_to_write 38 | } 39 | 40 | # Write/Rewrite presets.json with new section 41 | with open(utility.get_json_filepath(), 'w') as data_file: 42 | json.dump(presets, data_file) 43 | 44 | # print '\nSaved new Preset!' 45 | return preset_number 46 | 47 | 48 | def delete_preset(preset_number): 49 | config = ConfigParser.RawConfigParser() 50 | config.read(utility.get_config_path()) 51 | current_preset = config.get('Configuration', 'current_preset') 52 | 53 | with open(utility.get_json_filepath()) as data_file: 54 | presets = json.load(data_file) 55 | key = 'preset_' + str(preset_number) 56 | 57 | if presets[key]['preset_name'] == current_preset: 58 | utility.write_config('Configuration', 'current_preset', '') 59 | 60 | del presets[key] 61 | 62 | with open(utility.get_json_filepath(), 'w') as f: 63 | json.dump(presets, f) 64 | 65 | # print '\nDeleted Preset!' 66 | 67 | 68 | def apply_preset(preset_number): 69 | with open(utility.get_json_filepath()) as data_file: 70 | presets = json.load(data_file) 71 | 72 | preset_index = 'preset_' + str(preset_number) 73 | preset = presets[preset_index] 74 | utility.write_config('Configuration', 'auto_start', preset['autostart']) 75 | utility.write_config('Configuration', 'current_preset', preset['preset_name']) 76 | 77 | utility.write_config('Light Settings', 'min_bri', preset['min_bri']) 78 | utility.write_config('Light Settings', 'max_bri', preset['max_bri']) 79 | utility.write_config('Light Settings', 'update', preset['update']) 80 | utility.write_config('Light Settings', 'update_buffer', preset['update_buffer']) 81 | utility.write_config('Light Settings', 'zone_state', preset['zone_state']) 82 | utility.write_config('Light Settings', 'zones', preset['zones']) 83 | utility.write_config('Light Settings', 'active', preset['active']) 84 | utility.write_config('Light Settings', 'bulb_settings', preset['bulb_settings']) 85 | utility.write_config('Light Settings', 'display_index', preset['display_index']) 86 | 87 | utility.write_config('Party Mode', 'running', preset['party_mode']) 88 | 89 | view_logic.stop_screenbloom() 90 | if preset['autostart']: 91 | view_logic.start_screenbloom() 92 | 93 | return preset 94 | 95 | 96 | def update_preset(preset_number, preset_name, icon): 97 | with open(utility.get_json_filepath()) as data_file: 98 | presets = json.load(data_file) 99 | 100 | preset_to_edit = None 101 | for preset in presets: 102 | if int(preset_number) == presets[preset]['preset_number']: 103 | preset_to_edit = preset 104 | 105 | preset_number = presets[preset_to_edit]['preset_number'] 106 | presets[preset_to_edit] = utility.get_config_dict() 107 | presets[preset_to_edit]['preset_name'] = preset_name 108 | presets[preset_to_edit]['icon_class'] = icon 109 | presets[preset_to_edit]['preset_number'] = preset_number 110 | 111 | with open(utility.get_json_filepath(), 'w') as f: 112 | json.dump(presets, f) 113 | 114 | # print '\nUpdated Preset!' 115 | 116 | 117 | # Checking to see if current presets need to be updated with new version features 118 | # Refactor this 119 | def update_presets_if_necessary(): 120 | needs_update = False 121 | config = utility.get_config_dict() 122 | all_lights = hue_interface.get_lights_list(config['ip'], config['username']) 123 | all_lights_list = [int(i) for i in all_lights] 124 | all_lights_str = ','.join([str(i) for i in all_lights_list]) 125 | 126 | # Return if presets file does not exist yet 127 | try: 128 | with open(utility.get_json_filepath()) as data_file: 129 | presets = json.load(data_file) 130 | except IOError: 131 | return 132 | 133 | current_light_settings = utility.get_current_light_settings() 134 | presets_to_write = {} 135 | for preset_name in presets: 136 | # Check each preset for key errors (new values needing defaults) 137 | preset = presets[preset_name] 138 | bulbs = json.loads(preset['bulb_settings']) 139 | 140 | # Check if active bulbs needs to be updated 141 | active_bulbs = preset['active'] 142 | active_bulbs_list = active_bulbs.split(',') 143 | new_active_bulbs = [] 144 | for index, bulb_id in enumerate(all_lights_list): 145 | try: 146 | bulb = int(active_bulbs_list[index]) 147 | new_active_bulbs.append(bulb) 148 | except IndexError: 149 | needs_update = True 150 | new_active_bulbs.append(0) 151 | 152 | active_bulbs = ','.join([str(i) for i in new_active_bulbs]) 153 | # Add new bulb to current_light_settings if necessary 154 | 155 | for bulb_id in current_light_settings: 156 | try: 157 | bulb = bulbs[bulb_id] 158 | except KeyError: # Add new bulb with default values 159 | needs_update = True 160 | bulb = current_light_settings[bulb_id] 161 | bulb['max_bri'] = 254 162 | bulb['min_bri'] = 1 163 | bulbs[bulb_id] = bulb 164 | 165 | for bulb_id in bulbs: 166 | bulb = bulbs[bulb_id] 167 | bulb_current_settings = current_light_settings[str(bulb_id)] 168 | 169 | # Check bulbs for missing key->value pairs here 170 | try: 171 | model_id = bulb['model_id'] 172 | except KeyError: 173 | needs_update = True 174 | bulb['model_id'] = bulb_current_settings['model_id'] 175 | try: 176 | gamut = bulb['gamut'] 177 | except KeyError: 178 | needs_update = True 179 | bulb['gamut'] = bulb_current_settings['gamut'] 180 | try: 181 | name = bulb['name'] 182 | except KeyError: 183 | needs_update = True 184 | bulb['name'] = bulb_current_settings['name'] 185 | 186 | # Version 2.2 Updates ################################################# 187 | try: 188 | sat = preset['sat'] 189 | except KeyError: 190 | needs_update = True 191 | sat = 1.0 192 | 193 | if needs_update: 194 | preset['bulb_settings'] = json.dumps(bulbs) 195 | preset['active'] = active_bulbs 196 | preset['all_lights'] = all_lights_str 197 | preset['sat'] = sat 198 | presets_to_write[preset_name] = preset 199 | 200 | if needs_update: 201 | # print 'Updating presets...' 202 | with open(utility.get_json_filepath(), 'w') as f: 203 | json.dump(presets_to_write, f) 204 | 205 | current_preset = config['current_preset'] 206 | if current_preset: 207 | for key in presets: 208 | preset = presets[key] 209 | name = preset['preset_name'] 210 | if name == current_preset: 211 | preset_number = key[key.find('_') + 1:] 212 | # print 'Applying %s...' % str(key) 213 | apply_preset(preset_number) 214 | -------------------------------------------------------------------------------- /app/modules/icon_names.py: -------------------------------------------------------------------------------- 1 | preset_icon_names = [ 2 | 'fa-binoculars', 'fa-birthday-cake', 'fa-bitbucket', 'fa-bitbucket-square', 'fa-bitcoin', 'fa-bold', 'fa-bolt', 3 | 'fa-bomb', 'fa-book', 'fa-bookmark', 'fa-bookmark-o', 'fa-briefcase', 'fa-btc', 'fa-bug', 4 | 'fa-building', 'fa-building-o', 'fa-bullhorn', 'fa-bullseye', 'fa-bus', 'fa-buysellads', 'fa-cab', 5 | 'fa-calculator', 'fa-calendar', 'fa-calendar-o', 'fa-camera', 'fa-camera-retro', 'fa-car', 'fa-caret-down', 6 | 'fa-caret-left', 'fa-caret-right', 'fa-caret-square-o-down', 'fa-caret-square-o-left', 'fa-caret-square-o-right', 'fa-caret-square-o-up', 'fa-caret-up', 7 | 'fa-cart-arrow-down', 'fa-cart-plus', 'fa-cc', 'fa-cc-amex', 'fa-cc-discover', 'fa-cc-mastercard', 'fa-cc-paypal', 8 | 'fa-cc-stripe', 'fa-cc-visa', 'fa-certificate', 'fa-chain', 'fa-chain-broken', 'fa-check', 'fa-check-circle', 9 | 'fa-check-circle-o', 'fa-check-square', 'fa-check-square-o', 'fa-chevron-circle-down', 'fa-chevron-circle-left', 'fa-chevron-circle-right', 'fa-chevron-circle-up', 10 | 'fa-chevron-down', 'fa-chevron-left', 'fa-chevron-right', 'fa-chevron-up', 'fa-child', 'fa-circle', 'fa-circle-o', 11 | 'fa-circle-o-notch', 'fa-circle-thin', 'fa-clipboard', 'fa-clock-o', 'fa-close', 'fa-cloud', 'fa-cloud-download', 12 | 'fa-cloud-upload', 'fa-cny', 'fa-code', 'fa-code-fork', 'fa-codepen', 'fa-coffee', 'fa-cog', 13 | 'fa-cogs', 'fa-columns', 'fa-comment', 'fa-comment-o', 'fa-comments', 'fa-comments-o', 'fa-compass', 14 | 'fa-compress', 'fa-connectdevelop', 'fa-copy', 'fa-copyright', 'fa-credit-card', 'fa-crop', 'fa-crosshairs', 15 | 'fa-css3', 'fa-cube', 'fa-cubes', 'fa-cut', 'fa-cutlery', 'fa-dashboard', 'fa-dashcube', 16 | 'fa-database', 'fa-dedent', 'fa-delicious', 'fa-desktop', 'fa-deviantart', 'fa-diamond', 'fa-digg', 17 | 'fa-dollar', 'fa-dot-circle-o', 'fa-download', 'fa-dribbble', 'fa-dropbox', 'fa-drupal', 'fa-edit', 18 | 'fa-eject', 'fa-ellipsis-h', 'fa-ellipsis-v', 'fa-empire', 'fa-envelope', 'fa-envelope-o', 'fa-envelope-square', 19 | 'fa-eraser', 'fa-eur', 'fa-euro', 'fa-exchange', 'fa-exclamation', 'fa-exclamation-circle', 'fa-exclamation-triangle', 20 | 'fa-expand', 'fa-external-link', 'fa-external-link-square', 'fa-eye', 'fa-eye-slash', 'fa-eyedropper', 'fa-facebook', 21 | 'fa-facebook-f', 'fa-facebook-official', 'fa-facebook-square', 'fa-fast-backward', 'fa-fast-forward', 'fa-fax', 'fa-female', 22 | 'fa-fighter-jet', 'fa-file', 'fa-file-archive-o', 'fa-file-audio-o', 'fa-file-code-o', 'fa-file-excel-o', 'fa-file-image-o', 23 | 'fa-file-movie-o', 'fa-file-o', 'fa-file-pdf-o', 'fa-file-photo-o', 'fa-file-picture-o', 'fa-file-powerpoint-o', 'fa-file-sound-o', 24 | 'fa-file-text', 'fa-file-text-o', 'fa-file-video-o', 'fa-file-word-o', 'fa-file-zip-o', 'fa-files-o', 'fa-film', 25 | 'fa-filter', 'fa-fire', 'fa-fire-extinguisher', 'fa-flag', 'fa-flag-checkered', 'fa-flag-o', 'fa-flash', 26 | 'fa-flask', 'fa-flickr', 'fa-floppy-o', 'fa-folder', 'fa-folder-o', 'fa-folder-open', 'fa-folder-open-o', 27 | 'fa-font', 'fa-forumbee', 'fa-forward', 'fa-foursquare', 'fa-frown-o', 'fa-futbol-o', 'fa-gamepad', 28 | 'fa-gavel', 'fa-gbp', 'fa-ge', 'fa-gear', 'fa-gears', 'fa-genderless', 'fa-gift', 29 | 'fa-git', 'fa-git-square', 'fa-github', 'fa-github-alt', 'fa-github-square', 'fa-gittip', 'fa-glass', 30 | 'fa-globe', 'fa-google', 'fa-google-plus', 'fa-google-plus-square', 'fa-google-wallet', 'fa-graduation-cap', 'fa-gratipay', 31 | 'fa-group', 'fa-h-square', 'fa-hacker-news', 'fa-hand-o-down', 'fa-hand-o-left', 'fa-hand-o-right', 'fa-hand-o-up', 32 | 'fa-hdd-o', 'fa-header', 'fa-headphones', 'fa-heart', 'fa-heart-o', 'fa-heartbeat', 'fa-history', 33 | 'fa-home', 'fa-hospital-o', 'fa-hotel', 'fa-html5', 'fa-ils', 'fa-image', 'fa-inbox', 34 | 'fa-indent', 'fa-info', 'fa-info-circle', 'fa-inr', 'fa-instagram', 'fa-institution', 'fa-ioxhost', 35 | 'fa-italic', 'fa-joomla', 'fa-jpy', 'fa-jsfiddle', 'fa-key', 'fa-keyboard-o', 'fa-krw', 36 | 'fa-language', 'fa-laptop', 'fa-lastfm', 'fa-lastfm-square', 'fa-leaf', 'fa-leanpub', 'fa-legal', 37 | 'fa-lemon-o', 'fa-level-down', 'fa-level-up', 'fa-life-bouy', 'fa-life-buoy', 'fa-life-ring', 'fa-life-saver', 38 | 'fa-lightbulb-o', 'fa-line-chart', 'fa-link', 'fa-linkedin', 'fa-linkedin-square', 'fa-linux', 'fa-list', 39 | 'fa-list-alt', 'fa-list-ol', 'fa-list-ul', 'fa-location-arrow', 'fa-lock', 'fa-long-arrow-down', 'fa-long-arrow-left', 40 | 'fa-long-arrow-right', 'fa-long-arrow-up', 'fa-magic', 'fa-magnet', 'fa-mail-forward', 'fa-mail-reply', 'fa-mail-reply-all', 41 | 'fa-male', 'fa-map-marker', 'fa-mars', 'fa-mars-double', 'fa-mars-stroke', 'fa-mars-stroke-h', 'fa-mars-stroke-v', 42 | 'fa-maxcdn', 'fa-meanpath', 'fa-medium', 'fa-medkit', 'fa-meh-o', 'fa-mercury', 'fa-microphone', 43 | 'fa-microphone-slash', 'fa-minus', 'fa-minus-circle', 'fa-minus-square', 'fa-minus-square-o', 'fa-mobile', 'fa-mobile-phone', 44 | 'fa-money', 'fa-moon-o', 'fa-mortar-board', 'fa-motorcycle', 'fa-music', 'fa-navicon', 'fa-neuter', 45 | 'fa-newspaper-o', 'fa-openid', 'fa-outdent', 'fa-pagelines', 'fa-paint-brush', 'fa-paper-plane', 'fa-paper-plane-o', 46 | 'fa-paperclip', 'fa-paragraph', 'fa-paste', 'fa-pause', 'fa-paw', 'fa-paypal', 'fa-pencil', 47 | 'fa-pencil-square', 'fa-pencil-square-o', 'fa-phone', 'fa-phone-square', 'fa-photo', 'fa-picture-o', 'fa-pie-chart', 48 | 'fa-pied-piper', 'fa-pied-piper-alt', 'fa-pinterest', 'fa-pinterest-p', 'fa-pinterest-square', 'fa-plane', 'fa-play', 49 | 'fa-play-circle', 'fa-play-circle-o', 'fa-plug', 'fa-plus', 'fa-plus-circle', 'fa-plus-square', 'fa-plus-square-o', 50 | 'fa-power-off', 'fa-print', 'fa-puzzle-piece', 'fa-qq', 'fa-qrcode', 'fa-question', 'fa-question-circle', 51 | 'fa-quote-left', 'fa-quote-right', 'fa-ra', 'fa-random', 'fa-rebel', 'fa-recycle', 'fa-reddit', 52 | 'fa-reddit-square', 'fa-refresh', 'fa-remove', 'fa-renren', 'fa-reorder', 'fa-repeat', 'fa-reply', 53 | 'fa-reply-all', 'fa-retweet', 'fa-rmb', 'fa-road', 'fa-rocket', 'fa-rotate-left', 'fa-rotate-right', 54 | 'fa-rouble', 'fa-rss', 'fa-rss-square', 'fa-rub', 'fa-ruble', 'fa-rupee', 'fa-save', 55 | 'fa-scissors', 'fa-search', 'fa-search-minus', 'fa-search-plus', 'fa-sellsy', 'fa-send', 'fa-send-o', 56 | 'fa-server', 'fa-share', 'fa-share-alt', 'fa-share-alt-square', 'fa-share-square', 'fa-share-square-o', 'fa-shekel', 57 | 'fa-sheqel', 'fa-shield', 'fa-ship', 'fa-shirtsinbulk', 'fa-shopping-cart', 'fa-sign-in', 'fa-sign-out', 58 | 'fa-signal', 'fa-simplybuilt', 'fa-sitemap', 'fa-skyatlas', 'fa-skype', 'fa-slack', 'fa-sliders', 59 | 'fa-slideshare', 'fa-smile-o', 'fa-soccer-ball-o', 'fa-sort', 'fa-sort-alpha-asc', 'fa-sort-alpha-desc', 'fa-sort-amount-asc', 60 | 'fa-sort-amount-desc', 'fa-sort-asc', 'fa-sort-desc', 'fa-sort-down', 'fa-sort-numeric-asc', 'fa-sort-numeric-desc', 'fa-sort-up', 61 | 'fa-soundcloud', 'fa-space-shuttle', 'fa-spinner', 'fa-spoon', 'fa-spotify', 'fa-square', 'fa-square-o', 62 | 'fa-stack-exchange', 'fa-stack-overflow', 'fa-star', 'fa-star-half', 'fa-star-half-empty', 'fa-star-half-full', 'fa-star-half-o', 63 | 'fa-star-o', 'fa-steam', 'fa-steam-square', 'fa-step-backward', 'fa-step-forward', 'fa-stethoscope', 'fa-stop', 64 | 'fa-street-view', 'fa-strikethrough', 'fa-stumbleupon', 'fa-stumbleupon-circle', 'fa-subscript', 'fa-subway', 'fa-suitcase', 65 | 'fa-sun-o', 'fa-superscript', 'fa-support', 'fa-table', 'fa-tablet', 'fa-tachometer', 'fa-tag', 66 | 'fa-tags', 'fa-tasks', 'fa-taxi', 'fa-tencent-weibo', 'fa-terminal', 'fa-text-height', 'fa-text-width', 67 | 'fa-th', 'fa-th-large', 'fa-th-list', 'fa-thumb-tack', 'fa-thumbs-down', 'fa-thumbs-o-down', 'fa-thumbs-o-up', 68 | 'fa-thumbs-up', 'fa-ticket', 'fa-times', 'fa-times-circle', 'fa-times-circle-o', 'fa-tint', 'fa-toggle-down', 69 | 'fa-toggle-left', 'fa-toggle-off', 'fa-toggle-on', 'fa-toggle-right', 'fa-toggle-up', 'fa-train', 'fa-transgender', 70 | 'fa-transgender-alt', 'fa-trash', 'fa-trash-o', 'fa-tree', 'fa-trello', 'fa-trophy', 'fa-truck', 71 | 'fa-try', 'fa-tty', 'fa-tumblr', 'fa-tumblr-square', 'fa-turkish-lira', 'fa-twitch', 'fa-twitter', 72 | 'fa-twitter-square', 'fa-umbrella', 'fa-underline', 'fa-undo', 'fa-university', 'fa-unlink', 'fa-unlock', 73 | 'fa-unlock-alt', 'fa-unsorted', 'fa-upload', 'fa-usd', 'fa-user', 'fa-user-md', 'fa-user-plus', 74 | 'fa-user-secret', 'fa-user-times', 'fa-users', 'fa-venus', 'fa-venus-double', 'fa-venus-mars', 'fa-viacoin', 75 | 'fa-video-camera', 'fa-vimeo-square', 'fa-vine', 'fa-vk', 'fa-volume-down', 'fa-volume-off', 'fa-volume-up', 76 | 'fa-warning', 'fa-wechat', 'fa-weibo', 'fa-weixin', 'fa-whatsapp', 'fa-wheelchair', 'fa-wifi', 77 | 'fa-windows', 'fa-won', 'fa-wordpress', 'fa-wrench', 'fa-xing', 'fa-xing-square', 'fa-yahoo', 78 | ] 79 | -------------------------------------------------------------------------------- /app/static/js/screenBloomPresets.js: -------------------------------------------------------------------------------- 1 | var screenBloomPresets = {}; 2 | 3 | screenBloomPresets.init = function() { 4 | var wrapper = $('.presets-wrapper'), 5 | icon = wrapper.find('#settings-preset-icon'), 6 | inputWrapper = wrapper.find('.setting-input'), 7 | saveNewPresetBtn = $('#save-preset'), 8 | closeBtn = wrapper.find('.setting-input-close'), 9 | preset = $('.saved-preset'), 10 | savedPresetContainer = $('#saved-preset-container'), 11 | iconSelect = '.saved-preset .edit-preset-container .setting-wrapper-container .select-preset-icon-container .preset-icon-select', 12 | deletePresetBtn = '.saved-preset .edit-preset-container .delete-preset', 13 | savePresetBtn = '.saved-preset .edit-preset-container .save-preset', 14 | closeEditContainer = '.saved-preset .setting-wrapper-container .close-edit-preset-container'; 15 | 16 | preset.each(function() { 17 | $(this).css({ 18 | 'border-color': randomColor({'luminosity' : 'dark'}) 19 | }); 20 | }); 21 | 22 | // Events 23 | icon.on('click', function() { 24 | icon.toggleClass('active'); 25 | inputWrapper.toggleClass('hidden'); 26 | if (icon.hasClass('active')) { 27 | inputWrapper.find('.setting-input-label div').colorWave(screenBloom.config.colors); 28 | } 29 | }); 30 | 31 | closeBtn.on('click', function() { 32 | inputWrapper.addClass('hidden'); 33 | icon.removeClass('active'); 34 | }); 35 | 36 | // Edit preset button 37 | savedPresetContainer.on('click', '.saved-preset .edit-preset', function() { 38 | $('.edit-preset-container').addClass('hidden'); 39 | $('.saved-preset .edit-preset').removeClass('active'); 40 | $(this).parent().find('.edit-preset-container').removeClass('hidden'); 41 | $(this).addClass('active'); 42 | 43 | $('.saved-preset').each(function() { 44 | $(this).removeClass('active'); 45 | var presetName = $(this).find('.preset-label').text(); 46 | if (presetName === screenBloom.config.currentPreset) { 47 | $(this).addClass('active'); 48 | } 49 | }); 50 | 51 | $(this).parents('.saved-preset').addClass('active'); 52 | }); 53 | 54 | savedPresetContainer.on('click', closeEditContainer, function() { 55 | var parent = $(this).parents('.saved-preset'), 56 | container = parent.find('.edit-preset-container'), 57 | presetName = parent.find('.preset-label').text(); 58 | 59 | if (presetName !== screenBloom.config.currentPreset) { 60 | parent.removeClass('active'); 61 | } 62 | 63 | container.addClass('hidden'); 64 | }); 65 | 66 | savedPresetContainer.on('click', '.saved-preset', function(e) { 67 | var presetNumber = $(this).data('preset-number'), 68 | target = $(e.target), 69 | wrapper = $(this).find('.saved-preset-wrapper'), 70 | loader = $(this).find('.preset-loading'); 71 | 72 | if (target.hasClass('preset-icon') || target.hasClass('saved-preset') || target.hasClass('preset-label')) { 73 | console.log('Applying preset...'); 74 | $('.saved-preset').removeClass('active'); 75 | $(this).addClass('active'); 76 | wrapper.addClass('hidden'); 77 | loader.removeClass('hidden'); 78 | $.ajax({ 79 | url : '/apply-preset', 80 | method : 'POST', 81 | data : JSON.stringify(presetNumber), 82 | contentType : 'application/json;charset=UTF-8', 83 | success : function (result) { 84 | notification(result.message); 85 | notification('Reloading the page...'); 86 | location.reload(); 87 | }, 88 | error : function (result) { 89 | console.log(result); 90 | wrapper.removeClass('hidden'); 91 | loader.addClass('hidden'); 92 | } 93 | }); 94 | } 95 | }); 96 | 97 | saveNewPresetBtn.on('click', function() { 98 | $.ajax({ 99 | url : '/save-preset', 100 | method : 'POST', 101 | contentType : 'application/json;charset=UTF-8', 102 | success : function (result) { 103 | var clone = $('#base-saved-preset').clone(), 104 | presetName = 'Preset ' + result.preset_number, 105 | icon = clone.find('.preset-icon'); 106 | 107 | screenBloom.config.currentPreset = presetName; 108 | $('.saved-preset').removeClass('active'); 109 | clone.removeAttr('id'); 110 | clone.addClass('active'); 111 | clone.css({ 112 | 'border-color' : randomColor(), 113 | 'display' : 'inline-block' 114 | }); 115 | clone.attr('data-preset-number', result.preset_number); 116 | clone.find('p').text(presetName); 117 | clone.find('input').val(presetName); 118 | icon.removeClass().addClass('fa ' + result.icon_class + ' preset-icon'); 119 | savedPresetContainer.append(clone); 120 | clone.find('.preset-icon-select').each(function() { 121 | var icon = $(this).find('i'); 122 | $(this).removeClass('active'); 123 | if (icon.hasClass(result.icon_class)) { 124 | $(this).addClass('active'); 125 | } 126 | }); 127 | notification(result.message); 128 | }, 129 | error : function (result) { 130 | console.log(result); 131 | } 132 | }); 133 | }); 134 | 135 | savedPresetContainer.on('click', deletePresetBtn, function() { 136 | var presetNumber = $(this).parents('.saved-preset').data('preset-number'), 137 | presetDiv = $(this).parents('.saved-preset'); 138 | $.ajax({ 139 | url : '/delete-preset', 140 | method : 'POST', 141 | data : JSON.stringify(presetNumber), 142 | contentType : 'application/json;charset=UTF-8', 143 | success : function (result) { 144 | notification(result.message); 145 | presetDiv.remove(); 146 | }, 147 | error : function (result) { 148 | console.log(result); 149 | } 150 | }); 151 | }); 152 | 153 | savedPresetContainer.on('click', savePresetBtn, function() { 154 | var thisBtn = $(this), 155 | presetNumber = $(this).parents('.saved-preset').data('preset-number'), 156 | presetName = $(this).parent().find('input').val(), 157 | parent = thisBtn.parents('.saved-preset'), 158 | iconsContainer = parent.find('.select-preset-icon-container'), 159 | iconClass = '', 160 | dataToSend = { 161 | 'presetNumber' : presetNumber, 162 | 'presetName' : presetName 163 | }; 164 | 165 | iconsContainer.find('.preset-icon-select ').each(function() { 166 | if ($(this).hasClass('active')) { 167 | iconClass = $(this).data('class'); 168 | } 169 | }); 170 | dataToSend.iconClass = iconClass; 171 | 172 | $.ajax({ 173 | url : '/update-preset', 174 | method : 'POST', 175 | data : JSON.stringify(dataToSend), 176 | contentType : 'application/json;charset=UTF-8', 177 | success : function (result) { 178 | var icon = parent.find('.preset-icon'); 179 | notification(result.message); 180 | icon.removeClass().addClass('fa ' + iconClass + ' preset-icon'); 181 | parent.find('.preset-label').text(presetName); 182 | parent.find('.edit-preset').removeClass('active'); 183 | thisBtn.parent().addClass('hidden'); 184 | 185 | if (presetName === screenBloom.config.currentPreset) { 186 | console.log('This is the current preset being edited'); 187 | } else { 188 | console.log('Not current preset, should remove active class'); 189 | parent.removeClass('active'); 190 | } 191 | }, 192 | error : function (result) { 193 | console.log(result); 194 | } 195 | }); 196 | }); 197 | 198 | savedPresetContainer.on('click', iconSelect, function() { 199 | $(this).parent().find('.preset-icon-select').removeClass('active'); 200 | $(this).addClass('active'); 201 | }); 202 | }; -------------------------------------------------------------------------------- /website/static/js/screenBloomAnalytics.js: -------------------------------------------------------------------------------- 1 | var analytics = {}; 2 | 3 | analytics.config = { 4 | 'dates' : [], 5 | 'groupedDates' : [], 6 | 'downloads' : [], 7 | 'dataUrl' : '', 8 | 'loader' : $('.loading-icon'), 9 | 'containerDiv' : $('.container'), 10 | 'countSpan' : $('#download-count'), 11 | 'labelSpan' : $('#daterange-label'), 12 | 'dataTable' : $('#downloads-table') 13 | }; 14 | 15 | analytics.init = function() { 16 | var date1 = moment().subtract('days', 7).startOf('day').format('YYYY-MM-DD hh:mm:ss'), 17 | date2 = moment().add('days', 1).startOf('day').format('YYYY-MM-DD hh:mm:ss'); 18 | 19 | dateRangeInit(); 20 | getAnalyticsData(date1, date2, 'Last 7 Days'); 21 | } 22 | 23 | function dateRangeInit() { 24 | $('input[name="daterange"]').daterangepicker( 25 | { 26 | locale: { 27 | format: 'MMM Do \'YY', 28 | }, 29 | startDate: moment().subtract('days', 6).startOf('day'), 30 | endDate: moment().startOf('day').startOf('day'), 31 | ranges: { 32 | 'This Week' : [moment().subtract('days', 7).startOf('day'), moment().add('days', 1).startOf('day')], 33 | 'This Month' : [moment().startOf('month'), moment().endOf('month')], 34 | 'Last Month' : [moment().subtract('months', 1).startOf('month'), moment().subtract('months', 1).endOf('month')], 35 | 'Two Months Ago': [moment().subtract('months', 2).startOf('month'), moment().subtract('months', 2).endOf('month')], 36 | 'Since May \'16': [moment('2016-05-06').format('YYYY-MM-DD hh:mm:ss'), moment().add('days', 1).endOf('day')] 37 | } 38 | }, 39 | function(start, end, label) { 40 | var startDate = start.format('YYYY-MM-DD hh:mm:ss'), 41 | endDate = end.format('YYYY-MM-DD hh:mm:ss'); 42 | 43 | analytics.config.loader.removeClass('hidden'); 44 | analytics.config.containerDiv.addClass('hidden'); 45 | getAnalyticsData(startDate, endDate, label); 46 | }); 47 | } 48 | 49 | function getAnalyticsData(date1, date2, label) { 50 | console.log('Grabbing analytics data...'); 51 | var data = { 52 | 'date1' : date1, 53 | 'date2' : date2 54 | }; 55 | 56 | $.ajax({ 57 | url : analytics.config.dataUrl, 58 | method : 'POST', 59 | contentType : 'application/json;charset=UTF-8', 60 | data : JSON.stringify(data), 61 | success : function (result) { 62 | analytics.config.downloads = result.downloads; 63 | analytics.config.countSpan.text(analytics.config.downloads.length); 64 | 65 | if (label === 'Custom Range') { 66 | date1 = moment(date1).format('MMM Do'); 67 | date2 = moment(date2).format('MMM Do'); 68 | label = 'between ' + date1 + ' and ' + date2; 69 | } 70 | 71 | analytics.config.labelSpan.text(label); 72 | analytics.config.loader.addClass('hidden'); 73 | analytics.config.containerDiv.removeClass('hidden'); 74 | 75 | populateDownloadsTable(); 76 | getGroupedDates(); 77 | createDownloadsChart(); 78 | analytics.config.dataTable.dataTable().fnDestroy(); 79 | analytics.config.dataTable.DataTable({ 80 | 'order' : [0, 'desc'] 81 | }); 82 | }, 83 | error : function (result) { 84 | console.log(result); 85 | } 86 | }); 87 | } 88 | 89 | function populateDownloadsTable() { 90 | $('#downloads-table tbody').remove(); 91 | analytics.config.dataTable.append(''); 92 | for (var i=0; i' + 97 | download.id + '' + date + '' + 98 | download.version + '' + 99 | download.build + '' + 100 | location + ''; 101 | 102 | $('#downloads-table tbody').append(html); 103 | } 104 | } 105 | 106 | function getLocationString(locStr) { 107 | if (locStr !== null && locStr !== '') { 108 | var locObj = JSON.parse(locStr), 109 | city = locObj.city, 110 | country = getCountryName(locObj.country), 111 | region = locObj.region, 112 | finalStr = ''; 113 | 114 | if (locPropertyExists(city)) { 115 | finalStr += city + ', '; 116 | } 117 | if (locPropertyExists(region)) { 118 | finalStr += region + ' | '; 119 | } 120 | if (locPropertyExists(country)) { 121 | finalStr += '' + country + ''; 122 | } 123 | 124 | return finalStr; 125 | } else { 126 | return 'No data' 127 | } 128 | } 129 | 130 | function locPropertyExists(prop) { 131 | if (prop !== null && prop !== undefined && prop.length) { 132 | return true; 133 | } 134 | return false; 135 | } 136 | 137 | function getGroupedDates() { 138 | var downloads = analytics.config.downloads, 139 | dates = []; 140 | 141 | for (var i in downloads) { 142 | var download = downloads[i], 143 | dateString = moment(download.date).format('M/D/YYYY'); 144 | dates.push([dateString, download.id]); 145 | } 146 | 147 | newDates = _.groupBy(dates, getDateString); 148 | analytics.config.groupedDates = newDates; 149 | } 150 | 151 | function getDateString(listElement) { 152 | return listElement[0]; 153 | } 154 | 155 | function createDownloadsChart() { 156 | var chartData = getChartData(), 157 | backgroundColors = [], 158 | numDates = Object.keys(analytics.config.groupedDates).length; 159 | 160 | for (var i=0; i