diff --git a/app/__init__.py b/app/__init__.py index 8ae241e..f97bfc2 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -52,6 +52,9 @@ app.config['TRANSLATIONS'] = json.load(open( app.config['THEMES'] = json.load(open( os.path.join(app.config['STATIC_FOLDER'], 'settings/themes.json'), encoding='utf-8')) +app.config['HEADER_TABS'] = json.load(open( + os.path.join(app.config['STATIC_FOLDER'], 'settings/header_tabs.json'), + encoding='utf-8')) app.config['CONFIG_PATH'] = os.getenv( 'CONFIG_VOLUME', os.path.join(app.config['STATIC_FOLDER'], 'config')) diff --git a/app/filter.py b/app/filter.py index a5fe928..94aab09 100644 --- a/app/filter.py +++ b/app/filter.py @@ -1,5 +1,6 @@ from app.models.config import Config from app.models.endpoint import Endpoint +from app.models.g_classes import GClasses from app.request import VALID_PARAMS, MAPS_URL from app.utils.misc import read_config_bool from app.utils.results import * @@ -13,6 +14,15 @@ from urllib.parse import parse_qs import os minimal_mode_sections = ['Top stories', 'Images', 'People also ask'] +unsupported_g_pages = [ + 'support.google.com', + 'accounts.google.com', + 'policies.google.com', + 'google.com/preferences', + 'google.com/intl', + 'advanced_search', + 'tbm=shop' +] def extract_q(q_str: str, href: str) -> str: @@ -80,6 +90,7 @@ class Filter: self.remove_block_url() self.collapse_sections() self.update_styling(soup) + self.remove_block_tabs(soup) for img in [_ for _ in soup.find_all('img') if 'src' in _.attrs]: self.update_element_src(img, 'image/png') @@ -143,6 +154,21 @@ class Filter: if block_url.search(_.attrs['href']) is not None] _ = div.decompose() if len(block_divs) else None + def remove_block_tabs(self, soup) -> None: + if self.main_divs: + for div in self.main_divs.find_all( + 'div', + attrs={'class': f'{GClasses.main_tbm_tab}'} + ): + _ = div.decompose() + else: + # when in images tab + for div in soup.find_all( + 'div', + attrs={'class': f'{GClasses.images_tbm_tab}'} + ): + _ = div.decompose() + def collapse_sections(self) -> None: """Collapses long result sections ("people also asked", "related searches", etc) into "details" elements @@ -273,6 +299,26 @@ class Filter: except AttributeError: pass + # Fix body max width on images tab + style = soup.find('style') + div = soup.find('div', attrs={'class': f'{GClasses.images_tbm_tab}'}) + if style and div and not self.mobile: + css = style.string + css_html_tag = ( + 'html{' + 'font-family: Roboto, Helvetica Neue, Arial, sans-serif;' + 'font-size: 14px;' + 'line-height: 20px;' + 'text-size-adjust: 100%;' + 'word-wrap: break-word;' + '}' + ) + css = f"{css_html_tag}{css}" + css = re.sub('body{(.*?)}', + 'body{padding:0 8px;margin:0 auto;max-width:736px;}', + css) + style.string = css + def update_link(self, link: Tag) -> None: """Update internal link paths with encrypted path, otherwise remove unnecessary redirects and/or marketing params from the url @@ -284,14 +330,15 @@ class Filter: None (the tag is updated directly) """ - # Replace href with only the intended destination (no "utm" type tags) - href = link['href'].replace('https://www.google.com', '') - if 'advanced_search' in href or 'tbm=shop' in href: + # Remove any elements that direct to unsupported Google pages + if any(url in link['href'] for url in unsupported_g_pages): # FIXME: The "Shopping" tab requires further filtering (see #136) # Temporarily removing all links to that tab for now. link.decompose() return + # Replace href with only the intended destination (no "utm" type tags) + href = link['href'].replace('https://www.google.com', '') result_link = urlparse.urlparse(href) q = extract_q(result_link.query, href) @@ -362,11 +409,8 @@ class Filter: """ # get some tags that are unchanged between mobile and pc versions - search_input = soup.find_all('td', attrs={'class': "O4cRJf"})[0] - search_options = soup.find_all('div', attrs={'class': "M7pB2"})[0] cor_suggested = soup.find_all('table', attrs={'class': "By0U9"}) next_pages = soup.find_all('table', attrs={'class': "uZgmoc"})[0] - information = soup.find_all('div', attrs={'class': "TuS8Ad"})[0] results = [] # find results div @@ -404,12 +448,7 @@ class Filter: results=results, view_label="View Image"), features='html.parser') - # replace search input object - soup.find_all('td', - attrs={'class': "O4cRJf"})[0].replaceWith(search_input) - # replace search options object (All, Images, Videos, etc.) - soup.find_all('div', - attrs={'class': "M7pB2"})[0].replaceWith(search_options) + # replace correction suggested by google object if exists if len(cor_suggested): soup.find_all( @@ -419,7 +458,4 @@ class Filter: # replace next page object at the bottom of the page soup.find_all('table', attrs={'class': "uZgmoc"})[0].replaceWith(next_pages) - # replace information about user connection at the bottom of the page - soup.find_all('div', - attrs={'class': "TuS8Ad"})[0].replaceWith(information) return soup diff --git a/app/models/g_classes.py b/app/models/g_classes.py new file mode 100644 index 0000000..2bd53a3 --- /dev/null +++ b/app/models/g_classes.py @@ -0,0 +1,16 @@ +from enum import Enum + + +class GClasses(Enum): + """A class for tracking obfuscated class names used in Google results that + are directly referenced in Whoogle's filtering code. + + Note: Using these should be a last resort. It is always preferred to filter + results using structural cues instead of referencing class names, as these + are liable to change at any moment. + """ + main_tbm_tab = 'KP7LCb' + images_tbm_tab = 'n692Zd' + + def __str__(self): + return self.value diff --git a/app/routes.py b/app/routes.py index b8ddb45..a112ad8 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,6 +1,7 @@ import argparse import base64 import io +import os import json import os import pickle @@ -17,14 +18,15 @@ from app.request import Request, TorError from app.utils.bangs import resolve_bang from app.utils.misc import read_config_bool, get_client_ip, get_request_url from app.utils.results import add_ip_card, bold_search_terms,\ - add_currency_card, check_currency -from app.utils.search import * + add_currency_card, check_currency, get_tabs_content +from app.utils.search import Search, needs_https, has_captcha from app.utils.session import generate_user_key, valid_user_session from bs4 import BeautifulSoup as bsoup from flask import jsonify, make_response, request, redirect, render_template, \ - send_file, session, url_for + send_file, session, url_for, g from requests import exceptions, get from requests.models import PreparedRequest +from cryptography.fernet import Fernet # Load DDG bang json files only on init bang_json = json.load(open(app.config['BANG_FILE'])) or {} @@ -347,6 +349,12 @@ def search(): html_soup = bsoup(str(response), 'html.parser') response = add_ip_card(html_soup, get_client_ip(request)) + # Update tabs content + tabs = get_tabs_content(app.config['HEADER_TABS'], + search_util.full_query, + search_util.search_type, + translation) + # Feature to display currency_card conversion = check_currency(str(response)) if conversion: @@ -373,15 +381,14 @@ def search(): ) and not search_util.search_type, # Standard search queries only response=response, version_number=app.config['VERSION_NUMBER'], - search_header=(render_template( + search_header=render_template( 'header.html', config=g.user_config, logo=render_template('logo.html', dark=g.user_config.dark), query=urlparse.unquote(query), search_type=search_util.search_type, - mobile=g.user_request.mobile) - if 'isch' not in - search_util.search_type else '')), 200 + mobile=g.user_request.mobile, + tabs=tabs)) @app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT']) diff --git a/app/static/css/dark-theme.css b/app/static/css/dark-theme.css index 4634fa0..0d0985e 100644 --- a/app/static/css/dark-theme.css +++ b/app/static/css/dark-theme.css @@ -74,6 +74,7 @@ select { .ZINbbc.luh4tb { background: var(--whoogle-dark-result-bg) !important; + margin-bottom: 24px !important; } .bRsWnc { @@ -204,3 +205,7 @@ path { .cb:focus { color: var(--whoogle-dark-contrast-text) !important; } + +.desktop-header, .mobile-header { + background-color: var(--whoogle-dark-result-bg) !important; +} diff --git a/app/static/css/header.css b/app/static/css/header.css index 441de8a..1724097 100644 --- a/app/static/css/header.css +++ b/app/static/css/header.css @@ -98,6 +98,11 @@ header { border: 0px !important; } +.autocomplete-mobile{ + display: -webkit-box; + width: 100%; +} + .desktop-header-logo { height: 1.65em; } @@ -106,3 +111,113 @@ header { width: 100%; flex: 1 } + +a { + color: #1967D2; + text-decoration: none; + tap-highlight-color: rgba(0, 0, 0, .10); +} + +.header-tab-div { + border-radius: 0 0 8px 8px; + box-shadow: 0 2px 3px rgba(32, 33, 36, 0.18); + margin-bottom: 20px; + overflow: hidden; +} + +.header-tab-div-2 { + border-top: 1px solid #dadce0; + height: 39px; + overflow: hidden; +} + +.header-tab-div-3 { + height: 51px; + overflow-x: auto; + overflow-y: hidden; +} + +.desktop-header { + height: 39px; + display: box; + display: flex; + width: 100%; +} + +.header-tab { + box-pack: justify; + font-size: 14px; + line-height: 37px; + justify-content: space-between; +} + +.desktop-header a, .desktop-header span { + color: #70757a; + display: block; + flex: none; + padding: 0 16px; + text-align: center; + text-transform: uppercase; +} + +span.header-tab-span { + border-bottom: 2px solid #4285f4; + color: #4285f4; + font-weight: bold; +} + +.mobile-header { + height: 39px; + display: box; + display: flex; + overflow-x: scroll; + width: 100%; + padding-left: 12px; +} + +.mobile-header a, .mobile-header span { + color: #70757a; + text-decoration: none; + display: inline-block; + /* padding: 8px 12px 8px 12px; */ +} + +span.mobile-tab-span { + border-bottom: 2px solid #202124; + color: #202124; + height: 26px; + /* margin: 0 12px; */ + /* padding: 0; */ +} + +.desktop-header input { + margin: 2px 4px 2px 8px; +} + +a.header-tab-a:visited { + color: #70757a; +} + +.header-tab-div-end { + border-left: 1px solid rgba(0, 0, 0, 0.12); +} + +.search-bar-input { + display: block; + font-size: 16px; + padding: 0 0 0 8px; + flex: 1; + height: 35px; + outline: none; + border: none; + width: 100%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + overflow: hidden; +} + +@media (max-width: 801px) { + .header-tab-div { + margin-bottom: 10px !important + } +} + diff --git a/app/static/css/light-theme.css b/app/static/css/light-theme.css index 56810ed..76dd155 100644 --- a/app/static/css/light-theme.css +++ b/app/static/css/light-theme.css @@ -43,6 +43,8 @@ select { .ZINbbc.luh4tb { background: var(--whoogle-result-bg) !important; + box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important; + margin-bottom: 24px !important; } .bRsWnc { @@ -190,3 +192,7 @@ path { .cb:focus { color: var(--whoogle-text) !important; } + +.desktop-header, .mobile-header { + background-color: var(--whoogle-result-bg) !important; +} diff --git a/app/static/css/search.css b/app/static/css/search.css index 23484a2..121026a 100644 --- a/app/static/css/search.css +++ b/app/static/css/search.css @@ -31,8 +31,9 @@ body { } details summary { - padding: 10px; + margin-bottom: 20px; font-weight: bold; + padding-left: 10px; } details summary span { @@ -53,8 +54,18 @@ details summary span { padding-top: 0 !important; } +.footer { + text-align: center; +} + @media (min-width: 801px) { body { min-width: 736px !important; } } + +@media (max-width: 801px) { + details summary { + margin-bottom: 10px !important + } +} diff --git a/app/static/settings/header_tabs.json b/app/static/settings/header_tabs.json new file mode 100644 index 0000000..0d8b239 --- /dev/null +++ b/app/static/settings/header_tabs.json @@ -0,0 +1,38 @@ +{ + "all": { + "tbm": null, + "href": "search?q={query}", + "name": "All", + "selected": true + }, + "images": { + "tbm": "isch", + "href": "search?q={query}", + "name": "Images", + "selected": false + }, + "maps": { + "tbm": null, + "href": "https://maps.google.com/maps?q={query}", + "name": "Maps", + "selected": false + }, + "videos": { + "tbm": "vid", + "href": "search?q={query}", + "name": "Videos", + "selected": false + }, + "news": { + "tbm": "nws", + "href": "search?q={query}", + "name": "News", + "selected": false + }, + "books": { + "tbm": "bks", + "href": "search?q={query}", + "name": "Books", + "selected": false + } +} \ No newline at end of file diff --git a/app/static/settings/translations.json b/app/static/settings/translations.json index 94a8889..4615e3a 100644 --- a/app/static/settings/translations.json +++ b/app/static/settings/translations.json @@ -35,7 +35,13 @@ "dark": "dark", "system": "system", "ratelimit": "Instance has been ratelimited", - "continue-search": "Continue your search with Farside" + "continue-search": "Continue your search with Farside", + "all": "All", + "images": "Images", + "maps": "Maps", + "videos": "Videos", + "news": "News", + "books": "Books" }, "lang_nl": { "search": "Zoeken", @@ -73,7 +79,13 @@ "dark": "donker", "system": "systeeminstellingen", "ratelimit": "Instantie is beperkt in snelheid", - "continue-search": "Ga verder met zoeken met Farside" + "continue-search": "Ga verder met zoeken met Farside", + "all": "Alle", + "images": "Afbeeldingen", + "maps": "Maps", + "videos": "Videos", + "news": "Nieuws", + "books": "Boeken" }, "lang_de": { "search": "Suchen", @@ -111,7 +123,13 @@ "dark": "dunkel", "system": "Systemeinstellung", "ratelimit": "Instanz wurde ratenbegrenzt", - "continue-search": "Setzen Sie Ihre Suche fort mit Farside" + "continue-search": "Setzen Sie Ihre Suche fort mit Farside", + "all": "Alle", + "images": "Bilder", + "maps": "Maps", + "videos": "Videos", + "news": "Nieuws", + "books": "Bücher" }, "lang_es": { "search": "Buscar", @@ -149,7 +167,13 @@ "dark": "oscuro", "system": "configuración del sistema", "ratelimit": "La instancia ha sido ratelimited", - "continue-search": "Continúe su búsqueda con Farside" + "continue-search": "Continúe su búsqueda con Farside", + "all": "Todo", + "images": "Imágenes", + "maps": "Maps", + "videos": "Vídeos", + "news": "Noticias", + "books": "Libros" }, "lang_it": { "search": "Cerca", @@ -187,7 +211,13 @@ "dark": "notte", "system": "impostazioni di sistema", "ratelimit": "L'istanza è stata limitata alla velocità", - "continue-search": "Continua la tua ricerca con Farside" + "continue-search": "Continua la tua ricerca con Farside", + "all": "Tutti", + "images": "Immagini", + "maps": "Maps", + "videos": "Video", + "news": "Notizie", + "books": "Libri" }, "lang_pt": { "search": "Pesquisar", @@ -225,7 +255,13 @@ "dark": "escuro", "system": "configuração de sistema", "ratelimit": "A instância foi limitada pela taxa", - "continue-search": "Continue sua pesquisa com Farside" + "continue-search": "Continue sua pesquisa com Farside", + "all": "Todas", + "images": "Imagens", + "maps": "Maps", + "videos": "Vídeos", + "news": "Notícias", + "books": "Livros" }, "lang_ru": { "search": "Поиск", @@ -263,7 +299,13 @@ "dark": "темное", "system": "системное", "ratelimit": "Число экземпляров ограничено", - "continue-search": "Продолжайте поиск с Farside" + "continue-search": "Продолжайте поиск с Farside", + "all": "Все", + "images": "Картинки", + "maps": "Карты", + "videos": "Видео", + "news": "Новости", + "books": "Книги" }, "lang_zh-CN": { "search": "搜索", @@ -301,7 +343,13 @@ "dark": "黑暗的", "system": "系统设置", "ratelimit": "实例已被限速", - "continue-search": "继续搜索 Farside" + "continue-search": "继续搜索 Farside", + "all": "全部", + "images": "圖片", + "maps": "影片", + "videos": "地圖", + "news": "新聞", + "books": "書籍" }, "lang_si": { "search": "සොයන්න", @@ -339,7 +387,13 @@ "dark": "අඳුරු", "system": "පද්ධතිය", "ratelimit": "සේවාදායකය අනුපාතනය කර ඇත", - "continue-search": "Farside සමඟ ඔබගේ සෙවුම කරගෙන යන්න" + "continue-search": "Farside සමඟ ඔබගේ සෙවුම කරගෙන යන්න", + "all": "සියල්ල", + "images": "රූප", + "maps": "සිතියම්", + "videos": "වීඩියෝ", + "news": "අනුරූප", + "books": "පොත්" }, "lang_fr": { "search": "Chercher", @@ -377,7 +431,13 @@ "dark": "sombre", "system": "système", "ratelimit": "Le débit de l'instance a été limité", - "continue-search": "Continuez votre recherche avec Farside" + "continue-search": "Continuez votre recherche avec Farside", + "all": "Tous", + "images": "Images", + "maps": "Maps", + "videos": "Vidéos", + "news": "Actualités", + "books": "Livres" }, "lang_fa": { "search": "جستجو", @@ -415,7 +475,13 @@ "dark": "تیره", "system": "سیستم", "ratelimit": "نمونه با نرخ محدود شده است", - "continue-search": "Farside جستجوی خود را با " + "continue-search": "Farside جستجوی خود را با ", + "all": "همه", + "images": "تصاویر", + "maps": "نقشه‌ها", + "videos": "ویدئوها", + "news": "اخبار", + "books": "کتاب‌ها" }, "lang_cs": { "search": "Hledat", @@ -453,7 +519,13 @@ "dark": "Tmavý", "system": "Systémový", "ratelimit": "Instance byla omezena sazbou", - "continue-search": "Pokračujte ve vyhledávání pomocí Farside" + "continue-search": "Pokračujte ve vyhledávání pomocí Farside", + "all": "Vše", + "images": "Obrázky", + "maps": "Mapy", + "videos": "Videa", + "news": "Zprávy", + "books": "Knihy" }, "lang_zh-TW": { "search": "搜尋", @@ -491,7 +563,13 @@ "dark": "黑暗的", "system": "依系統", "ratelimit": "實例已被限速", - "continue-search": "繼續搜索 Farside" + "continue-search": "繼續搜索 Farside", + "all": "全部", + "images": "圖片", + "maps": "影片", + "videos": "地圖", + "news": "新聞", + "books": "書籍" }, "lang_bg": { "search": "Търсене", @@ -529,7 +607,13 @@ "dark": "тъмна", "system": "системна", "ratelimit": "Екземплярът е с ограничена скорост", - "continue-search": "Продължете търсенето си с Farside" + "continue-search": "Продължете търсенето си с Farside", + "all": "Всичкo", + "images": "Изображения", + "maps": "Видеоклипове", + "videos": "Новини", + "news": "Карти", + "books": "Книги" }, "lang_hi": { "search": "खोज", @@ -567,7 +651,13 @@ "dark": "अंधेरा", "system": "प्रणाली", "ratelimit": "इंस्टेंस को सीमित कर दिया गया है", - "continue-search": "के साथ अपनी खोज जारी रखें Farside" + "continue-search": "के साथ अपनी खोज जारी रखें Farside", + "all": "सभी", + "images": "इमेज", + "maps": "वीडियो", + "videos": "मैप", + "news": "समाचार", + "books": "किताबें" }, "lang_ja": { "search": "検索", @@ -605,6 +695,12 @@ "dark": "ダーク", "system": "自動", "ratelimit": "インスタンスはレート制限されています", - "continue-search": "で検索を続ける Farside" + "continue-search": "で検索を続ける Farside", + "all": "すべて", + "images": "画像", + "maps": "地図", + "videos": "動画", + "news": "ニュース", + "books": "書籍" } } diff --git a/app/templates/header.html b/app/templates/header.html index e9c04e2..31d0f24 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -1,7 +1,7 @@ {% if mobile %}
-
-
+
-
+
+
+
+
+
+
+ {% for tab_id, tab_content in tabs.items() %} + {% if tab_content['selected'] %} + {{ tab_content['name'] }} + {% else %} + {{ tab_content['name'] }} + {% endif %} + {% endfor %} +
+
+
+
+
+
+
{% else %}
@@ -53,7 +72,7 @@ autocapitalize="none" autocomplete="off" autocorrect="off" - class="search-bar-desktop noHIxc" + class="search-bar-desktop search-bar-input" name="q" spellcheck="false" type="text" @@ -67,6 +86,25 @@
+
+
+
+
+
+ {% for tab_id, tab_content in tabs.items() %} + {% if tab_content['selected'] %} + {{ tab_content['name'] }} + {% else %} + {{ tab_content['name'] }} + {% endif %} + {% endfor %} +
+
+
+
+
+
+
{% endif %} diff --git a/app/templates/imageresults.html b/app/templates/imageresults.html index fc09b29..cc3f6d6 100644 --- a/app/templates/imageresults.html +++ b/app/templates/imageresults.html @@ -1,116 +1,390 @@ - - - - - - - +
- - - -
-
- - Google - -
-
-
- - - - - - - - - - -
- -
-
-
-
- -
-
-
-
- - -
-
+
+ + +
+
{% for i in range((length // 4) + 1) %} {% for j in range([length - (i*4), 4]|min) %} - - {% endfor %} - + + {% endfor %} + {% endfor %}
+
-
-
- -
-
- - +
+
diff --git a/app/utils/results.py b/app/utils/results.py index 6c710d6..808a1b5 100644 --- a/app/utils/results.py +++ b/app/utils/results.py @@ -1,5 +1,6 @@ from app.models.endpoint import Endpoint from bs4 import BeautifulSoup, NavigableString +import copy import html import os import urllib.parse as urlparse @@ -329,3 +330,39 @@ def add_currency_card(soup: BeautifulSoup, element1.insert_before(conversion_box) return soup + + +def get_tabs_content(tabs: dict, + full_query: str, + search_type: str, + translation: dict) -> dict: + """Takes the default tabs content and updates it according to the query. + + Args: + tabs: The default content for the tabs + full_query: The original search query + search_type: The current search_type + translation: The translation to get the names of the tabs + + Returns: + dict: contains the name, the href and if the tab is selected or not + """ + tabs = copy.deepcopy(tabs) + for tab_id, tab_content in tabs.items(): + # update name to desired language + if tab_id in translation: + tab_content['name'] = translation[tab_id] + + # update href with query + query = full_query.replace(f'&tbm={search_type}', '') + + if tab_content['tbm'] is not None: + query = f"{query}&tbm={tab_content['tbm']}" + + tab_content['href'] = tab_content['href'].format(query=query) + + # update if selected tab (default all tab is selected) + if tab_content['tbm'] == search_type: + tabs['all']['selected'] = False + tab_content['selected'] = True + return tabs diff --git a/app/utils/search.py b/app/utils/search.py index 546ec88..d476b5b 100644 --- a/app/utils/search.py +++ b/app/utils/search.py @@ -120,6 +120,7 @@ class Search: full_query = gen_query(self.query, self.request_params, self.config) + self.full_query = full_query # force mobile search when view image is true and # the request is not already made by a mobile