on master help sign in
add multi theming support
08eaffeMatej Cotman
Makefile+2−2
@@ -44,13 +44,13 @@ minimal: bin/buildout minimal.cfg setup.py
44 bin/buildout -c minimal.cfg $(options)
44 bin/buildout -c minimal.cfg $(options)
45
45
46styles:
46styles:
47− @lessc -x searx/static/less/style.less > searx/static/css/style.css
47+ @lessc -x searx/static/default/less/style.less > searx/static/default/css/style.css
48
48
49locales:
49locales:
50 @pybabel compile -d searx/translations
50 @pybabel compile -d searx/translations
51
51
52clean:
52clean:
53 @rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
53 @rm -rf .installed.cfg .mr.developer.cfg bin parts develop-eggs \
54− searx.egg-info lib include .coverage coverage searx/static/css/*.css
54+ searx.egg-info lib include .coverage coverage searx/static/default/css/*.css
55
55
56.PHONY: all tests robot flake8 coverage production minimal styles locales clean
56.PHONY: all tests robot flake8 coverage production minimal styles locales clean
searx/settings.yml+2
@@ -4,6 +4,8 @@ server:
4 debug : True
4 debug : True
5 request_timeout : 2.0 # seconds
5 request_timeout : 2.0 # seconds
6 base_url : False
6 base_url : False
7+ themes_path : ""
8+ default_theme : default
7
9
8engines:
10engines:
9 - name : wikipedia
11 - name : wikipedia
searx/static/css/style.css → searx/static/default/css/style.cssrenamed

The content is the same.

searx/static/img/favicon.png → searx/static/default/img/favicon.pngrenamed

Binary file not shown.

searx/static/img/github_ribbon.png → searx/static/default/img/github_ribbon.pngrenamed

Binary file not shown.

searx/static/img/icon_github.ico → searx/static/default/img/icon_github.icorenamed

Binary file not shown.

searx/static/img/icon_soundcloud.ico → searx/static/default/img/icon_soundcloud.icorenamed

Binary file not shown.

searx/static/img/icon_stackoverflow.ico → searx/static/default/img/icon_stackoverflow.icorenamed

Binary file not shown.

searx/static/img/icon_twitter.ico → searx/static/default/img/icon_twitter.icorenamed

Binary file not shown.

searx/static/img/icon_vimeo.ico → searx/static/default/img/icon_vimeo.icorenamed

Binary file not shown.

searx/static/img/icon_wikipedia.ico → searx/static/default/img/icon_wikipedia.icorenamed

Binary file not shown.

searx/static/img/icon_youtube.ico → searx/static/default/img/icon_youtube.icorenamed

Binary file not shown.

searx/static/img/preference-icon.png → searx/static/default/img/preference-icon.pngrenamed

Binary file not shown.

searx/static/img/search-icon.png → searx/static/default/img/search-icon.pngrenamed

Binary file not shown.

searx/static/img/searx.png → searx/static/default/img/searx.pngrenamed

Binary file not shown.

searx/static/img/searx_logo.svg → searx/static/default/img/searx_logo.svgrenamed

The content is the same.

searx/static/js/mootools-autocompleter-1.1.2-min.js → searx/static/default/js/mootools-autocompleter-1.1.2-min.jsrenamed

The content is the same.

searx/static/js/mootools-core-1.4.5-min.js → searx/static/default/js/mootools-core-1.4.5-min.jsrenamed

The content is the same.

searx/static/js/searx.js → searx/static/default/js/searx.jsrenamed

The content is the same.

searx/static/less/autocompleter.less → searx/static/default/less/autocompleter.lessrenamed

The content is the same.

searx/static/less/definitions.less → searx/static/default/less/definitions.lessrenamed

The content is the same.

searx/static/less/mixins.less → searx/static/default/less/mixins.lessrenamed

The content is the same.

searx/static/less/search.less → searx/static/default/less/search.lessrenamed

The content is the same.

searx/static/less/style.less → searx/static/default/less/style.lessrenamed

The content is the same.

searx/templates/about.html → searx/templates/default/about.htmlrenamed+2−2
@@ -1,6 +1,6 @@
1−{% extends 'base.html' %}
1+{% extends 'default/base.html' %}
2{% block content %}
2{% block content %}
3−{% include 'github_ribbon.html' %}
3+{% include 'default/github_ribbon.html' %}
4<div class="row">
4<div class="row">
5 <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
5 <h1>About <a href="{{ url_for('index') }}">searx</a></h1>
6
6
searx/templates/base.html → searx/templates/default/base.htmlrenamed

The content is the same.

searx/templates/categories.html → searx/templates/default/categories.htmlrenamed

The content is the same.

searx/templates/github_ribbon.html → searx/templates/default/github_ribbon.htmlrenamed

The content is the same.

searx/templates/index.html → searx/templates/default/index.htmlrenamed+2−2
@@ -1,8 +1,8 @@
1−{% extends "base.html" %}
1+{% extends "default/base.html" %}
2{% block content %}
2{% block content %}
3<div class="center">
3<div class="center">
4 <div class="title"><h1>searx</h1></div>
4 <div class="title"><h1>searx</h1></div>
5− {% include 'search.html' %}
5+ {% include 'default/search.html' %}
6 <p class="top_margin">
6 <p class="top_margin">
7 <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a>
7 <a href="{{ url_for('about') }}" class="hmarg">{{ _('about') }}</a>
8 <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a>
8 <a href="{{ url_for('preferences') }}" class="hmarg">{{ _('preferences') }}</a>
searx/templates/opensearch.xml → searx/templates/default/opensearch.xmlrenamed

The content is the same.

searx/templates/opensearch_response_rss.xml → searx/templates/default/opensearch_response_rss.xmlrenamed

The content is the same.

searx/templates/preferences.html → searx/templates/default/preferences.htmlrenamed+12−2
@@ -1,4 +1,4 @@
1−{% extends "base.html" %}
1+{% extends "default/base.html" %}
2{% block head %} {% endblock %}
2{% block head %} {% endblock %}
3{% block content %}
3{% block content %}
4<div class="row">
4<div class="row">
@@ -8,7 +8,7 @@
8 <fieldset>
8 <fieldset>
9 <legend>{{ _('Default categories') }}</legend>
9 <legend>{{ _('Default categories') }}</legend>
10 <p>
10 <p>
11− {% include 'categories.html' %}
11+ {% include 'default/categories.html' %}
12 </p>
12 </p>
13 </fieldset>
13 </fieldset>
14 <fieldset>
14 <fieldset>
@@ -52,6 +52,16 @@
52 </select>
52 </select>
53 </p>
53 </p>
54 </fieldset>
54 </fieldset>
55+ <fieldset>
56+ <legend>{{ _('Themes') }}</legend>
57+ <p>
58+ <select name="theme">
59+ {% for name in themes %}
60+ <option value="{{ name }}" {% if name == theme %}selected="selected"{% endif %}>{{ name }}</option>
61+ {% endfor %}
62+ </select>
63+ </p>
64+ </fieldset>
55 <fieldset>
65 <fieldset>
56 <legend>{{ _('Currently used search engines') }}</legend>
66 <legend>{{ _('Currently used search engines') }}</legend>
57
67
searx/templates/result_templates/default.html → searx/templates/default/result_templates/default.htmlrenamed+1−1
@@ -1,7 +1,7 @@
1<div class="result {{ result.class }}">
1<div class="result {{ result.class }}">
2
2
3 {% if result['favicon'] %}
3 {% if result['favicon'] %}
4− <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
4+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
5 {% endif %}
5 {% endif %}
6
6
7 <div>
7 <div>
searx/templates/result_templates/images.html → searx/templates/default/result_templates/images.htmlrenamed

The content is the same.

searx/templates/result_templates/torrent.html → searx/templates/default/result_templates/torrent.htmlrenamed

The content is the same.

searx/templates/result_templates/videos.html → searx/templates/default/result_templates/videos.htmlrenamed+1−1
@@ -1,6 +1,6 @@
1<div class="result">
1<div class="result">
2 {% if result['favicon'] %}
2 {% if result['favicon'] %}
3− <img width="14" height="14" class="favicon" src="static/img/icon_{{result['favicon']}}.ico" />
3+ <img width="14" height="14" class="favicon" src="static/{{theme}}/img/icon_{{result['favicon']}}.ico" />
4 {% endif %}
4 {% endif %}
5
5
6 <p>
6 <p>
searx/templates/results.html → searx/templates/default/results.htmlrenamed+4−4
@@ -1,9 +1,9 @@
1−{% extends "base.html" %}
1+{% extends "default/base.html" %}
2{% block title %}{{ q }} - {% endblock %}
2{% block title %}{{ q }} - {% endblock %}
3{% block content %}
3{% block content %}
4<div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
4<div class="right"><a href="{{ url_for('preferences') }}" id="preferences"><span>preferences</span></a></div>
5<div class="small search center">
5<div class="small search center">
6− {% include 'search.html' %}
6+ {% include 'default/search.html' %}
7</div>
7</div>
8<div id="results">
8<div id="results">
9 <div id="sidebar">
9 <div id="sidebar">
@@ -43,9 +43,9 @@
43
43
44 {% for result in results %}
44 {% for result in results %}
45 {% if result['template'] %}
45 {% if result['template'] %}
46− {% include 'result_templates/'+result['template'] %}
46+ {% include 'default/result_templates/'+result['template'] %}
47 {% else %}
47 {% else %}
48− {% include 'result_templates/default.html' %}
48+ {% include 'default/result_templates/default.html' %}
49 {% endif %}
49 {% endif %}
50 {% endfor %}
50 {% endfor %}
51
51
searx/templates/search.html → searx/templates/default/search.htmlrenamed+1−1
@@ -3,5 +3,5 @@
3 <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/>
3 <input type="text" placeholder="{{ _('Search for...') }}" id="q" class="q" name="q" tabindex="1" autocomplete="off" {% if q %}value="{{ q }}"{% endif %}/>
4 <input type="submit" value="search" id="search_submit" />
4 <input type="submit" value="search" id="search_submit" />
5 </div>
5 </div>
6− {% include 'categories.html' %}
6+ {% include 'default/categories.html' %}
7</form>
7</form>
searx/templates/stats.html → searx/templates/default/stats.htmlrenamed+1−1
@@ -1,4 +1,4 @@
1−{% extends "base.html" %}
1+{% extends "default/base.html" %}
2{% block head %} {% endblock %}
2{% block head %} {% endblock %}
3{% block content %}
3{% block content %}
4<h2>{{ _('Engine stats') }}</h2>
4<h2>{{ _('Engine stats') }}</h2>
searx/utils.py+19−3
@@ -1,10 +1,12 @@
1−from HTMLParser import HTMLParser
2#import htmlentitydefs
1#import htmlentitydefs
3−import csv
4from codecs import getincrementalencoder
2from codecs import getincrementalencoder
3+from HTMLParser import HTMLParser
4+from random import choice
5+
5import cStringIO
6import cStringIO
7+import csv
8+import os
6import re
9import re
7−from random import choice
8
10
9ua_versions = ('26.0', '27.0', '28.0')
11ua_versions = ('26.0', '27.0', '28.0')
10ua_os = ('Windows NT 6.3; WOW64',
12ua_os = ('Windows NT 6.3; WOW64',
@@ -110,3 +112,17 @@ class UnicodeWriter:
110 def writerows(self, rows):
112 def writerows(self, rows):
111 for row in rows:
113 for row in rows:
112 self.writerow(row)
114 self.writerow(row)
115+
116+
117+def get_themes(root):
118+ """Returns available themes list."""
119+
120+ static_path = os.path.join(root, 'static')
121+ static_names = set(os.listdir(static_path))
122+ templates_path = os.path.join(root, 'templates')
123+ templates_names = set(os.listdir(templates_path))
124+
125+ themes = []
126+ for name in static_names.intersection(templates_names):
127+ themes += [name]
128+ return static_path, templates_path, themes
searx/webapp.py+56−9
@@ -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
40)
40)
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
43+)
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
45
47
46
48
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'
53+
47app = Flask(
54app = Flask(
48 __name__,
55 __name__,
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
51)
58)
52
59
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
91
98
92
99
100+def get_current_theme_name(override=None):
101+ """Returns theme name.
102+
103+ Checks in this order:
104+ 1. override
105+ 2. cookies
106+ 3. settings"""
107+
108+ if override and override in themes:
109+ return override
110+ theme_name = request.cookies.get('theme', default_theme)
111+ if theme_name not in themes:
112+ theme_name = default_theme
113+ return theme_name
114+
115+
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)
121+
122+
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(',')
95
125
96 autocomplete = request.cookies.get('autocomplete')
126 autocomplete = request.cookies.get('autocomplete')
@@ -125,7 +155,13 @@ def render(template_name, **kwargs):
125
155
126 kwargs['method'] = request.cookies.get('method', 'POST')
156 kwargs['method'] = request.cookies.get('method', 'POST')
127
157
158+ # override url_for function in templates
159+ kwargs['url_for'] = url_for_theme
160+
161+ kwargs['theme'] = get_current_theme_name(override=override_theme)
162+
128− return render_template(template_name, **kwargs)
163+ return render_template(
164+ '{}/{}'.format(kwargs['theme'], template_name), **kwargs)
129
165
130
166
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()
236 )
273 )
237
274
238
275
@@ -290,7 +327,7 @@ def preferences():
290
327
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
318
357
319 resp = make_response(redirect(url_for('index')))
358 resp = make_response(redirect(url_for('index')))
320
359
@@ -352,6 +391,9 @@ def preferences():
352
391
353 resp.set_cookie('method', method, max_age=cookie_max_age)
392 resp.set_cookie('method', method, max_age=cookie_max_age)
354
393
394+ resp.set_cookie(
395+ 'theme', theme, max_age=cookie_max_age)
396+
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()},
407+ themes=themes,
408+ theme=get_current_theme_name())
365
409
366
410
367@app.route('/stats', methods=['GET'])
411@app.route('/stats', methods=['GET'])
@@ -404,7 +448,10 @@ def opensearch():
404
448
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,
452+ 'static',
453+ get_current_theme_name(),
454+ 'img'),
408 'favicon.png',
455 'favicon.png',
409 mimetype='image/vnd.microsoft.icon')
456 mimetype='image/vnd.microsoft.icon')
410
457