@@ -38,16 +38,23 @@ from searx.engines import (
38 search as do_search, categories, engines, get_engines_stats,
38 search as do_search, categories, engines, get_engines_stats,
39 engine_shortcuts
39 engine_shortcuts
41+from searx.utils import (
41−from searx.utils import UnicodeWriter, highlight_content, html_to_text
42+ UnicodeWriter, highlight_content, html_to_text, get_themes
42from searx.languages import language_codes
44from searx.languages import language_codes
43from searx.search import Search
45from searx.search import Search
44from searx.autocomplete import backends as autocomplete_backends
46from searx.autocomplete import backends as autocomplete_backends
49+static_path, templates_path, themes = get_themes(settings['themes_path'] if \
50+ settings.get('themes_path', None) else searx_dir)
51+default_theme = settings['default_theme'] if \
52+ settings.get('default_theme', None) else 'default'
47app = Flask(
54app = Flask(
49− static_folder=os.path.join(searx_dir, 'static'),
56+ static_folder=static_path,
50− template_folder=os.path.join(searx_dir, 'templates')
57+ template_folder=templates_path
53app.secret_key = settings['server']['secret_key']
60app.secret_key = settings['server']['secret_key']
@@ -90,7 +97,30 @@ def get_base_url():
90 return hostname
97 return hostname
100+def get_current_theme_name(override=None):
101+ """Returns theme name.
103+ Checks in this order:
108+ if override and override in themes:
110+ theme_name = request.cookies.get('theme', default_theme)
111+ if theme_name not in themes:
112+ theme_name = default_theme
116+def url_for_theme(endpoint, override_theme=None, **values):
117+ if endpoint == 'static' and values.get('filename', None):
118+ theme_name = get_current_theme_name(override=override_theme)
119+ values['filename'] = "{}/{}".format(theme_name, values['filename'])
120+ return url_for(endpoint, **values)
93−def render(template_name, **kwargs):
123+def render(template_name, override_theme=None, **kwargs):
94 blocked_engines = request.cookies.get('blocked_engines', '').split(',')
124 blocked_engines = request.cookies.get('blocked_engines', '').split(',')
96 autocomplete = request.cookies.get('autocomplete')
126 autocomplete = request.cookies.get('autocomplete')
@@ -125,7 +155,13 @@ def render(template_name, **kwargs):
126 kwargs['method'] = request.cookies.get('method', 'POST')
156 kwargs['method'] = request.cookies.get('method', 'POST')
158+ # override url_for function in templates
159+ kwargs['url_for'] = url_for_theme
161+ kwargs['theme'] = get_current_theme_name(override=override_theme)
128− return render_template(template_name, **kwargs)
163+ return render_template(
164+ '{}/{}'.format(kwargs['theme'], template_name), **kwargs)
131@app.route('/search', methods=['GET', 'POST'])
167@app.route('/search', methods=['GET', 'POST'])
@@ -232,7 +268,8 @@ def index():
232 paging=search.paging,
268 paging=search.paging,
233 pageno=search.pageno,
269 pageno=search.pageno,
234 base_url=get_base_url(),
270 base_url=get_base_url(),
235− suggestions=search.suggestions
271+ suggestions=search.suggestions,
272+ theme=get_current_theme_name()
@@ -290,7 +327,7 @@ def preferences():
291 if request.method == 'GET':
328 if request.method == 'GET':
292 blocked_engines = request.cookies.get('blocked_engines', '').split(',')
329 blocked_engines = request.cookies.get('blocked_engines', '').split(',')
293− else:
330+ else: # on save
294 selected_categories = []
331 selected_categories = []
295 locale = None
332 locale = None
296 autocomplete = ''
333 autocomplete = ''
@@ -315,6 +352,8 @@ def preferences():
315 engine_name = pd_name.replace('engine_', '', 1)
352 engine_name = pd_name.replace('engine_', '', 1)
316 if engine_name in engines:
353 if engine_name in engines:
317 blocked_engines.append(engine_name)
354 blocked_engines.append(engine_name)
355+ elif pd_name == 'theme':
356+ theme = pd if pd in themes else default_theme
319 resp = make_response(redirect(url_for('index')))
358 resp = make_response(redirect(url_for('index')))
@@ -352,6 +391,9 @@ def preferences():
353 resp.set_cookie('method', method, max_age=cookie_max_age)
392 resp.set_cookie('method', method, max_age=cookie_max_age)
395+ 'theme', theme, max_age=cookie_max_age)
355 return resp
397 return resp
356 return render('preferences.html',
398 return render('preferences.html',
357 locales=settings['locales'],
399 locales=settings['locales'],
@@ -361,7 +403,9 @@ def preferences():
361 categs=categories.items(),
403 categs=categories.items(),
362 blocked_engines=blocked_engines,
404 blocked_engines=blocked_engines,
363 autocomplete_backends=autocomplete_backends,
405 autocomplete_backends=autocomplete_backends,
364− shortcuts={y: x for x, y in engine_shortcuts.items()})
406+ shortcuts={y: x for x, y in engine_shortcuts.items()},
408+ theme=get_current_theme_name())
367@app.route('/stats', methods=['GET'])
411@app.route('/stats', methods=['GET'])
@@ -404,7 +448,10 @@ def opensearch():
405@app.route('/favicon.ico')
449@app.route('/favicon.ico')
406def favicon():
450def favicon():
407− return send_from_directory(os.path.join(app.root_path, 'static/img'),
451+ return send_from_directory(os.path.join(app.root_path,
453+ get_current_theme_name(),
408 'favicon.png',
455 'favicon.png',
409 mimetype='image/vnd.microsoft.icon')
456 mimetype='image/vnd.microsoft.icon')