add multi theming support
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						| @ -44,13 +44,13 @@ minimal: bin/buildout minimal.cfg setup.py | ||||
| 	bin/buildout -c minimal.cfg $(options) | ||||
| 
 | ||||
| styles: | ||||
| 	@lessc -x searx/static/less/style.less > searx/static/css/style.css | ||||
| 	@lessc -x searx/static/default/less/style.less > searx/static/default/css/style.css | ||||
| 
 | ||||
| locales: | ||||
| 	@pybabel compile -d searx/translations | ||||
| 
 | ||||
| clean: | ||||
| 	@rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
 | ||||
| 		searx.egg-info lib include .coverage coverage searx/static/css/*.css | ||||
| 		searx.egg-info lib include .coverage coverage searx/static/default/css/*.css | ||||
| 
 | ||||
| .PHONY: all tests robot flake8 coverage production minimal styles locales clean | ||||
|  | ||||
| @ -4,6 +4,8 @@ server: | ||||
|     debug : True | ||||
|     request_timeout : 2.0 # seconds | ||||
|     base_url : False | ||||
|     themes_path : "" | ||||
|     default_theme : default | ||||
| 
 | ||||
| engines: | ||||
|   - name : wikipedia | ||||
|  | ||||
| Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB | 
| Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB | 
| Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 837 B | 
| Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB | 
| Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB | 
| Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB | 
| @ -1,6 +1,6 @@ | ||||
| {% extends 'base.html' %} | ||||
| {% extends 'default/base.html' %} | ||||
| {% block content %} | ||||
| {% include 'github_ribbon.html' %} | ||||
| {% include 'default/github_ribbon.html' %} | ||||
| <div class="row"> | ||||
|     <h1>About <a href="{{ url_for('index') }}">searx</a></h1> | ||||
| 
 | ||||
| @ -1,8 +1,8 @@ | ||||
| {% extends "base.html" %} | ||||
| {% extends "default/base.html" %} | ||||
| {% block content %} | ||||
| <div class="center"> | ||||
|     <div class="title"><h1>searx</h1></div> | ||||
|     {% include 'search.html' %} | ||||
|     {% include 'default/search.html' %} | ||||
|     <p class="top_margin"> | ||||
|         <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a> | ||||
|         <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a> | ||||
| @ -1,4 +1,4 @@ | ||||
| {% extends "base.html" %} | ||||
| {% extends "default/base.html" %} | ||||
| {% block head %} {% endblock %} | ||||
| {% block content %} | ||||
| <div class="row"> | ||||
| @ -8,7 +8,7 @@ | ||||
|     <fieldset> | ||||
|         <legend>{{ _('Default categories') }}</legend> | ||||
|         <p> | ||||
|         {% include 'categories.html' %} | ||||
|         {% include 'default/categories.html' %} | ||||
|         </p> | ||||
|     </fieldset> | ||||
|     <fieldset> | ||||
| @ -52,6 +52,16 @@ | ||||
|         </select> | ||||
|         </p> | ||||
|     </fieldset> | ||||
|     <fieldset> | ||||
|         <legend>{{ _('Themes') }}</legend> | ||||
|         <p> | ||||
|         <select name="theme"> | ||||
|             {% for name in themes %} | ||||
|             <option value="{{ name }}" {% if name == theme %}selected="selected"{% endif %}>{{ name }}</option> | ||||
|             {% endfor %} | ||||
|         </select> | ||||
|         </p> | ||||
|     </fieldset> | ||||
|     <fieldset> | ||||
|     <legend>{{ _('Currently used search engines') }}</legend> | ||||
| 
 | ||||
| @ -1,7 +1,7 @@ | ||||
| <div class="result {{ result.class }}"> | ||||
| 
 | ||||
|   {% if result['favicon'] %} | ||||
|     <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" /> | ||||
|     <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" /> | ||||
|   {% endif %} | ||||
| 
 | ||||
|   <div> | ||||
| @ -1,6 +1,6 @@ | ||||
| <div class="result"> | ||||
|   {% if result['favicon'] %} | ||||
|     <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" /> | ||||
|     <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" /> | ||||
|   {% endif %} | ||||
| 
 | ||||
|     <p> | ||||
| @ -1,9 +1,9 @@ | ||||
| {% extends "base.html" %} | ||||
| {% extends "default/base.html" %} | ||||
| {% block title %}{{ q }} - {% endblock %} | ||||
| {% block content %} | ||||
| <div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div> | ||||
| <div class="small search center"> | ||||
|     {% include 'search.html' %} | ||||
|     {% include 'default/search.html' %} | ||||
| </div> | ||||
| <div id="results"> | ||||
|     <div id="sidebar"> | ||||
| @ -43,9 +43,9 @@ | ||||
| 
 | ||||
|     {% for result in results %} | ||||
|         {% if result['template'] %} | ||||
|             {% include 'result_templates/'+result['template'] %} | ||||
|             {% include 'default/result_templates/'+result['template'] %} | ||||
|         {% else %} | ||||
|             {% include 'result_templates/default.html' %} | ||||
|             {% include 'default/result_templates/default.html' %} | ||||
|         {% endif %} | ||||
|     {% endfor %} | ||||
| 
 | ||||
| @ -3,5 +3,5 @@ | ||||
|     <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/> | ||||
|     <input type="submit" value="search" id="search_submit" /> | ||||
|   </div> | ||||
|   {% include 'categories.html' %} | ||||
|   {% include 'default/categories.html' %} | ||||
| </form> | ||||
| @ -1,4 +1,4 @@ | ||||
| {% extends "base.html" %} | ||||
| {% extends "default/base.html" %} | ||||
| {% block head %} {% endblock %} | ||||
| {% block content %} | ||||
| <h2>{{ _('Engine stats') }}</h2> | ||||
| @ -1,11 +1,13 @@ | ||||
| from HTMLParser import HTMLParser | ||||
| #import htmlentitydefs | ||||
| import csv | ||||
| from codecs import getincrementalencoder | ||||
| import cStringIO | ||||
| import re | ||||
| from HTMLParser import HTMLParser | ||||
| from random import choice | ||||
| 
 | ||||
| import cStringIO | ||||
| import csv | ||||
| import os | ||||
| import re | ||||
| 
 | ||||
| ua_versions = ('26.0', '27.0', '28.0') | ||||
| ua_os = ('Windows NT 6.3; WOW64', | ||||
|          'X11; Linux x86_64', | ||||
| @ -110,3 +112,17 @@ class UnicodeWriter: | ||||
|     def writerows(self, rows): | ||||
|         for row in rows: | ||||
|             self.writerow(row) | ||||
| 
 | ||||
| 
 | ||||
| def get_themes(root): | ||||
|     """Returns available themes list.""" | ||||
| 
 | ||||
|     static_path = os.path.join(root, 'static') | ||||
|     static_names = set(os.listdir(static_path)) | ||||
|     templates_path = os.path.join(root, 'templates') | ||||
|     templates_names = set(os.listdir(templates_path)) | ||||
| 
 | ||||
|     themes = [] | ||||
|     for name in static_names.intersection(templates_names): | ||||
|         themes += [name] | ||||
|     return static_path, templates_path, themes | ||||
|  | ||||
| @ -38,16 +38,23 @@ from searx.engines import ( | ||||
|     search as do_search, categories, engines, get_engines_stats, | ||||
|     engine_shortcuts | ||||
| ) | ||||
| from searx.utils import UnicodeWriter, highlight_content, html_to_text | ||||
| from searx.utils import ( | ||||
|     UnicodeWriter, highlight_content, html_to_text, get_themes | ||||
| ) | ||||
| from searx.languages import language_codes | ||||
| from searx.search import Search | ||||
| from searx.autocomplete import backends as autocomplete_backends | ||||
| 
 | ||||
| 
 | ||||
| static_path, templates_path, themes = get_themes(settings['themes_path'] if \ | ||||
|     settings.get('themes_path', None) else searx_dir) | ||||
| default_theme = settings['default_theme'] if \ | ||||
|     settings.get('default_theme', None) else 'default' | ||||
| 
 | ||||
| app = Flask( | ||||
|     __name__, | ||||
|     static_folder=os.path.join(searx_dir, 'static'), | ||||
|     template_folder=os.path.join(searx_dir, 'templates') | ||||
|     static_folder=static_path, | ||||
|     template_folder=templates_path | ||||
| ) | ||||
| 
 | ||||
| app.secret_key = settings['server']['secret_key'] | ||||
| @ -90,7 +97,30 @@ def get_base_url(): | ||||
|     return hostname | ||||
| 
 | ||||
| 
 | ||||
| def render(template_name, **kwargs): | ||||
| def get_current_theme_name(override=None): | ||||
|     """Returns theme name. | ||||
| 
 | ||||
|     Checks in this order: | ||||
|     1. override | ||||
|     2. cookies | ||||
|     3. settings""" | ||||
| 
 | ||||
|     if override and override in themes: | ||||
|         return override | ||||
|     theme_name = request.cookies.get('theme', default_theme) | ||||
|     if theme_name not in themes: | ||||
|         theme_name = default_theme | ||||
|     return theme_name | ||||
| 
 | ||||
| 
 | ||||
| def url_for_theme(endpoint, override_theme=None, **values): | ||||
|     if endpoint == 'static' and values.get('filename', None): | ||||
|         theme_name = get_current_theme_name(override=override_theme) | ||||
|         values['filename'] = "{}/{}".format(theme_name, values['filename']) | ||||
|     return url_for(endpoint, **values) | ||||
| 
 | ||||
| 
 | ||||
| def render(template_name, override_theme=None, **kwargs): | ||||
|     blocked_engines = request.cookies.get('blocked_engines', '').split(',') | ||||
| 
 | ||||
|     autocomplete = request.cookies.get('autocomplete') | ||||
| @ -125,7 +155,13 @@ def render(template_name, **kwargs): | ||||
| 
 | ||||
|     kwargs['method'] = request.cookies.get('method', 'POST') | ||||
| 
 | ||||
|     return render_template(template_name, **kwargs) | ||||
|     # override url_for function in templates | ||||
|     kwargs['url_for'] = url_for_theme | ||||
| 
 | ||||
|     kwargs['theme'] = get_current_theme_name(override=override_theme) | ||||
| 
 | ||||
|     return render_template( | ||||
|         '{}/{}'.format(kwargs['theme'], template_name), **kwargs) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/search', methods=['GET', 'POST']) | ||||
| @ -232,7 +268,8 @@ def index(): | ||||
|         paging=search.paging, | ||||
|         pageno=search.pageno, | ||||
|         base_url=get_base_url(), | ||||
|         suggestions=search.suggestions | ||||
|         suggestions=search.suggestions, | ||||
|         theme=get_current_theme_name() | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @ -290,7 +327,7 @@ def preferences(): | ||||
| 
 | ||||
|     if request.method == 'GET': | ||||
|         blocked_engines = request.cookies.get('blocked_engines', '').split(',') | ||||
|     else: | ||||
|     else:  # on save | ||||
|         selected_categories = [] | ||||
|         locale = None | ||||
|         autocomplete = '' | ||||
| @ -315,6 +352,8 @@ def preferences(): | ||||
|                 engine_name = pd_name.replace('engine_', '', 1) | ||||
|                 if engine_name in engines: | ||||
|                     blocked_engines.append(engine_name) | ||||
|             elif pd_name == 'theme': | ||||
|                 theme = pd if pd in themes else default_theme | ||||
| 
 | ||||
|         resp = make_response(redirect(url_for('index'))) | ||||
| 
 | ||||
| @ -352,6 +391,9 @@ def preferences(): | ||||
| 
 | ||||
|         resp.set_cookie('method', method, max_age=cookie_max_age) | ||||
| 
 | ||||
|         resp.set_cookie( | ||||
|             'theme', theme, max_age=cookie_max_age) | ||||
| 
 | ||||
|         return resp | ||||
|     return render('preferences.html', | ||||
|                   locales=settings['locales'], | ||||
| @ -361,7 +403,9 @@ def preferences(): | ||||
|                   categs=categories.items(), | ||||
|                   blocked_engines=blocked_engines, | ||||
|                   autocomplete_backends=autocomplete_backends, | ||||
|                   shortcuts={y: x for x, y in engine_shortcuts.items()}) | ||||
|                   shortcuts={y: x for x, y in engine_shortcuts.items()}, | ||||
|                   themes=themes, | ||||
|                   theme=get_current_theme_name()) | ||||
| 
 | ||||
| 
 | ||||
| @app.route('/stats', methods=['GET']) | ||||
| @ -404,7 +448,10 @@ def opensearch(): | ||||
| 
 | ||||
| @app.route('/favicon.ico') | ||||
| def favicon(): | ||||
|     return send_from_directory(os.path.join(app.root_path, 'static/img'), | ||||
|     return send_from_directory(os.path.join(app.root_path, | ||||
|                                             'static', | ||||
|                                             get_current_theme_name(), | ||||
|                                             'img'), | ||||
|                                'favicon.png', | ||||
|                                mimetype='image/vnd.microsoft.icon') | ||||
| 
 | ||||
|  | ||||