upload
BIN
.DS_Store
vendored
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.git/
|
||||||
|
venv/
|
||||||
|
test/
|
9
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# These are supported funding model platforms
|
||||||
|
github: benbusby
|
||||||
|
ko_fi: benbusby
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
45
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a bug report to help fix an issue with Whoogle
|
||||||
|
title: "[BUG] <brief bug description>"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Deployment Method**
|
||||||
|
- [ ] Heroku (one-click deploy)
|
||||||
|
- [ ] Docker
|
||||||
|
- [ ] `run` executable
|
||||||
|
- [ ] pip/pipx
|
||||||
|
- [ ] Other: [describe setup]
|
||||||
|
|
||||||
|
**Version of Whoogle Search**
|
||||||
|
- [ ] Latest build from [source] (i.e. GitHub, Docker Hub, pip, etc)
|
||||||
|
- [ ] Version [version number]
|
||||||
|
- [ ] Not sure
|
||||||
|
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest a feature that would improve Whoogle
|
||||||
|
title: "[FEATURE] <description of feature>"
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
DO NOT REQUEST UI/THEME/GUI/APPEARANCE IMPROVEMENTS HERE
|
||||||
|
THESE SHOULD GO IN ISSUE #60
|
||||||
|
REQUESTING A NEW FEATURE SHOULD BE STRICTLY RELATED TO NEW FUNCTIONALITY
|
||||||
|
-->
|
||||||
|
|
||||||
|
**Describe the feature you'd like to see added**
|
||||||
|
A short description of the feature, and what it would accomplish.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
38
.github/ISSUE_TEMPLATE/new-theme.md
vendored
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
name: New theme
|
||||||
|
about: Create a new theme for Whoogle
|
||||||
|
title: "[THEME] <your theme name>"
|
||||||
|
labels: theme
|
||||||
|
assignees: benbusby
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Use the following template to design your theme, replacing the blank spaces with the colors of your choice.
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
/* LIGHT THEME COLORS */
|
||||||
|
--whoogle-logo: #______;
|
||||||
|
--whoogle-page-bg: #______;
|
||||||
|
--whoogle-element-bg: #______;
|
||||||
|
--whoogle-text: #______;
|
||||||
|
--whoogle-contrast-text: #______;
|
||||||
|
--whoogle-secondary-text: #______;
|
||||||
|
--whoogle-result-bg: #______;
|
||||||
|
--whoogle-result-title: #______;
|
||||||
|
--whoogle-result-url: #______;
|
||||||
|
--whoogle-result-visited: #______;
|
||||||
|
|
||||||
|
/* DARK THEME COLORS */
|
||||||
|
--whoogle-dark-logo: #______;
|
||||||
|
--whoogle-dark-page-bg: #______;
|
||||||
|
--whoogle-dark-element-bg: #______;
|
||||||
|
--whoogle-dark-text: #______;
|
||||||
|
--whoogle-dark-contrast-text: #______;
|
||||||
|
--whoogle-dark-secondary-text: #______;
|
||||||
|
--whoogle-dark-result-bg: #______;
|
||||||
|
--whoogle-dark-result-title: #______;
|
||||||
|
--whoogle-dark-result-url: #______;
|
||||||
|
--whoogle-dark-result-visited: #______;
|
||||||
|
}
|
||||||
|
```
|
10
.github/ISSUE_TEMPLATE/question.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a (simple) question about Whoogle
|
||||||
|
title: "[QUESTION] <question here>"
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Type out your question here. Please make sure that this is a topic that isn't already covered in the README.
|
59
.github/workflows/buildx.yml
vendored
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
name: buildx
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["docker_main"]
|
||||||
|
branches: [main]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
on-success:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Wait for tests to succeed
|
||||||
|
if: ${{ github.event.workflow_run.conclusion != 'success' && startsWith(github.ref, 'refs/tags') != true }}
|
||||||
|
run: exit 1
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: install buildx
|
||||||
|
id: buildx
|
||||||
|
uses: crazy-max/ghaction-docker-buildx@v1
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
- name: Login to ghcr.io
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: build and push the image
|
||||||
|
if: startsWith(github.ref, 'refs/heads/main') && github.actor == 'benbusby'
|
||||||
|
run: |
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
|
docker buildx ls
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag benbusby/whoogle-search:latest \
|
||||||
|
--platform linux/amd64,linux/arm64 .
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag ghcr.io/benbusby/whoogle-search:latest \
|
||||||
|
--platform linux/amd64,linux/arm64 .
|
||||||
|
- name: build and push tag
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
run: |
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
|
docker buildx ls
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
||||||
|
docker buildx build --push \
|
||||||
|
--tag ghcr.io/benbusby/whoogle-search:${GITHUB_REF#refs/*/v}\
|
||||||
|
--platform linux/amd64,linux/arm/v7,linux/arm64 .
|
28
.github/workflows/docker_main.yml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: docker_main
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["tests"]
|
||||||
|
branches: [main]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
# TODO: Needs refactoring to use reusable workflows and share w/ docker_tests
|
||||||
|
jobs:
|
||||||
|
on-success:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: build and test (docker)
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
|
||||||
|
- name: build and test (docker-compose)
|
||||||
|
run: |
|
||||||
|
docker rm -f whoogle-search-nocompose
|
||||||
|
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
26
.github/workflows/docker_tests.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
name: docker_tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
pull_request:
|
||||||
|
branches: main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: build and test (docker)
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
docker run --publish 5000:5000 --detach --name whoogle-search-nocompose whoogle-search:test
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search-nocompose curl -f http://localhost:5000/healthz || exit 1
|
||||||
|
- name: build and test (docker compose)
|
||||||
|
run: |
|
||||||
|
docker rm -f whoogle-search-nocompose
|
||||||
|
WHOOGLE_IMAGE="whoogle-search:test" docker compose up --detach
|
||||||
|
sleep 15
|
||||||
|
docker exec whoogle-search curl -f http://localhost:5000/healthz || exit 1
|
67
.github/workflows/pypi.yml
vendored
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
name: pypi
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: main
|
||||||
|
tags: v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-test:
|
||||||
|
name: Build and publish to TestPyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
pip install
|
||||||
|
build
|
||||||
|
setuptools
|
||||||
|
--user
|
||||||
|
- name: Set dev timestamp
|
||||||
|
run: echo "DEV_BUILD=$(date +%s)" >> $GITHUB_ENV
|
||||||
|
- name: Build binary wheel and source tarball
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
build
|
||||||
|
--sdist
|
||||||
|
--wheel
|
||||||
|
--outdir dist/
|
||||||
|
.
|
||||||
|
- name: Publish distribution to TestPyPI
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||||
|
repository_url: https://test.pypi.org/legacy/
|
||||||
|
publish:
|
||||||
|
name: Build and publish to PyPI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python 3.9
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
pip install
|
||||||
|
build
|
||||||
|
--user
|
||||||
|
- name: Build binary wheel and source tarball
|
||||||
|
run: >-
|
||||||
|
python -m
|
||||||
|
build
|
||||||
|
--sdist
|
||||||
|
--wheel
|
||||||
|
--outdir dist/
|
||||||
|
.
|
||||||
|
- name: Publish distribution to PyPI
|
||||||
|
if: startsWith(github.ref, 'refs/tags')
|
||||||
|
uses: pypa/gh-action-pypi-publish@master
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
19
.github/workflows/scan.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
name: scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
scan:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Build the container image
|
||||||
|
run: |
|
||||||
|
docker build --tag whoogle-search:test .
|
||||||
|
- name: Initiate grype scan
|
||||||
|
run: |
|
||||||
|
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b .
|
||||||
|
chmod +x ./grype
|
||||||
|
./grype whoogle-search:test --only-fixed
|
17
.github/workflows/tests.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: tests
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install --upgrade pip && pip install -r requirements.txt
|
||||||
|
- name: Run tests
|
||||||
|
run: ./run test
|
27
.gitignore
vendored
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
.idea/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pem
|
||||||
|
*.conf
|
||||||
|
*.key
|
||||||
|
config.json
|
||||||
|
test/static
|
||||||
|
flask_session/
|
||||||
|
app/static/config
|
||||||
|
app/static/custom_config
|
||||||
|
app/static/bangs/*
|
||||||
|
!app/static/bangs/00-whoogle.json
|
||||||
|
|
||||||
|
# pip stuff
|
||||||
|
/build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|
||||||
|
# env
|
||||||
|
whoogle.env
|
||||||
|
|
||||||
|
# vim
|
||||||
|
*~
|
||||||
|
*.swp
|
1
.replit
Normal file
|
@ -0,0 +1 @@
|
||||||
|
entrypoint = "misc/replit.py"
|
103
Dockerfile
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
FROM python:3.12.6-alpine3.20 AS builder
|
||||||
|
|
||||||
|
RUN apk --update add \
|
||||||
|
build-base \
|
||||||
|
libxml2-dev \
|
||||||
|
libxslt-dev \
|
||||||
|
openssl-dev \
|
||||||
|
libffi-dev
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
RUN pip install --upgrade pip
|
||||||
|
RUN pip install --prefix /install --no-warn-script-location --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
FROM python:3.12.6-alpine3.20
|
||||||
|
|
||||||
|
RUN apk add --update --no-cache tor curl openrc libstdc++
|
||||||
|
# git go //for obfs4proxy
|
||||||
|
# libcurl4-openssl-dev
|
||||||
|
|
||||||
|
RUN apk -U upgrade
|
||||||
|
|
||||||
|
# uncomment to build obfs4proxy
|
||||||
|
# RUN git clone https://gitlab.com/yawning/obfs4.git
|
||||||
|
# WORKDIR /obfs4
|
||||||
|
# RUN go build -o obfs4proxy/obfs4proxy ./obfs4proxy
|
||||||
|
# RUN cp ./obfs4proxy/obfs4proxy /usr/bin/obfs4proxy
|
||||||
|
|
||||||
|
ARG DOCKER_USER=whoogle
|
||||||
|
ARG DOCKER_USERID=927
|
||||||
|
ARG config_dir=/config
|
||||||
|
RUN mkdir -p $config_dir
|
||||||
|
RUN chmod a+w $config_dir
|
||||||
|
VOLUME $config_dir
|
||||||
|
|
||||||
|
ARG url_prefix=''
|
||||||
|
ARG username=''
|
||||||
|
ARG password=''
|
||||||
|
ARG proxyuser=''
|
||||||
|
ARG proxypass=''
|
||||||
|
ARG proxytype=''
|
||||||
|
ARG proxyloc=''
|
||||||
|
ARG whoogle_dotenv=''
|
||||||
|
ARG use_https=''
|
||||||
|
ARG whoogle_port=5000
|
||||||
|
ARG twitter_alt='farside.link/nitter'
|
||||||
|
ARG youtube_alt='farside.link/invidious'
|
||||||
|
ARG reddit_alt='farside.link/libreddit'
|
||||||
|
ARG medium_alt='farside.link/scribe'
|
||||||
|
ARG translate_alt='farside.link/lingva'
|
||||||
|
ARG imgur_alt='farside.link/rimgo'
|
||||||
|
ARG wikipedia_alt='farside.link/wikiless'
|
||||||
|
ARG imdb_alt='farside.link/libremdb'
|
||||||
|
ARG quora_alt='farside.link/quetre'
|
||||||
|
ARG so_alt='farside.link/anonymousoverflow'
|
||||||
|
|
||||||
|
ENV CONFIG_VOLUME=$config_dir \
|
||||||
|
WHOOGLE_URL_PREFIX=$url_prefix \
|
||||||
|
WHOOGLE_USER=$username \
|
||||||
|
WHOOGLE_PASS=$password \
|
||||||
|
WHOOGLE_PROXY_USER=$proxyuser \
|
||||||
|
WHOOGLE_PROXY_PASS=$proxypass \
|
||||||
|
WHOOGLE_PROXY_TYPE=$proxytype \
|
||||||
|
WHOOGLE_PROXY_LOC=$proxyloc \
|
||||||
|
WHOOGLE_DOTENV=$whoogle_dotenv \
|
||||||
|
HTTPS_ONLY=$use_https \
|
||||||
|
EXPOSE_PORT=$whoogle_port \
|
||||||
|
WHOOGLE_ALT_TW=$twitter_alt \
|
||||||
|
WHOOGLE_ALT_YT=$youtube_alt \
|
||||||
|
WHOOGLE_ALT_RD=$reddit_alt \
|
||||||
|
WHOOGLE_ALT_MD=$medium_alt \
|
||||||
|
WHOOGLE_ALT_TL=$translate_alt \
|
||||||
|
WHOOGLE_ALT_IMG=$imgur_alt \
|
||||||
|
WHOOGLE_ALT_WIKI=$wikipedia_alt \
|
||||||
|
WHOOGLE_ALT_IMDB=$imdb_alt \
|
||||||
|
WHOOGLE_ALT_QUORA=$quora_alt \
|
||||||
|
WHOOGLE_ALT_SO=$so_alt
|
||||||
|
|
||||||
|
WORKDIR /whoogle
|
||||||
|
|
||||||
|
COPY --from=builder /install /usr/local
|
||||||
|
COPY misc/tor/torrc /etc/tor/torrc
|
||||||
|
COPY misc/tor/start-tor.sh misc/tor/start-tor.sh
|
||||||
|
COPY app/ app/
|
||||||
|
COPY run whoogle.env* ./
|
||||||
|
|
||||||
|
# Create user/group to run as
|
||||||
|
RUN adduser -D -g $DOCKER_USERID -u $DOCKER_USERID $DOCKER_USER
|
||||||
|
|
||||||
|
# Fix ownership / permissions
|
||||||
|
RUN chown -R ${DOCKER_USER}:${DOCKER_USER} /whoogle /var/lib/tor
|
||||||
|
|
||||||
|
# Allow writing symlinks to build dir
|
||||||
|
RUN chown $DOCKER_USERID:$DOCKER_USERID app/static/build
|
||||||
|
|
||||||
|
USER $DOCKER_USER:$DOCKER_USER
|
||||||
|
|
||||||
|
EXPOSE $EXPOSE_PORT
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s \
|
||||||
|
CMD curl -f http://localhost:${EXPOSE_PORT}/healthz || exit 1
|
||||||
|
|
||||||
|
CMD misc/tor/start-tor.sh & ./run
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 Ben Busby
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
6
MANIFEST.in
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
graft app/static
|
||||||
|
graft app/templates
|
||||||
|
graft app/misc
|
||||||
|
include requirements.txt
|
||||||
|
recursive-include test
|
||||||
|
global-exclude *.pyc
|
194
app.json
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
{
|
||||||
|
"name": "Whoogle Search",
|
||||||
|
"description": "A lightweight, privacy-oriented, containerized Google search proxy for desktop/mobile that removes Javascript, AMP links, tracking, and ads/sponsored content",
|
||||||
|
"repository": "https://github.com/benbusby/whoogle-search",
|
||||||
|
"logo": "https://raw.githubusercontent.com/benbusby/whoogle-search/master/app/static/img/favicon/ms-icon-150x150.png",
|
||||||
|
"keywords": [
|
||||||
|
"search",
|
||||||
|
"metasearch",
|
||||||
|
"flask",
|
||||||
|
"docker",
|
||||||
|
"heroku",
|
||||||
|
"adblock",
|
||||||
|
"degoogle",
|
||||||
|
"privacy"
|
||||||
|
],
|
||||||
|
"stack": "container",
|
||||||
|
"env": {
|
||||||
|
"WHOOGLE_URL_PREFIX": {
|
||||||
|
"description": "The URL prefix to use for the whoogle instance (i.e. \"/whoogle\")",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_USER": {
|
||||||
|
"description": "The username for basic auth. WHOOGLE_PASS must also be set if used. Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_PASS": {
|
||||||
|
"description": "The password for basic auth. WHOOGLE_USER must also be set if used. Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_PROXY_USER": {
|
||||||
|
"description": "The username of the proxy server. Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_PROXY_PASS": {
|
||||||
|
"description": "The password of the proxy server. Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_PROXY_TYPE": {
|
||||||
|
"description": "The type of the proxy server. For example \"socks5\". Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_PROXY_LOC": {
|
||||||
|
"description": "The location of the proxy server (host or ip). Leave empty to disable.",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_TW": {
|
||||||
|
"description": "The site to use as a replacement for twitter.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/nitter",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_YT": {
|
||||||
|
"description": "The site to use as a replacement for youtube.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/invidious",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_RD": {
|
||||||
|
"description": "The site to use as a replacement for reddit.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/libreddit",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_MD": {
|
||||||
|
"description": "The site to use as a replacement for medium.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/scribe",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_TL": {
|
||||||
|
"description": "The Google Translate alternative to use for all searches following the 'translate ___' structure.",
|
||||||
|
"value": "farside.link/lingva",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_IMG": {
|
||||||
|
"description": "The site to use as a replacement for imgur.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/rimgo",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_WIKI": {
|
||||||
|
"description": "The site to use as a replacement for wikipedia.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/wikiless",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_IMDB": {
|
||||||
|
"description": "The site to use as a replacement for imdb.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/libremdb",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_QUORA": {
|
||||||
|
"description": "The site to use as a replacement for quora.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/quetre",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_ALT_SO": {
|
||||||
|
"description": "The site to use as a replacement for stackoverflow.com when site alternatives are enabled in the config.",
|
||||||
|
"value": "farside.link/anonymousoverflow",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_MINIMAL": {
|
||||||
|
"description": "Remove everything except basic result cards from all search queries (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_COUNTRY": {
|
||||||
|
"description": "[CONFIG] The country to use for restricting search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/countries.json)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_TIME_PERIOD" : {
|
||||||
|
"description": "[CONFIG] The time period to use for restricting search results",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_LANGUAGE": {
|
||||||
|
"description": "[CONFIG] The language to use for the interface (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_SEARCH_LANGUAGE": {
|
||||||
|
"description": "[CONFIG] The language to use for search results (use values from https://raw.githubusercontent.com/benbusby/whoogle-search/develop/app/static/settings/languages.json)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_DISABLE": {
|
||||||
|
"description": "[CONFIG] Disable ability for client to change config (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_BLOCK": {
|
||||||
|
"description": "[CONFIG] Block websites from search results (comma-separated list)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_THEME": {
|
||||||
|
"description": "[CONFIG] Set theme to 'dark', 'light', or 'system'",
|
||||||
|
"value": "system",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_SAFE": {
|
||||||
|
"description": "[CONFIG] Use safe mode for searches (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_ALTS": {
|
||||||
|
"description": "[CONFIG] Use social media alternatives (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_NEAR": {
|
||||||
|
"description": "[CONFIG] Restrict results to only those near a particular city",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_TOR": {
|
||||||
|
"description": "[CONFIG] Use Tor, if available (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_NEW_TAB": {
|
||||||
|
"description": "[CONFIG] Always open results in new tab (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_VIEW_IMAGE": {
|
||||||
|
"description": "[CONFIG] Enable View Image option (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_GET_ONLY": {
|
||||||
|
"description": "[CONFIG] Search using GET requests only (set to 1 or leave blank)",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_STYLE": {
|
||||||
|
"description": "[CONFIG] Custom CSS styling (paste in CSS or leave blank)",
|
||||||
|
"value": ":root { /* LIGHT THEME COLORS */ --whoogle-background: #d8dee9; --whoogle-accent: #2e3440; --whoogle-text: #3B4252; --whoogle-contrast-text: #eceff4; --whoogle-secondary-text: #70757a; --whoogle-result-bg: #fff; --whoogle-result-title: #4c566a; --whoogle-result-url: #81a1c1; --whoogle-result-visited: #a3be8c; /* DARK THEME COLORS */ --whoogle-dark-background: #222; --whoogle-dark-accent: #685e79; --whoogle-dark-text: #fff; --whoogle-dark-contrast-text: #000; --whoogle-dark-secondary-text: #bbb; --whoogle-dark-result-bg: #000; --whoogle-dark-result-title: #1967d2; --whoogle-dark-result-url: #4b11a8; --whoogle-dark-result-visited: #bbbbff; }",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED": {
|
||||||
|
"description": "[CONFIG] Encrypt preferences token, requires WHOOGLE_CONFIG_PREFERENCES_KEY to be set",
|
||||||
|
"value": "",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"WHOOGLE_CONFIG_PREFERENCES_KEY": {
|
||||||
|
"description": "[CONFIG] Key to encrypt preferences",
|
||||||
|
"value": "NEEDS_TO_BE_MODIFIED",
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
BIN
app/.DS_Store
vendored
Normal file
199
app/__init__.py
Executable file
|
@ -0,0 +1,199 @@
|
||||||
|
from app.filter import clean_query
|
||||||
|
from app.request import send_tor_signal
|
||||||
|
from app.utils.session import generate_key
|
||||||
|
from app.utils.bangs import gen_bangs_json, load_all_bangs
|
||||||
|
from app.utils.misc import gen_file_hash, read_config_bool
|
||||||
|
from base64 import b64encode
|
||||||
|
from bs4 import MarkupResemblesLocatorWarning
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from flask import Flask
|
||||||
|
import json
|
||||||
|
import logging.config
|
||||||
|
import os
|
||||||
|
from stem import Signal
|
||||||
|
import threading
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
|
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
|
from app.version import __version__
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder=os.path.dirname(
|
||||||
|
os.path.abspath(__file__)) + '/static')
|
||||||
|
|
||||||
|
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||||
|
|
||||||
|
# look for WHOOGLE_ENV, else look in parent directory
|
||||||
|
dot_env_path = os.getenv(
|
||||||
|
"WHOOGLE_DOTENV_PATH",
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "../whoogle.env"))
|
||||||
|
|
||||||
|
# Load .env file if enabled
|
||||||
|
if os.path.exists(dot_env_path):
|
||||||
|
load_dotenv(dot_env_path)
|
||||||
|
|
||||||
|
app.enc_key = generate_key()
|
||||||
|
|
||||||
|
if read_config_bool('HTTPS_ONLY'):
|
||||||
|
app.config['SESSION_COOKIE_NAME'] = '__Secure-session'
|
||||||
|
app.config['SESSION_COOKIE_SECURE'] = True
|
||||||
|
|
||||||
|
app.config['VERSION_NUMBER'] = __version__
|
||||||
|
app.config['APP_ROOT'] = os.getenv(
|
||||||
|
'APP_ROOT',
|
||||||
|
os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
app.config['STATIC_FOLDER'] = os.getenv(
|
||||||
|
'STATIC_FOLDER',
|
||||||
|
os.path.join(app.config['APP_ROOT'], 'static'))
|
||||||
|
app.config['BUILD_FOLDER'] = os.path.join(
|
||||||
|
app.config['STATIC_FOLDER'], 'build')
|
||||||
|
app.config['CACHE_BUSTING_MAP'] = {}
|
||||||
|
app.config['LANGUAGES'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/languages.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
app.config['COUNTRIES'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/countries.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
app.config['TIME_PERIODS'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/time_periods.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
app.config['TRANSLATIONS'] = json.load(open(
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'settings/translations.json'),
|
||||||
|
encoding='utf-8'))
|
||||||
|
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'))
|
||||||
|
app.config['DEFAULT_CONFIG'] = os.path.join(
|
||||||
|
app.config['CONFIG_PATH'],
|
||||||
|
'config.json')
|
||||||
|
app.config['CONFIG_DISABLE'] = read_config_bool('WHOOGLE_CONFIG_DISABLE')
|
||||||
|
app.config['SESSION_FILE_DIR'] = os.path.join(
|
||||||
|
app.config['CONFIG_PATH'],
|
||||||
|
'session')
|
||||||
|
app.config['MAX_SESSION_SIZE'] = 4000 # Sessions won't exceed 4KB
|
||||||
|
app.config['BANG_PATH'] = os.getenv(
|
||||||
|
'CONFIG_VOLUME',
|
||||||
|
os.path.join(app.config['STATIC_FOLDER'], 'bangs'))
|
||||||
|
app.config['BANG_FILE'] = os.path.join(
|
||||||
|
app.config['BANG_PATH'],
|
||||||
|
'bangs.json')
|
||||||
|
|
||||||
|
# Ensure all necessary directories exist
|
||||||
|
if not os.path.exists(app.config['CONFIG_PATH']):
|
||||||
|
os.makedirs(app.config['CONFIG_PATH'])
|
||||||
|
|
||||||
|
if not os.path.exists(app.config['SESSION_FILE_DIR']):
|
||||||
|
os.makedirs(app.config['SESSION_FILE_DIR'])
|
||||||
|
|
||||||
|
if not os.path.exists(app.config['BANG_PATH']):
|
||||||
|
os.makedirs(app.config['BANG_PATH'])
|
||||||
|
|
||||||
|
if not os.path.exists(app.config['BUILD_FOLDER']):
|
||||||
|
os.makedirs(app.config['BUILD_FOLDER'])
|
||||||
|
|
||||||
|
# Session values
|
||||||
|
app_key_path = os.path.join(app.config['CONFIG_PATH'], 'whoogle.key')
|
||||||
|
if os.path.exists(app_key_path):
|
||||||
|
try:
|
||||||
|
app.config['SECRET_KEY'] = open(app_key_path, 'r').read()
|
||||||
|
except PermissionError:
|
||||||
|
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
||||||
|
else:
|
||||||
|
app.config['SECRET_KEY'] = str(b64encode(os.urandom(32)))
|
||||||
|
with open(app_key_path, 'w') as key_file:
|
||||||
|
key_file.write(app.config['SECRET_KEY'])
|
||||||
|
key_file.close()
|
||||||
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=365)
|
||||||
|
|
||||||
|
# NOTE: SESSION_COOKIE_SAMESITE must be set to 'lax' to allow the user's
|
||||||
|
# previous session to persist when accessing the instance from an external
|
||||||
|
# link. Setting this value to 'strict' causes Whoogle to revalidate a new
|
||||||
|
# session, and fail, resulting in cookies being disabled.
|
||||||
|
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
||||||
|
|
||||||
|
# Config fields that are used to check for updates
|
||||||
|
app.config['RELEASES_URL'] = 'https://github.com/' \
|
||||||
|
'benbusby/whoogle-search/releases'
|
||||||
|
app.config['LAST_UPDATE_CHECK'] = datetime.now() - timedelta(hours=24)
|
||||||
|
app.config['HAS_UPDATE'] = ''
|
||||||
|
|
||||||
|
# The alternative to Google Translate is treated a bit differently than other
|
||||||
|
# social media site alternatives, in that it is used for any translation
|
||||||
|
# related searches.
|
||||||
|
translate_url = os.getenv('WHOOGLE_ALT_TL', 'https://farside.link/lingva')
|
||||||
|
if not translate_url.startswith('http'):
|
||||||
|
translate_url = 'https://' + translate_url
|
||||||
|
app.config['TRANSLATE_URL'] = translate_url
|
||||||
|
|
||||||
|
app.config['CSP'] = 'default-src \'none\';' \
|
||||||
|
'frame-src ' + translate_url + ';' \
|
||||||
|
'manifest-src \'self\';' \
|
||||||
|
'img-src \'self\' data:;' \
|
||||||
|
'style-src \'self\' \'unsafe-inline\';' \
|
||||||
|
'script-src \'self\';' \
|
||||||
|
'media-src \'self\';' \
|
||||||
|
'connect-src \'self\';'
|
||||||
|
|
||||||
|
# Generate DDG bang filter
|
||||||
|
generating_bangs = False
|
||||||
|
if not os.path.exists(app.config['BANG_FILE']):
|
||||||
|
generating_bangs = True
|
||||||
|
json.dump({}, open(app.config['BANG_FILE'], 'w'))
|
||||||
|
bangs_thread = threading.Thread(
|
||||||
|
target=gen_bangs_json,
|
||||||
|
args=(app.config['BANG_FILE'],))
|
||||||
|
bangs_thread.start()
|
||||||
|
|
||||||
|
# Build new mapping of static files for cache busting
|
||||||
|
cache_busting_dirs = ['css', 'js']
|
||||||
|
for cb_dir in cache_busting_dirs:
|
||||||
|
full_cb_dir = os.path.join(app.config['STATIC_FOLDER'], cb_dir)
|
||||||
|
for cb_file in os.listdir(full_cb_dir):
|
||||||
|
# Create hash from current file state
|
||||||
|
full_cb_path = os.path.join(full_cb_dir, cb_file)
|
||||||
|
cb_file_link = gen_file_hash(full_cb_dir, cb_file)
|
||||||
|
build_path = os.path.join(app.config['BUILD_FOLDER'], cb_file_link)
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.symlink(full_cb_path, build_path)
|
||||||
|
except FileExistsError:
|
||||||
|
# Symlink hasn't changed, ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create mapping for relative path urls
|
||||||
|
map_path = build_path.replace(app.config['APP_ROOT'], '')
|
||||||
|
if map_path.startswith('/'):
|
||||||
|
map_path = map_path[1:]
|
||||||
|
app.config['CACHE_BUSTING_MAP'][cb_file] = map_path
|
||||||
|
|
||||||
|
# Templating functions
|
||||||
|
app.jinja_env.globals.update(clean_query=clean_query)
|
||||||
|
app.jinja_env.globals.update(
|
||||||
|
cb_url=lambda f: app.config['CACHE_BUSTING_MAP'][f.lower()])
|
||||||
|
|
||||||
|
# Attempt to acquire tor identity, to determine if Tor config is available
|
||||||
|
send_tor_signal(Signal.HEARTBEAT)
|
||||||
|
|
||||||
|
# Suppress spurious warnings from BeautifulSoup
|
||||||
|
warnings.simplefilter('ignore', MarkupResemblesLocatorWarning)
|
||||||
|
|
||||||
|
from app import routes # noqa
|
||||||
|
|
||||||
|
# The gen_bangs_json function takes care of loading bangs, so skip it here if
|
||||||
|
# it's already being loaded
|
||||||
|
if not generating_bangs:
|
||||||
|
load_all_bangs(app.config['BANG_FILE'])
|
||||||
|
|
||||||
|
# Disable logging from imported modules
|
||||||
|
logging.config.dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': True,
|
||||||
|
})
|
3
app/__main__.py
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
from .routes import run_app
|
||||||
|
|
||||||
|
run_app()
|
785
app/filter.py
Executable file
|
@ -0,0 +1,785 @@
|
||||||
|
import cssutils
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from bs4.element import ResultSet, Tag
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from flask import render_template
|
||||||
|
import html
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
import re
|
||||||
|
|
||||||
|
from app.models.g_classes import GClasses
|
||||||
|
from app.request import VALID_PARAMS, MAPS_URL
|
||||||
|
from app.utils.misc import get_abs_url, read_config_bool
|
||||||
|
from app.utils.results import (
|
||||||
|
BLANK_B64, GOOG_IMG, GOOG_STATIC, G_M_LOGO_URL, LOGO_URL, SITE_ALTS,
|
||||||
|
has_ad_content, filter_link_args, append_anon_view, get_site_alt,
|
||||||
|
)
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
from app.models.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
MAPS_ARGS = ['q', 'daddr']
|
||||||
|
|
||||||
|
minimal_mode_sections = ['Top stories', 'Images', 'People also ask']
|
||||||
|
unsupported_g_pages = [
|
||||||
|
'google.com/aclk'
|
||||||
|
'*.googleapis.com'
|
||||||
|
'*.gstatic.com'
|
||||||
|
'*.google-analytics.com'
|
||||||
|
'adservice.google.com'
|
||||||
|
'support.google.com',
|
||||||
|
'accounts.google.com',
|
||||||
|
'policies.google.com',
|
||||||
|
'google.com/preferences',
|
||||||
|
'google.com/intl',
|
||||||
|
'advanced_search',
|
||||||
|
'tbm=shop',
|
||||||
|
'ageverification.google.co.kr'
|
||||||
|
]
|
||||||
|
|
||||||
|
unsupported_g_divs = [
|
||||||
|
'google.com/preferences?hl=',
|
||||||
|
'ageverification.google.co.kr'
|
||||||
|
'google.com/aclk?sa='
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_q(q_str: str, href: str) -> str:
|
||||||
|
"""Extracts the 'q' element from a result link. This is typically
|
||||||
|
either the link to a result's website, or a string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
q_str: The result link to parse
|
||||||
|
href: The full url to check for standalone 'q' elements first,
|
||||||
|
rather than parsing the whole query string and then checking.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The 'q' element of the link, or an empty string
|
||||||
|
"""
|
||||||
|
return parse_qs(q_str, keep_blank_values=True)['q'][0] if ('&q=' in href or '?q=' in href) else ''
|
||||||
|
|
||||||
|
|
||||||
|
def build_map_url(href: str) -> str:
|
||||||
|
"""Tries to extract known args that explain the location in the url. If a
|
||||||
|
location is found, returns the default url with it. Otherwise, returns the
|
||||||
|
url unchanged.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
href: The full url to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The parsed url, or the url unchanged.
|
||||||
|
"""
|
||||||
|
# parse the url
|
||||||
|
parsed_url = parse_qs(href)
|
||||||
|
# iterate through the known parameters and try build the url
|
||||||
|
for param in MAPS_ARGS:
|
||||||
|
if param in parsed_url:
|
||||||
|
return MAPS_URL + "?q=" + parsed_url[param][0]
|
||||||
|
|
||||||
|
# query could not be extracted returning unchanged url
|
||||||
|
return href
|
||||||
|
|
||||||
|
|
||||||
|
def clean_query(query: str) -> str:
|
||||||
|
"""Strips the blocked site list from the query, if one is being
|
||||||
|
used.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The query string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The query string without any "-site:..." filters
|
||||||
|
"""
|
||||||
|
return query[:query.find('-site:')] if '-site:' in query else query
|
||||||
|
|
||||||
|
|
||||||
|
def clean_css(css: str, page_url: str) -> str:
|
||||||
|
"""Removes all remote URLs from a CSS string.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
css: The CSS string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The filtered CSS, with URLs proxied through Whoogle
|
||||||
|
"""
|
||||||
|
sheet = cssutils.parseString(css)
|
||||||
|
urls = cssutils.getUrls(sheet)
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
abs_url = get_abs_url(url, page_url)
|
||||||
|
if abs_url.startswith('data:'):
|
||||||
|
continue
|
||||||
|
css = css.replace(
|
||||||
|
url,
|
||||||
|
f'{Endpoint.element}?type=image/png&url={abs_url}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return css
|
||||||
|
|
||||||
|
|
||||||
|
class Filter:
|
||||||
|
# Limit used for determining if a result is a "regular" result or a list
|
||||||
|
# type result (such as "people also asked", "related searches", etc)
|
||||||
|
RESULT_CHILD_LIMIT = 7
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
user_key: str,
|
||||||
|
config: Config,
|
||||||
|
root_url='',
|
||||||
|
page_url='',
|
||||||
|
query='',
|
||||||
|
mobile=False) -> None:
|
||||||
|
self.soup = None
|
||||||
|
self.config = config
|
||||||
|
self.mobile = mobile
|
||||||
|
self.user_key = user_key
|
||||||
|
self.page_url = page_url
|
||||||
|
self.query = query
|
||||||
|
self.main_divs = ResultSet('')
|
||||||
|
self._elements = 0
|
||||||
|
self._av = set()
|
||||||
|
|
||||||
|
self.root_url = root_url[:-1] if root_url.endswith('/') else root_url
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return getattr(self, name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def elements(self):
|
||||||
|
return self._elements
|
||||||
|
|
||||||
|
def encrypt_path(self, path, is_element=False) -> str:
|
||||||
|
# Encrypts path to avoid plaintext results in logs
|
||||||
|
if is_element:
|
||||||
|
# Element paths are encrypted separately from text, to allow key
|
||||||
|
# regeneration once all items have been served to the user
|
||||||
|
enc_path = Fernet(self.user_key).encrypt(path.encode()).decode()
|
||||||
|
self._elements += 1
|
||||||
|
return enc_path
|
||||||
|
|
||||||
|
return Fernet(self.user_key).encrypt(path.encode()).decode()
|
||||||
|
|
||||||
|
def clean(self, soup) -> BeautifulSoup:
|
||||||
|
self.soup = soup
|
||||||
|
self.main_divs = self.soup.find('div', {'id': 'main'})
|
||||||
|
self.remove_ads()
|
||||||
|
self.remove_block_titles()
|
||||||
|
self.remove_block_url()
|
||||||
|
self.collapse_sections()
|
||||||
|
self.update_css()
|
||||||
|
self.update_styling()
|
||||||
|
self.remove_block_tabs()
|
||||||
|
|
||||||
|
# self.main_divs is only populated for the main page of search results
|
||||||
|
# (i.e. not images/news/etc).
|
||||||
|
if self.main_divs:
|
||||||
|
for div in self.main_divs:
|
||||||
|
self.sanitize_div(div)
|
||||||
|
|
||||||
|
for img in [_ for _ in self.soup.find_all('img') if 'src' in _.attrs]:
|
||||||
|
self.update_element_src(img, 'image/png')
|
||||||
|
|
||||||
|
for audio in [_ for _ in self.soup.find_all('audio') if 'src' in _.attrs]:
|
||||||
|
self.update_element_src(audio, 'audio/mpeg')
|
||||||
|
audio['controls'] = ''
|
||||||
|
|
||||||
|
for link in self.soup.find_all('a', href=True):
|
||||||
|
self.update_link(link)
|
||||||
|
self.add_favicon(link)
|
||||||
|
|
||||||
|
if self.config.alts:
|
||||||
|
self.site_alt_swap()
|
||||||
|
|
||||||
|
input_form = self.soup.find('form')
|
||||||
|
if input_form is not None:
|
||||||
|
input_form['method'] = 'GET' if self.config.get_only else 'POST'
|
||||||
|
# Use a relative URI for submissions
|
||||||
|
input_form['action'] = 'search'
|
||||||
|
|
||||||
|
# Ensure no extra scripts passed through
|
||||||
|
for script in self.soup('script'):
|
||||||
|
script.decompose()
|
||||||
|
|
||||||
|
# Update default footer and header
|
||||||
|
footer = self.soup.find('footer')
|
||||||
|
if footer:
|
||||||
|
# Remove divs that have multiple links beyond just page navigation
|
||||||
|
[_.decompose() for _ in footer.find_all('div', recursive=False)
|
||||||
|
if len(_.find_all('a', href=True)) > 3]
|
||||||
|
for link in footer.find_all('a', href=True):
|
||||||
|
link['href'] = f'{link["href"]}&preferences={self.config.preferences}'
|
||||||
|
|
||||||
|
header = self.soup.find('header')
|
||||||
|
if header:
|
||||||
|
header.decompose()
|
||||||
|
self.remove_site_blocks(self.soup)
|
||||||
|
return self.soup
|
||||||
|
|
||||||
|
def sanitize_div(self, div) -> None:
|
||||||
|
"""Removes escaped script and iframe tags from results
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup object is modified directly)
|
||||||
|
"""
|
||||||
|
if not div:
|
||||||
|
return
|
||||||
|
|
||||||
|
for d in div.find_all('div', recursive=True):
|
||||||
|
d_text = d.find(text=True, recursive=False)
|
||||||
|
|
||||||
|
# Ensure we're working with tags that contain text content
|
||||||
|
if not d_text or not d.string:
|
||||||
|
continue
|
||||||
|
|
||||||
|
d.string = html.unescape(d_text)
|
||||||
|
div_soup = BeautifulSoup(d.string, 'html.parser')
|
||||||
|
|
||||||
|
# Remove all valid script or iframe tags in the div
|
||||||
|
for script in div_soup.find_all('script'):
|
||||||
|
script.decompose()
|
||||||
|
|
||||||
|
for iframe in div_soup.find_all('iframe'):
|
||||||
|
iframe.decompose()
|
||||||
|
|
||||||
|
d.string = str(div_soup)
|
||||||
|
|
||||||
|
def add_favicon(self, link) -> None:
|
||||||
|
"""Adds icons for each returned result, using the result site's favicon
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup object is modified directly)
|
||||||
|
"""
|
||||||
|
# Skip empty, parentless, or internal links
|
||||||
|
show_favicons = read_config_bool('WHOOGLE_SHOW_FAVICONS', True)
|
||||||
|
is_valid_link = link and link.parent and link['href'].startswith('http')
|
||||||
|
if not show_favicons or not is_valid_link:
|
||||||
|
return
|
||||||
|
|
||||||
|
parent = link.parent
|
||||||
|
is_result_div = False
|
||||||
|
|
||||||
|
# Check each parent to make sure that the div doesn't already have a
|
||||||
|
# favicon attached, and that the div is a result div
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.attrs.get('class') or []
|
||||||
|
if 'has-favicon' in p_cls or GClasses.scroller_class in p_cls:
|
||||||
|
return
|
||||||
|
elif GClasses.result_class_a not in p_cls:
|
||||||
|
parent = parent.parent
|
||||||
|
else:
|
||||||
|
is_result_div = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not is_result_div:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Construct the html for inserting the icon into the parent div
|
||||||
|
parsed = urlparse.urlparse(link['href'])
|
||||||
|
favicon = self.encrypt_path(
|
||||||
|
f'{parsed.scheme}://{parsed.netloc}/favicon.ico',
|
||||||
|
is_element=True)
|
||||||
|
src = f'{self.root_url}/{Endpoint.element}?url={favicon}' + \
|
||||||
|
'&type=image/x-icon'
|
||||||
|
html = f'<img class="site-favicon" src="{src}">'
|
||||||
|
|
||||||
|
favicon = BeautifulSoup(html, 'html.parser')
|
||||||
|
link.parent.insert(0, favicon)
|
||||||
|
|
||||||
|
# Update all parents to indicate that a favicon has been attached
|
||||||
|
parent = link.parent
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.get('class') or []
|
||||||
|
p_cls.append('has-favicon')
|
||||||
|
parent['class'] = p_cls
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
if GClasses.result_class_a in p_cls:
|
||||||
|
break
|
||||||
|
|
||||||
|
def remove_site_blocks(self, soup) -> None:
|
||||||
|
if not self.config.block or not soup.body:
|
||||||
|
return
|
||||||
|
search_string = ' '.join(['-site:' +
|
||||||
|
_ for _ in self.config.block.split(',')])
|
||||||
|
selected = soup.body.findAll(text=re.compile(search_string))
|
||||||
|
|
||||||
|
for result in selected:
|
||||||
|
result.string.replace_with(result.string.replace(
|
||||||
|
search_string, ''))
|
||||||
|
|
||||||
|
def remove_ads(self) -> None:
|
||||||
|
"""Removes ads found in the list of search result divs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup object is modified directly)
|
||||||
|
"""
|
||||||
|
if not self.main_divs:
|
||||||
|
return
|
||||||
|
|
||||||
|
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
||||||
|
div_ads = [_ for _ in div.find_all('span', recursive=True)
|
||||||
|
if has_ad_content(_.text)]
|
||||||
|
_ = div.decompose() if len(div_ads) else None
|
||||||
|
|
||||||
|
def remove_block_titles(self) -> None:
|
||||||
|
if not self.main_divs or not self.config.block_title:
|
||||||
|
return
|
||||||
|
block_title = re.compile(self.config.block_title)
|
||||||
|
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
||||||
|
block_divs = [_ for _ in div.find_all('h3', recursive=True)
|
||||||
|
if block_title.search(_.text) is not None]
|
||||||
|
_ = div.decompose() if len(block_divs) else None
|
||||||
|
|
||||||
|
def remove_block_url(self) -> None:
|
||||||
|
if not self.main_divs or not self.config.block_url:
|
||||||
|
return
|
||||||
|
block_url = re.compile(self.config.block_url)
|
||||||
|
for div in [_ for _ in self.main_divs.find_all('div', recursive=True)]:
|
||||||
|
block_divs = [_ for _ in div.find_all('a', recursive=True)
|
||||||
|
if block_url.search(_.attrs['href']) is not None]
|
||||||
|
_ = div.decompose() if len(block_divs) else None
|
||||||
|
|
||||||
|
def remove_block_tabs(self) -> 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 self.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
|
||||||
|
|
||||||
|
These sections are typically the only sections in the results page that
|
||||||
|
have more than ~5 child divs within a primary result div.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup object is modified directly)
|
||||||
|
"""
|
||||||
|
minimal_mode = read_config_bool('WHOOGLE_MINIMAL')
|
||||||
|
|
||||||
|
def pull_child_divs(result_div: BeautifulSoup):
|
||||||
|
try:
|
||||||
|
return result_div.findChildren(
|
||||||
|
'div', recursive=False
|
||||||
|
)[0].findChildren(
|
||||||
|
'div', recursive=False)
|
||||||
|
except IndexError:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not self.main_divs:
|
||||||
|
return
|
||||||
|
#töörölni kell People also ask,
|
||||||
|
search_terms = ["People also search for", "Related searches", "Kapcsolódó keresések", "Mások ezeket keresték még"]
|
||||||
|
details_list = []
|
||||||
|
|
||||||
|
# Loop through results and check for the number of child divs in each
|
||||||
|
for result in self.main_divs.find_all():
|
||||||
|
result_children = pull_child_divs(result)
|
||||||
|
if minimal_mode:
|
||||||
|
if any(f">{x}</span" in str(s) for s in result_children
|
||||||
|
for x in minimal_mode_sections):
|
||||||
|
result.decompose()
|
||||||
|
continue
|
||||||
|
for s in result_children:
|
||||||
|
if ('Twitter ›' in str(s)):
|
||||||
|
result.decompose()
|
||||||
|
continue
|
||||||
|
if len(result_children) < self.RESULT_CHILD_LIMIT:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(result_children) < self.RESULT_CHILD_LIMIT:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Find and decompose the first element with an inner HTML text val.
|
||||||
|
# This typically extracts the title of the section (i.e. "Related
|
||||||
|
# Searches", "People also ask", etc)
|
||||||
|
# If there are more than one child tags with text
|
||||||
|
# parenthesize the rest except the first
|
||||||
|
label = 'Collapsed Results'
|
||||||
|
subtitle = None
|
||||||
|
for elem in result_children:
|
||||||
|
if elem.text:
|
||||||
|
content = list(elem.strings)
|
||||||
|
label = content[0]
|
||||||
|
if len(content) > 1:
|
||||||
|
subtitle = '<span> (' + \
|
||||||
|
''.join(content[1:]) + ')</span>'
|
||||||
|
elem.decompose()
|
||||||
|
break
|
||||||
|
|
||||||
|
# Determine the class based on the label content
|
||||||
|
if any(term in label for term in search_terms):
|
||||||
|
details_class = 'search-recommendations'
|
||||||
|
details_attrs = {'class': details_class, 'open': 'true'}
|
||||||
|
else:
|
||||||
|
details_class = 'other-results'
|
||||||
|
details_attrs = {'class': details_class}
|
||||||
|
|
||||||
|
|
||||||
|
# Create the new details element to wrap around the result's
|
||||||
|
# first parent
|
||||||
|
parent = None
|
||||||
|
idx = 0
|
||||||
|
while not parent and idx < len(result_children):
|
||||||
|
parent = result_children[idx].parent
|
||||||
|
idx += 1
|
||||||
|
|
||||||
|
details = BeautifulSoup(features='html.parser').new_tag('details', attrs=details_attrs)
|
||||||
|
summary = BeautifulSoup(features='html.parser').new_tag('summary', attrs={'class': "summary_div"})
|
||||||
|
summary.string = label
|
||||||
|
|
||||||
|
if subtitle:
|
||||||
|
soup = BeautifulSoup(subtitle, 'html.parser')
|
||||||
|
summary.append(soup)
|
||||||
|
|
||||||
|
details.append(summary)
|
||||||
|
|
||||||
|
if parent and not minimal_mode:
|
||||||
|
parent.wrap(details)
|
||||||
|
elif parent and minimal_mode:
|
||||||
|
# Remove parent element from document if "minimal mode" is
|
||||||
|
# enabled
|
||||||
|
parent.decompose()
|
||||||
|
|
||||||
|
for details in details_list:
|
||||||
|
self.main_divs.append(details)
|
||||||
|
|
||||||
|
def update_element_src(self, element: Tag, mime: str, attr='src') -> None:
|
||||||
|
"""Encrypts the original src of an element and rewrites the element src
|
||||||
|
to use the "/element?src=" pass-through.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup element is modified directly)
|
||||||
|
|
||||||
|
"""
|
||||||
|
src = element[attr].split(' ')[0]
|
||||||
|
|
||||||
|
if src.startswith('//'):
|
||||||
|
src = 'https:' + src
|
||||||
|
elif src.startswith('data:'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if src.startswith(LOGO_URL):
|
||||||
|
# Re-brand with Whoogle logo
|
||||||
|
element.replace_with(BeautifulSoup(
|
||||||
|
render_template('logo.html'),
|
||||||
|
features='html.parser'))
|
||||||
|
return
|
||||||
|
elif src.startswith(G_M_LOGO_URL):
|
||||||
|
# Re-brand with single-letter Whoogle logo
|
||||||
|
element['src'] = 'static/img/favicon/apple-icon.png'
|
||||||
|
element.parent['href'] = 'home'
|
||||||
|
return
|
||||||
|
elif src.startswith(GOOG_IMG) or GOOG_STATIC in src:
|
||||||
|
element['src'] = BLANK_B64
|
||||||
|
return
|
||||||
|
|
||||||
|
element[attr] = f'{self.root_url}/{Endpoint.element}?url=' + (
|
||||||
|
self.encrypt_path(
|
||||||
|
src,
|
||||||
|
is_element=True
|
||||||
|
) + '&type=' + urlparse.quote(mime)
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_css(self) -> None:
|
||||||
|
"""Updates URLs used in inline styles to be proxied by Whoogle
|
||||||
|
using the /element endpoint.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (The soup element is modified directly)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Filter all <style> tags
|
||||||
|
for style in self.soup.find_all('style'):
|
||||||
|
style.string = clean_css(style.string, self.page_url)
|
||||||
|
|
||||||
|
# TODO: Convert remote stylesheets to style tags and proxy all
|
||||||
|
# remote requests
|
||||||
|
# for link in soup.find_all('link', attrs={'rel': 'stylesheet'}):
|
||||||
|
# print(link)
|
||||||
|
|
||||||
|
def update_styling(self) -> None:
|
||||||
|
# Update CSS classes for result divs
|
||||||
|
soup = GClasses.replace_css_classes(self.soup)
|
||||||
|
|
||||||
|
# Remove unnecessary button(s)
|
||||||
|
for button in self.soup.find_all('button'):
|
||||||
|
button.decompose()
|
||||||
|
|
||||||
|
# Remove svg logos
|
||||||
|
for svg in self.soup.find_all('svg'):
|
||||||
|
svg.decompose()
|
||||||
|
|
||||||
|
# Update logo
|
||||||
|
logo = self.soup.find('a', {'class': 'l'})
|
||||||
|
if logo and self.mobile:
|
||||||
|
logo['style'] = ('display:flex; justify-content:center; '
|
||||||
|
'align-items:center; color:#685e79; '
|
||||||
|
'font-size:18px; ')
|
||||||
|
|
||||||
|
# Fix search bar length on mobile
|
||||||
|
""" try:
|
||||||
|
search_bar = self.soup.find('header').find('form').find('div')
|
||||||
|
search_bar['style'] = 'width: 100%;'
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fix body max width on images tab
|
||||||
|
style = self.soup.find('style')
|
||||||
|
div = self.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:900px;}',
|
||||||
|
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
|
||||||
|
|
||||||
|
Args:
|
||||||
|
link: A bs4 Tag element to inspect and update
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (the tag is updated directly)
|
||||||
|
|
||||||
|
"""
|
||||||
|
parsed_link = urlparse.urlparse(link['href'])
|
||||||
|
if '/url?q=' in link['href']:
|
||||||
|
link_netloc = extract_q(parsed_link.query, link['href'])
|
||||||
|
else:
|
||||||
|
link_netloc = parsed_link.netloc
|
||||||
|
|
||||||
|
# Remove any elements that direct to unsupported Google pages
|
||||||
|
if any(url in link_netloc for url in unsupported_g_pages):
|
||||||
|
# FIXME: The "Shopping" tab requires further filtering (see #136)
|
||||||
|
# Temporarily removing all links to that tab for now.
|
||||||
|
|
||||||
|
# Replaces the /url google unsupported link to the direct url
|
||||||
|
link['href'] = link_netloc
|
||||||
|
parent = link.parent
|
||||||
|
|
||||||
|
if any(divlink in link_netloc for divlink in unsupported_g_divs):
|
||||||
|
# Handle case where a search is performed in a different
|
||||||
|
# language than what is configured. This usually returns a
|
||||||
|
# div with the same classes as normal search results, but with
|
||||||
|
# a link to configure language preferences through Google.
|
||||||
|
# Since we want all language config done through Whoogle, we
|
||||||
|
# can safely decompose this element.
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.attrs.get('class') or []
|
||||||
|
if f'{GClasses.result_class_a}' in p_cls:
|
||||||
|
parent.decompose()
|
||||||
|
break
|
||||||
|
parent = parent.parent
|
||||||
|
else:
|
||||||
|
# Remove cases where google links appear in the footer
|
||||||
|
while parent:
|
||||||
|
p_cls = parent.attrs.get('class') or []
|
||||||
|
if parent.name == 'footer' or f'{GClasses.footer}' in p_cls:
|
||||||
|
link.decompose()
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
if link.decomposed:
|
||||||
|
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)
|
||||||
|
|
||||||
|
if q.startswith('/') and q not in self.query and 'spell=1' not in href:
|
||||||
|
# Internal google links (i.e. mail, maps, etc) should still
|
||||||
|
# be forwarded to Google
|
||||||
|
link['href'] = 'https://google.com' + q
|
||||||
|
elif q.startswith('https://accounts.google.com'):
|
||||||
|
# Remove Sign-in link
|
||||||
|
link.decompose()
|
||||||
|
return
|
||||||
|
elif '/search?q=' in href:
|
||||||
|
# "li:1" implies the query should be interpreted verbatim,
|
||||||
|
# which is accomplished by wrapping the query in double quotes
|
||||||
|
if 'li:1' in href:
|
||||||
|
q = '"' + q + '"'
|
||||||
|
new_search = 'search?q=' + self.encrypt_path(q)
|
||||||
|
|
||||||
|
query_params = parse_qs(urlparse.urlparse(href).query)
|
||||||
|
for param in VALID_PARAMS:
|
||||||
|
if param not in query_params:
|
||||||
|
continue
|
||||||
|
param_val = query_params[param][0]
|
||||||
|
new_search += '&' + param + '=' + param_val
|
||||||
|
link['href'] = new_search
|
||||||
|
elif 'url?q=' in href:
|
||||||
|
# Strip unneeded arguments
|
||||||
|
link['href'] = filter_link_args(q)
|
||||||
|
|
||||||
|
# Add alternate viewing options for results,
|
||||||
|
# if the result doesn't already have an AV link
|
||||||
|
netloc = urlparse.urlparse(link['href']).netloc
|
||||||
|
if self.config.anon_view and netloc not in self._av:
|
||||||
|
self._av.add(netloc)
|
||||||
|
append_anon_view(link, self.config)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if href.startswith(MAPS_URL):
|
||||||
|
# Maps links don't work if a site filter is applied
|
||||||
|
link['href'] = build_map_url(link['href'])
|
||||||
|
elif (href.startswith('/?') or href.startswith('/search?') or
|
||||||
|
href.startswith('/imgres?')):
|
||||||
|
# make sure that tags can be clicked as relative URLs
|
||||||
|
link['href'] = href[1:]
|
||||||
|
elif href.startswith('/intl/'):
|
||||||
|
# do nothing, keep original URL for ToS
|
||||||
|
pass
|
||||||
|
elif href.startswith('/preferences'):
|
||||||
|
# there is no config specific URL, remove this
|
||||||
|
link.decompose()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
link['href'] = href
|
||||||
|
|
||||||
|
if self.config.new_tab and (
|
||||||
|
link["href"].startswith("http")
|
||||||
|
or link["href"].startswith("imgres?")
|
||||||
|
):
|
||||||
|
link["target"] = "_blank"
|
||||||
|
|
||||||
|
def site_alt_swap(self) -> None:
|
||||||
|
"""Replaces link locations and page elements if "alts" config
|
||||||
|
is enabled
|
||||||
|
"""
|
||||||
|
for site, alt in SITE_ALTS.items():
|
||||||
|
if site != "medium.com" and alt != "":
|
||||||
|
# Ignore medium.com replacements since these are handled
|
||||||
|
# specifically in the link description replacement, and medium
|
||||||
|
# results are never given their own "card" result where this
|
||||||
|
# replacement would make sense.
|
||||||
|
# Also ignore if the alt is empty, since this is used to indicate
|
||||||
|
# that the alt is not enabled.
|
||||||
|
for div in self.soup.find_all('div', text=re.compile(site)):
|
||||||
|
# Use the number of words in the div string to determine if the
|
||||||
|
# string is a result description (shouldn't replace domains used
|
||||||
|
# in desc text).
|
||||||
|
if len(div.string.split(' ')) == 1:
|
||||||
|
div.string = div.string.replace(site, alt)
|
||||||
|
|
||||||
|
for link in self.soup.find_all('a', href=True):
|
||||||
|
# Search and replace all link descriptions
|
||||||
|
# with alternative location
|
||||||
|
link['href'] = get_site_alt(link['href'])
|
||||||
|
link_desc = link.find_all(
|
||||||
|
text=re.compile('|'.join(SITE_ALTS.keys())))
|
||||||
|
if len(link_desc) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Replace link description
|
||||||
|
link_desc = link_desc[0]
|
||||||
|
if site not in link_desc or not alt:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_desc = BeautifulSoup(features='html.parser').new_tag('div')
|
||||||
|
link_str = str(link_desc)
|
||||||
|
|
||||||
|
# Medium links should be handled differently, since 'medium.com'
|
||||||
|
# is a common substring of domain names, but shouldn't be
|
||||||
|
# replaced (i.e. 'philomedium.com' should stay as it is).
|
||||||
|
if 'medium.com' in link_str:
|
||||||
|
if link_str.startswith('medium.com') or '.medium.com' in link_str:
|
||||||
|
link_str = SITE_ALTS['medium.com'] + link_str[
|
||||||
|
link_str.find('medium.com') + len('medium.com'):]
|
||||||
|
new_desc.string = link_str
|
||||||
|
else:
|
||||||
|
new_desc.string = link_str.replace(site, alt)
|
||||||
|
|
||||||
|
link_desc.replace_with(new_desc)
|
||||||
|
|
||||||
|
|
||||||
|
def view_image(self, soup) -> BeautifulSoup:
|
||||||
|
"""Replaces the soup with a new one that handles mobile results and
|
||||||
|
adds the link of the image full res to the results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: A BeautifulSoup object containing the image mobile results.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup: The new BeautifulSoup object
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get some tags that are unchanged between mobile and pc versions
|
||||||
|
cor_suggested = soup.find_all(class_="By0U9")
|
||||||
|
next_pages = soup.find(class_= "uZgmoc")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
# find results div
|
||||||
|
results_div = soup.find('div', attrs={'class': "nQvrDb"})
|
||||||
|
# find all the results (if any)
|
||||||
|
results_all = []
|
||||||
|
if results_div:
|
||||||
|
results_all = results_div.find_all('div', attrs={'class': "lIMUZd"})
|
||||||
|
|
||||||
|
for item in results_all:
|
||||||
|
urls = item.find('a')['href'].split('&imgrefurl=')
|
||||||
|
|
||||||
|
# Skip urls that are not two-element lists
|
||||||
|
if len(urls) != 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
img_url = urlparse.unquote(urls[0].replace(
|
||||||
|
f'/{Endpoint.imgres}?imgurl=', ''))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to strip out only the necessary part of the web page link
|
||||||
|
web_page = urlparse.unquote(urls[1].split('&')[0])
|
||||||
|
except IndexError:
|
||||||
|
web_page = urlparse.unquote(urls[1])
|
||||||
|
|
||||||
|
img_tbn = urlparse.unquote(item.find('a').find('img')['src'])
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'domain': urlparse.urlparse(web_page).netloc,
|
||||||
|
'img_url': img_url,
|
||||||
|
'web_page': web_page,
|
||||||
|
'img_tbn': img_tbn,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Create a new BeautifulSoup object with the template
|
||||||
|
soup = BeautifulSoup(render_template('imageresults.html',
|
||||||
|
length=len(results),
|
||||||
|
results=results,
|
||||||
|
view_label="Kép megtekintése"),
|
||||||
|
features='html.parser')
|
||||||
|
|
||||||
|
# replace correction suggested by google object if exists
|
||||||
|
if len(cor_suggested):
|
||||||
|
soup.find_all(class_= "By0U9"
|
||||||
|
)[0].replaceWith(cor_suggested[0])
|
||||||
|
|
||||||
|
# replace next page object at the bottom of the page
|
||||||
|
soup.find_all(class_= "uZgmoc")[0].replaceWith(next_pages)
|
||||||
|
|
||||||
|
return soup
|
BIN
app/models/.DS_Store
vendored
Executable file
0
app/models/__init__.py
Executable file
267
app/models/config.py
Executable file
|
@ -0,0 +1,267 @@
|
||||||
|
from inspect import Attribute
|
||||||
|
from typing import Optional
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
|
from flask import current_app
|
||||||
|
import os
|
||||||
|
from base64 import urlsafe_b64encode, urlsafe_b64decode
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
import hashlib
|
||||||
|
import brotli
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
import cssutils
|
||||||
|
from cssutils.css.cssstylesheet import CSSStyleSheet
|
||||||
|
from cssutils.css.cssstylerule import CSSStyleRule
|
||||||
|
|
||||||
|
# removes warnings from cssutils
|
||||||
|
cssutils.log.setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rule_for_selector(stylesheet: CSSStyleSheet,
|
||||||
|
selector: str) -> Optional[CSSStyleRule]:
|
||||||
|
"""Search for a rule that matches a given selector in a stylesheet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
stylesheet (CSSStyleSheet) -- the stylesheet to search
|
||||||
|
selector (str) -- the selector to search for
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[CSSStyleRule] -- the rule that matches the selector or None
|
||||||
|
"""
|
||||||
|
for rule in stylesheet.cssRules:
|
||||||
|
if hasattr(rule, "selectorText") and selector == rule.selectorText:
|
||||||
|
return rule
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
app_config = current_app.config
|
||||||
|
self.url = os.getenv('WHOOGLE_CONFIG_URL', '')
|
||||||
|
self.lang_search = os.getenv('WHOOGLE_CONFIG_SEARCH_LANGUAGE', '')
|
||||||
|
self.lang_interface = os.getenv('WHOOGLE_CONFIG_LANGUAGE', default='lang_hu')
|
||||||
|
self.style_modified = os.getenv(
|
||||||
|
'WHOOGLE_CONFIG_STYLE', '')
|
||||||
|
self.block = os.getenv('WHOOGLE_CONFIG_BLOCK', '')
|
||||||
|
self.block_title = os.getenv('WHOOGLE_CONFIG_BLOCK_TITLE', '')
|
||||||
|
self.block_url = os.getenv('WHOOGLE_CONFIG_BLOCK_URL', '')
|
||||||
|
self.country = os.getenv('WHOOGLE_CONFIG_COUNTRY', '')
|
||||||
|
self.tbs = os.getenv('WHOOGLE_CONFIG_TIME_PERIOD', '')
|
||||||
|
self.theme = os.getenv('WHOOGLE_CONFIG_THEME', 'dark')
|
||||||
|
self.safe = read_config_bool('WHOOGLE_CONFIG_SAFE', default=True)
|
||||||
|
self.dark = read_config_bool('WHOOGLE_CONFIG_DARK') # deprecated
|
||||||
|
self.alts = read_config_bool('WHOOGLE_CONFIG_ALTS')
|
||||||
|
self.nojs = read_config_bool('WHOOGLE_CONFIG_NOJS')
|
||||||
|
self.tor = read_config_bool('WHOOGLE_CONFIG_TOR')
|
||||||
|
self.near = os.getenv('WHOOGLE_CONFIG_NEAR', '')
|
||||||
|
self.new_tab = read_config_bool('WHOOGLE_CONFIG_NEW_TAB')
|
||||||
|
self.view_image = read_config_bool('WHOOGLE_CONFIG_VIEW_IMAGE', default=True)
|
||||||
|
self.get_only = read_config_bool('WHOOGLE_CONFIG_GET_ONLY')
|
||||||
|
self.anon_view = read_config_bool('WHOOGLE_CONFIG_ANON_VIEW')
|
||||||
|
self.preferences_encrypted = read_config_bool('WHOOGLE_CONFIG_PREFERENCES_ENCRYPTED')
|
||||||
|
self.preferences_key = os.getenv('WHOOGLE_CONFIG_PREFERENCES_KEY', '')
|
||||||
|
|
||||||
|
self.accept_language = False
|
||||||
|
|
||||||
|
self.safe_keys = [
|
||||||
|
'lang_search',
|
||||||
|
'lang_interface',
|
||||||
|
'country',
|
||||||
|
'theme',
|
||||||
|
'alts',
|
||||||
|
'new_tab',
|
||||||
|
'view_image',
|
||||||
|
'block',
|
||||||
|
'safe',
|
||||||
|
'nojs',
|
||||||
|
'anon_view',
|
||||||
|
'preferences_encrypted',
|
||||||
|
'tbs'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Skip setting custom config if there isn't one
|
||||||
|
if kwargs:
|
||||||
|
mutable_attrs = self.get_mutable_attrs()
|
||||||
|
for attr in mutable_attrs:
|
||||||
|
if attr in kwargs.keys():
|
||||||
|
setattr(self, attr, kwargs[attr])
|
||||||
|
elif attr not in kwargs.keys() and mutable_attrs[attr] == bool:
|
||||||
|
setattr(self, attr, False)
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return getattr(self, name)
|
||||||
|
|
||||||
|
def __setitem__(self, name, value):
|
||||||
|
return setattr(self, name, value)
|
||||||
|
|
||||||
|
def __delitem__(self, name):
|
||||||
|
return delattr(self, name)
|
||||||
|
|
||||||
|
def __contains__(self, name):
|
||||||
|
return hasattr(self, name)
|
||||||
|
|
||||||
|
def get_mutable_attrs(self):
|
||||||
|
return {name: type(attr) for name, attr in self.__dict__.items()
|
||||||
|
if not name.startswith("__")
|
||||||
|
and (type(attr) is bool or type(attr) is str)}
|
||||||
|
|
||||||
|
def get_attrs(self):
|
||||||
|
return {name: attr for name, attr in self.__dict__.items()
|
||||||
|
if not name.startswith("__")
|
||||||
|
and (type(attr) is bool or type(attr) is str)}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def style(self) -> str:
|
||||||
|
"""Returns the default style updated with specified modifications.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- the new style
|
||||||
|
"""
|
||||||
|
style_sheet = cssutils.parseString(
|
||||||
|
open(os.path.join(current_app.config['STATIC_FOLDER'],
|
||||||
|
'css/variables.css')).read()
|
||||||
|
)
|
||||||
|
|
||||||
|
modified_sheet = cssutils.parseString(self.style_modified)
|
||||||
|
for rule in modified_sheet:
|
||||||
|
rule_default = get_rule_for_selector(style_sheet,
|
||||||
|
rule.selectorText)
|
||||||
|
# if modified rule is in default stylesheet, update it
|
||||||
|
if rule_default is not None:
|
||||||
|
# TODO: update this in a smarter way to handle :root better
|
||||||
|
# for now if we change a varialbe in :root all other default
|
||||||
|
# variables need to be also present
|
||||||
|
rule_default.style = rule.style
|
||||||
|
# else add the new rule to the default stylesheet
|
||||||
|
else:
|
||||||
|
style_sheet.add(rule)
|
||||||
|
return str(style_sheet.cssText, 'utf-8')
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preferences(self) -> str:
|
||||||
|
# if encryption key is not set will uncheck preferences encryption
|
||||||
|
if self.preferences_encrypted:
|
||||||
|
self.preferences_encrypted = bool(self.preferences_key)
|
||||||
|
|
||||||
|
|
||||||
|
# add a tag for visibility if preferences token startswith 'e' it means
|
||||||
|
# the token is encrypted, 'u' means the token is unencrypted and can be
|
||||||
|
# used by other whoogle instances
|
||||||
|
encrypted_flag = "e" if self.preferences_encrypted else 'u'
|
||||||
|
preferences_digest = self._encode_preferences()
|
||||||
|
return f"{encrypted_flag}{preferences_digest}"
|
||||||
|
|
||||||
|
def is_safe_key(self, key) -> bool:
|
||||||
|
"""Establishes a group of config options that are safe to set
|
||||||
|
in the url.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str) -- the key to check against
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool -- True/False depending on if the key is in the "safe"
|
||||||
|
array
|
||||||
|
"""
|
||||||
|
|
||||||
|
return key in self.safe_keys
|
||||||
|
|
||||||
|
def get_localization_lang(self):
|
||||||
|
"""Returns the correct language to use for localization, but falls
|
||||||
|
back to english if not set.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- the localization language string
|
||||||
|
"""
|
||||||
|
if (self.lang_interface and
|
||||||
|
self.lang_interface in current_app.config['TRANSLATIONS']):
|
||||||
|
return self.lang_interface
|
||||||
|
|
||||||
|
return 'lang_en'
|
||||||
|
|
||||||
|
def from_params(self, params) -> 'Config':
|
||||||
|
"""Modify user config with search parameters. This is primarily
|
||||||
|
used for specifying configuration on a search-by-search basis on
|
||||||
|
public instances.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params -- the url arguments (can be any deemed safe by is_safe())
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Config -- a modified config object
|
||||||
|
"""
|
||||||
|
if 'preferences' in params:
|
||||||
|
params_new = self._decode_preferences(params['preferences'])
|
||||||
|
# if preferences leads to an empty dictionary it means preferences
|
||||||
|
# parameter was not decrypted successfully
|
||||||
|
if len(params_new):
|
||||||
|
params = params_new
|
||||||
|
|
||||||
|
for param_key in params.keys():
|
||||||
|
if not self.is_safe_key(param_key):
|
||||||
|
continue
|
||||||
|
param_val = params.get(param_key)
|
||||||
|
|
||||||
|
if param_val == 'off':
|
||||||
|
param_val = False
|
||||||
|
elif isinstance(param_val, str):
|
||||||
|
if param_val.isdigit():
|
||||||
|
param_val = int(param_val)
|
||||||
|
|
||||||
|
self[param_key] = param_val
|
||||||
|
return self
|
||||||
|
|
||||||
|
def to_params(self, keys: list = []) -> str:
|
||||||
|
"""Generates a set of safe params for using in Whoogle URLs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keys (list) -- optional list of keys of URL parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str -- a set of URL parameters
|
||||||
|
"""
|
||||||
|
if not len(keys):
|
||||||
|
keys = self.safe_keys
|
||||||
|
|
||||||
|
param_str = ''
|
||||||
|
for safe_key in keys:
|
||||||
|
if not self[safe_key]:
|
||||||
|
continue
|
||||||
|
param_str = param_str + f'&{safe_key}={self[safe_key]}'
|
||||||
|
|
||||||
|
return param_str
|
||||||
|
|
||||||
|
def _get_fernet_key(self, password: str) -> bytes:
|
||||||
|
hash_object = hashlib.md5(password.encode())
|
||||||
|
key = urlsafe_b64encode(hash_object.hexdigest().encode())
|
||||||
|
return key
|
||||||
|
|
||||||
|
def _encode_preferences(self) -> str:
|
||||||
|
preferences_json = json.dumps(self.get_attrs()).encode()
|
||||||
|
compressed_preferences = brotli.compress(preferences_json)
|
||||||
|
|
||||||
|
if self.preferences_encrypted and self.preferences_key:
|
||||||
|
key = self._get_fernet_key(self.preferences_key)
|
||||||
|
encrypted_preferences = Fernet(key).encrypt(compressed_preferences)
|
||||||
|
compressed_preferences = brotli.compress(encrypted_preferences)
|
||||||
|
|
||||||
|
return urlsafe_b64encode(compressed_preferences).decode()
|
||||||
|
|
||||||
|
def _decode_preferences(self, preferences: str) -> dict:
|
||||||
|
mode = preferences[0]
|
||||||
|
preferences = preferences[1:]
|
||||||
|
|
||||||
|
try:
|
||||||
|
decoded_data = brotli.decompress(urlsafe_b64decode(preferences.encode() + b'=='))
|
||||||
|
|
||||||
|
if mode == 'e' and self.preferences_key:
|
||||||
|
# preferences are encrypted
|
||||||
|
key = self._get_fernet_key(self.preferences_key)
|
||||||
|
decrypted_data = Fernet(key).decrypt(decoded_data)
|
||||||
|
decoded_data = brotli.decompress(decrypted_data)
|
||||||
|
|
||||||
|
config = json.loads(decoded_data)
|
||||||
|
except Exception:
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
return config
|
22
app/models/endpoint.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Endpoint(Enum):
|
||||||
|
autocomplete = 'autocomplete'
|
||||||
|
home = 'home'
|
||||||
|
healthz = 'healthz'
|
||||||
|
config = 'config'
|
||||||
|
opensearch = 'opensearch.xml'
|
||||||
|
search = 'search'
|
||||||
|
search_html = 'search.html'
|
||||||
|
url = 'url'
|
||||||
|
imgres = 'imgres'
|
||||||
|
element = 'element'
|
||||||
|
window = 'window'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
def in_path(self, path: str) -> bool:
|
||||||
|
return path.startswith(self.value) or \
|
||||||
|
path.startswith(f'/{self.value}')
|
48
app/models/g_classes.py
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
class GClasses:
|
||||||
|
"""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'
|
||||||
|
footer = 'TuS8Ad'
|
||||||
|
result_class_a = 'ZINbbc'
|
||||||
|
result_class_b = 'luh4td'
|
||||||
|
scroller_class = 'idg8be'
|
||||||
|
line_tag = 'BsXmcf'
|
||||||
|
|
||||||
|
result_classes = {
|
||||||
|
result_class_a: ['Gx5Zad'],
|
||||||
|
result_class_b: ['fP1Qef']
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def replace_css_classes(cls, soup: BeautifulSoup) -> BeautifulSoup:
|
||||||
|
"""Replace updated Google classes with the original class names that
|
||||||
|
Whoogle relies on for styling.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: The result page as a BeautifulSoup object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup: The new BeautifulSoup
|
||||||
|
"""
|
||||||
|
result_divs = soup.find_all('div', {
|
||||||
|
'class': [_ for c in cls.result_classes.values() for _ in c]
|
||||||
|
})
|
||||||
|
|
||||||
|
for div in result_divs:
|
||||||
|
new_class = ' '.join(div['class'])
|
||||||
|
for key, val in cls.result_classes.items():
|
||||||
|
new_class = ' '.join(new_class.replace(_, key) for _ in val)
|
||||||
|
div['class'] = new_class.split(' ')
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.value
|
352
app/request.py
Executable file
|
@ -0,0 +1,352 @@
|
||||||
|
from app.models.config import Config
|
||||||
|
from app.utils.misc import read_config_bool
|
||||||
|
from datetime import datetime
|
||||||
|
from defusedxml import ElementTree as ET
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
from requests import Response, ConnectionError
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
import os
|
||||||
|
from stem import Signal, SocketError
|
||||||
|
from stem.connection import AuthenticationFailure
|
||||||
|
from stem.control import Controller
|
||||||
|
from stem.connection import authenticate_cookie, authenticate_password
|
||||||
|
|
||||||
|
MAPS_URL = 'https://maps.google.com/maps'
|
||||||
|
AUTOCOMPLETE_URL = ('https://suggestqueries.google.com/'
|
||||||
|
'complete/search?client=toolbar&')
|
||||||
|
|
||||||
|
MOBILE_UA = '{}/5.0 (Android 0; Mobile; rv:54.0) Gecko/54.0 {}/59.0'
|
||||||
|
DESKTOP_UA = '{}/5.0 (X11; {} x86_64; rv:75.0) Gecko/20100101 {}/75.0'
|
||||||
|
|
||||||
|
# Valid query params
|
||||||
|
VALID_PARAMS = ['tbs', 'tbm', 'start', 'near', 'source', 'nfpr']
|
||||||
|
|
||||||
|
|
||||||
|
class TorError(Exception):
|
||||||
|
"""Exception raised for errors in Tor requests.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
message: a message describing the error that occurred
|
||||||
|
disable: optionally disables Tor in the user config (note:
|
||||||
|
this should only happen if the connection has been dropped
|
||||||
|
altogether).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message, disable=False) -> None:
|
||||||
|
self.message = message
|
||||||
|
self.disable = disable
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
def send_tor_signal(signal: Signal) -> bool:
|
||||||
|
use_pass = read_config_bool('WHOOGLE_TOR_USE_PASS')
|
||||||
|
|
||||||
|
confloc = './misc/tor/control.conf'
|
||||||
|
# Check that the custom location of conf is real.
|
||||||
|
temp = os.getenv('WHOOGLE_TOR_CONF', '')
|
||||||
|
if os.path.isfile(temp):
|
||||||
|
confloc = temp
|
||||||
|
|
||||||
|
# Attempt to authenticate and send signal.
|
||||||
|
try:
|
||||||
|
with Controller.from_port(port=9051) as c:
|
||||||
|
if use_pass:
|
||||||
|
with open(confloc, "r") as conf:
|
||||||
|
# Scan for the last line of the file.
|
||||||
|
for line in conf:
|
||||||
|
pass
|
||||||
|
secret = line.strip('\n')
|
||||||
|
authenticate_password(c, password=secret)
|
||||||
|
else:
|
||||||
|
cookie_path = '/var/lib/tor/control_auth_cookie'
|
||||||
|
authenticate_cookie(c, cookie_path=cookie_path)
|
||||||
|
c.signal(signal)
|
||||||
|
os.environ['TOR_AVAILABLE'] = '1'
|
||||||
|
return True
|
||||||
|
except (SocketError, AuthenticationFailure,
|
||||||
|
ConnectionRefusedError, ConnectionError):
|
||||||
|
# TODO: Handle Tor authentication (password and cookie)
|
||||||
|
os.environ['TOR_AVAILABLE'] = '0'
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def gen_user_agent(is_mobile) -> str:
|
||||||
|
user_agent = os.environ.get('WHOOGLE_USER_AGENT', '')
|
||||||
|
user_agent_mobile = os.environ.get('WHOOGLE_USER_AGENT_MOBILE', '')
|
||||||
|
if user_agent and not is_mobile:
|
||||||
|
return user_agent
|
||||||
|
|
||||||
|
if user_agent_mobile and is_mobile:
|
||||||
|
return user_agent_mobile
|
||||||
|
|
||||||
|
firefox = random.choice(['Choir', 'Squier', 'Higher', 'Wire']) + 'fox'
|
||||||
|
linux = random.choice(['Win', 'Sin', 'Gin', 'Fin', 'Kin']) + 'ux'
|
||||||
|
|
||||||
|
if is_mobile:
|
||||||
|
return MOBILE_UA.format("Mozilla", firefox)
|
||||||
|
|
||||||
|
return DESKTOP_UA.format("Mozilla", linux, firefox)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_query(query, args, config) -> str:
|
||||||
|
param_dict = {key: '' for key in VALID_PARAMS}
|
||||||
|
|
||||||
|
# Use :past(hour/day/week/month/year) if available
|
||||||
|
# example search "new restaurants :past month"
|
||||||
|
lang = ''
|
||||||
|
if ':past' in query and 'tbs' not in args:
|
||||||
|
time_range = str.strip(query.split(':past', 1)[-1])
|
||||||
|
param_dict['tbs'] = '&tbs=' + ('qdr:' + str.lower(time_range[0]))
|
||||||
|
elif 'tbs' in args or 'tbs' in config:
|
||||||
|
result_tbs = args.get('tbs') if 'tbs' in args else config['tbs']
|
||||||
|
param_dict['tbs'] = '&tbs=' + result_tbs
|
||||||
|
|
||||||
|
# Occasionally the 'tbs' param provided by google also contains a
|
||||||
|
# field for 'lr', but formatted strangely. This is a rough solution
|
||||||
|
# for this.
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# &tbs=qdr:h,lr:lang_1pl
|
||||||
|
# -- the lr param needs to be extracted and remove the leading '1'
|
||||||
|
result_params = [_ for _ in result_tbs.split(',') if 'lr:' in _]
|
||||||
|
if len(result_params) > 0:
|
||||||
|
result_param = result_params[0]
|
||||||
|
lang = result_param[result_param.find('lr:') + 3:len(result_param)]
|
||||||
|
|
||||||
|
# Ensure search query is parsable
|
||||||
|
query = urlparse.quote(query)
|
||||||
|
|
||||||
|
# Pass along type of results (news, images, books, etc)
|
||||||
|
if 'tbm' in args:
|
||||||
|
param_dict['tbm'] = '&tbm=' + args.get('tbm')
|
||||||
|
|
||||||
|
# Get results page start value (10 per page, ie page 2 start val = 20)
|
||||||
|
if 'start' in args:
|
||||||
|
param_dict['start'] = '&start=' + args.get('start')
|
||||||
|
|
||||||
|
# Search for results near a particular city, if available
|
||||||
|
if config.near:
|
||||||
|
param_dict['near'] = '&near=' + urlparse.quote(config.near)
|
||||||
|
|
||||||
|
# Set language for results (lr) if source isn't set, otherwise use the
|
||||||
|
# result language param provided in the results
|
||||||
|
if 'source' in args:
|
||||||
|
param_dict['source'] = '&source=' + args.get('source')
|
||||||
|
param_dict['lr'] = ('&lr=' + ''.join(
|
||||||
|
[_ for _ in lang if not _.isdigit()]
|
||||||
|
)) if lang else ''
|
||||||
|
else:
|
||||||
|
param_dict['lr'] = (
|
||||||
|
'&lr=' + config.lang_search
|
||||||
|
) if config.lang_search else ''
|
||||||
|
|
||||||
|
# 'nfpr' defines the exclusion of results from an auto-corrected query
|
||||||
|
if 'nfpr' in args:
|
||||||
|
param_dict['nfpr'] = '&nfpr=' + args.get('nfpr')
|
||||||
|
|
||||||
|
# 'chips' is used in image tabs to pass the optional 'filter' to add to the
|
||||||
|
# given search term
|
||||||
|
if 'chips' in args:
|
||||||
|
param_dict['chips'] = '&chips=' + args.get('chips')
|
||||||
|
|
||||||
|
param_dict['gl'] = (
|
||||||
|
'&gl=' + config.country
|
||||||
|
) if config.country else ''
|
||||||
|
param_dict['hl'] = (
|
||||||
|
'&hl=' + config.lang_interface.replace('lang_', '')
|
||||||
|
) if config.lang_interface else ''
|
||||||
|
param_dict['safe'] = '&safe=' + ('active' if config.safe else 'off')
|
||||||
|
|
||||||
|
# Block all sites specified in the user config
|
||||||
|
unquoted_query = urlparse.unquote(query)
|
||||||
|
for blocked_site in config.block.replace(' ', '').split(','):
|
||||||
|
if not blocked_site:
|
||||||
|
continue
|
||||||
|
block = (' -site:' + blocked_site)
|
||||||
|
query += block if block not in unquoted_query else ''
|
||||||
|
|
||||||
|
for val in param_dict.values():
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
query += val
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
|
"""Class used for handling all outbound requests, including search queries,
|
||||||
|
search suggestions, and loading of external content (images, audio, etc).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
normal_ua: the user's current user agent
|
||||||
|
root_path: the root path of the whoogle instance
|
||||||
|
config: the user's current whoogle configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, normal_ua, root_path, config: Config):
|
||||||
|
self.search_url = 'https://www.google.com/search?gbv=1&num=' + str(
|
||||||
|
os.getenv('WHOOGLE_RESULTS_PER_PAGE', 12)) + '&q='
|
||||||
|
# Send heartbeat to Tor, used in determining if the user can or cannot
|
||||||
|
# enable Tor for future requests
|
||||||
|
send_tor_signal(Signal.HEARTBEAT)
|
||||||
|
|
||||||
|
self.language = (
|
||||||
|
config.lang_search if config.lang_search else ''
|
||||||
|
)
|
||||||
|
|
||||||
|
self.country = config.country if config.country else ''
|
||||||
|
|
||||||
|
# For setting Accept-language Header
|
||||||
|
self.lang_interface = ''
|
||||||
|
if config.accept_language:
|
||||||
|
self.lang_interface = config.lang_interface
|
||||||
|
|
||||||
|
self.mobile = bool(normal_ua) and ('Android' in normal_ua
|
||||||
|
or 'iPhone' in normal_ua)
|
||||||
|
self.modified_user_agent = gen_user_agent(self.mobile)
|
||||||
|
if not self.mobile:
|
||||||
|
self.modified_user_agent_mobile = gen_user_agent(True)
|
||||||
|
|
||||||
|
# Set up proxy, if previously configured
|
||||||
|
proxy_path = os.environ.get('WHOOGLE_PROXY_LOC', '')
|
||||||
|
if proxy_path:
|
||||||
|
proxy_type = os.environ.get('WHOOGLE_PROXY_TYPE', '')
|
||||||
|
proxy_user = os.environ.get('WHOOGLE_PROXY_USER', '')
|
||||||
|
proxy_pass = os.environ.get('WHOOGLE_PROXY_PASS', '')
|
||||||
|
auth_str = ''
|
||||||
|
if proxy_user:
|
||||||
|
auth_str = f'{proxy_user}:{proxy_pass}@'
|
||||||
|
|
||||||
|
proxy_str = f'{proxy_type}://{auth_str}{proxy_path}'
|
||||||
|
self.proxies = {
|
||||||
|
'https': proxy_str,
|
||||||
|
'http': proxy_str
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
self.proxies = {
|
||||||
|
'http': 'socks5://127.0.0.1:9050',
|
||||||
|
'https': 'socks5://127.0.0.1:9050'
|
||||||
|
} if config.tor else {}
|
||||||
|
self.tor = config.tor
|
||||||
|
self.tor_valid = False
|
||||||
|
self.root_path = root_path
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return getattr(self, name)
|
||||||
|
|
||||||
|
def autocomplete(self, query) -> list:
|
||||||
|
"""Sends a query to Google's search suggestion service
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The in-progress query to send
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The list of matches for possible search suggestions
|
||||||
|
|
||||||
|
"""
|
||||||
|
ac_query = dict(q=query)
|
||||||
|
if self.language:
|
||||||
|
ac_query['lr'] = self.language
|
||||||
|
if self.country:
|
||||||
|
ac_query['gl'] = self.country
|
||||||
|
if self.lang_interface:
|
||||||
|
ac_query['hl'] = self.lang_interface
|
||||||
|
|
||||||
|
response = self.send(base_url=AUTOCOMPLETE_URL,
|
||||||
|
query=urlparse.urlencode(ac_query)).text
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(response)
|
||||||
|
return [_.attrib['data'] for _ in
|
||||||
|
root.findall('.//suggestion/[@data]')]
|
||||||
|
except ET.ParseError:
|
||||||
|
# Malformed XML response
|
||||||
|
return []
|
||||||
|
|
||||||
|
def send(self, base_url='', query='', attempt=0,
|
||||||
|
force_mobile=True, user_agent='') -> Response:
|
||||||
|
"""Sends an outbound request to a URL. Optionally sends the request
|
||||||
|
using Tor, if enabled by the user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: The URL to use in the request
|
||||||
|
query: The optional query string for the request
|
||||||
|
attempt: The number of attempts made for the request
|
||||||
|
(used for cycling through Tor identities, if enabled)
|
||||||
|
force_mobile: Optional flag to enable a mobile user agent
|
||||||
|
(used for fetching full size images in search results)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The Response object returned by the requests call
|
||||||
|
|
||||||
|
"""
|
||||||
|
use_client_user_agent = int(os.environ.get('WHOOGLE_USE_CLIENT_USER_AGENT', '0'))
|
||||||
|
if user_agent and use_client_user_agent == 1:
|
||||||
|
modified_user_agent = user_agent
|
||||||
|
else:
|
||||||
|
if force_mobile and not self.mobile:
|
||||||
|
modified_user_agent = self.modified_user_agent_mobile
|
||||||
|
else:
|
||||||
|
modified_user_agent = self.modified_user_agent
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'User-Agent': modified_user_agent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Adding the Accept-Language to the Header if possible
|
||||||
|
if self.lang_interface:
|
||||||
|
headers.update({'Accept-Language':
|
||||||
|
self.lang_interface.replace('lang_', '')
|
||||||
|
+ ';q=1.0'})
|
||||||
|
|
||||||
|
# view is suppressed correctly
|
||||||
|
now = datetime.now()
|
||||||
|
cookies = {
|
||||||
|
'CONSENT': 'PENDING+987',
|
||||||
|
'SOCS': 'CAESHAgBEhIaAB',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate Tor conn and request new identity if the last one failed
|
||||||
|
if self.tor and not send_tor_signal(
|
||||||
|
Signal.NEWNYM if attempt > 0 else Signal.HEARTBEAT):
|
||||||
|
raise TorError(
|
||||||
|
"Tor was previously enabled, but the connection has been "
|
||||||
|
"dropped. Please check your Tor configuration and try again.",
|
||||||
|
disable=True)
|
||||||
|
|
||||||
|
# Make sure that the tor connection is valid, if enabled
|
||||||
|
if self.tor:
|
||||||
|
try:
|
||||||
|
tor_check = requests.get('https://check.torproject.org/',
|
||||||
|
proxies=self.proxies, headers=headers)
|
||||||
|
self.tor_valid = 'Congratulations' in tor_check.text
|
||||||
|
|
||||||
|
if not self.tor_valid:
|
||||||
|
raise TorError(
|
||||||
|
"Tor connection succeeded, but the connection could "
|
||||||
|
"not be validated by torproject.org",
|
||||||
|
disable=True)
|
||||||
|
except ConnectionError:
|
||||||
|
raise TorError(
|
||||||
|
"Error raised during Tor connection validation",
|
||||||
|
disable=True)
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
(base_url or self.search_url) + query,
|
||||||
|
proxies=self.proxies,
|
||||||
|
headers=headers,
|
||||||
|
cookies=cookies)
|
||||||
|
|
||||||
|
# Retry query with new identity if using Tor (max 10 attempts)
|
||||||
|
if 'form id="captcha-form"' in response.text and self.tor:
|
||||||
|
attempt += 1
|
||||||
|
if attempt > 10:
|
||||||
|
raise TorError("Tor query failed -- max attempts exceeded 10")
|
||||||
|
return self.send((base_url or self.search_url), query, attempt)
|
||||||
|
|
||||||
|
return response
|
726
app/routes.py
Executable file
|
@ -0,0 +1,726 @@
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import re
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
import uuid
|
||||||
|
import validators
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
|
import waitress
|
||||||
|
from app import app
|
||||||
|
from app.models.config import Config
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
from app.request import Request, TorError
|
||||||
|
from app.utils.bangs import suggest_bang, resolve_bang
|
||||||
|
from app.utils.misc import empty_gif, placeholder_img, get_proxy_host_url, \
|
||||||
|
fetch_favicon
|
||||||
|
from app.filter import Filter
|
||||||
|
from app.utils.misc import read_config_bool, get_client_ip, get_request_url, \
|
||||||
|
check_for_update, encrypt_string
|
||||||
|
from app.utils.widgets import *
|
||||||
|
from app.utils.results import bold_search_terms,\
|
||||||
|
add_currency_card, check_currency, get_tabs_content
|
||||||
|
from app.utils.search import Search, needs_https, has_captcha
|
||||||
|
from app.utils.session import valid_user_session
|
||||||
|
from bs4 import BeautifulSoup as bsoup
|
||||||
|
from flask import Flask, jsonify, make_response, request, redirect, render_template, \
|
||||||
|
send_file, session, url_for, g
|
||||||
|
from requests import exceptions
|
||||||
|
from requests.models import PreparedRequest
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from werkzeug.datastructures import MultiDict
|
||||||
|
|
||||||
|
ac_var = 'WHOOGLE_AUTOCOMPLETE'
|
||||||
|
autocomplete_enabled = os.getenv(ac_var, '1')
|
||||||
|
|
||||||
|
def get_search_name(tbm):
|
||||||
|
for tab in app.config['HEADER_TABS'].values():
|
||||||
|
if tab['tbm'] == tbm:
|
||||||
|
return tab['name']
|
||||||
|
|
||||||
|
|
||||||
|
def auth_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
# do not ask password if cookies already present
|
||||||
|
if (
|
||||||
|
valid_user_session(session)
|
||||||
|
and 'cookies_disabled' not in request.args
|
||||||
|
and session['auth']
|
||||||
|
):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
auth = request.authorization
|
||||||
|
|
||||||
|
# Skip if username/password not set
|
||||||
|
whoogle_user = os.getenv('WHOOGLE_USER', '')
|
||||||
|
whoogle_pass = os.getenv('WHOOGLE_PASS', '')
|
||||||
|
if (not whoogle_user or not whoogle_pass) or (
|
||||||
|
auth
|
||||||
|
and whoogle_user == auth.username
|
||||||
|
and whoogle_pass == auth.password):
|
||||||
|
session['auth'] = True
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return make_response('Not logged in', 401, {
|
||||||
|
'WWW-Authenticate': 'Basic realm="Login Required"'})
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def session_required(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not valid_user_session(session):
|
||||||
|
session.pop('_permanent', None)
|
||||||
|
|
||||||
|
# Note: This sets all requests to use the encryption key determined per
|
||||||
|
# instance on app init. This can be updated in the future to use a key
|
||||||
|
# that is unique for their session (session['key']) but this should use
|
||||||
|
# a config setting to enable the session based key. Otherwise there can
|
||||||
|
# be problems with searches performed by users with cookies blocked if
|
||||||
|
# a session based key is always used.
|
||||||
|
g.session_key = app.enc_key
|
||||||
|
|
||||||
|
# Clear out old sessions
|
||||||
|
invalid_sessions = []
|
||||||
|
for user_session in os.listdir(app.config['SESSION_FILE_DIR']):
|
||||||
|
file_path = os.path.join(
|
||||||
|
app.config['SESSION_FILE_DIR'],
|
||||||
|
user_session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ignore files that are larger than the max session file size
|
||||||
|
if os.path.getsize(file_path) > app.config['MAX_SESSION_SIZE']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(file_path, 'rb') as session_file:
|
||||||
|
_ = pickle.load(session_file)
|
||||||
|
data = pickle.load(session_file)
|
||||||
|
if isinstance(data, dict) and 'valid' in data:
|
||||||
|
continue
|
||||||
|
invalid_sessions.append(file_path)
|
||||||
|
except Exception:
|
||||||
|
# Broad exception handling here due to how instances installed
|
||||||
|
# with pip seem to have issues storing unrelated files in the
|
||||||
|
# same directory as sessions
|
||||||
|
pass
|
||||||
|
|
||||||
|
for invalid_session in invalid_sessions:
|
||||||
|
try:
|
||||||
|
os.remove(invalid_session)
|
||||||
|
except FileNotFoundError:
|
||||||
|
# Don't throw error if the invalid session has been removed
|
||||||
|
pass
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def before_request_func():
|
||||||
|
session.permanent = True
|
||||||
|
|
||||||
|
# Check for latest version if needed
|
||||||
|
now = datetime.now()
|
||||||
|
needs_update_check = now - timedelta(hours=24) > app.config['LAST_UPDATE_CHECK']
|
||||||
|
if read_config_bool('WHOOGLE_UPDATE_CHECK', True) and needs_update_check:
|
||||||
|
app.config['LAST_UPDATE_CHECK'] = now
|
||||||
|
app.config['HAS_UPDATE'] = check_for_update(
|
||||||
|
app.config['RELEASES_URL'],
|
||||||
|
app.config['VERSION_NUMBER'])
|
||||||
|
|
||||||
|
g.request_params = (
|
||||||
|
request.args if request.method == 'GET' else request.form
|
||||||
|
)
|
||||||
|
|
||||||
|
default_config = json.load(open(app.config['DEFAULT_CONFIG'])) \
|
||||||
|
if os.path.exists(app.config['DEFAULT_CONFIG']) else {}
|
||||||
|
|
||||||
|
# Generate session values for user if unavailable
|
||||||
|
if not valid_user_session(session):
|
||||||
|
session['config'] = default_config
|
||||||
|
session['uuid'] = str(uuid.uuid4())
|
||||||
|
session['key'] = app.enc_key
|
||||||
|
session['auth'] = False
|
||||||
|
|
||||||
|
# Establish config values per user session
|
||||||
|
g.user_config = Config(**session['config'])
|
||||||
|
|
||||||
|
# Update user config if specified in search args
|
||||||
|
g.user_config = g.user_config.from_params(g.request_params)
|
||||||
|
|
||||||
|
if not g.user_config.url:
|
||||||
|
g.user_config.url = get_request_url(request.url_root)
|
||||||
|
|
||||||
|
g.user_request = Request(
|
||||||
|
request.headers.get('User-Agent'),
|
||||||
|
get_request_url(request.url_root),
|
||||||
|
config=g.user_config)
|
||||||
|
|
||||||
|
g.app_location = g.user_config.url
|
||||||
|
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def after_request_func(resp):
|
||||||
|
resp.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
resp.headers['X-Frame-Options'] = 'DENY'
|
||||||
|
resp.headers['Cache-Control'] = 'max-age=86400'
|
||||||
|
|
||||||
|
if os.getenv('WHOOGLE_CSP', False):
|
||||||
|
resp.headers['Content-Security-Policy'] = app.config['CSP']
|
||||||
|
if os.environ.get('HTTPS_ONLY', False):
|
||||||
|
resp.headers['Content-Security-Policy'] += \
|
||||||
|
'upgrade-insecure-requests'
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def unknown_page(e):
|
||||||
|
app.logger.warn(e)
|
||||||
|
return redirect(g.app_location)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.healthz}', methods=['GET'])
|
||||||
|
def healthz():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET'])
|
||||||
|
@app.route(f'/{Endpoint.home}', methods=['GET'])
|
||||||
|
@auth_required
|
||||||
|
def index():
|
||||||
|
# Redirect if an error was raised
|
||||||
|
if 'error_message' in session and session['error_message']:
|
||||||
|
error_message = session['error_message']
|
||||||
|
session['error_message'] = ''
|
||||||
|
return render_template('error.html', error_message=error_message)
|
||||||
|
|
||||||
|
return render_template('index.html',
|
||||||
|
has_update=app.config['HAS_UPDATE'],
|
||||||
|
languages=app.config['LANGUAGES'],
|
||||||
|
countries=app.config['COUNTRIES'],
|
||||||
|
time_periods=app.config['TIME_PERIODS'],
|
||||||
|
themes=app.config['THEMES'],
|
||||||
|
autocomplete_enabled=autocomplete_enabled,
|
||||||
|
translation=app.config['TRANSLATIONS'][
|
||||||
|
g.user_config.get_localization_lang()
|
||||||
|
],
|
||||||
|
logo=render_template(
|
||||||
|
'logo.html',
|
||||||
|
dark=g.user_config.dark),
|
||||||
|
config_disabled=(
|
||||||
|
app.config['CONFIG_DISABLE'] or
|
||||||
|
not valid_user_session(session)),
|
||||||
|
config=g.user_config,
|
||||||
|
tor_available=int(os.environ.get('TOR_AVAILABLE')),
|
||||||
|
version_number=app.config['VERSION_NUMBER'])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.opensearch}', methods=['GET'])
|
||||||
|
def opensearch():
|
||||||
|
opensearch_url = g.app_location
|
||||||
|
if opensearch_url.endswith('/'):
|
||||||
|
opensearch_url = opensearch_url[:-1]
|
||||||
|
|
||||||
|
# Enforce https for opensearch template
|
||||||
|
if needs_https(opensearch_url):
|
||||||
|
opensearch_url = opensearch_url.replace('http://', 'https://', 1)
|
||||||
|
|
||||||
|
get_only = g.user_config.get_only or 'Chrome' in request.headers.get(
|
||||||
|
'User-Agent')
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'opensearch.xml',
|
||||||
|
main_url=opensearch_url,
|
||||||
|
request_type='' if get_only else 'method="post"',
|
||||||
|
search_type=request.args.get('tbm'),
|
||||||
|
search_name=get_search_name(request.args.get('tbm'))
|
||||||
|
), 200, {'Content-Type': 'application/xml'}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.search_html}', methods=['GET'])
|
||||||
|
def search_html():
|
||||||
|
search_url = g.app_location
|
||||||
|
if search_url.endswith('/'):
|
||||||
|
search_url = search_url[:-1]
|
||||||
|
return render_template('search.html', url=search_url)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.autocomplete}', methods=['GET', 'POST'])
|
||||||
|
def autocomplete():
|
||||||
|
if os.getenv(ac_var) and not read_config_bool(ac_var):
|
||||||
|
return jsonify({})
|
||||||
|
|
||||||
|
q = g.request_params.get('q')
|
||||||
|
if not q:
|
||||||
|
# FF will occasionally (incorrectly) send the q field without a
|
||||||
|
# mimetype in the format "b'q=<query>'" through the request.data field
|
||||||
|
q = str(request.data).replace('q=', '')
|
||||||
|
|
||||||
|
# Search bangs if the query begins with "!", but not "! " (feeling lucky)
|
||||||
|
if q.startswith('!') and len(q) > 1 and not q.startswith('! '):
|
||||||
|
return jsonify([q, suggest_bang(q)])
|
||||||
|
|
||||||
|
if not q and not request.data:
|
||||||
|
return jsonify({'?': []})
|
||||||
|
elif request.data:
|
||||||
|
q = urlparse.unquote_plus(
|
||||||
|
request.data.decode('utf-8').replace('q=', ''))
|
||||||
|
|
||||||
|
# Return a list of suggestions for the query
|
||||||
|
#
|
||||||
|
# Note: If Tor is enabled, this returns nothing, as the request is
|
||||||
|
# almost always rejected
|
||||||
|
return jsonify([
|
||||||
|
q,
|
||||||
|
g.user_request.autocomplete(q) if not g.user_config.tor else []
|
||||||
|
])
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.search}', methods=['GET', 'POST'])
|
||||||
|
@session_required
|
||||||
|
@auth_required
|
||||||
|
def search():
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Redirect as a GET request with an encrypted query
|
||||||
|
post_data = MultiDict(request.form)
|
||||||
|
post_data['q'] = encrypt_string(g.session_key, post_data['q'])
|
||||||
|
get_req_str = urlparse.urlencode(post_data)
|
||||||
|
return redirect(url_for('.search') + '?' + get_req_str)
|
||||||
|
|
||||||
|
search_util = Search(request, g.user_config, g.session_key)
|
||||||
|
query = search_util.new_search_query()
|
||||||
|
|
||||||
|
bang = resolve_bang(query)
|
||||||
|
if bang:
|
||||||
|
return redirect(bang)
|
||||||
|
|
||||||
|
# Redirect to home if invalid/blank search
|
||||||
|
if not query:
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
|
||||||
|
# Generate response and number of external elements from the page
|
||||||
|
try:
|
||||||
|
response = search_util.generate_response()
|
||||||
|
except TorError as e:
|
||||||
|
session['error_message'] = e.message + (
|
||||||
|
"\\n\\nTor config is now disabled!" if e.disable else "")
|
||||||
|
session['config']['tor'] = False if e.disable else session['config'][
|
||||||
|
'tor']
|
||||||
|
return redirect(url_for('.index'))
|
||||||
|
|
||||||
|
if search_util.feeling_lucky:
|
||||||
|
return redirect(response, code=303)
|
||||||
|
|
||||||
|
# If the user is attempting to translate a string, determine the correct
|
||||||
|
# string for formatting the lingva.ml url
|
||||||
|
localization_lang = g.user_config.get_localization_lang()
|
||||||
|
translation = app.config['TRANSLATIONS'][localization_lang]
|
||||||
|
translate_to = localization_lang.replace('lang_', '')
|
||||||
|
|
||||||
|
# removing st-card to only use whoogle time selector
|
||||||
|
soup = bsoup(response, "html.parser");
|
||||||
|
for x in soup.find_all(attrs={"id": "st-card"}):
|
||||||
|
x.replace_with("")
|
||||||
|
|
||||||
|
response = str(soup)
|
||||||
|
|
||||||
|
# Return 503 if temporarily blocked by captcha
|
||||||
|
if has_captcha(str(response)):
|
||||||
|
app.logger.error('503 (CAPTCHA)')
|
||||||
|
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
||||||
|
if (fallback_engine):
|
||||||
|
return redirect(fallback_engine + query)
|
||||||
|
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
blocked=True,
|
||||||
|
error_message=translation['ratelimit'],
|
||||||
|
translation=translation,
|
||||||
|
farside='https://farside.link',
|
||||||
|
config=g.user_config,
|
||||||
|
query=urlparse.unquote(query),
|
||||||
|
params=g.user_config.to_params(keys=['preferences'])), 503
|
||||||
|
|
||||||
|
response = bold_search_terms(response, query)
|
||||||
|
|
||||||
|
# check for widgets and add if requested
|
||||||
|
if search_util.widget != '':
|
||||||
|
html_soup = bsoup(str(response), 'html.parser')
|
||||||
|
if search_util.widget == 'ip':
|
||||||
|
response = add_ip_card(html_soup, get_client_ip(request))
|
||||||
|
elif search_util.widget == 'calculator' and not 'nojs' in request.args:
|
||||||
|
response = add_calculator_card(html_soup)
|
||||||
|
|
||||||
|
# Update tabs content
|
||||||
|
tabs = get_tabs_content(app.config['HEADER_TABS'],
|
||||||
|
search_util.full_query,
|
||||||
|
search_util.search_type,
|
||||||
|
g.user_config.preferences,
|
||||||
|
translation)
|
||||||
|
|
||||||
|
# Feature to display currency_card
|
||||||
|
# Since this is determined by more than just the
|
||||||
|
# query is it not defined as a standard widget
|
||||||
|
conversion = check_currency(str(response))
|
||||||
|
if conversion:
|
||||||
|
html_soup = bsoup(str(response), 'html.parser')
|
||||||
|
response = add_currency_card(html_soup, conversion)
|
||||||
|
|
||||||
|
preferences = g.user_config.preferences
|
||||||
|
home_url = f"home?preferences={preferences}" if preferences else "home"
|
||||||
|
cleanresponse = str(response).replace("andlt;","<").replace("andgt;",">")
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'display.html',
|
||||||
|
has_update=app.config['HAS_UPDATE'],
|
||||||
|
query=urlparse.unquote(query),
|
||||||
|
search_type=search_util.search_type,
|
||||||
|
search_name=get_search_name(search_util.search_type),
|
||||||
|
config=g.user_config,
|
||||||
|
autocomplete_enabled=autocomplete_enabled,
|
||||||
|
lingva_url=app.config['TRANSLATE_URL'],
|
||||||
|
translation=translation,
|
||||||
|
translate_to=translate_to,
|
||||||
|
translate_str=query.replace(
|
||||||
|
'translate', ''
|
||||||
|
).replace(
|
||||||
|
translation['translate'], ''
|
||||||
|
),
|
||||||
|
is_translation=any(
|
||||||
|
_ in query.lower() for _ in [translation['translate'], 'translate']
|
||||||
|
) and not search_util.search_type, # Standard search queries only
|
||||||
|
response=cleanresponse,
|
||||||
|
version_number=app.config['VERSION_NUMBER'],
|
||||||
|
search_header=render_template(
|
||||||
|
'header.html',
|
||||||
|
home_url=home_url,
|
||||||
|
config=g.user_config,
|
||||||
|
translation=translation,
|
||||||
|
languages=app.config['LANGUAGES'],
|
||||||
|
countries=app.config['COUNTRIES'],
|
||||||
|
time_periods=app.config['TIME_PERIODS'],
|
||||||
|
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,
|
||||||
|
tabs=tabs)).replace(" ", "")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.config}', methods=['GET', 'POST', 'PUT'])
|
||||||
|
@session_required
|
||||||
|
@auth_required
|
||||||
|
def config():
|
||||||
|
config_disabled = (
|
||||||
|
app.config['CONFIG_DISABLE'] or
|
||||||
|
not valid_user_session(session))
|
||||||
|
|
||||||
|
name = ''
|
||||||
|
if 'name' in request.args:
|
||||||
|
name = os.path.normpath(request.args.get('name'))
|
||||||
|
if not re.match(r'^[A-Za-z0-9_.+-]+$', name):
|
||||||
|
return make_response('Invalid config name', 400)
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
return json.dumps(g.user_config.__dict__)
|
||||||
|
elif request.method == 'PUT' and not config_disabled:
|
||||||
|
if name:
|
||||||
|
config_pkl = os.path.join(app.config['CONFIG_PATH'], name)
|
||||||
|
session['config'] = (pickle.load(open(config_pkl, 'rb'))
|
||||||
|
if os.path.exists(config_pkl)
|
||||||
|
else session['config'])
|
||||||
|
return json.dumps(session['config'])
|
||||||
|
else:
|
||||||
|
return json.dumps({})
|
||||||
|
elif not config_disabled:
|
||||||
|
config_data = request.form.to_dict()
|
||||||
|
if 'url' not in config_data or not config_data['url']:
|
||||||
|
config_data['url'] = g.user_config.url
|
||||||
|
|
||||||
|
# Save config by name to allow a user to easily load later
|
||||||
|
if 'name' in request.args:
|
||||||
|
pickle.dump(
|
||||||
|
config_data,
|
||||||
|
open(os.path.join(
|
||||||
|
app.config['CONFIG_PATH'],
|
||||||
|
name), 'wb'))
|
||||||
|
|
||||||
|
session['config'] = config_data
|
||||||
|
return redirect(config_data['url'])
|
||||||
|
else:
|
||||||
|
return redirect(url_for('.index'), code=403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.imgres}')
|
||||||
|
@session_required
|
||||||
|
@auth_required
|
||||||
|
def imgres():
|
||||||
|
return redirect(request.args.get('imgurl'))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.element}')
|
||||||
|
@session_required
|
||||||
|
@auth_required
|
||||||
|
def element():
|
||||||
|
element_url = src_url = request.args.get('url')
|
||||||
|
if element_url.startswith('gAAAAA'):
|
||||||
|
try:
|
||||||
|
cipher_suite = Fernet(g.session_key)
|
||||||
|
src_url = cipher_suite.decrypt(element_url.encode()).decode()
|
||||||
|
except (InvalidSignature, InvalidToken) as e:
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
error_message=str(e)), 401
|
||||||
|
|
||||||
|
src_type = request.args.get('type')
|
||||||
|
|
||||||
|
# Ensure requested element is from a valid domain
|
||||||
|
domain = urlparse.urlparse(src_url).netloc
|
||||||
|
if not validators.domain(domain):
|
||||||
|
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = g.user_request.send(base_url=src_url)
|
||||||
|
|
||||||
|
# Display an empty gif if the requested element couldn't be retrieved
|
||||||
|
if response.status_code != 200 or len(response.content) == 0:
|
||||||
|
if 'favicon' in src_url:
|
||||||
|
favicon = fetch_favicon(src_url)
|
||||||
|
return send_file(io.BytesIO(favicon), mimetype='image/png')
|
||||||
|
else:
|
||||||
|
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||||
|
|
||||||
|
file_data = response.content
|
||||||
|
tmp_mem = io.BytesIO()
|
||||||
|
tmp_mem.write(file_data)
|
||||||
|
tmp_mem.seek(0)
|
||||||
|
|
||||||
|
return send_file(tmp_mem, mimetype=src_type)
|
||||||
|
except exceptions.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return send_file(io.BytesIO(empty_gif), mimetype='image/gif')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route(f'/{Endpoint.window}')
|
||||||
|
@session_required
|
||||||
|
@auth_required
|
||||||
|
def window():
|
||||||
|
target_url = request.args.get('location')
|
||||||
|
if target_url.startswith('gAAAAA'):
|
||||||
|
cipher_suite = Fernet(g.session_key)
|
||||||
|
target_url = cipher_suite.decrypt(target_url.encode()).decode()
|
||||||
|
|
||||||
|
content_filter = Filter(
|
||||||
|
g.session_key,
|
||||||
|
root_url=request.url_root,
|
||||||
|
config=g.user_config)
|
||||||
|
target = urlparse.urlparse(target_url)
|
||||||
|
|
||||||
|
# Ensure requested URL has a valid domain
|
||||||
|
if not validators.domain(target.netloc):
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
error_message='Invalid location'), 400
|
||||||
|
|
||||||
|
host_url = f'{target.scheme}://{target.netloc}'
|
||||||
|
|
||||||
|
get_body = g.user_request.send(base_url=target_url).text
|
||||||
|
|
||||||
|
results = bsoup(get_body, 'html.parser')
|
||||||
|
src_attrs = ['src', 'href', 'srcset', 'data-srcset', 'data-src']
|
||||||
|
|
||||||
|
# Parse HTML response and replace relative links w/ absolute
|
||||||
|
for element in results.find_all():
|
||||||
|
for attr in src_attrs:
|
||||||
|
if not element.has_attr(attr) or not element[attr].startswith('/'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
element[attr] = host_url + element[attr]
|
||||||
|
|
||||||
|
# Replace or remove javascript sources
|
||||||
|
for script in results.find_all('script', {'src': True}):
|
||||||
|
if 'nojs' in request.args:
|
||||||
|
script.decompose()
|
||||||
|
else:
|
||||||
|
content_filter.update_element_src(script, 'application/javascript')
|
||||||
|
|
||||||
|
# Replace all possible image attributes
|
||||||
|
img_sources = ['src', 'data-src', 'data-srcset', 'srcset']
|
||||||
|
for img in results.find_all('img'):
|
||||||
|
_ = [
|
||||||
|
content_filter.update_element_src(img, 'image/png', attr=_)
|
||||||
|
for _ in img_sources if img.has_attr(_)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Replace all stylesheet sources
|
||||||
|
for link in results.find_all('link', {'href': True}):
|
||||||
|
content_filter.update_element_src(link, 'text/css', attr='href')
|
||||||
|
|
||||||
|
# Use anonymous view for all links on page
|
||||||
|
for a in results.find_all('a', {'href': True}):
|
||||||
|
a['href'] = f'{Endpoint.window}?location=' + a['href'] + (
|
||||||
|
'&nojs=1' if 'nojs' in request.args else '')
|
||||||
|
|
||||||
|
# Remove all iframes -- these are commonly used inside of <noscript> tags
|
||||||
|
# to enforce loading Google Analytics
|
||||||
|
for iframe in results.find_all('iframe'):
|
||||||
|
iframe.decompose()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'display.html',
|
||||||
|
response=results,
|
||||||
|
translation=app.config['TRANSLATIONS'][
|
||||||
|
g.user_config.get_localization_lang()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/robots.txt')
|
||||||
|
def robots():
|
||||||
|
response = make_response(
|
||||||
|
'''User-Agent: *
|
||||||
|
Disallow: /''', 200)
|
||||||
|
response.mimetype = 'text/plain'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def favicon():
|
||||||
|
return app.send_static_file('img/favicon.ico')
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(Exception)
|
||||||
|
def internal_error(e):
|
||||||
|
query = ''
|
||||||
|
if request.method == 'POST':
|
||||||
|
query = request.form.get('q')
|
||||||
|
else:
|
||||||
|
query = request.args.get('q')
|
||||||
|
|
||||||
|
# Attempt to parse the query
|
||||||
|
try:
|
||||||
|
search_util = Search(request, g.user_config, g.session_key)
|
||||||
|
query = search_util.new_search_query()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(traceback.format_exc(), file=sys.stderr)
|
||||||
|
|
||||||
|
fallback_engine = os.environ.get('WHOOGLE_FALLBACK_ENGINE_URL', '')
|
||||||
|
if (fallback_engine):
|
||||||
|
return redirect(fallback_engine + query)
|
||||||
|
|
||||||
|
localization_lang = g.user_config.get_localization_lang()
|
||||||
|
translation = app.config['TRANSLATIONS'][localization_lang]
|
||||||
|
|
||||||
|
# Itt kapja meg a hiba részleteit, és ezeket fogja megjeleníteni.
|
||||||
|
error_detail = str(e) # Konvertálja a hiba objektumot szöveges formátumba
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'error.html',
|
||||||
|
error_message=f'Internal server error (500): {error_detail}', # Ide kerül a pontos hibaüzenet
|
||||||
|
translation=translation,
|
||||||
|
farside='https://farside.link',
|
||||||
|
config=g.user_config,
|
||||||
|
query=unquote(query),
|
||||||
|
params=g.user_config.to_params(keys=['preferences'])), 500
|
||||||
|
|
||||||
|
|
||||||
|
def run_app() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='RaveSearch console runner')
|
||||||
|
parser.add_argument(
|
||||||
|
'--port',
|
||||||
|
default=5000,
|
||||||
|
metavar='<port number>',
|
||||||
|
help='Specifies a port to run on (default 5000)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--host',
|
||||||
|
default='127.0.0.1',
|
||||||
|
metavar='<ip address>',
|
||||||
|
help='Specifies the host address to use (default 127.0.0.1)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--unix-socket',
|
||||||
|
default='',
|
||||||
|
metavar='</path/to/unix.sock>',
|
||||||
|
help='Listen for app on unix socket instead of host:port')
|
||||||
|
parser.add_argument(
|
||||||
|
'--unix-socket-perms',
|
||||||
|
default='600',
|
||||||
|
metavar='<octal permissions>',
|
||||||
|
help='Octal permissions to use for the Unix domain socket (default 600)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Activates debug mode for the server (default False)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--https-only',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help='Enforces HTTPS redirects for all requests')
|
||||||
|
parser.add_argument(
|
||||||
|
'--userpass',
|
||||||
|
default='',
|
||||||
|
metavar='<username:password>',
|
||||||
|
help='Sets a username/password basic auth combo (default None)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--proxyauth',
|
||||||
|
default='',
|
||||||
|
metavar='<username:password>',
|
||||||
|
help='Sets a username/password for a HTTP/SOCKS proxy (default None)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--proxytype',
|
||||||
|
default='',
|
||||||
|
metavar='<socks4|socks5|http>',
|
||||||
|
help='Sets a proxy type for all connections (default None)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--proxyloc',
|
||||||
|
default='',
|
||||||
|
metavar='<location:port>',
|
||||||
|
help='Sets a proxy location for all connections (default None)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.userpass:
|
||||||
|
user_pass = args.userpass.split(':')
|
||||||
|
os.environ['WHOOGLE_USER'] = user_pass[0]
|
||||||
|
os.environ['WHOOGLE_PASS'] = user_pass[1]
|
||||||
|
|
||||||
|
if args.proxytype and args.proxyloc:
|
||||||
|
if args.proxyauth:
|
||||||
|
proxy_user_pass = args.proxyauth.split(':')
|
||||||
|
os.environ['WHOOGLE_PROXY_USER'] = proxy_user_pass[0]
|
||||||
|
os.environ['WHOOGLE_PROXY_PASS'] = proxy_user_pass[1]
|
||||||
|
os.environ['WHOOGLE_PROXY_TYPE'] = args.proxytype
|
||||||
|
os.environ['WHOOGLE_PROXY_LOC'] = args.proxyloc
|
||||||
|
|
||||||
|
if args.https_only:
|
||||||
|
os.environ['HTTPS_ONLY'] = '1'
|
||||||
|
|
||||||
|
if args.debug:
|
||||||
|
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||||
|
elif args.unix_socket:
|
||||||
|
waitress.serve(app, unix_socket=args.unix_socket, unix_socket_perms=args.unix_socket_perms)
|
||||||
|
else:
|
||||||
|
waitress.serve(
|
||||||
|
app,
|
||||||
|
listen="{}:{}".format(args.host, args.port),
|
||||||
|
url_prefix=os.environ.get('WHOOGLE_URL_PREFIX', ''))
|
BIN
app/static/.DS_Store
vendored
Executable file
14
app/static/bangs/00-whoogle.json
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"!i": {
|
||||||
|
"url": "search?q={}&tbm=isch",
|
||||||
|
"suggestion": "!i (Whoogle Images)"
|
||||||
|
},
|
||||||
|
"!v": {
|
||||||
|
"url": "search?q={}&tbm=vid",
|
||||||
|
"suggestion": "!v (Whoogle Videos)"
|
||||||
|
},
|
||||||
|
"!n": {
|
||||||
|
"url": "search?q={}&tbm=nws",
|
||||||
|
"suggestion": "!n (Whoogle News)"
|
||||||
|
}
|
||||||
|
}
|
2
app/static/build/.gitignore
vendored
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
864
app/static/css/dark-theme.css
Executable file
|
@ -0,0 +1,864 @@
|
||||||
|
:root {
|
||||||
|
/* DARK THEME COLORS */
|
||||||
|
--primary: #ed0f59;
|
||||||
|
--primary-active: #bc134b;
|
||||||
|
--primary-grad2: #e24c41;
|
||||||
|
--grad2-active: #a53028;
|
||||||
|
--primary-grad3: #dc7541;
|
||||||
|
--grad3-active: #a34e24;
|
||||||
|
--body-bg: #0a0c0d;
|
||||||
|
--body-color: #e4e8ea;
|
||||||
|
--ad-bg: #282e31;
|
||||||
|
--gray-200: #1b1f21;
|
||||||
|
--gray-300: #23292c;
|
||||||
|
--gray-400: #282e31;
|
||||||
|
--gray-800: #9aa2a6;
|
||||||
|
--gray-900: #50585c;
|
||||||
|
--gray-950: #31373a;
|
||||||
|
--danger: #ff3333;
|
||||||
|
--danger-active: #e52e2e;
|
||||||
|
|
||||||
|
--visited: #8e7263;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* :root {
|
||||||
|
/* DARK THEME COLORS */
|
||||||
|
/*--primary: #105d5e;
|
||||||
|
--primary-active: #137071;
|
||||||
|
--primary-grad2: #009a6e;
|
||||||
|
--grad2-active: #00af7e;
|
||||||
|
--primary-grad3: #b3eda9;
|
||||||
|
--grad3-active: #d0fdc8;
|
||||||
|
--primary-grad4: #ebfadb;
|
||||||
|
--ad-bg: #293e33;
|
||||||
|
--silver-bg: #767f7d;
|
||||||
|
--silver-light-bg: #c2cbc9;
|
||||||
|
--active: #e8e300;
|
||||||
|
--body-bg: #0a0c0d;
|
||||||
|
--body-color: #e4e8ea;
|
||||||
|
--body-color: #edf3f5;
|
||||||
|
|
||||||
|
--gray-200: #1b1f21;
|
||||||
|
--gray-300: #23292c;
|
||||||
|
--gray-400: #282e31;
|
||||||
|
--gray-800: #9aa2a6;
|
||||||
|
--gray-900: #50585c;
|
||||||
|
--gray-950: #31373a;
|
||||||
|
--danger: #ff3333;
|
||||||
|
--danger-active: #e52e2e;
|
||||||
|
} */
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--body-bg) !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background:var(--body-bg) !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* label {
|
||||||
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
color: var(--whoogle-dark-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anon-view {
|
||||||
|
color: var(--bo) !important;
|
||||||
|
padding: 30px;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* textarea {
|
||||||
|
background: rgb(29, 29, 29) !important;
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* Látogatott találat címke */
|
||||||
|
a:visited h3 div {
|
||||||
|
color: var(--visited) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Találat címke */
|
||||||
|
a:link h3 div {
|
||||||
|
color: var(--primary-grad3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link div {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Találat címke hover */
|
||||||
|
a:link div:hover {
|
||||||
|
color: var(--grad3-active) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link div:visited {
|
||||||
|
color: var(--visited) !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wikipedia cím */
|
||||||
|
.lU7jec h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 26px;
|
||||||
|
color: var(--primary-grad3) !important;
|
||||||
|
margin: 0 0 24px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wikipedia favicon */
|
||||||
|
.BNeawe.s3v9rd.AP7Wnd.has-favicon > .BNeawe.has-favicon > img.site-favicon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helyek favicon */
|
||||||
|
div > .other-results > .ZINbbc.xpd.Et0od.pkph0e.has-favicon > .has-favicon > img.site-favicon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tAd8D.AP7Wnd {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tAd8D.AP7Wnd {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div span {
|
||||||
|
color: var(--primary-grad3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: var(--gray-300) !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus-visible {
|
||||||
|
border: 1px solid var(--primary-active) !important;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: transparent !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------- */
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
background-color: var(--gray-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete:hover {
|
||||||
|
background-image: linear-gradient(90deg, var(--primary), var(--primary));
|
||||||
|
background-clip: padding-box, border-box;
|
||||||
|
background-origin: border-box;
|
||||||
|
border: 1px solid var(--primary-grad3);
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete:focus {
|
||||||
|
background-image: linear-gradient(90deg, var(--primary), var(--primary));
|
||||||
|
background-clip: padding-box, border-box;
|
||||||
|
background-origin: border-box;
|
||||||
|
border: 1px solid var(--primary-grad3);
|
||||||
|
border-radius: 100px;
|
||||||
|
}
|
||||||
|
/* Keresőgomb */
|
||||||
|
#search-submit {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
||||||
|
border: 1px solid var(--grad3-active) !important;
|
||||||
|
border-radius: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-submit:hover {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
||||||
|
border: 1px solid var(--grad3-active) !important;
|
||||||
|
border-radius: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kattide {
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
background: linear-gradient(45deg, var(--primary) 0%, var(--primary-grad2) 80%, var(--primary-grad3) 100%);
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
padding-right: 0.8rem;
|
||||||
|
-webkit-box-decoration-break: clone;
|
||||||
|
box-decoration-break: clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kattide:hover {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keresőkategóriák megjelenítése */
|
||||||
|
.selected-header-button,
|
||||||
|
button.selected-header-button:active,
|
||||||
|
button.selected-header-button:focus-within {
|
||||||
|
color: var(--primary-grad3) !important;
|
||||||
|
border-bottom: 2px solid var(--primary-grad2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button:hover {
|
||||||
|
color: var(--primary) !important;
|
||||||
|
border-bottom: 2px solid var(--primary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Keresési találat div keret */
|
||||||
|
#main > div:focus-within {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 6px 1px var(--primary-grad3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc {
|
||||||
|
background-color: #29292987 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 25px !important;
|
||||||
|
padding: 15px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ezeket keresték még */
|
||||||
|
.gGQDvd {
|
||||||
|
padding: 10px 0 0 10px !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kjGX2 {
|
||||||
|
left: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toMBf {
|
||||||
|
left: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 601px) {
|
||||||
|
.zBAuLc {
|
||||||
|
line-height: normal;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 0 0 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.s3v9rd {
|
||||||
|
padding: 8px 0 0 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.j039Wc {
|
||||||
|
padding-top: 34px !important;
|
||||||
|
margin-bottom: -14px !important;
|
||||||
|
}
|
||||||
|
/* Keresés tartalom */
|
||||||
|
.s3v9rd {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.KP7LCb {
|
||||||
|
box-shadow: 0 0 0 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BVG0Nb {
|
||||||
|
box-shadow: 0 0 0 0 !important;
|
||||||
|
background-color: var(--whoogle-dark-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.luh4tb {
|
||||||
|
background: var(--whoogle-dark-result-bg) !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bRsWnc {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x54gtf {
|
||||||
|
background-color: var(--whoogle-dark-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Q0HXG {
|
||||||
|
background-color: var(--whoogle-dark-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LKSyXe {
|
||||||
|
background-color: var(--whoogle-dark-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tAd8D {
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa1toc {
|
||||||
|
background: var(--whoogle-dark-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qFvlD {
|
||||||
|
border: 1px solid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xeDNfc {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:after {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background: transparent !important;
|
||||||
|
border: 2px solid grey !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content,
|
||||||
|
.result-config {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active:after {
|
||||||
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--whoogle-dark-contrast-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--whoogle-dark-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Keresési ajánlások */
|
||||||
|
.autocomplete-items {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div {
|
||||||
|
color: var(--body-color);
|
||||||
|
background-color: rgb(51, 51, 51);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div:last-child {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div:hover {
|
||||||
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-active {
|
||||||
|
background: linear-gradient(200deg, var(--primary) 0%, var(--primary-grad2) 60%, var(--primary-grad3) 100%) !important;
|
||||||
|
color: var(--body-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--whoogle-dark-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-div {
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
background-color: var(--whoogle-dark-result-bg) !important;
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-desktop {
|
||||||
|
color: var(--whoogle-dark-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div,
|
||||||
|
.update_available,
|
||||||
|
.cb_label,
|
||||||
|
.cb {
|
||||||
|
color: var(--whoogle-dark-secondary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb:focus {
|
||||||
|
color: var(--whoogle-dark-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header,
|
||||||
|
.mobile-header {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keresés feletti mini url */
|
||||||
|
.UPmit.AP7Wnd {
|
||||||
|
color: #d0bc8e !important;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lcJF1d {
|
||||||
|
margin: 25px 18px 18px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egMi0 {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qN9Ked {
|
||||||
|
display: table-caption !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kCrYT {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BsXmcf {
|
||||||
|
position: relative !important;
|
||||||
|
height: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Q6Xouf {
|
||||||
|
max-width: 555px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Helyek cím */
|
||||||
|
.deIvCb {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nGphre > .site-favicon {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Képtalálatok */
|
||||||
|
.results {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.result-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #29292987;
|
||||||
|
width: calc(33.333% - 16px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-title {
|
||||||
|
color: var(--primary-grad3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-url {
|
||||||
|
color: var(--body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-url:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--grad2-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stílusok a javaslatokhoz */
|
||||||
|
.suggestions {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stílusok a lapozáshoz */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.e3goi {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TxbwNb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RAyV4b {
|
||||||
|
width: 100%;
|
||||||
|
height: 130px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t0fcAb {
|
||||||
|
scale: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tor4Ec {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qXLe6d {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FbhRzb {
|
||||||
|
border-left: thin solid #dadce0;
|
||||||
|
border-right: thin solid #dadce0;
|
||||||
|
border-top: thin solid #dadce0;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.n692Zd {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.cvifge {
|
||||||
|
height: 40px;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.QvGUP {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 8px 0 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.O4cRJf {
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
.O1ePr {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.kgJEQe {
|
||||||
|
height: 36px;
|
||||||
|
width: 98px;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.lXLRf {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.MhzMZd {
|
||||||
|
border: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.xB0fq {
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #4285f4;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.xB0fq:focus {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
.M7pB2 {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
margin: 0 0 3px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.euZec {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
table.euZec td {
|
||||||
|
padding: 0;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
.QIqI7 {
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4285f4;
|
||||||
|
}
|
||||||
|
.EY24We {
|
||||||
|
border-bottom: 2px solid #4285f4;
|
||||||
|
}
|
||||||
|
.CsQyDc {
|
||||||
|
display: inline-block;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.TuS8Ad {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.HddGcc {
|
||||||
|
padding: 8px;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.dzp8ae {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
.rEM8G {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.bookcf {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
.InWNIe {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.uZgmoc {
|
||||||
|
width: 350px;
|
||||||
|
display: flex;
|
||||||
|
border: none;
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
/* background-color: #29292987; */
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 35px auto;
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.frGj1b {
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BnJWBc {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 0 13px 0;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.X6ZCif {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.TwVfHd {
|
||||||
|
border-radius: 16px;
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.yekiAe {
|
||||||
|
background-color: #dadce0;
|
||||||
|
}
|
||||||
|
.svla5d {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #29292987;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
display: grid;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.ezO2md {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
padding: 12px 16px 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.K35ahc {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.owohpf {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.fYyStc {
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
.img_button {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 1px solid #84dbff;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #3c4043;
|
||||||
|
background-color: #2e97c3;
|
||||||
|
}
|
||||||
|
.ynsChf {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.Fj3V3b {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.FrIlee {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.F9iS2e {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.WMQ2Le {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.x3G5ab {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.fuLhoc {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.epoveb {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
.dXDvrc {
|
||||||
|
color: #0d652d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.dloBPe {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.YVIcad {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.JkVVdd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.oXZRFd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.MQHtg {
|
||||||
|
color: #fbbc04;
|
||||||
|
}
|
||||||
|
.pyMRrb {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.EtTZid {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.M3vVJe {
|
||||||
|
color: #1967d2;
|
||||||
|
}
|
||||||
|
.NHQNef {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.Cb8Z7c {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
a.ZWRArf {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a .CVA68e:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
9
app/static/css/error.css
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
html {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
html {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
284
app/static/css/header.css
Executable file
|
@ -0,0 +1,284 @@
|
||||||
|
html {
|
||||||
|
font-family: "Ubuntu", sans-serif !important;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #3c4043;
|
||||||
|
word-wrap: break-word;
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-search {
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.mobile-logo {
|
||||||
|
width: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 5px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-link,
|
||||||
|
.logo-letter {
|
||||||
|
text-decoration: none !important;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-config {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
font: 22px/36px Futura, Arial, sans-serif;
|
||||||
|
padding-left: 5px;
|
||||||
|
/* display: flex; */
|
||||||
|
width: 10rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-div {
|
||||||
|
margin-top: 20px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
text-align: center;
|
||||||
|
font: 22pt Futura, Arial, sans-serif;
|
||||||
|
padding: 10px 0 5px 0;
|
||||||
|
height: 37px;
|
||||||
|
font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-desktop {
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
height: 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-div {
|
||||||
|
margin-top: 30px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: contents;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
background: none;
|
||||||
|
margin: 2px 4px 2px 8px;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracking-link {
|
||||||
|
font-size: large;
|
||||||
|
text-align: center;
|
||||||
|
margin: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#mobile-header-logo {
|
||||||
|
height: 1.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-input-div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 0 0 20px;
|
||||||
|
margin-right: -25px;
|
||||||
|
-webkit-box-flex: 1;
|
||||||
|
outline: none;
|
||||||
|
width: 100%;
|
||||||
|
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-mobile {
|
||||||
|
display: -webkit-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header-logo {
|
||||||
|
height: 1.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-autocomplete {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--primary-grad2) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--grad2-active) !important;
|
||||||
|
text-decoration: none;
|
||||||
|
text-decoration-line: none;
|
||||||
|
tap-highlight-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab-div {
|
||||||
|
overflow: auto;
|
||||||
|
margin: 30px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header {
|
||||||
|
height: 39px;
|
||||||
|
display: box;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-tab {
|
||||||
|
box-pack: justify;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 37px;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
color: #4285f4;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
height: 65px;
|
||||||
|
display: table;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header a,
|
||||||
|
.mobile-header span {
|
||||||
|
color: #70757a;
|
||||||
|
text-decoration: none;
|
||||||
|
display: table-cell;
|
||||||
|
word-spacing: 30px;
|
||||||
|
height: auto;
|
||||||
|
/* padding: 8px 12px 8px 12px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.mobile-header a,
|
||||||
|
.mobile-header span {
|
||||||
|
padding: 0 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-search {
|
||||||
|
font-size: 30px;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adv-search:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#adv-search-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-collapsible {
|
||||||
|
max-height: 0px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.25s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
#result-country {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 801px) {
|
||||||
|
.header-tab-div {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
.header-tab-div-2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 802px) {
|
||||||
|
.header-tab-div {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
.header-tab-div-2 {
|
||||||
|
margin: 50px 20px 0 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.header-tab {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
48
app/static/css/input.css
Executable file
|
@ -0,0 +1,48 @@
|
||||||
|
#search-bar {
|
||||||
|
height: 48px !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-reset {
|
||||||
|
all: unset;
|
||||||
|
margin-left: -40px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: transparent !important;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 45px;
|
||||||
|
width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 300px) {
|
||||||
|
#search-reset {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.xpd.O9g5cc.uUPGi input::-webkit-outer-spin-button,
|
||||||
|
input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb {
|
||||||
|
width: 40%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 28px;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #5f6368;
|
||||||
|
font-size: 14px !important;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 0 0 12px;
|
||||||
|
margin: 10px 10px 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversion_box {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.xpd.O9g5cc.uUPGi input:focus-visible {
|
||||||
|
outline: 0;
|
||||||
|
}
|
205
app/static/css/light-theme.css
Executable file
|
@ -0,0 +1,205 @@
|
||||||
|
html {
|
||||||
|
background: var(--whoogle-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--whoogle-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li a {
|
||||||
|
color: var(--whoogle-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.anon-view {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
background: var(--whoogle-page-bg) !important;
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: var(--whoogle-page-bg) !important;
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
box-shadow: 0 1px 6px rgba(32,33,36,0.28) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BVG0Nb {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ZINbbc.luh4tb {
|
||||||
|
background: var(--whoogle-result-bg) !important;
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bRsWnc {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.x54gtf {
|
||||||
|
background-color: var(--whoogle-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Q0HXG {
|
||||||
|
background-color: var(--whoogle-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LKSyXe {
|
||||||
|
background-color: var(--whoogle-divider) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a:visited h3 div {
|
||||||
|
color: var(--whoogle-result-visited) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link h3 div {
|
||||||
|
color: var(--whoogle-result-title) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link div {
|
||||||
|
color: var(--whoogle-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div span {
|
||||||
|
color: var(--whoogle-secondary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
background-color: var(--whoogle-page-bg) !important;
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
background-color: var(--whoogle-page-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-search {
|
||||||
|
border-color: var(--whoogle-element-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
background-color: var(--whoogle-page-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-submit {
|
||||||
|
border: 1px solid var(--whoogle-element-bg) !important;
|
||||||
|
background: var(--whoogle-element-bg) !important;
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
opacity: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:after {
|
||||||
|
color: var(--whoogle-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
background-color: var(--whoogle-element-bg) !important;
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content, .result-config {
|
||||||
|
background-color: var(--whoogle-element-bg) !important;
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active:after {
|
||||||
|
color: var(--whoogle-contrast-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--whoogle-element-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-color {
|
||||||
|
color: var(--whoogle-result-url) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items {
|
||||||
|
border: 1px solid var(--whoogle-element-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div {
|
||||||
|
background-color: var(--whoogle-page-bg);
|
||||||
|
border-bottom: 1px solid var(--whoogle-element-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div:hover {
|
||||||
|
background-color: var(--whoogle-element-bg);
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-active {
|
||||||
|
background-color: var(--whoogle-element-bg) !important;
|
||||||
|
color: var(--whoogle-contrast-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
color: var(--whoogle-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: var(--whoogle-logo);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-div {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-reset {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-bar {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar-desktop {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
color: var(--whoogle-text);
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div, .update_available, .cb_label, .cb {
|
||||||
|
color: var(--whoogle-secondary-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cb:focus {
|
||||||
|
color: var(--whoogle-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desktop-header, .mobile-header {
|
||||||
|
background-color: var(--whoogle-result-bg) !important;
|
||||||
|
}
|
30
app/static/css/logo.css
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
.cls-1 {
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
svg {
|
||||||
|
margin-top: 0.3em;
|
||||||
|
height: 70%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rplogo {
|
||||||
|
max-width: 40rem;
|
||||||
|
display: block;
|
||||||
|
margin: 30px auto;
|
||||||
|
position: relative;
|
||||||
|
border-radius: inherit;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
380
app/static/css/main.css
Executable file
|
@ -0,0 +1,380 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: "Ubuntu";
|
||||||
|
src: url("fonts/ubuntu.woff2") format("woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Ubuntu", sans-serif !important;
|
||||||
|
max-width: 900px !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 25px auto !important;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
main {
|
||||||
|
margin: 0;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
clear: both;
|
||||||
|
min-height: 4rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 45px;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
.logo-container {
|
||||||
|
background: no-repeat;
|
||||||
|
min-height: 4rem;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-search {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
::placeholder {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: inherit;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
grid-auto-flow: dense;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px){
|
||||||
|
.social-column {
|
||||||
|
grid-template-columns: repeat(1, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpkick {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpkick:hover {
|
||||||
|
background-color: rgba(83, 250, 22, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rptwitch {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
.rptwitch:hover {
|
||||||
|
background-color: rgba(101, 66, 166, 0.14);
|
||||||
|
}
|
||||||
|
.rpdiscord {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpdiscord:hover {
|
||||||
|
background-color: rgba(98, 102, 243, 0.08);
|
||||||
|
}
|
||||||
|
.rpgit {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpgit:hover {
|
||||||
|
background-color: rgba(204, 204, 204, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpyoutube {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpyoutube:hover {
|
||||||
|
background-color: rgba(129, 0, 0, 0.054);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpweb {
|
||||||
|
grid-row-end: span 2;
|
||||||
|
grid-column-end: span 1;
|
||||||
|
height: 50px;
|
||||||
|
width: 200px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background-color 0.3s ease-out 0.3ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rpweb:hover {
|
||||||
|
background-color: rgba(251, 255, 33, 0.049);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
margin-top: 26vh;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-form,
|
||||||
|
#search-fields {
|
||||||
|
max-width: 650px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: inherit;
|
||||||
|
border: inherit;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-items {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search-bar {
|
||||||
|
width: 95%;
|
||||||
|
margin: 0 auto;
|
||||||
|
height: 3rem;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 24px;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 100px;
|
||||||
|
padding: 0 0 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#search-submit {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 100%;
|
||||||
|
height: 3rem;
|
||||||
|
font-size: 18px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-options {
|
||||||
|
max-height: 370px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-buttons {
|
||||||
|
max-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-div {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
button::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible {
|
||||||
|
background-color: rgba(0, 0, 0, 0);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 35px 10px 35px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 686px;
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
outline: none;
|
||||||
|
font-size: 15px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
margin: 1.5rem 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsible:after {
|
||||||
|
content: "\002B";
|
||||||
|
font-weight: bold;
|
||||||
|
float: right;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active:after {
|
||||||
|
content: "\2212";
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
max-width: 646px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 18px;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.2s ease-out;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
border-left: 2px solid grey;
|
||||||
|
border-right: 2px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 60px;
|
||||||
|
border-bottom: 2px solid grey;
|
||||||
|
background-color: #1d1d1d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#config-style {
|
||||||
|
resize: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whoogle-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.whoogle-svg {
|
||||||
|
width: 80%;
|
||||||
|
height: initial;
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 1px solid #ffffff29;
|
||||||
|
border-radius: 100px;
|
||||||
|
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .autocomplete:hover {
|
||||||
|
border: 1px solid #2e97c3;
|
||||||
|
box-shadow: 0 1px 4px 0 #2e97c3, 0 1px 2px 0 #2e97c3;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
.autocomplete-items {
|
||||||
|
width: 650px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 99;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: opacity 0s ease 0s, visibility 0s ease 0s, all 0.5s ease 0s;
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
padding: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile styles */
|
||||||
|
|
||||||
|
|
||||||
|
.BNeawe.UPmit.AP7Wnd.lRVwie {
|
||||||
|
font-variant: all-small-caps;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w1C3Le,
|
||||||
|
.BmP5tf,
|
||||||
|
.G5NbBd,
|
||||||
|
.CS4w5b {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lcJF1d {
|
||||||
|
margin-left: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.egMi0 {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lRVwie {
|
||||||
|
text-wrap: balance !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
185
app/static/css/search.css
Executable file
|
@ -0,0 +1,185 @@
|
||||||
|
body {
|
||||||
|
max-width: 900px !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 25px auto !important;
|
||||||
|
color: white;
|
||||||
|
padding: 0 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vvjwJb {
|
||||||
|
font-size: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
position: relative;
|
||||||
|
display: -webkit-box;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border: 1px solid #ffffff29;
|
||||||
|
border-radius: 100px;
|
||||||
|
box-shadow: 0 2px 16px 0 rgba(5, 5, 6, 15%), 0 4px 8px 0 rgba(5, 5, 6, 35%);
|
||||||
|
background: #1d1d1d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items {
|
||||||
|
position: absolute;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top: none;
|
||||||
|
z-index: 99;
|
||||||
|
|
||||||
|
/*position the autocomplete items to be the same width as the container:*/
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-items div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary span {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
#lingva-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 650px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-address-div {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ip-text-div {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-favicon {
|
||||||
|
float: left;
|
||||||
|
width: 18px;
|
||||||
|
padding: 0 5px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.site-favicon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 601px) {
|
||||||
|
.has-favicon .sCuL3 {
|
||||||
|
padding: 0 0 0 30px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#flex_text_audio_icon_chunk {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rp-credit {
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio {
|
||||||
|
display: block;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 801px) {
|
||||||
|
body {
|
||||||
|
max-width: 900px !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 801px) {
|
||||||
|
details summary {
|
||||||
|
margin-bottom: 10px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-button {
|
||||||
|
background-color: inherit;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 10px 35px;
|
||||||
|
display: table-cell;
|
||||||
|
align-items: center;
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: none;
|
||||||
|
border-bottom-width: medium;
|
||||||
|
border-bottom-style: none;
|
||||||
|
border-bottom-color: currentcolor;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.sCuL3 {
|
||||||
|
top: -3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l97dzf {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tAd8D.AP7Wnd {
|
||||||
|
padding: 20px 0 20px 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skVgpb {
|
||||||
|
display: ruby !important;
|
||||||
|
padding: 10px 0 0 40px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.VGHMXd {
|
||||||
|
display: contents !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vbShOe {
|
||||||
|
padding: 10px 0 0 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .BNeawe.uEec3.AP7Wnd {
|
||||||
|
display: none !important;
|
||||||
|
} */
|
||||||
|
|
||||||
|
.RWuggc.kCrYT > div > .BNeawe.tAd8D.AP7Wnd {
|
||||||
|
padding: 20px 0 20px 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BVG0Nb {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Xdlr0d {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* > span > .BNeawe.uEec3.AP7Wnd {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AVsepf {
|
||||||
|
padding: 0 0 10px 10px !important;
|
||||||
|
}
|
81
app/static/css/variables.css
Executable file
|
@ -0,0 +1,81 @@
|
||||||
|
/* Colors */
|
||||||
|
:root {
|
||||||
|
/* LIGHT THEME COLORS */
|
||||||
|
--whoogle-logo: #685e79;
|
||||||
|
--whoogle-page-bg: #ffffff;
|
||||||
|
--whoogle-element-bg: #4285f4;
|
||||||
|
--whoogle-text: #000000;
|
||||||
|
--whoogle-contrast-text: #ffffff;
|
||||||
|
--whoogle-secondary-text: #70757a;
|
||||||
|
--whoogle-result-bg: #ffffff;
|
||||||
|
--whoogle-result-title: #1967d2;
|
||||||
|
--whoogle-result-url: #0d652d;
|
||||||
|
--whoogle-result-visited: #4b11a8;
|
||||||
|
|
||||||
|
/* DARK THEME COLORS */
|
||||||
|
/* --primary: #ff1a66;
|
||||||
|
--primary-active: #e5175c;
|
||||||
|
--primary-grad2: #ff5448;
|
||||||
|
--grad2-active: #e54b41;
|
||||||
|
--primary-grad3: #ff8448;
|
||||||
|
--grad3-active: #e57741; */
|
||||||
|
--body-bg: #0a0c0d;
|
||||||
|
--body-color: #e4e8ea;
|
||||||
|
--ad-bg: #282e31;
|
||||||
|
--gray-200: #1b1f21;
|
||||||
|
--gray-300: #23292c;
|
||||||
|
--gray-400: #282e31;
|
||||||
|
--gray-800: #9aa2a6;
|
||||||
|
--gray-900: #50585c;
|
||||||
|
--gray-950: #31373a;
|
||||||
|
--danger: #ff3333;
|
||||||
|
--danger-active: #e52e2e;
|
||||||
|
|
||||||
|
|
||||||
|
--aa: #D1D1D1;
|
||||||
|
--ab: #DBDBDB;
|
||||||
|
--bb: #85C7F2;
|
||||||
|
--ba: #636363;
|
||||||
|
--bc: #4C4C4C;
|
||||||
|
|
||||||
|
--qw: #F3E8EE;
|
||||||
|
--primary-grad2: #BACDB0;
|
||||||
|
--primary: #729B79;
|
||||||
|
--primary-grad3: #475B63;
|
||||||
|
--ew: #2E2C2F;
|
||||||
|
|
||||||
|
--re: #2D3142;
|
||||||
|
--er: #BFC0C0;
|
||||||
|
--rr: #FFFFFF;
|
||||||
|
--ee: #EF8354;
|
||||||
|
--rw: #4F5D75;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#whoogle-w {
|
||||||
|
fill: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-h {
|
||||||
|
fill: #ea4335;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-o-1 {
|
||||||
|
fill: #fbbc05;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-o-2 {
|
||||||
|
fill: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-g {
|
||||||
|
fill: #34a853;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-l {
|
||||||
|
fill: #ea4335;
|
||||||
|
}
|
||||||
|
|
||||||
|
#whoogle-e {
|
||||||
|
fill: #fbbc05;
|
||||||
|
}
|
BIN
app/static/img/.DS_Store
vendored
Executable file
BIN
app/static/img/favicon.ico
Executable file
After Width: | Height: | Size: 15 KiB |
BIN
app/static/img/favicon/.DS_Store
vendored
Executable file
BIN
app/static/img/favicon/android-chrome-192x192.png
Executable file
After Width: | Height: | Size: 20 KiB |
BIN
app/static/img/favicon/android-chrome-256x256.png
Executable file
After Width: | Height: | Size: 32 KiB |
BIN
app/static/img/favicon/apple-touch-icon.png
Executable file
After Width: | Height: | Size: 18 KiB |
9
app/static/img/favicon/browserconfig.xml
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
BIN
app/static/img/favicon/favicon-16x16.png
Executable file
After Width: | Height: | Size: 833 B |
BIN
app/static/img/favicon/favicon-32x32.png
Executable file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/static/img/favicon/favicon.ico
Executable file
After Width: | Height: | Size: 15 KiB |
44
app/static/img/favicon/manifest.json
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "RaveSearch",
|
||||||
|
"short_name": "RaveSearch",
|
||||||
|
"display": "fullscreen",
|
||||||
|
"scope": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon-32x32.png",
|
||||||
|
"sizes": "36x36",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "0.75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "favicon-32x32.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "favicon-32x32.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "2.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "144x144",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "3.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "4.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
BIN
app/static/img/favicon/mstile-150x150.png
Executable file
After Width: | Height: | Size: 10 KiB |
523
app/static/img/favicon/safari-pinned-tab.svg
Executable file
|
@ -0,0 +1,523 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="352.000000pt" height="352.000000pt" viewBox="0 0 352.000000 352.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,352.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M1300 3362 c0 -104 30 -223 80 -322 25 -48 110 -180 117 -180 3 0 2
|
||||||
|
15 -1 33 -5 24 -4 27 3 12 5 -11 10 -30 10 -41 1 -12 25 -57 54 -101 92 -139
|
||||||
|
155 -295 160 -400 2 -40 1 -73 -3 -73 -3 0 -14 23 -24 51 -10 28 -25 53 -32
|
||||||
|
56 -7 3 -27 29 -44 59 -16 30 -34 54 -39 54 -5 0 -30 34 -56 75 -25 41 -51 77
|
||||||
|
-56 81 -6 3 2 -12 17 -35 15 -23 25 -43 23 -46 -3 -2 -41 29 -85 70 -85 78
|
||||||
|
-153 173 -259 365 -60 109 -82 140 -97 140 -17 0 -7 -37 22 -83 38 -61 38 -67
|
||||||
|
-1 -22 -17 19 -34 33 -39 30 -18 -11 -32 -94 -22 -124 10 -27 9 -30 -8 -28
|
||||||
|
-28 4 -49 26 -81 83 -16 28 -57 75 -92 105 -34 29 -49 39 -34 22 17 -19 25
|
||||||
|
-36 21 -46 -4 -12 25 -48 107 -130 119 -118 213 -226 205 -235 -10 -10 -181
|
||||||
|
135 -210 178 -17 24 -32 38 -34 32 -2 -7 -40 7 -102 38 -60 30 -106 47 -117
|
||||||
|
44 -15 -5 -15 -4 -4 4 12 9 9 12 -15 18 -16 4 -31 10 -34 14 -6 9 -110 62
|
||||||
|
-110 56 0 -3 90 -77 200 -166 110 -88 200 -163 200 -165 0 -2 -17 -2 -37 1
|
||||||
|
-27 3 -14 -4 42 -24 101 -36 140 -56 192 -99 50 -40 152 -168 113 -141 -161
|
||||||
|
112 -187 120 -43 14 100 -73 124 -96 103 -96 -22 0 -170 81 -265 144 -54 36
|
||||||
|
-100 65 -102 63 -1 -2 11 -16 27 -32 17 -16 30 -34 30 -41 0 -7 14 -23 32 -35
|
||||||
|
30 -22 31 -23 10 -32 -31 -14 -141 16 -232 64 -77 40 -220 151 -220 170 0 7
|
||||||
|
17 9 53 4 67 -9 59 -1 -20 19 -34 9 -112 36 -174 61 -61 25 -113 44 -115 43
|
||||||
|
-5 -6 67 -113 75 -113 5 0 13 -4 18 -9 5 -5 3 -6 -4 -2 -17 10 -16 0 3 -29 15
|
||||||
|
-24 15 -25 -2 -26 -34 -1 16 -17 55 -18 19 -1 68 -17 108 -37 71 -35 90 -50
|
||||||
|
80 -60 -6 -6 -122 32 -162 54 -16 9 -34 17 -40 17 -5 1 -32 10 -60 20 -44 17
|
||||||
|
-47 20 -25 25 20 5 16 8 -23 14 -27 4 -51 5 -54 3 -2 -3 22 -33 54 -67 l59
|
||||||
|
-63 88 -2 c86 -3 207 -32 251 -62 11 -7 27 -13 37 -13 10 0 18 -4 18 -8 0 -5
|
||||||
|
-26 -10 -57 -11 -32 -1 -74 -6 -93 -11 -19 -5 -55 -11 -80 -14 -92 -12 -166
|
||||||
|
-24 -209 -36 -24 -6 -62 -13 -85 -15 -38 -3 -66 -10 -91 -23 -26 -14 17 25 47
|
||||||
|
42 28 17 31 17 19 3 -7 -9 -9 -17 -3 -17 5 0 15 7 22 15 11 13 93 55 108 55 3
|
||||||
|
0 -3 -10 -14 -21 -16 -18 -12 -17 20 5 21 14 47 26 57 26 39 0 25 17 -24 30
|
||||||
|
-29 7 -57 17 -64 22 -8 6 -18 5 -30 -2 -17 -11 -17 -11 0 -6 11 3 24 3 30 -1
|
||||||
|
7 -5 28 -14 47 -20 34 -12 33 -12 -23 -8 -32 2 -65 9 -72 15 -8 7 -19 9 -25 5
|
||||||
|
-20 -12 -9 -23 33 -30 38 -8 39 -9 13 -13 -38 -5 -78 6 -70 19 3 6 -9 3 -27
|
||||||
|
-7 -36 -18 -155 -132 -147 -140 3 -2 81 10 174 27 155 29 180 31 303 26 74 -4
|
||||||
|
138 -10 142 -14 5 -5 -70 -29 -167 -53 l-175 -45 53 -9 53 -9 -55 -13 c-30 -7
|
||||||
|
-66 -13 -80 -14 -42 -1 -1 -20 43 -20 73 0 27 -18 -65 -25 -137 -11 -146 -20
|
||||||
|
-37 -34 97 -13 125 -19 118 -25 -2 -2 -45 -32 -95 -67 l-90 -64 113 -1 c71 -1
|
||||||
|
118 -5 126 -13 8 -8 7 -11 -6 -11 -25 0 -206 -98 -267 -145 -50 -39 -89 -75
|
||||||
|
-79 -75 21 1 179 39 165 39 -31 1 -13 10 38 20 26 5 49 5 52 0 3 -5 13 -5 24
|
||||||
|
1 24 13 33 13 25 0 -13 -21 49 -8 128 27 76 34 130 66 82 49 -40 -15 -142 -34
|
||||||
|
-205 -39 l-65 -6 144 71 c188 93 305 117 297 62 -1 -9 -9 -20 -17 -25 -10 -6
|
||||||
|
-6 -9 15 -9 23 0 27 -3 23 -20 -4 -15 0 -20 14 -20 24 0 24 -9 2 -25 -15 -11
|
||||||
|
-10 -12 30 -8 38 5 53 1 81 -18 26 -18 32 -27 24 -36 -8 -10 -4 -13 20 -13 40
|
||||||
|
0 61 -25 33 -40 -12 -6 -21 -16 -21 -22 0 -9 3 -9 12 0 15 15 70 16 85 1 15
|
||||||
|
-15 -12 -59 -36 -59 -9 0 -23 -5 -31 -11 -11 -7 -6 -9 20 -4 19 4 45 18 58 32
|
||||||
|
15 16 37 27 59 30 20 2 43 7 52 12 20 10 43 3 35 -11 -3 -5 13 4 37 22 32 23
|
||||||
|
51 30 70 26 18 -3 32 1 44 14 9 10 30 23 46 29 l29 10 -16 -25 c-9 -13 -15
|
||||||
|
-27 -12 -29 2 -3 17 16 33 40 28 46 42 58 31 29 -5 -14 -4 -15 9 -4 18 15 21
|
||||||
|
-1 5 -31 -8 -13 -7 -19 0 -19 6 0 7 -6 1 -17 -4 -10 -25 -67 -45 -128 l-36
|
||||||
|
-110 5 70 5 69 -39 -66 c-68 -117 -115 -168 -105 -115 3 16 0 14 -10 -8 -8
|
||||||
|
-16 -45 -60 -83 -96 -37 -37 -78 -80 -91 -95 l-23 -29 22 34 c13 20 18 36 12
|
||||||
|
40 -6 3 -8 29 -6 56 2 28 4 57 5 65 0 8 11 26 23 40 12 14 30 48 39 77 10 28
|
||||||
|
31 71 48 95 l31 43 -47 -45 c-25 -24 -46 -40 -46 -36 0 4 -27 -17 -61 -48 -57
|
||||||
|
-53 -172 -136 -187 -136 -4 0 -23 -13 -42 -28 -45 -37 -80 -56 -80 -43 0 5 5
|
||||||
|
13 11 17 6 4 25 36 41 73 17 36 37 74 46 84 8 9 12 17 8 17 -4 0 2 17 13 38
|
||||||
|
23 43 52 112 46 112 -2 0 -34 -39 -72 -86 -43 -54 -95 -106 -142 -140 -73 -54
|
||||||
|
-91 -62 -91 -45 0 6 -23 0 -51 -11 -34 -15 -46 -24 -37 -29 10 -7 9 -9 -4 -9
|
||||||
|
-10 0 -32 -14 -50 -31 -20 -18 -25 -26 -13 -21 11 5 40 13 65 16 l45 7 -35
|
||||||
|
-30 c-40 -35 -31 -40 18 -11 l34 21 -7 -22 c-6 -17 6 -11 51 28 33 28 69 56
|
||||||
|
82 62 43 22 14 -40 -42 -91 -33 -31 -45 -47 -34 -48 9 0 29 10 45 23 51 41 82
|
||||||
|
57 113 57 34 0 35 -2 18 -33 -6 -12 21 8 61 46 70 65 91 79 91 56 0 -6 16 1
|
||||||
|
35 15 37 28 37 28 49 -25 12 -55 -75 -174 -189 -258 -27 -21 -83 -57 -122 -80
|
||||||
|
-77 -45 -90 -57 -47 -41 14 6 27 10 29 10 2 0 -9 -30 -25 -67 -24 -54 -26 -62
|
||||||
|
-9 -40 19 25 58 40 44 17 -3 -6 -2 -10 3 -10 13 0 -33 -92 -57 -114 -12 -11
|
||||||
|
-21 -25 -21 -32 0 -7 -5 -16 -10 -19 -7 -4 -9 2 -4 19 12 50 -14 25 -43 -41
|
||||||
|
-31 -69 -76 -203 -70 -209 4 -4 110 122 150 177 16 21 33 39 39 39 6 0 3 -10
|
||||||
|
-8 -21 -11 -13 3 -6 32 14 l51 37 6 -98 c8 -119 35 -220 89 -330 45 -89 68
|
||||||
|
-118 68 -86 1 24 26 229 39 314 14 86 14 110 1 90 -7 -10 -10 21 -9 97 1 103
|
||||||
|
2 113 24 133 l24 23 -5 -54 c-3 -30 -7 -74 -9 -99 -4 -41 -3 -42 4 -10 13 51
|
||||||
|
61 183 72 194 15 16 10 -78 -6 -117 -24 -58 -17 -56 18 5 18 31 41 66 50 76 9
|
||||||
|
10 17 23 17 28 0 5 24 35 53 68 l54 59 7 -34 c8 -42 0 -133 -21 -221 -17 -78
|
||||||
|
-12 -109 10 -58 l15 35 10 -25 c29 -73 108 -228 166 -323 36 -60 66 -111 66
|
||||||
|
-113 0 -3 -6 -2 -12 2 -8 4 -10 3 -5 -2 5 -5 14 -9 20 -9 7 0 26 -25 42 -56
|
||||||
|
41 -76 129 -218 135 -218 6 0 94 142 135 218 17 31 35 56 42 56 6 0 15 4 20 9
|
||||||
|
5 5 3 6 -4 2 -7 -4 -13 -5 -13 -2 0 2 30 53 66 113 58 95 137 250 166 323 l10
|
||||||
|
25 15 -35 c22 -51 27 -20 10 58 -21 88 -29 179 -21 221 l7 34 54 -59 c29 -33
|
||||||
|
53 -63 53 -68 0 -5 8 -18 17 -28 9 -10 32 -45 50 -76 35 -61 42 -63 18 -5 -16
|
||||||
|
39 -21 133 -6 117 11 -11 59 -143 72 -194 7 -32 8 -31 4 10 -2 25 -6 69 -9 99
|
||||||
|
l-5 54 24 -23 c22 -20 23 -30 24 -133 1 -76 -2 -107 -9 -97 -13 20 -13 -4 1
|
||||||
|
-90 13 -85 38 -290 39 -314 0 -32 23 -3 68 86 54 110 81 211 89 330 l6 98 51
|
||||||
|
-37 c29 -20 43 -27 32 -14 -11 11 -14 21 -8 21 6 0 23 -18 39 -39 40 -55 146
|
||||||
|
-181 150 -177 6 6 -39 140 -70 209 -29 66 -55 91 -43 41 5 -17 3 -23 -4 -19
|
||||||
|
-5 3 -10 12 -10 19 0 7 -9 21 -21 32 -24 22 -70 114 -57 114 5 0 6 5 3 10 -14
|
||||||
|
23 25 8 44 -18 17 -21 15 -13 -9 41 -16 37 -27 67 -25 67 2 0 15 -4 29 -10 43
|
||||||
|
-16 30 -4 -46 41 -40 23 -95 59 -123 80 -114 84 -201 203 -189 258 12 53 12
|
||||||
|
53 49 25 19 -14 35 -21 35 -15 0 23 21 9 91 -56 40 -38 67 -58 61 -46 -17 31
|
||||||
|
-16 33 18 33 31 0 62 -16 113 -57 16 -13 36 -23 45 -23 11 1 -1 17 -34 48 -56
|
||||||
|
51 -85 113 -42 91 13 -6 49 -34 82 -62 45 -39 57 -45 51 -28 l-7 22 34 -21
|
||||||
|
c49 -29 58 -24 18 11 l-35 30 45 -7 c25 -3 54 -11 65 -16 12 -5 7 3 -13 21
|
||||||
|
-18 17 -40 31 -50 31 -13 0 -14 2 -4 9 9 5 -3 14 -37 29 -28 11 -51 17 -51 11
|
||||||
|
0 -17 -18 -9 -91 45 -47 34 -99 86 -142 140 -38 47 -70 86 -72 86 -6 0 23 -69
|
||||||
|
46 -113 11 -20 17 -37 13 -37 -4 0 0 -8 8 -17 9 -10 29 -48 46 -84 16 -37 35
|
||||||
|
-69 41 -73 6 -4 11 -12 11 -17 0 -13 -35 6 -80 43 -19 15 -38 28 -42 28 -15 0
|
||||||
|
-130 83 -187 136 -34 31 -61 52 -61 48 0 -4 -21 12 -46 36 l-47 45 31 -43 c17
|
||||||
|
-24 38 -67 48 -95 9 -29 27 -63 39 -77 12 -14 23 -32 23 -40 1 -8 3 -37 5 -65
|
||||||
|
2 -27 0 -53 -6 -56 -6 -4 -1 -20 12 -40 l22 -34 -23 29 c-13 15 -54 58 -91 95
|
||||||
|
-38 36 -75 80 -83 96 -10 22 -13 24 -10 8 10 -53 -37 -2 -105 115 l-39 66 5
|
||||||
|
-69 5 -70 -36 110 c-20 61 -41 118 -45 128 -6 11 -5 17 1 17 7 0 8 6 0 19 -16
|
||||||
|
30 -13 46 5 31 13 -11 14 -10 9 4 -11 29 3 17 31 -29 16 -24 31 -43 33 -40 3
|
||||||
|
2 -3 16 -12 29 l-16 25 29 -10 c16 -6 37 -19 46 -29 12 -13 26 -17 44 -14 19
|
||||||
|
4 38 -3 70 -26 24 -18 40 -27 37 -22 -8 14 15 21 35 11 9 -5 32 -10 52 -12 22
|
||||||
|
-3 44 -14 59 -30 13 -14 39 -28 58 -32 26 -5 31 -3 20 4 -8 6 -22 11 -31 11
|
||||||
|
-24 0 -51 44 -36 59 15 15 70 14 85 -1 9 -9 12 -9 12 0 0 6 -9 16 -21 22 -28
|
||||||
|
15 -7 40 33 40 24 0 28 3 20 13 -8 9 -2 18 24 36 28 19 43 23 81 18 40 -4 45
|
||||||
|
-3 30 8 -22 16 -22 25 2 25 14 0 18 5 14 20 -4 17 0 20 23 20 21 0 25 3 15 9
|
||||||
|
-8 5 -16 16 -17 25 -8 55 109 31 297 -62 l144 -71 -65 6 c-63 5 -165 24 -205
|
||||||
|
39 -48 17 6 -15 82 -49 79 -35 141 -48 128 -27 -8 13 1 13 25 0 11 -6 21 -6
|
||||||
|
24 -1 3 5 26 5 52 0 51 -10 69 -19 38 -20 -14 0 144 -38 165 -39 10 0 -29 36
|
||||||
|
-79 75 -61 47 -242 145 -267 145 -13 0 -14 3 -6 11 8 8 55 12 126 13 l113 1
|
||||||
|
-90 64 c-50 35 -93 65 -95 67 -7 6 21 12 118 25 109 14 100 23 -37 34 -92 7
|
||||||
|
-138 25 -65 25 44 0 85 19 43 20 -14 1 -50 7 -80 14 l-55 13 53 9 53 9 -175
|
||||||
|
45 c-97 24 -172 48 -167 53 4 4 68 10 142 14 123 5 148 3 303 -26 93 -17 171
|
||||||
|
-29 174 -27 8 8 -111 122 -147 140 -18 10 -30 13 -27 7 8 -13 -32 -24 -70 -19
|
||||||
|
-26 4 -25 5 14 13 41 7 52 18 32 30 -6 4 -17 2 -25 -5 -7 -6 -40 -13 -72 -15
|
||||||
|
-56 -4 -57 -4 -23 8 19 6 40 15 47 20 6 4 19 4 30 1 17 -5 17 -5 0 6 -12 7
|
||||||
|
-22 8 -30 2 -7 -5 -35 -15 -64 -22 -49 -13 -63 -30 -24 -30 10 0 36 -12 57
|
||||||
|
-26 32 -22 36 -23 20 -5 -11 11 -17 21 -14 21 15 0 97 -42 108 -55 7 -8 17
|
||||||
|
-15 22 -15 6 0 4 8 -3 17 -12 14 -9 14 19 -3 41 -24 64 -48 36 -38 -10 4 -23
|
||||||
|
9 -28 12 -5 3 -28 6 -51 8 -23 1 -62 8 -86 14 -43 12 -117 24 -209 36 -25 3
|
||||||
|
-61 9 -80 14 -19 5 -61 10 -92 11 -32 1 -58 6 -58 11 0 4 8 8 18 8 10 0 26 6
|
||||||
|
37 13 44 30 165 59 251 62 l88 2 59 63 c32 34 56 64 54 67 -3 2 -27 1 -54 -3
|
||||||
|
-39 -6 -43 -9 -23 -14 22 -5 19 -8 -25 -25 -28 -10 -54 -19 -60 -20 -5 0 -24
|
||||||
|
-8 -40 -17 -40 -22 -156 -60 -162 -54 -10 10 9 25 80 60 40 20 89 36 108 37
|
||||||
|
39 1 89 17 55 18 -17 1 -17 2 -2 26 19 29 20 39 4 29 -8 -4 -10 -3 -5 2 5 5
|
||||||
|
13 9 18 9 8 0 80 107 75 113 -2 1 -54 -18 -115 -43 -62 -25 -140 -52 -174 -61
|
||||||
|
-79 -20 -87 -28 -19 -19 35 5 52 3 52 -4 0 -19 -143 -130 -220 -170 -91 -48
|
||||||
|
-201 -78 -232 -64 -21 9 -20 10 10 32 18 12 32 28 32 35 0 7 13 25 30 41 16
|
||||||
|
16 28 30 27 32 -2 2 -48 -27 -102 -63 -95 -63 -243 -144 -265 -144 -21 0 3 23
|
||||||
|
103 96 144 106 118 98 -43 -14 -39 -27 63 101 113 141 52 43 91 63 192 99 56
|
||||||
|
20 69 27 43 24 -21 -3 -38 -3 -38 -1 0 2 90 77 200 165 110 89 200 163 200
|
||||||
|
166 0 6 -104 -47 -110 -56 -3 -4 -18 -10 -34 -14 -24 -6 -27 -9 -15 -18 11 -8
|
||||||
|
11 -9 -4 -4 -11 3 -57 -14 -117 -44 -62 -31 -100 -45 -102 -38 -2 6 -17 -8
|
||||||
|
-34 -32 -29 -43 -200 -188 -210 -178 -8 9 86 117 205 235 82 82 111 118 107
|
||||||
|
130 -4 10 4 27 21 46 15 17 0 7 -34 -22 -35 -30 -76 -77 -92 -105 -32 -57 -53
|
||||||
|
-79 -81 -83 -17 -2 -18 1 -8 28 10 30 -4 113 -22 124 -5 3 -22 -11 -39 -30
|
||||||
|
-39 -45 -39 -39 -1 22 29 46 39 83 22 83 -15 0 -37 -31 -97 -140 -106 -192
|
||||||
|
-174 -287 -259 -365 -44 -41 -82 -72 -85 -70 -2 3 8 23 23 46 15 23 23 38 17
|
||||||
|
35 -5 -4 -31 -40 -56 -81 -26 -41 -51 -75 -56 -75 -5 0 -23 -24 -39 -54 -17
|
||||||
|
-30 -37 -56 -44 -59 -7 -3 -22 -28 -32 -56 -10 -28 -21 -51 -24 -51 -4 0 -5
|
||||||
|
33 -3 73 5 105 68 261 161 400 28 44 52 89 53 101 0 11 5 30 10 41 7 15 8 12
|
||||||
|
3 -12 -3 -18 -4 -33 -1 -33 6 0 92 131 116 178 54 106 84 232 79 332 l-3 65
|
||||||
|
-24 -65 c-13 -36 -42 -100 -63 -142 -21 -43 -38 -83 -38 -90 0 -7 -10 -26 -21
|
||||||
|
-43 -12 -16 -18 -22 -15 -11 7 20 -32 116 -47 116 -5 0 -6 -8 -3 -17 6 -14 3
|
||||||
|
-14 -15 3 -13 11 -19 13 -13 4 4 -8 7 -23 5 -34 -1 -11 2 -16 8 -12 6 4 11 2
|
||||||
|
11 -3 0 -6 -4 -11 -10 -11 -5 0 -10 -9 -10 -20 0 -11 -6 -20 -12 -20 -10 0
|
||||||
|
-10 -2 0 -9 10 -6 10 -11 -3 -21 -13 -11 -18 -11 -24 -1 -5 7 -10 12 -12 10
|
||||||
|
-2 -1 -15 2 -28 6 l-24 7 24 13 c13 7 27 11 30 9 4 -2 5 4 2 15 -3 13 -11 17
|
||||||
|
-24 14 -10 -3 -27 1 -37 8 -15 12 -16 12 -5 -2 10 -12 10 -18 0 -28 -10 -11
|
||||||
|
-8 -11 11 -1 13 6 26 9 29 7 2 -3 -5 -10 -16 -17 -21 -11 -21 -11 9 -41 30
|
||||||
|
-30 40 -69 19 -69 -6 0 -9 9 -6 20 3 11 1 18 -4 14 -5 -3 -9 -12 -9 -19 0 -9
|
||||||
|
-6 -12 -17 -8 -15 5 -15 3 2 -11 18 -16 18 -17 2 -12 -10 3 -21 6 -23 6 -2 0
|
||||||
|
-4 9 -4 20 0 11 -4 20 -10 20 -5 0 -10 -6 -10 -13 0 -15 45 -57 61 -57 6 0 7
|
||||||
|
-5 2 -12 -8 -13 -10 -19 -16 -50 -1 -9 -14 -26 -27 -37 l-25 -20 23 -1 c16 0
|
||||||
|
21 -4 15 -12 -4 -7 -6 -21 -5 -30 2 -13 -8 -20 -42 -29 l-46 -11 0 -184 c0
|
||||||
|
-116 -4 -184 -10 -184 -6 0 -10 68 -10 183 l0 182 -31 17 c-17 10 -28 22 -24
|
||||||
|
27 3 5 1 12 -5 16 -5 3 -10 11 -10 16 0 6 5 7 10 4 14 -9 58 19 93 59 20 22
|
||||||
|
30 27 34 18 4 -10 11 -5 24 15 10 15 19 33 19 38 -1 6 -5 2 -11 -9 -9 -15 -13
|
||||||
|
-16 -20 -5 -7 12 -9 11 -9 -1 0 -8 -18 -26 -40 -39 -21 -14 -37 -27 -35 -31 2
|
||||||
|
-3 -4 -6 -13 -6 -9 -1 -24 -3 -32 -5 -8 -2 -15 3 -15 11 -2 26 5 30 28 18 20
|
||||||
|
-10 21 -10 8 6 -13 15 -12 16 15 6 25 -9 26 -8 12 3 -15 12 -15 14 0 25 14 10
|
||||||
|
13 11 -5 2 -30 -14 -48 -13 -37 3 11 14 11 37 1 54 -4 7 1 14 14 18 14 5 18
|
||||||
|
10 11 17 -6 6 -16 7 -24 2 -20 -13 -40 -14 -57 -3 -12 8 -10 8 8 3 22 -6 23
|
||||||
|
-4 19 24 -4 26 -2 29 11 22 13 -6 16 -3 16 16 0 13 -3 24 -7 24 -5 0 -8 10 -7
|
||||||
|
23 0 21 1 21 9 2 6 -14 9 -16 10 -5 2 8 2 18 1 23 -2 13 17 8 34 -9 9 -9 14
|
||||||
|
-22 12 -29 -3 -6 1 -12 9 -12 7 0 11 4 8 8 -5 9 26 49 39 49 4 0 5 -4 2 -10
|
||||||
|
-3 -5 -2 -10 3 -10 6 0 12 7 16 15 3 8 13 15 21 15 9 0 13 5 10 10 -3 6 -15
|
||||||
|
10 -26 10 -12 0 -17 -4 -13 -11 5 -8 1 -8 -14 0 -25 13 -43 15 -23 2 12 -8 12
|
||||||
|
-12 -2 -26 -15 -15 -19 -15 -39 -1 -12 9 -17 16 -10 16 6 0 12 5 12 11 0 8 -4
|
||||||
|
8 -13 1 -8 -7 -29 -9 -50 -5 -25 4 -37 2 -37 -6 0 -8 8 -10 22 -5 20 6 21 5 9
|
||||||
|
-10 -20 -24 -40 -20 -36 9 1 14 -2 28 -7 31 -5 4 8 17 29 30 35 21 36 23 13
|
||||||
|
24 -15 0 -40 -14 -63 -35 -21 -19 -41 -35 -45 -35 -4 0 2 7 12 15 11 8 16 15
|
||||||
|
11 15 -4 0 0 10 11 21 19 22 19 22 -1 10 -11 -7 -31 -28 -45 -46 -19 -26 -27
|
||||||
|
-30 -33 -19 -4 8 -5 19 -2 24 3 6 4 10 1 10 -13 0 -56 -105 -50 -124 4 -15 -1
|
||||||
|
-12 -15 9 -12 17 -21 36 -21 43 0 7 -17 47 -38 90 -21 42 -49 105 -62 140 -12
|
||||||
|
34 -24 62 -26 62 -2 0 -4 -30 -4 -68z m320 -182 c0 -5 -2 -10 -4 -10 -3 0 -8
|
||||||
|
5 -11 10 -3 6 -1 10 4 10 6 0 11 -4 11 -10z m-750 -112 c0 -6 -7 -5 -15 2 -8
|
||||||
|
7 -15 17 -15 22 0 6 7 5 15 -2 8 -7 15 -17 15 -22z m1780 7 c-7 -9 -15 -13
|
||||||
|
-18 -10 -3 2 1 11 8 20 7 9 15 13 18 10 3 -2 -1 -11 -8 -20z m-1906 -90 c11
|
||||||
|
-8 16 -15 10 -15 -5 0 -18 7 -28 15 -11 8 -16 15 -10 15 5 0 18 -7 28 -15z
|
||||||
|
m922 0 c-11 -8 -25 -15 -30 -15 -6 0 -2 7 8 15 11 8 25 15 30 15 6 0 2 -7 -8
|
||||||
|
-15z m1108 0 c-10 -8 -23 -15 -28 -15 -6 0 -1 7 10 15 10 8 23 15 28 15 6 0 1
|
||||||
|
-7 -10 -15z m-1219 -136 c47 -92 56 -131 16 -65 -21 34 -54 116 -46 116 3 0
|
||||||
|
16 -23 30 -51z m410 10 c-15 -40 -53 -109 -61 -109 -5 0 12 42 41 99 31 62 39
|
||||||
|
66 20 10z m-736 -44 c0 -5 -13 6 -29 25 -17 19 -30 39 -30 45 0 5 14 -6 30
|
||||||
|
-25 16 -19 29 -39 29 -45z m1076 30 c-15 -20 -29 -34 -32 -31 -3 2 7 21 22 41
|
||||||
|
15 19 30 33 33 31 3 -3 -8 -22 -23 -41z m-1074 -105 c57 -55 108 -100 113
|
||||||
|
-100 11 0 -10 26 -78 98 -28 28 -46 52 -41 52 12 0 121 -103 158 -148 15 -19
|
||||||
|
27 -28 27 -22 0 18 56 -20 120 -82 68 -67 139 -197 142 -263 1 -22 6 -46 11
|
||||||
|
-53 5 -7 8 -15 5 -17 -2 -3 -17 21 -34 51 -16 31 -34 56 -39 56 -18 -1 -36 21
|
||||||
|
-62 71 -37 73 -117 191 -121 178 -2 -6 22 -51 53 -102 65 -104 69 -142 7 -53
|
||||||
|
-43 59 -199 200 -211 189 -3 -4 19 -29 49 -57 139 -128 161 -150 176 -181 15
|
||||||
|
-28 9 -25 -44 26 -33 31 -64 57 -69 57 -5 0 -31 21 -59 48 -28 26 -74 67 -103
|
||||||
|
90 -36 30 -50 49 -46 61 3 12 -10 25 -50 49 -31 18 -53 37 -50 42 3 5 29 0 58
|
||||||
|
-10 28 -9 65 -21 81 -24 l28 -7 -25 23 c-49 46 -120 128 -109 128 5 0 57 -45
|
||||||
|
113 -100z m1114 48 c-26 -29 -59 -63 -72 -76 l-25 -23 28 7 c16 3 53 15 81 24
|
||||||
|
29 10 55 15 58 10 3 -5 -19 -24 -50 -42 -40 -24 -53 -37 -50 -49 4 -12 -10
|
||||||
|
-31 -46 -61 -29 -23 -75 -64 -103 -90 -28 -27 -54 -48 -59 -48 -5 0 -36 -26
|
||||||
|
-69 -57 -53 -51 -59 -54 -44 -26 15 31 37 53 176 181 30 28 52 53 49 57 -12
|
||||||
|
11 -168 -130 -211 -189 -62 -89 -58 -51 7 53 31 51 55 96 53 102 -4 13 -84
|
||||||
|
-105 -122 -179 -25 -49 -41 -67 -60 -67 -5 0 -23 -26 -40 -57 -16 -32 -32 -56
|
||||||
|
-34 -53 -3 2 0 10 5 17 5 7 10 31 11 53 3 66 74 196 142 263 64 62 120 100
|
||||||
|
120 82 0 -6 12 3 27 22 37 45 146 148 158 148 5 0 -13 -24 -41 -52 -68 -72
|
||||||
|
-89 -98 -78 -98 5 0 56 45 113 100 110 106 155 135 76 48z m-1807 -81 c34 -33
|
||||||
|
46 -63 13 -33 -11 10 -24 15 -29 12 -13 -8 -112 72 -112 90 0 19 82 -25 128
|
||||||
|
-69z m2552 69 c0 -18 -99 -98 -112 -90 -5 3 -18 -2 -29 -12 -33 -30 -21 0 14
|
||||||
|
33 25 24 109 80 125 83 1 0 2 -6 2 -14z m-1386 -32 c17 -6 22 -24 8 -24 -5 0
|
||||||
|
-17 7 -28 15 -19 15 -8 20 20 9z m112 -9 c-23 -17 -36 -19 -36 -6 0 11 18 19
|
||||||
|
40 20 11 0 10 -3 -4 -14z m-1408 -99 c60 -28 86 -55 40 -41 -31 10 -105 53
|
||||||
|
-114 66 -4 7 -3 10 2 8 5 -2 37 -17 72 -33z m2752 19 c-16 -20 -119 -69 -127
|
||||||
|
-61 -9 8 -6 10 62 44 69 34 82 38 65 17z m-2895 -195 c4 -7 -5 -9 -26 -5 -44
|
||||||
|
8 -50 15 -11 15 17 0 34 -5 37 -10z m3034 6 c-10 -11 -70 -16 -64 -6 3 5 20
|
||||||
|
10 37 10 17 0 29 -2 27 -4z m-2205 -15 c3 -5 18 -12 33 -16 36 -8 93 -52 93
|
||||||
|
-70 0 -7 12 -27 26 -44 14 -17 23 -31 20 -31 -19 0 -105 52 -127 77 -39 43
|
||||||
|
-72 93 -60 93 5 0 12 -4 15 -9z m124 -20 c32 -16 75 -48 95 -70 20 -23 37 -38
|
||||||
|
37 -34 0 3 -7 15 -17 25 -9 10 -14 21 -10 24 13 13 131 -71 122 -87 -4 -5 1
|
||||||
|
-6 9 -3 24 9 66 -74 66 -132 l-1 -49 -14 32 c-8 18 -25 38 -39 44 -14 7 -44
|
||||||
|
38 -68 71 -24 32 -44 54 -46 48 -2 -6 -22 9 -44 32 -40 42 -40 42 4 -12 24
|
||||||
|
-30 48 -61 53 -69 14 -24 -82 54 -111 90 -15 19 -46 53 -68 77 -22 23 -37 42
|
||||||
|
-33 42 4 0 34 -13 65 -29z m1096 -13 c-22 -24 -53 -58 -68 -77 -29 -36 -125
|
||||||
|
-114 -111 -90 5 8 29 39 53 69 44 54 44 54 4 12 -22 -23 -42 -38 -44 -32 -2 6
|
||||||
|
-22 -16 -46 -48 -24 -33 -54 -64 -68 -71 -14 -6 -31 -26 -39 -44 l-14 -32 -1
|
||||||
|
49 c0 58 42 141 66 132 8 -3 13 -2 9 3 -9 16 109 100 122 87 4 -3 -1 -14 -10
|
||||||
|
-24 -10 -10 -17 -22 -17 -25 0 -4 17 11 37 34 32 36 131 97 158 99 5 0 -9 -19
|
||||||
|
-31 -42z m95 10 c-39 -58 -73 -89 -121 -114 -61 -31 -65 -30 -34 7 14 17 26
|
||||||
|
37 26 44 0 18 57 62 93 70 15 4 30 11 33 16 3 5 10 9 15 9 6 0 0 -15 -12 -32z
|
||||||
|
m-1702 6 c-3 -3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m2110 0 c-3
|
||||||
|
-3 -12 -4 -19 -1 -8 3 -5 6 6 6 11 1 17 -2 13 -5z m-1255 -131 c30 -36 30 -47
|
||||||
|
0 -19 -18 17 -32 31 -32 33 0 9 19 1 32 -14z m408 14 c0 -2 -14 -16 -32 -33
|
||||||
|
-30 -28 -30 -17 0 19 13 15 32 23 32 14z m-1263 -29 c-2 -7 3 -18 12 -25 14
|
||||||
|
-11 14 -13 -4 -27 -11 -8 -26 -16 -32 -16 -27 0 -11 -18 22 -24 47 -9 52 -22
|
||||||
|
14 -41 l-31 -17 32 -19 c20 -13 30 -25 26 -34 -3 -8 0 -15 7 -15 19 -1 -35
|
||||||
|
-38 -62 -43 -52 -10 -210 51 -234 90 -4 7 -17 13 -27 13 -26 0 -26 17 0 25 11
|
||||||
|
3 25 15 30 25 6 10 28 29 50 41 22 13 40 27 40 31 0 5 19 17 43 28 49 22 121
|
||||||
|
27 114 8z m924 -10 c11 -21 11 -22 -4 -9 -10 7 -17 17 -17 22 0 15 9 10 21
|
||||||
|
-13z m245 -6 c-18 -16 -18 -16 -6 6 6 13 14 21 18 18 3 -4 -2 -14 -12 -24z
|
||||||
|
m1028 8 c25 -10 46 -23 46 -28 0 -4 18 -18 40 -31 22 -12 44 -31 50 -41 5 -10
|
||||||
|
19 -22 30 -25 26 -8 26 -25 0 -25 -10 0 -23 -6 -27 -13 -24 -39 -182 -100
|
||||||
|
-234 -90 -27 5 -81 42 -62 43 7 0 10 7 7 15 -4 9 6 21 26 34 l32 19 -31 17
|
||||||
|
c-38 19 -33 32 14 41 33 6 49 24 23 24 -7 0 -22 8 -33 16 -18 14 -18 16 -4 27
|
||||||
|
9 7 14 18 12 25 -7 19 59 15 111 -8z m-1681 -67 c3 -5 -6 -16 -18 -26 -27 -19
|
||||||
|
-45 -6 -25 18 13 16 36 20 43 8z m1097 -8 c20 -24 2 -37 -25 -18 -12 10 -21
|
||||||
|
21 -18 26 7 12 30 8 43 -8z m-842 -60 c-3 -3 -9 2 -12 12 -6 14 -5 15 5 6 7
|
||||||
|
-7 10 -15 7 -18z m555 10 c-3 -9 -8 -14 -10 -11 -3 3 -2 9 2 15 9 16 15 13 8
|
||||||
|
-4z m-713 -9 c0 -3 -4 -8 -10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10
|
||||||
|
-2 10 -4z m880 -6 c0 -5 -2 -10 -4 -10 -3 0 -8 5 -11 10 -3 6 -1 10 4 10 6 0
|
||||||
|
11 -4 11 -10z m-470 -29 c0 -4 9 -7 20 -7 11 0 20 3 20 7 0 4 10 9 21 12 17 4
|
||||||
|
19 3 10 -6 -20 -20 -11 -30 11 -11 18 16 21 17 15 2 -3 -9 -3 -19 1 -21 4 -2
|
||||||
|
1 -2 -5 -1 -18 5 -16 -7 4 -21 14 -10 15 -16 5 -28 -18 -21 -55 -30 -48 -11 5
|
||||||
|
14 1 16 -34 16 -35 0 -39 -2 -34 -16 7 -19 -30 -10 -48 11 -10 12 -9 18 5 28
|
||||||
|
20 14 22 26 5 21 -7 -1 -10 -1 -6 1 4 2 4 12 1 21 -6 15 -3 14 15 -2 22 -19
|
||||||
|
31 -9 11 11 -9 9 -7 10 10 6 11 -3 21 -8 21 -12z m-112 -47 c12 -8 12 -19 5
|
||||||
|
-57 -9 -42 -8 -49 15 -82 16 -23 28 -31 33 -24 4 6 12 8 18 5 25 -16 9 12 -24
|
||||||
|
41 -19 17 -35 37 -35 45 1 7 14 -2 30 -21 16 -19 31 -33 33 -30 2 2 11 16 20
|
||||||
|
32 10 15 26 27 37 27 11 0 27 -12 37 -27 9 -16 18 -30 20 -32 2 -3 17 11 33
|
||||||
|
30 16 19 29 28 30 21 0 -8 -16 -28 -35 -45 -33 -29 -49 -57 -24 -41 6 3 14 1
|
||||||
|
18 -5 5 -7 17 1 33 24 23 33 24 40 15 82 -7 38 -7 49 5 57 8 6 17 18 20 26 6
|
||||||
|
17 38 -51 38 -81 0 -11 4 -19 9 -19 5 0 15 -38 21 -83 11 -72 11 -90 -3 -128
|
||||||
|
l-15 -44 -2 55 c-1 52 -1 54 -17 35 -15 -19 -15 -19 -10 10 2 17 2 38 -2 47
|
||||||
|
-4 12 -14 4 -38 -37 -31 -53 -31 -54 -7 -45 30 12 31 -2 5 -52 -11 -19 -18
|
||||||
|
-38 -16 -40 2 -2 -14 -24 -36 -48 -44 -48 -59 -72 -59 -92 1 -7 7 2 15 20 8
|
||||||
|
17 19 32 25 32 7 0 7 -7 -2 -22 -7 -13 -19 -52 -28 -88 -10 -40 -22 -65 -30
|
||||||
|
-65 -8 0 -20 25 -30 65 -9 36 -21 75 -28 88 -9 15 -9 22 -2 22 6 0 17 -15 25
|
||||||
|
-32 8 -18 14 -27 15 -20 0 20 -15 44 -59 92 -22 24 -38 46 -36 48 2 2 -5 21
|
||||||
|
-16 40 -26 50 -25 64 5 52 24 -9 24 -8 -7 45 -24 41 -34 49 -38 37 -4 -9 -4
|
||||||
|
-30 -2 -47 5 -29 5 -29 -10 -10 -16 19 -16 17 -17 -35 l-2 -55 -15 44 c-14 38
|
||||||
|
-14 56 -3 128 6 45 16 83 21 83 5 0 9 8 9 19 0 30 32 98 38 81 3 -8 12 -20 20
|
||||||
|
-26z m-1196 16 c65 -19 58 -33 -13 -25 -35 3 -73 9 -84 12 -18 6 -18 7 5 14
|
||||||
|
36 10 51 10 92 -1z m2748 1 c23 -7 23 -8 5 -14 -44 -13 -148 -19 -141 -8 15
|
||||||
|
23 90 35 136 22z m-1960 -27 c0 -8 -4 -12 -10 -9 -5 3 -10 10 -10 16 0 5 5 9
|
||||||
|
10 9 6 0 10 -7 10 -16z m1100 7 c0 -6 -4 -13 -10 -16 -5 -3 -10 1 -10 9 0 9 5
|
||||||
|
16 10 16 6 0 10 -4 10 -9z m-619 -58 c12 -15 12 -16 -6 -9 -16 6 -18 5 -7 -5
|
||||||
|
17 -18 15 -34 -2 -20 -19 16 -27 51 -11 51 7 0 19 -8 26 -17z m152 -3 c-3 -10
|
||||||
|
-11 -25 -19 -31 -17 -14 -19 2 -1 20 10 10 8 11 -8 5 -18 -7 -18 -6 -6 9 19
|
||||||
|
24 40 22 34 -3z m-1363 -40 c-36 -17 -72 -34 -80 -40 -29 -20 -121 -60 -138
|
||||||
|
-60 -15 1 -14 2 3 15 11 8 25 18 30 23 18 15 142 80 161 85 10 2 35 5 54 5 33
|
||||||
|
1 31 -1 -30 -28z m2646 -16 c38 -21 74 -42 79 -46 6 -5 19 -15 30 -23 17 -13
|
||||||
|
18 -14 3 -15 -17 0 -109 40 -138 60 -8 6 -44 24 -80 41 l-65 30 51 -5 c33 -3
|
||||||
|
75 -18 120 -42z m-1661 -54 c-10 -11 -20 -18 -22 -16 -6 5 30 44 36 39 2 -1
|
||||||
|
-4 -12 -14 -23z m595 -17 c0 -3 -9 2 -20 12 -11 10 -20 22 -20 27 0 4 9 -1 20
|
||||||
|
-12 11 -11 20 -23 20 -27z m-926 -14 c-6 -11 -24 -2 -24 11 0 5 7 7 15 4 8 -4
|
||||||
|
12 -10 9 -15z m1251 2 c-3 -6 -11 -8 -17 -5 -6 4 -5 9 3 15 16 10 23 4 14 -10z
|
||||||
|
m-1013 -18 c-6 -9 -18 -18 -28 -19 -15 -2 -14 1 6 16 31 24 33 24 22 3z m798
|
||||||
|
-18 c0 -11 -34 5 -42 19 -9 17 -9 17 16 1 14 -10 26 -19 26 -20z m-990 15 c0
|
||||||
|
-5 -9 -10 -21 -10 -11 0 -17 5 -14 10 3 6 13 10 21 10 8 0 14 -4 14 -10z
|
||||||
|
m1175 0 c3 -5 -3 -10 -14 -10 -12 0 -21 5 -21 10 0 6 6 10 14 10 8 0 18 -4 21
|
||||||
|
-10z m-1139 -15 c-26 -19 -42 -19 -26 0 7 8 20 15 29 15 13 -1 13 -3 -3 -15z
|
||||||
|
m1094 0 c16 -19 0 -19 -26 0 -16 12 -16 14 -3 15 9 0 22 -7 29 -15z m-1029 -9
|
||||||
|
c-7 -8 -21 -14 -31 -13 -26 2 -5 27 22 27 17 0 18 -3 9 -14z m977 1 c8 -10 6
|
||||||
|
-13 -8 -14 -10 -1 -24 5 -31 13 -9 11 -8 14 9 14 11 0 24 -6 30 -13z m-617
|
||||||
|
-169 c11 -22 18 -42 15 -45 -2 -3 -14 12 -25 34 -11 21 -20 31 -21 23 -1 -22
|
||||||
|
-30 19 -30 43 0 10 -4 15 -10 12 -11 -7 -14 8 -3 18 9 9 50 -37 74 -85z m-20
|
||||||
|
75 c13 -16 12 -17 -3 -4 -10 7 -18 15 -18 17 0 8 8 3 21 -13z m279 13 c0 -2
|
||||||
|
-8 -10 -17 -17 -16 -13 -17 -12 -4 4 13 16 21 21 21 13z m40 -17 c0 -6 -4 -7
|
||||||
|
-10 -4 -5 3 -10 -2 -10 -12 0 -24 -29 -65 -30 -43 -1 8 -10 -2 -21 -23 -11
|
||||||
|
-22 -23 -37 -25 -34 -8 7 31 79 58 110 22 24 38 27 38 6z m-374 -116 c-3 -10
|
||||||
|
-7 -36 -10 -58 l-6 -40 15 34 c9 18 18 31 20 29 9 -9 -43 -187 -72 -248 -39
|
||||||
|
-80 -41 -62 -13 105 11 66 20 135 20 154 0 25 4 32 15 27 8 -3 15 -1 15 4 0 6
|
||||||
|
5 10 11 10 6 0 8 -8 5 -17z m384 7 c0 -5 7 -7 15 -4 11 5 15 -2 15 -27 0 -19
|
||||||
|
9 -88 20 -154 28 -167 26 -185 -13 -105 -29 61 -81 239 -72 248 2 2 11 -11 20
|
||||||
|
-29 l15 -34 -6 40 c-3 22 -7 48 -10 58 -3 9 -1 17 5 17 6 0 11 -4 11 -10z
|
||||||
|
m-450 -52 c0 -73 -28 -180 -66 -255 -37 -74 -173 -238 -159 -192 7 24 -24 -15
|
||||||
|
-69 -84 -9 -16 -19 -27 -22 -25 -6 7 52 178 83 243 16 33 41 79 56 102 15 24
|
||||||
|
27 47 27 53 0 16 42 101 47 96 3 -3 0 -21 -7 -41 -7 -20 -13 -51 -14 -68 -2
|
||||||
|
-18 -6 -45 -10 -62 -10 -46 24 30 64 140 18 50 36 100 41 113 14 36 29 25 29
|
||||||
|
-20z m563 -73 c46 -128 82 -208 71 -160 -4 17 -8 44 -10 62 -1 17 -7 48 -14
|
||||||
|
68 -7 20 -10 38 -7 41 5 5 47 -80 47 -96 0 -6 12 -29 27 -53 15 -23 40 -69 56
|
||||||
|
-102 31 -65 89 -236 83 -243 -3 -2 -13 9 -22 25 -45 69 -76 108 -69 84 14 -46
|
||||||
|
-122 118 -159 192 -38 75 -66 182 -66 255 0 69 19 47 63 -73z m-362 -60 c16
|
||||||
|
-33 32 -75 35 -92 3 -18 10 -33 14 -33 4 0 11 15 14 33 6 34 55 141 77 169 9
|
||||||
|
11 19 15 27 10 10 -7 7 -26 -11 -90 -27 -92 -21 -100 14 -16 18 47 24 53 30
|
||||||
|
37 10 -28 -21 -134 -68 -228 -22 -44 -48 -104 -57 -132 -10 -29 -21 -53 -26
|
||||||
|
-53 -5 0 -16 24 -26 53 -9 28 -35 88 -57 132 -47 94 -78 200 -68 228 6 16 12
|
||||||
|
10 30 -37 35 -84 41 -76 14 16 -18 64 -21 83 -11 90 16 10 32 -10 69 -87z
|
||||||
|
m-412 -60 c-19 -36 -37 -65 -41 -65 -10 0 -10 1 3 41 13 38 58 103 66 95 3 -3
|
||||||
|
-10 -35 -28 -71z m944 12 c22 -47 29 -77 20 -77 -8 0 -73 119 -73 134 0 16 38
|
||||||
|
-25 53 -57z m-649 -106 c9 -52 15 -96 12 -98 -9 -9 -36 81 -42 144 -10 100 11
|
||||||
|
69 30 -46z m362 43 c-6 -60 -33 -150 -42 -141 -5 5 25 188 33 200 12 20 15 1
|
||||||
|
9 -59z m-755 -119 c-25 -40 -65 -96 -89 -125 -24 -29 -50 -63 -58 -74 -12 -17
|
||||||
|
-16 -18 -23 -7 -7 11 -9 11 -14 0 -8 -18 2 57 13 96 4 17 14 56 21 89 12 57
|
||||||
|
33 81 49 56 10 -16 8 -28 -9 -59 -8 -14 -9 -21 -2 -17 6 4 18 21 27 38 8 16
|
||||||
|
18 27 21 25 3 -3 -2 -20 -10 -36 -9 -17 -12 -31 -7 -31 4 0 13 14 20 31 7 17
|
||||||
|
31 59 53 92 l41 62 7 -33 c6 -29 1 -44 -40 -107z m1162 18 c17 -35 35 -63 39
|
||||||
|
-63 3 0 0 14 -9 31 -8 16 -13 33 -10 36 3 2 13 -9 21 -25 9 -17 21 -34 27 -38
|
||||||
|
7 -4 6 3 -2 17 -17 31 -19 43 -9 59 16 25 37 1 49 -56 7 -33 17 -72 21 -89 11
|
||||||
|
-39 21 -114 13 -96 -5 11 -7 11 -14 0 -7 -11 -11 -10 -23 7 -8 11 -34 45 -58
|
||||||
|
74 -24 29 -64 85 -89 125 -38 59 -45 78 -41 105 l5 34 23 -30 c13 -16 39 -57
|
||||||
|
57 -91z m-1324 -22 c-42 -94 -119 -232 -144 -257 -7 -8 -18 -14 -23 -14 -6 0
|
||||||
|
-16 -14 -24 -30 -7 -17 -21 -33 -30 -36 -9 -3 -18 -15 -20 -26 -4 -25 -23 -35
|
||||||
|
-31 -15 -3 8 6 27 19 42 27 32 32 60 9 51 -9 -3 -15 0 -15 10 0 10 6 14 15 10
|
||||||
|
11 -4 20 4 29 27 44 104 60 130 117 190 55 58 63 56 20 -6 -15 -23 -41 -67
|
||||||
|
-55 -97 -15 -30 -32 -63 -38 -73 -5 -11 -8 -21 -5 -23 7 -7 74 99 129 204 61
|
||||||
|
116 78 145 84 140 2 -3 -15 -46 -37 -97z m1479 -24 c61 -117 132 -230 139
|
||||||
|
-223 3 2 0 12 -5 23 -6 10 -23 43 -38 73 -14 30 -40 74 -55 97 -43 62 -35 64
|
||||||
|
20 6 57 -60 73 -86 117 -190 9 -23 18 -31 29 -27 9 4 15 0 15 -10 0 -10 -6
|
||||||
|
-13 -15 -10 -23 9 -18 -19 9 -51 13 -15 22 -34 19 -42 -8 -20 -27 -10 -31 15
|
||||||
|
-2 11 -11 23 -20 26 -9 3 -23 19 -30 36 -8 16 -18 30 -24 30 -31 0 -122 160
|
||||||
|
-206 359 -3 9 -1 12 6 8 6 -4 37 -58 70 -120z m-960 26 c23 -77 72 -276 68
|
||||||
|
-280 -6 -6 -55 86 -72 136 -19 57 -31 181 -17 181 5 0 15 -17 21 -37z m429
|
||||||
|
-30 c-3 -38 -13 -89 -21 -114 -17 -50 -66 -142 -72 -136 -4 4 45 203 68 280
|
||||||
|
18 60 32 45 25 -30z m-287 -63 c29 -66 56 -120 60 -120 4 0 31 54 60 120 29
|
||||||
|
66 56 120 60 120 9 0 -1 -47 -24 -109 -26 -71 -17 -81 14 -16 16 34 29 52 29
|
||||||
|
41 1 -20 1 -20 16 0 8 10 15 16 15 12 0 -15 -61 -171 -93 -235 -19 -37 -43
|
||||||
|
-105 -53 -150 -10 -46 -21 -83 -24 -83 -3 0 -14 37 -24 83 -10 45 -34 113 -53
|
||||||
|
150 -32 64 -93 220 -93 235 0 4 7 -2 15 -12 15 -20 15 -20 16 0 0 11 13 -7 29
|
||||||
|
-41 31 -65 40 -55 14 16 -23 62 -33 109 -24 109 4 0 31 -54 60 -120z m-190
|
||||||
|
-25 c16 -35 30 -69 30 -75 0 -6 6 -23 14 -38 7 -15 16 -45 18 -67 3 -22 10
|
||||||
|
-51 17 -64 23 -42 3 -34 -29 12 -41 57 -83 159 -69 167 9 6 4 39 -16 108 -14
|
||||||
|
44 5 21 35 -43z m532 28 c-18 -49 -24 -86 -13 -92 14 -9 -28 -110 -69 -168
|
||||||
|
-32 -46 -52 -54 -29 -12 7 13 14 42 17 64 2 22 11 52 18 67 8 15 14 32 14 38
|
||||||
|
0 18 61 142 67 136 3 -3 1 -18 -5 -33z m-572 -129 c17 -43 42 -99 55 -124 13
|
||||||
|
-25 29 -59 34 -75 l11 -30 -25 30 c-13 16 -32 45 -42 64 -10 20 -30 57 -45 83
|
||||||
|
-30 52 -61 156 -51 171 7 12 23 -18 63 -119z m634 53 c-8 -31 -27 -79 -42
|
||||||
|
-105 -15 -26 -35 -63 -45 -83 -10 -19 -29 -48 -42 -64 l-25 -30 11 30 c5 16
|
||||||
|
21 50 34 75 13 25 37 79 54 120 43 110 54 131 62 123 5 -5 2 -34 -7 -66z
|
||||||
|
m-1314 -2 c-13 -14 -28 -25 -33 -25 -6 0 0 11 13 25 13 14 28 25 33 25 6 0 0
|
||||||
|
-11 -13 -25z m1960 0 c13 -14 19 -25 13 -25 -5 0 -20 11 -33 25 -13 14 -19 25
|
||||||
|
-13 25 5 0 20 -11 33 -25z m-1051 -220 c0 -12 -12 5 -30 40 -16 33 -29 69 -28
|
||||||
|
80 0 12 12 -5 30 -40 16 -33 29 -69 28 -80z m154 43 c-16 -37 -29 -56 -31 -46
|
||||||
|
-2 10 9 46 25 80 16 37 29 56 31 46 2 -10 -9 -46 -25 -80z m-903 -125 c-1 -27
|
||||||
|
-9 -66 -20 -88 -11 -22 -19 -47 -19 -55 0 -8 10 5 21 29 21 42 22 43 30 20 5
|
||||||
|
-13 7 -42 3 -64 -3 -22 -8 -65 -11 -95 l-6 -54 -15 39 c-8 22 -18 56 -20 75
|
||||||
|
-5 29 -7 32 -13 15 -6 -16 -9 -12 -15 20 -3 22 -11 49 -16 61 -16 33 -7 89 14
|
||||||
|
88 9 -1 17 3 17 9 0 12 31 45 43 46 4 1 7 -20 7 -46z m753 -16 c20 -34 39 -75
|
||||||
|
42 -90 9 -35 21 -35 30 0 7 29 75 156 80 151 2 -2 -16 -59 -39 -128 -27 -79
|
||||||
|
-47 -125 -56 -125 -9 0 -28 41 -51 110 -20 61 -40 118 -44 128 -15 38 5 14 38
|
||||||
|
-46z m897 46 c11 -10 20 -23 20 -29 0 -7 8 -11 18 -10 20 1 29 -55 13 -88 -5
|
||||||
|
-12 -13 -39 -16 -61 -6 -32 -9 -36 -15 -20 -6 17 -8 14 -13 -15 -2 -19 -12
|
||||||
|
-53 -20 -75 l-15 -39 -6 54 c-3 30 -8 73 -11 95 -4 22 -2 51 3 64 8 23 9 22
|
||||||
|
30 -20 11 -24 21 -37 21 -29 0 8 -8 33 -19 55 -17 35 -28 135 -15 135 3 0 14
|
||||||
|
-8 25 -17z m-972 -38 c-3 -3 -13 7 -23 22 l-17 28 23 -22 c12 -12 20 -25 17
|
||||||
|
-28z m286 20 c-9 -14 -19 -23 -22 -20 -5 4 26 45 35 45 2 0 -4 -11 -13 -25z
|
||||||
|
m-295 -72 l33 -48 -36 39 c-31 34 -44 56 -33 56 2 0 18 -21 36 -47z m303 9
|
||||||
|
c-12 -16 -29 -35 -39 -43 -9 -8 -2 7 17 33 18 27 35 46 38 44 2 -3 -5 -18 -16
|
||||||
|
-34z m-380 -89 c30 -31 108 -176 108 -201 0 -6 -85 105 -105 137 -5 9 -20 33
|
||||||
|
-32 52 -39 62 -25 67 29 12z m488 31 c0 -4 -10 -23 -22 -43 -13 -19 -27 -43
|
||||||
|
-33 -52 -20 -32 -105 -143 -105 -137 0 25 78 170 108 201 34 35 52 45 52 31z
|
||||||
|
m-1047 -263 c-16 -20 -32 124 -17 156 5 12 10 -11 14 -67 4 -47 5 -87 3 -89z
|
||||||
|
m1575 80 c-3 -62 -11 -93 -21 -80 -2 2 -1 42 1 87 3 56 8 80 14 74 6 -6 9 -41
|
||||||
|
6 -81z"/>
|
||||||
|
<path d="M1591 2353 c7 -12 15 -20 18 -17 3 2 -3 12 -13 22 -17 16 -18 16 -5
|
||||||
|
-5z"/>
|
||||||
|
<path d="M1900 2355 c-7 -9 -11 -17 -9 -20 3 -2 10 5 17 15 14 24 10 26 -8 5z"/>
|
||||||
|
<path d="M1376 2320 c14 -27 65 -100 70 -100 8 0 -25 55 -51 85 -15 17 -23 23
|
||||||
|
-19 15z"/>
|
||||||
|
<path d="M1460 2225 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
|
||||||
|
-5 8 -10 11 -10 5z"/>
|
||||||
|
<path d="M2098 2298 c-28 -37 -51 -78 -44 -78 4 0 45 58 70 98 15 24 -2 11
|
||||||
|
-26 -20z"/>
|
||||||
|
<path d="M2026 2215 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
||||||
|
<path d="M1600 1926 c0 -35 32 -100 41 -84 6 9 15 6 35 -12 l28 -25 -18 29
|
||||||
|
c-10 16 -34 49 -52 73 -31 41 -34 43 -34 19z"/>
|
||||||
|
<path d="M1865 1906 c-16 -22 -39 -54 -49 -71 l-20 -30 28 25 c20 18 29 21 35
|
||||||
|
12 10 -17 44 57 39 85 -2 17 -8 14 -33 -21z"/>
|
||||||
|
<path d="M1660 1720 c11 -22 23 -40 25 -40 3 0 -4 18 -15 40 -11 22 -23 40
|
||||||
|
-25 40 -3 0 4 -18 15 -40z"/>
|
||||||
|
<path d="M1830 1720 c-11 -22 -18 -40 -15 -40 2 0 14 18 25 40 11 22 18 40 15
|
||||||
|
40 -2 0 -14 -18 -25 -40z"/>
|
||||||
|
<path d="M1260 1150 c-6 -11 -8 -20 -6 -20 3 0 10 9 16 20 6 11 8 20 6 20 -3
|
||||||
|
0 -10 -9 -16 -20z"/>
|
||||||
|
<path d="M2230 1150 c6 -11 13 -20 16 -20 2 0 0 9 -6 20 -6 11 -13 20 -16 20
|
||||||
|
-2 0 0 -9 6 -20z"/>
|
||||||
|
<path d="M1640 1505 c0 -5 5 -17 10 -25 5 -8 10 -10 10 -5 0 6 -5 17 -10 25
|
||||||
|
-5 8 -10 11 -10 5z"/>
|
||||||
|
<path d="M1846 1495 c-9 -26 -7 -32 5 -12 6 10 9 21 6 23 -2 3 -7 -2 -11 -11z"/>
|
||||||
|
<path d="M1056 1088 c-9 -12 -15 -32 -15 -43 1 -17 2 -17 6 -2 2 9 9 17 14 17
|
||||||
|
5 0 9 9 9 19 0 11 6 22 13 24 9 4 9 6 0 6 -6 1 -19 -9 -27 -21z"/>
|
||||||
|
<path d="M2418 1103 c6 -2 12 -13 12 -24 0 -10 4 -19 9 -19 5 0 12 -8 14 -17
|
||||||
|
4 -15 5 -15 6 2 1 25 -25 65 -42 64 -9 0 -9 -2 1 -6z"/>
|
||||||
|
<path d="M932 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
||||||
|
<path d="M2562 570 c0 -19 2 -27 5 -17 2 9 2 25 0 35 -3 9 -5 1 -5 -18z"/>
|
||||||
|
<path d="M810 3416 c0 -31 191 -431 196 -411 1 6 4 38 6 73 3 49 1 62 -9 58
|
||||||
|
-8 -3 -15 5 -19 19 -3 14 -10 25 -15 25 -5 0 -9 7 -9 16 0 9 -9 28 -21 42 -11
|
||||||
|
15 -45 63 -75 106 -30 44 -54 76 -54 72z"/>
|
||||||
|
<path d="M2622 3324 c-35 -52 -68 -97 -73 -100 -5 -3 -9 -14 -9 -25 0 -10 -4
|
||||||
|
-19 -9 -19 -5 0 -12 -11 -15 -25 -4 -14 -11 -22 -19 -19 -10 4 -12 -9 -9 -58
|
||||||
|
2 -35 5 -67 6 -73 3 -13 116 210 161 319 20 48 35 89 33 91 -2 2 -32 -39 -66
|
||||||
|
-91z"/>
|
||||||
|
<path d="M1095 3393 c4 -10 19 -68 34 -128 30 -119 18 -99 180 -313 35 -46 86
|
||||||
|
-124 113 -174 27 -50 68 -116 92 -147 69 -90 117 -162 131 -194 7 -16 15 -25
|
||||||
|
18 -19 7 11 -47 96 -135 212 -31 41 -70 102 -86 135 -31 62 -77 193 -88 252
|
||||||
|
-3 18 -12 37 -20 44 -8 6 -14 17 -14 24 0 6 -20 43 -45 81 -55 85 -57 74 -6
|
||||||
|
-28 36 -72 57 -128 47 -128 -11 0 -98 170 -112 219 -9 31 -37 84 -62 119 -47
|
||||||
|
63 -60 76 -47 45z"/>
|
||||||
|
<path d="M2358 3348 c-25 -35 -53 -88 -62 -119 -14 -49 -101 -219 -112 -219
|
||||||
|
-10 0 11 56 47 128 51 102 49 113 -6 28 -25 -38 -45 -75 -45 -81 0 -7 -6 -18
|
||||||
|
-14 -24 -8 -7 -17 -26 -20 -44 -11 -59 -57 -190 -88 -252 -16 -33 -55 -94 -86
|
||||||
|
-135 -88 -116 -142 -201 -135 -212 3 -6 11 3 18 19 14 32 62 104 131 194 24
|
||||||
|
31 65 97 92 147 27 50 78 128 113 174 162 214 150 194 180 313 15 60 30 118
|
||||||
|
34 128 13 31 0 18 -47 -45z"/>
|
||||||
|
<path d="M1810 3250 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
|
||||||
|
-6z"/>
|
||||||
|
<path d="M1784 3231 c5 -8 2 -11 -9 -9 -10 2 -20 -2 -22 -9 -3 -7 3 -9 17 -6
|
||||||
|
26 7 34 19 19 29 -8 5 -9 3 -5 -5z"/>
|
||||||
|
<path d="M1880 3210 c0 -13 5 -18 15 -14 12 5 13 10 4 20 -15 19 -19 18 -19
|
||||||
|
-6z"/>
|
||||||
|
<path d="M1174 3082 c8 -16 22 -35 32 -43 10 -8 5 4 -11 29 -32 48 -42 55 -21
|
||||||
|
14z"/>
|
||||||
|
<path d="M1807 3097 c-3 -8 3 -14 15 -14 16 -2 17 1 8 13 -15 17 -17 17 -23 1z"/>
|
||||||
|
<path d="M2305 3068 c-16 -25 -21 -37 -11 -29 16 14 51 71 43 71 -3 0 -17 -19
|
||||||
|
-32 -42z"/>
|
||||||
|
<path d="M1750 3066 c0 -8 -3 -22 -7 -32 -4 -11 -3 -15 4 -11 15 9 23 46 12
|
||||||
|
53 -5 3 -9 -1 -9 -10z"/>
|
||||||
|
<path d="M1750 2959 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3
|
||||||
|
-20 1 -20 -4z"/>
|
||||||
|
<path d="M1707 2799 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
|
||||||
|
<path d="M820 2720 c-13 -8 -12 -10 3 -10 9 0 17 5 17 10 0 12 -1 12 -20 0z"/>
|
||||||
|
<path d="M2660 2720 c0 -6 7 -10 15 -10 8 0 15 2 15 4 0 2 -7 6 -15 10 -8 3
|
||||||
|
-15 1 -15 -4z"/>
|
||||||
|
<path d="M471 2512 c8 -13 20 -17 37 -14 24 5 24 5 -8 18 -41 18 -42 18 -29
|
||||||
|
-4z"/>
|
||||||
|
<path d="M540 2519 c0 -5 9 -9 20 -9 11 0 20 2 20 4 0 2 -9 6 -20 9 -11 3 -20
|
||||||
|
1 -20 -4z"/>
|
||||||
|
<path d="M2935 2520 c-19 -8 -19 -9 3 -9 12 -1 22 4 22 9 0 6 -1 10 -2 9 -2 0
|
||||||
|
-12 -4 -23 -9z"/>
|
||||||
|
<path d="M2996 2516 l-29 -13 25 -5 c17 -3 29 1 37 14 13 21 7 21 -33 4z"/>
|
||||||
|
<path d="M580 1854 c-30 -7 -68 -17 -85 -22 -27 -9 -25 -10 25 -10 39 -1 49 2
|
||||||
|
35 8 -18 8 -18 9 5 10 28 2 114 28 90 28 -8 0 -40 -6 -70 -14z"/>
|
||||||
|
<path d="M2875 1855 c22 -8 51 -14 65 -15 23 -1 23 -2 5 -10 -14 -6 -4 -9 35
|
||||||
|
-8 50 0 52 1 25 9 -34 12 -157 40 -165 38 -3 -1 13 -7 35 -14z"/>
|
||||||
|
<path d="M700 1840 c-8 -5 -40 -10 -70 -11 -40 -1 -48 -3 -30 -8 22 -6 13 -12
|
||||||
|
-60 -40 -47 -18 -116 -52 -155 -76 -67 -41 -230 -173 -223 -180 4 -5 102 18
|
||||||
|
108 24 3 4 41 7 85 9 l80 2 -54 -32 c-72 -44 -202 -146 -198 -157 1 -4 -9 -18
|
||||||
|
-23 -29 -16 -14 -20 -21 -11 -22 8 0 23 7 33 17 14 13 18 13 18 2 1 -11 3 -11
|
||||||
|
13 1 25 32 134 81 151 68 2 -2 13 6 25 16 12 11 21 16 21 12 0 -5 17 2 38 13
|
||||||
|
l37 21 -35 -30 -35 -31 30 7 c38 9 146 98 186 154 24 33 27 41 13 36 -33 -13
|
||||||
|
-158 -46 -173 -46 -7 0 7 18 33 41 25 23 71 74 102 114 30 41 71 87 92 104 34
|
||||||
|
28 36 43 2 21z m-90 -95 c0 -3 -15 -11 -32 -20 -18 -8 -40 -19 -48 -25 -26
|
||||||
|
-17 -148 -69 -177 -75 l-28 -6 22 19 c13 11 48 28 78 37 57 18 75 32 28 21
|
||||||
|
-24 -5 -25 -5 -9 5 48 29 166 61 166 44z m-72 -84 c-6 -17 -90 -81 -112 -85
|
||||||
|
-48 -10 -139 -16 -133 -9 15 14 97 41 97 32 0 -6 16 2 35 17 19 14 35 22 35
|
||||||
|
16 0 -6 12 0 26 13 26 24 59 34 52 16z m-83 -171 c-3 -5 -14 -10 -24 -10 -10
|
||||||
|
0 -24 -7 -31 -15 -7 -8 -18 -15 -25 -15 -7 0 -18 -7 -25 -15 -7 -8 -23 -15
|
||||||
|
-35 -15 -13 0 -25 -3 -27 -7 -4 -11 -76 -43 -91 -42 -7 0 10 11 36 24 27 12
|
||||||
|
43 25 35 27 -7 3 2 8 20 12 17 4 32 11 32 16 0 4 8 11 18 14 9 3 28 13 42 21
|
||||||
|
28 17 85 20 75 5z"/>
|
||||||
|
<path d="M2802 1820 c21 -17 62 -64 92 -105 31 -40 77 -91 102 -114 26 -23 40
|
||||||
|
-41 33 -41 -15 0 -140 33 -173 46 -14 5 -11 -3 13 -36 40 -56 148 -145 186
|
||||||
|
-154 l30 -7 -35 31 -35 30 38 -21 c20 -11 37 -18 37 -13 0 4 8 0 18 -9 9 -8
|
||||||
|
20 -16 25 -16 42 1 130 -40 154 -71 10 -12 12 -12 13 -1 0 11 4 11 18 -2 10
|
||||||
|
-10 25 -17 33 -17 9 1 5 8 -11 22 -14 11 -24 25 -23 29 4 11 -126 113 -198
|
||||||
|
157 l-54 32 80 -2 c44 -2 82 -5 85 -9 6 -6 104 -29 108 -24 7 7 -156 139 -223
|
||||||
|
180 -38 24 -108 58 -155 76 -72 28 -81 34 -60 39 18 5 9 8 -35 12 -33 2 -69 8
|
||||||
|
-80 12 -11 4 -3 -7 17 -24z m220 -105 c29 -13 41 -22 26 -19 -47 10 -31 -2 27
|
||||||
|
-21 30 -9 65 -26 78 -37 l22 -19 -28 6 c-29 6 -151 58 -177 75 -8 6 -30 17
|
||||||
|
-47 25 -18 9 -33 18 -33 21 0 12 80 -6 132 -31z m-8 -70 c14 -13 26 -19 26
|
||||||
|
-13 0 6 16 -2 35 -16 19 -15 35 -23 35 -17 0 9 82 -18 97 -32 6 -7 -85 -1
|
||||||
|
-133 9 -22 4 -106 68 -112 85 -7 18 26 8 52 -16z m106 -160 c14 -8 33 -18 43
|
||||||
|
-21 9 -3 17 -10 17 -14 0 -5 15 -12 33 -16 17 -4 26 -9 19 -12 -8 -2 8 -15 35
|
||||||
|
-27 26 -13 43 -24 36 -24 -15 -1 -87 31 -91 42 -2 4 -14 7 -27 7 -12 0 -28 7
|
||||||
|
-35 15 -7 8 -18 15 -25 15 -7 0 -18 7 -25 15 -7 8 -21 15 -31 15 -10 0 -21 5
|
||||||
|
-24 10 -10 15 47 12 75 -5z"/>
|
||||||
|
<path d="M1246 1578 c-43 -51 -83 -104 -90 -118 -16 -29 -37 -92 -33 -96 5 -6
|
||||||
|
105 131 126 173 11 23 29 48 38 55 15 11 48 78 39 78 -2 0 -38 -42 -80 -92z"/>
|
||||||
|
<path d="M2183 1640 c6 -19 20 -41 30 -48 9 -7 27 -32 38 -55 21 -42 121 -179
|
||||||
|
126 -173 3 3 -16 61 -30 93 -9 19 -120 158 -163 203 -12 12 -12 9 -1 -20z"/>
|
||||||
|
<path d="M980 1637 c-50 -19 -70 -31 -70 -39 0 -5 9 -4 19 2 11 5 32 15 47 20
|
||||||
|
25 10 26 9 13 -7 -22 -28 -42 -42 -61 -43 -25 0 -33 -20 -8 -20 26 0 93 61 89
|
||||||
|
82 -3 12 -9 13 -29 5z"/>
|
||||||
|
<path d="M2490 1631 c0 -24 64 -81 91 -81 24 0 15 20 -9 20 -19 1 -39 15 -61
|
||||||
|
43 -13 16 -12 17 13 7 15 -5 36 -15 47 -20 10 -6 19 -7 19 -2 0 9 -19 19 -72
|
||||||
|
41 -25 9 -28 8 -28 -8z"/>
|
||||||
|
<path d="M1286 1573 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
||||||
|
<path d="M2200 1581 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||||
|
<path d="M930 1535 c-7 -9 -11 -18 -8 -20 3 -3 11 1 18 10 7 9 11 18 8 20 -3
|
||||||
|
3 -11 -1 -18 -10z"/>
|
||||||
|
<path d="M2550 1542 c0 -5 7 -15 15 -22 8 -7 15 -8 15 -2 0 5 -7 15 -15 22 -8
|
||||||
|
7 -15 8 -15 2z"/>
|
||||||
|
<path d="M555 1290 l-40 -19 42 0 c41 -1 64 13 47 29 -5 4 -27 -1 -49 -10z"/>
|
||||||
|
<path d="M2894 1298 c-11 -18 12 -29 52 -26 l39 3 -42 17 c-27 10 -45 13 -49
|
||||||
|
6z"/>
|
||||||
|
<path d="M1431 538 c17 -34 99 -148 106 -148 3 0 -2 10 -10 23 -9 12 -35 50
|
||||||
|
-59 85 -43 64 -58 79 -37 40z"/>
|
||||||
|
<path d="M2032 498 c-24 -35 -50 -73 -59 -85 -8 -13 -13 -23 -10 -23 7 0 89
|
||||||
|
114 106 148 21 39 6 24 -37 -40z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 36 KiB |
19
app/static/img/favicon/site.webmanifest
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/android-chrome-256x256.png",
|
||||||
|
"sizes": "256x256",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
BIN
app/static/img/rp/.DS_Store
vendored
Executable file
BIN
app/static/img/rp/rpdiscordsearch.png
Executable file
After Width: | Height: | Size: 20 KiB |
BIN
app/static/img/rp/rpgiteasearch.png
Executable file
After Width: | Height: | Size: 20 KiB |
BIN
app/static/img/rp/rpkicksearch.png
Executable file
After Width: | Height: | Size: 13 KiB |
BIN
app/static/img/rp/rptwitchsearch.png
Executable file
After Width: | Height: | Size: 16 KiB |
BIN
app/static/img/rp/rpwebsearch.png
Executable file
After Width: | Height: | Size: 40 KiB |
BIN
app/static/img/rp/rpyoutubesearch.png
Executable file
After Width: | Height: | Size: 37 KiB |
BIN
app/static/img/rplogo.webp
Executable file
After Width: | Height: | Size: 66 KiB |
127
app/static/js/autocomplete.js
Executable file
|
@ -0,0 +1,127 @@
|
||||||
|
let searchInput;
|
||||||
|
let currentFocus;
|
||||||
|
let originalSearch;
|
||||||
|
let autocompleteResults;
|
||||||
|
|
||||||
|
const handleUserInput = () => {
|
||||||
|
let xhrRequest = new XMLHttpRequest();
|
||||||
|
xhrRequest.open("POST", "autocomplete");
|
||||||
|
xhrRequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||||
|
xhrRequest.onload = function () {
|
||||||
|
if (xhrRequest.readyState === 4 && xhrRequest.status !== 200) {
|
||||||
|
// Do nothing if failed to fetch autocomplete results
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill autocomplete with fetched results
|
||||||
|
autocompleteResults = JSON.parse(xhrRequest.responseText)[1];
|
||||||
|
updateAutocompleteList();
|
||||||
|
};
|
||||||
|
|
||||||
|
xhrRequest.send('q=' + searchInput.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeActive = suggestion => {
|
||||||
|
// Remove "autocomplete-active" class from previously active suggestion
|
||||||
|
for (let i = 0; i < suggestion.length; i++) {
|
||||||
|
suggestion[i].classList.remove("autocomplete-active");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addActive = (suggestion) => {
|
||||||
|
// Handle navigation outside of suggestion list
|
||||||
|
if (!suggestion || !suggestion[currentFocus]) {
|
||||||
|
if (currentFocus >= suggestion.length) {
|
||||||
|
// Move selection back to the beginning
|
||||||
|
currentFocus = 0;
|
||||||
|
} else if (currentFocus < 0) {
|
||||||
|
// Retrieve original search and remove active suggestion selection
|
||||||
|
currentFocus = -1;
|
||||||
|
searchInput.value = originalSearch;
|
||||||
|
removeActive(suggestion);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeActive(suggestion);
|
||||||
|
suggestion[currentFocus].classList.add("autocomplete-active");
|
||||||
|
|
||||||
|
// Autofill search bar with suggestion content (minus the "bang name" if using a bang operator)
|
||||||
|
let searchContent = suggestion[currentFocus].textContent;
|
||||||
|
if (searchContent.indexOf('(') > 0) {
|
||||||
|
searchInput.value = searchContent.substring(0, searchContent.indexOf('('));
|
||||||
|
} else {
|
||||||
|
searchInput.value = searchContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchInput.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
const autocompleteInput = (e) => {
|
||||||
|
// Handle navigation between autocomplete suggestions
|
||||||
|
let suggestion = document.getElementById("autocomplete-list");
|
||||||
|
if (suggestion) suggestion = suggestion.getElementsByTagName("div");
|
||||||
|
if (e.keyCode === 40) { // down
|
||||||
|
e.preventDefault();
|
||||||
|
currentFocus++;
|
||||||
|
addActive(suggestion);
|
||||||
|
} else if (e.keyCode === 38) { //up
|
||||||
|
e.preventDefault();
|
||||||
|
currentFocus--;
|
||||||
|
addActive(suggestion);
|
||||||
|
} else if (e.keyCode === 13) { // enter
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFocus > -1) {
|
||||||
|
if (suggestion) suggestion[currentFocus].click();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
originalSearch = searchInput.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateAutocompleteList = () => {
|
||||||
|
let autocompleteItem, i;
|
||||||
|
let val = originalSearch;
|
||||||
|
|
||||||
|
let autocompleteList = document.getElementById("autocomplete-list");
|
||||||
|
autocompleteList.innerHTML = "";
|
||||||
|
|
||||||
|
if (!val || !autocompleteResults) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentFocus = -1;
|
||||||
|
|
||||||
|
for (i = 0; i < autocompleteResults.length; i++) {
|
||||||
|
if (autocompleteResults[i].substr(0, val.length).toUpperCase() === val.toUpperCase()) {
|
||||||
|
autocompleteItem = document.createElement("div");
|
||||||
|
autocompleteItem.setAttribute("class", "autocomplete-item");
|
||||||
|
autocompleteItem.innerHTML = "<strong>" + autocompleteResults[i].substr(0, val.length) + "</strong>";
|
||||||
|
autocompleteItem.innerHTML += autocompleteResults[i].substr(val.length);
|
||||||
|
autocompleteItem.innerHTML += "<input type=\"hidden\" value=\"" + autocompleteResults[i] + "\">";
|
||||||
|
autocompleteItem.addEventListener("click", function () {
|
||||||
|
searchInput.value = this.getElementsByTagName("input")[0].value;
|
||||||
|
autocompleteList.innerHTML = "";
|
||||||
|
document.getElementById("search-form").submit();
|
||||||
|
});
|
||||||
|
autocompleteList.appendChild(autocompleteItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
let autocompleteList = document.createElement("div");
|
||||||
|
autocompleteList.setAttribute("id", "autocomplete-list");
|
||||||
|
autocompleteList.setAttribute("class", "autocomplete-items");
|
||||||
|
|
||||||
|
searchInput = document.getElementById("search-bar");
|
||||||
|
searchInput.parentNode.appendChild(autocompleteList);
|
||||||
|
|
||||||
|
searchInput.addEventListener("keydown", (event) => autocompleteInput(event));
|
||||||
|
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
autocompleteList.innerHTML = "";
|
||||||
|
});
|
||||||
|
});
|
88
app/static/js/controller.js
Executable file
|
@ -0,0 +1,88 @@
|
||||||
|
const setupSearchLayout = () => {
|
||||||
|
// Setup search field
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const searchBtn = document.getElementById("search-submit");
|
||||||
|
const arrowKeys = [37, 38, 39, 40];
|
||||||
|
let searchValue = searchBar.value;
|
||||||
|
|
||||||
|
// Automatically focus on search field
|
||||||
|
searchBar.focus();
|
||||||
|
searchBar.select();
|
||||||
|
|
||||||
|
searchBar.addEventListener("keyup", function(event) {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
event.preventDefault();
|
||||||
|
searchBtn.click();
|
||||||
|
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
||||||
|
searchValue = searchBar.value;
|
||||||
|
handleUserInput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setupConfigLayout = () => {
|
||||||
|
// Setup whoogle config
|
||||||
|
const collapsible = document.getElementById("config-collapsible");
|
||||||
|
collapsible.addEventListener("click", function() {
|
||||||
|
this.classList.toggle("active");
|
||||||
|
let content = this.nextElementSibling;
|
||||||
|
if (content.style.maxHeight) {
|
||||||
|
content.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
content.style.maxHeight = "400px";
|
||||||
|
}
|
||||||
|
|
||||||
|
content.classList.toggle("open");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadConfig = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
let config = prompt("Enter name of config:");
|
||||||
|
if (!config) {
|
||||||
|
alert("Must specify a name for the config to load");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let xhrPUT = new XMLHttpRequest();
|
||||||
|
xhrPUT.open("PUT", "config?name=" + config + ".conf");
|
||||||
|
xhrPUT.onload = function() {
|
||||||
|
if (xhrPUT.readyState === 4 && xhrPUT.status !== 200) {
|
||||||
|
alert("Error loading Whoogle config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
location.reload(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhrPUT.send();
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfig = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
let config = prompt("Enter name for this config:");
|
||||||
|
if (!config) {
|
||||||
|
alert("Must specify a name for the config to save");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let configForm = document.getElementById("config-form");
|
||||||
|
configForm.action = 'config?name=' + config + ".conf";
|
||||||
|
configForm.submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
document.getElementById("main");
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
setupSearchLayout();
|
||||||
|
setupConfigLayout();
|
||||||
|
|
||||||
|
document.getElementById("config-load").addEventListener("click", loadConfig);
|
||||||
|
document.getElementById("config-save").addEventListener("click", saveConfig);
|
||||||
|
|
||||||
|
// Focusing on the search input field requires a delay for elements to finish
|
||||||
|
// loading (seemingly only on FF)
|
||||||
|
setTimeout(function() { document.getElementById("search-bar").focus(); }, 250);
|
||||||
|
});
|
9
app/static/js/currency.js
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
const convert = (n1, n2, conversionFactor) => {
|
||||||
|
// id's for currency input boxes
|
||||||
|
let id1 = "cb" + n1;
|
||||||
|
let id2 = "cb" + n2;
|
||||||
|
// getting the value of the input box that just got filled
|
||||||
|
let inputBox = document.getElementById(id1).value;
|
||||||
|
// updating the other input box after conversion
|
||||||
|
document.getElementById(id2).value = ((inputBox * conversionFactor).toFixed(2));
|
||||||
|
}
|
67
app/static/js/header.js
Executable file
|
@ -0,0 +1,67 @@
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const advSearchToggle = document.getElementById("adv-search-toggle");
|
||||||
|
const advSearchDiv = document.getElementById("adv-search-div");
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const countrySelect = document.getElementById("result-country");
|
||||||
|
const timePeriodSelect = document.getElementById("result-time-period");
|
||||||
|
const arrowKeys = [37, 38, 39, 40];
|
||||||
|
let searchValue = searchBar.value;
|
||||||
|
|
||||||
|
countrySelect.onchange = () => {
|
||||||
|
let str = window.location.href;
|
||||||
|
n = str.lastIndexOf("/search");
|
||||||
|
if (n > 0) {
|
||||||
|
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
||||||
|
str = tackOnParams(str);
|
||||||
|
window.location.href = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timePeriodSelect.onchange = () => {
|
||||||
|
let str = window.location.href;
|
||||||
|
n = str.lastIndexOf("/search");
|
||||||
|
if (n > 0) {
|
||||||
|
str = str.substring(0, n) + `/search?q=${searchBar.value}`;
|
||||||
|
str = tackOnParams(str);
|
||||||
|
window.location.href = str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tackOnParams(str) {
|
||||||
|
if (timePeriodSelect.value != "") {
|
||||||
|
str = str + `&tbs=${timePeriodSelect.value}`;
|
||||||
|
}
|
||||||
|
if (countrySelect.value != "") {
|
||||||
|
str = str + `&country=${countrySelect.value}`;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAdvancedSearch = on => {
|
||||||
|
if (on) {
|
||||||
|
advSearchDiv.style.maxHeight = "70px";
|
||||||
|
} else {
|
||||||
|
advSearchDiv.style.maxHeight = "0px";
|
||||||
|
}
|
||||||
|
localStorage.advSearchToggled = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
toggleAdvancedSearch(JSON.parse(localStorage.advSearchToggled));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Did not recover advanced search toggle state");
|
||||||
|
}
|
||||||
|
|
||||||
|
advSearchToggle.onclick = () => {
|
||||||
|
toggleAdvancedSearch(advSearchToggle.checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBar.addEventListener("keyup", function(event) {
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
document.getElementById("search-form").submit();
|
||||||
|
} else if (searchBar.value !== searchValue && !arrowKeys.includes(event.keyCode)) {
|
||||||
|
searchValue = searchBar.value;
|
||||||
|
handleUserInput();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
62
app/static/js/keyboard.js
Executable file
|
@ -0,0 +1,62 @@
|
||||||
|
(function () {
|
||||||
|
let searchBar, results;
|
||||||
|
let shift = false;
|
||||||
|
const keymap = {
|
||||||
|
ArrowUp: goUp,
|
||||||
|
ArrowDown: goDown,
|
||||||
|
ShiftTab: goUp,
|
||||||
|
Tab: goDown,
|
||||||
|
k: goUp,
|
||||||
|
j: goDown,
|
||||||
|
'/': focusSearch,
|
||||||
|
};
|
||||||
|
let activeIdx = -1;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
searchBar = document.querySelector('#search-bar');
|
||||||
|
results = document.querySelectorAll('#main>div>div>div>a');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
shift = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.target.tagName === 'INPUT') return true;
|
||||||
|
if (typeof keymap[e.key] === 'function') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
keymap[`${shift && e.key == 'Tab' ? 'Shift' : ''}${e.key}`]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keyup', (e) => {
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
shift = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function goUp () {
|
||||||
|
if (activeIdx > 0) focusResult(activeIdx - 1);
|
||||||
|
else focusSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDown () {
|
||||||
|
if (activeIdx < results.length - 1) focusResult(activeIdx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusResult (idx) {
|
||||||
|
activeIdx = idx;
|
||||||
|
results[activeIdx].scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
||||||
|
results[activeIdx].focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearch () {
|
||||||
|
if (window.usingCalculator) {
|
||||||
|
// if this function exists, it means the calculator widget has been displayed
|
||||||
|
if (usingCalculator()) return;
|
||||||
|
}
|
||||||
|
activeIdx = -1;
|
||||||
|
searchBar.focus();
|
||||||
|
}
|
||||||
|
}());
|
76
app/static/js/utils.js
Executable file
|
@ -0,0 +1,76 @@
|
||||||
|
const checkForTracking = () => {
|
||||||
|
const mainDiv = document.getElementById("main");
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
// some pages (e.g. images) do not have these
|
||||||
|
if (!mainDiv || !searchBar)
|
||||||
|
return;
|
||||||
|
const query = searchBar.value.replace(/\s+/g, '');
|
||||||
|
|
||||||
|
// Note: regex functions for checking for tracking queries were derived
|
||||||
|
// from here -- https://stackoverflow.com/questions/619977
|
||||||
|
const matchTracking = {
|
||||||
|
"ups": {
|
||||||
|
"link": `https://www.ups.com/track?tracknum=${query}`,
|
||||||
|
"expr": [
|
||||||
|
/\b(1Z ?[0-9A-Z]{3} ?[0-9A-Z]{3} ?[0-9A-Z]{2} ?[0-9A-Z]{4} ?[0-9A-Z]{3} ?[0-9A-Z]|[\dT]\d\d\d ?\d\d\d\d ?\d\d\d)\b/
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"usps": {
|
||||||
|
"link": `https://tools.usps.com/go/TrackConfirmAction_input?origTrackNum=${query}`,
|
||||||
|
"expr": [
|
||||||
|
/(\b\d{30}\b)|(\b91\d+\b)|(\b\d{20}\b)/,
|
||||||
|
/^E\D{1}\d{9}\D{2}$|^9\d{15,21}$/,
|
||||||
|
/^91[0-9]+$/,
|
||||||
|
/^[A-Za-z]{2}[0-9]+US$/
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fedex": {
|
||||||
|
"link": `https://www.fedex.com/apps/fedextrack/?tracknumbers=${query}`,
|
||||||
|
"expr": [
|
||||||
|
/(\b96\d{20}\b)|(\b\d{15}\b)|(\b\d{12}\b)/,
|
||||||
|
/\b((98\d\d\d\d\d?\d\d\d\d|98\d\d) ?\d\d\d\d ?\d\d\d\d( ?\d\d\d)?)\b/,
|
||||||
|
/^[0-9]{15}$/
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Creates a link to a UPS/USPS/FedEx tracking page
|
||||||
|
const createTrackingLink = href => {
|
||||||
|
let link = document.createElement("a");
|
||||||
|
link.className = "tracking-link";
|
||||||
|
link.innerHTML = "View Tracking Info";
|
||||||
|
link.href = href;
|
||||||
|
mainDiv.prepend(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compares the query against a set of regex patterns
|
||||||
|
// for tracking numbers
|
||||||
|
const compareQuery = provider => {
|
||||||
|
provider.expr.some(regex => {
|
||||||
|
if (query.match(regex)) {
|
||||||
|
createTrackingLink(provider.link);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(matchTracking)) {
|
||||||
|
compareQuery(matchTracking[key]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
checkForTracking();
|
||||||
|
|
||||||
|
// Clear input if reset button tapped
|
||||||
|
const searchBar = document.getElementById("search-bar");
|
||||||
|
const resetBtn = document.getElementById("search-reset");
|
||||||
|
// some pages (e.g. images) do not have these
|
||||||
|
if (!searchBar || !resetBtn)
|
||||||
|
return;
|
||||||
|
resetBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
searchBar.value = "";
|
||||||
|
searchBar.focus();
|
||||||
|
});
|
||||||
|
});
|
978
app/static/settings/countries.json
Executable file
|
@ -0,0 +1,978 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "-------",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hungary",
|
||||||
|
"value": "HU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Afghanistan",
|
||||||
|
"value": "AF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Albania",
|
||||||
|
"value": "AL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Algeria",
|
||||||
|
"value": "DZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "American Samoa",
|
||||||
|
"value": "AS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Andorra",
|
||||||
|
"value": "AD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Angola",
|
||||||
|
"value": "AO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Anguilla",
|
||||||
|
"value": "AI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Antarctica",
|
||||||
|
"value": "AQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Antigua and Barbuda",
|
||||||
|
"value": "AG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Argentina",
|
||||||
|
"value": "AR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Armenia",
|
||||||
|
"value": "AM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Aruba",
|
||||||
|
"value": "AW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Australia",
|
||||||
|
"value": "AU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Austria",
|
||||||
|
"value": "AT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Azerbaijan",
|
||||||
|
"value": "AZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bahamas",
|
||||||
|
"value": "BS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bahrain",
|
||||||
|
"value": "BH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bangladesh",
|
||||||
|
"value": "BD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Barbados",
|
||||||
|
"value": "BB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Belarus",
|
||||||
|
"value": "BY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Belgium",
|
||||||
|
"value": "BE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Belize",
|
||||||
|
"value": "BZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Benin",
|
||||||
|
"value": "BJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bermuda",
|
||||||
|
"value": "BM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bhutan",
|
||||||
|
"value": "BT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bolivia",
|
||||||
|
"value": "BO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bosnia and Herzegovina",
|
||||||
|
"value": "BA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Botswana",
|
||||||
|
"value": "BW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bouvet Island",
|
||||||
|
"value": "BV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brazil",
|
||||||
|
"value": "BR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "British Indian Ocean Territory",
|
||||||
|
"value": "IO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brunei Darussalam",
|
||||||
|
"value": "BN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bulgaria",
|
||||||
|
"value": "BG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Burkina Faso",
|
||||||
|
"value": "BF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Burundi",
|
||||||
|
"value": "BI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cambodia",
|
||||||
|
"value": "KH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cameroon",
|
||||||
|
"value": "CM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Canada",
|
||||||
|
"value": "CA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cape Verde",
|
||||||
|
"value": "CV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cayman Islands",
|
||||||
|
"value": "KY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Central African Republic",
|
||||||
|
"value": "CF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chad",
|
||||||
|
"value": "TD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chile",
|
||||||
|
"value": "CL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "China",
|
||||||
|
"value": "CN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Christmas Island",
|
||||||
|
"value": "CX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cocos (Keeling) Islands",
|
||||||
|
"value": "CC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Colombia",
|
||||||
|
"value": "CO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Comoros",
|
||||||
|
"value": "KM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Congo",
|
||||||
|
"value": "CG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Congo, Democratic Republic of the",
|
||||||
|
"value": "CD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cook Islands",
|
||||||
|
"value": "CK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Costa Rica",
|
||||||
|
"value": "CR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cote D'ivoire",
|
||||||
|
"value": "CI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Croatia (Hrvatska)",
|
||||||
|
"value": "HR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cuba",
|
||||||
|
"value": "CU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cyprus",
|
||||||
|
"value": "CY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Czech Republic",
|
||||||
|
"value": "CZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Denmark",
|
||||||
|
"value": "DK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Djibouti",
|
||||||
|
"value": "DJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dominica",
|
||||||
|
"value": "DM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dominican Republic",
|
||||||
|
"value": "DO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "East Timor",
|
||||||
|
"value": "TP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ecuador",
|
||||||
|
"value": "EC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Egypt",
|
||||||
|
"value": "EG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "El Salvador",
|
||||||
|
"value": "SV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Equatorial Guinea",
|
||||||
|
"value": "GQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Eritrea",
|
||||||
|
"value": "ER"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Estonia",
|
||||||
|
"value": "EE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ethiopia",
|
||||||
|
"value": "ET"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "European Union",
|
||||||
|
"value": "EU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Falkland Islands (Malvinas)",
|
||||||
|
"value": "FK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Faroe Islands",
|
||||||
|
"value": "FO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fiji",
|
||||||
|
"value": "FJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Finland",
|
||||||
|
"value": "FI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "France",
|
||||||
|
"value": "FR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "France, Metropolitan",
|
||||||
|
"value": "FX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "French Guiana",
|
||||||
|
"value": "GF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "French Polynesia",
|
||||||
|
"value": "PF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "French Southern Territories",
|
||||||
|
"value": "TF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gabon",
|
||||||
|
"value": "GA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gambia",
|
||||||
|
"value": "GM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Georgia",
|
||||||
|
"value": "GE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Germany",
|
||||||
|
"value": "DE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ghana",
|
||||||
|
"value": "GH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Gibraltar",
|
||||||
|
"value": "GI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Greece",
|
||||||
|
"value": "GR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Greenland",
|
||||||
|
"value": "GL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Grenada",
|
||||||
|
"value": "GD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guadeloupe",
|
||||||
|
"value": "GP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guam",
|
||||||
|
"value": "GU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guatemala",
|
||||||
|
"value": "GT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guinea",
|
||||||
|
"value": "GN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guinea-Bissau",
|
||||||
|
"value": "GW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Guyana",
|
||||||
|
"value": "GY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Haiti",
|
||||||
|
"value": "HT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Heard Island and Mcdonald Islands",
|
||||||
|
"value": "HM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Holy See (Vatican City State)",
|
||||||
|
"value": "VA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Honduras",
|
||||||
|
"value": "HN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hong Kong",
|
||||||
|
"value": "HK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hungary",
|
||||||
|
"value": "HU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Iceland",
|
||||||
|
"value": "IS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "India",
|
||||||
|
"value": "IN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indonesia",
|
||||||
|
"value": "ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Iran, Islamic Republic of",
|
||||||
|
"value": "IR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Iraq",
|
||||||
|
"value": "IQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ireland",
|
||||||
|
"value": "IE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Israel",
|
||||||
|
"value": "IL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Italy",
|
||||||
|
"value": "IT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jamaica",
|
||||||
|
"value": "JM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Japan",
|
||||||
|
"value": "JP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jordan",
|
||||||
|
"value": "JO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kazakhstan",
|
||||||
|
"value": "KZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kenya",
|
||||||
|
"value": "KE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kiribati",
|
||||||
|
"value": "KI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Korea, Democratic People's Republic of",
|
||||||
|
"value": "KP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Korea, Republic of",
|
||||||
|
"value": "KR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kuwait",
|
||||||
|
"value": "KW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kyrgyzstan",
|
||||||
|
"value": "KG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lao People's Democratic Republic",
|
||||||
|
"value": "LA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Latvia",
|
||||||
|
"value": "LV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lebanon",
|
||||||
|
"value": "LB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lesotho",
|
||||||
|
"value": "LS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Liberia",
|
||||||
|
"value": "LR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Libyan Arab Jamahiriya",
|
||||||
|
"value": "LY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Liechtenstein",
|
||||||
|
"value": "LI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lithuania",
|
||||||
|
"value": "LT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Luxembourg",
|
||||||
|
"value": "LU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Macao",
|
||||||
|
"value": "MO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Macedonia, the Former Yugosalv Republic of",
|
||||||
|
"value": "MK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Madagascar",
|
||||||
|
"value": "MG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Malawi",
|
||||||
|
"value": "MW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Malaysia",
|
||||||
|
"value": "MY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Maldives",
|
||||||
|
"value": "MV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mali",
|
||||||
|
"value": "ML"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Malta",
|
||||||
|
"value": "MT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Marshall Islands",
|
||||||
|
"value": "MH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Martinique",
|
||||||
|
"value": "MQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mauritania",
|
||||||
|
"value": "MR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mauritius",
|
||||||
|
"value": "MU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mayotte",
|
||||||
|
"value": "YT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mexico",
|
||||||
|
"value": "MX"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Micronesia, Federated States of",
|
||||||
|
"value": "FM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Moldova, Republic of",
|
||||||
|
"value": "MD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Monaco",
|
||||||
|
"value": "MC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mongolia",
|
||||||
|
"value": "MN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Montserrat",
|
||||||
|
"value": "MS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Morocco",
|
||||||
|
"value": "MA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Mozambique",
|
||||||
|
"value": "MZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Myanmar",
|
||||||
|
"value": "MM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Namibia",
|
||||||
|
"value": "NA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nauru",
|
||||||
|
"value": "NR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nepal",
|
||||||
|
"value": "NP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Netherlands",
|
||||||
|
"value": "NL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Netherlands Antilles",
|
||||||
|
"value": "AN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "New Caledonia",
|
||||||
|
"value": "NC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "New Zealand",
|
||||||
|
"value": "NZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nicaragua",
|
||||||
|
"value": "NI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Niger",
|
||||||
|
"value": "NE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Nigeria",
|
||||||
|
"value": "NG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Niue",
|
||||||
|
"value": "NU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Norfolk Island",
|
||||||
|
"value": "NF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Northern Mariana Islands",
|
||||||
|
"value": "MP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Norway",
|
||||||
|
"value": "NO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Oman",
|
||||||
|
"value": "OM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pakistan",
|
||||||
|
"value": "PK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Palau",
|
||||||
|
"value": "PW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Palestinian Territory",
|
||||||
|
"value": "PS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Panama",
|
||||||
|
"value": "PA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Papua New Guinea",
|
||||||
|
"value": "PG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Paraguay",
|
||||||
|
"value": "PY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Peru",
|
||||||
|
"value": "PE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Philippines",
|
||||||
|
"value": "PH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Pitcairn",
|
||||||
|
"value": "PN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Poland",
|
||||||
|
"value": "PL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Portugal",
|
||||||
|
"value": "PT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Puerto Rico",
|
||||||
|
"value": "PR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Qatar",
|
||||||
|
"value": "QA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Reunion",
|
||||||
|
"value": "RE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Romania",
|
||||||
|
"value": "RO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Russian Federation",
|
||||||
|
"value": "RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rwanda",
|
||||||
|
"value": "RW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saint Helena",
|
||||||
|
"value": "SH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saint Kitts and Nevis",
|
||||||
|
"value": "KN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saint Lucia",
|
||||||
|
"value": "LC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saint Pierre and Miquelon",
|
||||||
|
"value": "PM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saint Vincent and the Grenadines",
|
||||||
|
"value": "VC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Samoa",
|
||||||
|
"value": "WS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "San Marino",
|
||||||
|
"value": "SM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sao Tome and Principe",
|
||||||
|
"value": "ST"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Saudi Arabia",
|
||||||
|
"value": "SA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Senegal",
|
||||||
|
"value": "SN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Serbia and Montenegro",
|
||||||
|
"value": "CS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Seychelles",
|
||||||
|
"value": "SC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sierra Leone",
|
||||||
|
"value": "SL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Singapore",
|
||||||
|
"value": "SG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slovakia",
|
||||||
|
"value": "SK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slovenia",
|
||||||
|
"value": "SI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Solomon Islands",
|
||||||
|
"value": "SB"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Somalia",
|
||||||
|
"value": "SO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "South Africa",
|
||||||
|
"value": "ZA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "South Georgia and the South Sandwich Islands",
|
||||||
|
"value": "GS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spain",
|
||||||
|
"value": "ES"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sri Lanka",
|
||||||
|
"value": "LK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sudan",
|
||||||
|
"value": "SD"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Suriname",
|
||||||
|
"value": "SR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Svalbard and Jan Mayen",
|
||||||
|
"value": "SJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swaziland",
|
||||||
|
"value": "SZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sweden",
|
||||||
|
"value": "SE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Switzerland",
|
||||||
|
"value": "CH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Syrian Arab Republic",
|
||||||
|
"value": "SY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Taiwan",
|
||||||
|
"value": "TW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tajikistan",
|
||||||
|
"value": "TJ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tanzania, United Republic of",
|
||||||
|
"value": "TZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Thailand",
|
||||||
|
"value": "TH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Togo",
|
||||||
|
"value": "TG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tokelau",
|
||||||
|
"value": "TK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tonga",
|
||||||
|
"value": "TO"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Trinidad and Tobago",
|
||||||
|
"value": "TT"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tunisia",
|
||||||
|
"value": "TN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Turkey",
|
||||||
|
"value": "TR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Turkmenistan",
|
||||||
|
"value": "TM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Turks and Caicos Islands",
|
||||||
|
"value": "TC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tuvalu",
|
||||||
|
"value": "TV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uganda",
|
||||||
|
"value": "UG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ukraine",
|
||||||
|
"value": "UA"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "United Arab Emirates",
|
||||||
|
"value": "AE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "United Kingdom",
|
||||||
|
"value": "UK"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "United States",
|
||||||
|
"value": "US"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "United States Minor Outlying Islands",
|
||||||
|
"value": "UM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uruguay",
|
||||||
|
"value": "UY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Uzbekistan",
|
||||||
|
"value": "UZ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vanuatu",
|
||||||
|
"value": "VU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Venezuela",
|
||||||
|
"value": "VE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vietnam",
|
||||||
|
"value": "VN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Virgin Islands, British",
|
||||||
|
"value": "VG"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Virgin Islands, U.S.",
|
||||||
|
"value": "VI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Wallis and Futuna",
|
||||||
|
"value": "WF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Western Sahara",
|
||||||
|
"value": "EH"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yemen",
|
||||||
|
"value": "YE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Yugoslavia",
|
||||||
|
"value": "YU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zambia",
|
||||||
|
"value": "ZM"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zimbabwe",
|
||||||
|
"value": "ZW"
|
||||||
|
}
|
||||||
|
]
|
32
app/static/settings/header_tabs.json
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"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://www.openstreetmap.org/search?query={map_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
|
||||||
|
}
|
||||||
|
}
|
210
app/static/settings/languages.json
Executable file
|
@ -0,0 +1,210 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "English",
|
||||||
|
"value": "lang_en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hungarian (Magyar)",
|
||||||
|
"value": "lang_hu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Afrikaans (Afrikaans)",
|
||||||
|
"value": "lang_af"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Arabic (عربى)",
|
||||||
|
"value": "lang_ar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Armenian (հայերեն)",
|
||||||
|
"value": "lang_hy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Azerbaijani (Azərbaycanca)",
|
||||||
|
"value": "lang_az"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Belarusian (Беларуская)",
|
||||||
|
"value": "lang_be"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Bulgarian (български)",
|
||||||
|
"value": "lang_bg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Catalan (Català)",
|
||||||
|
"value": "lang_ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chinese, Simplified (简体中文)",
|
||||||
|
"value": "lang_zh-CN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chinese, Traditional (正體中文)",
|
||||||
|
"value": "lang_zh-TW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Croatian (Hrvatski)",
|
||||||
|
"value": "lang_hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Czech (čeština)",
|
||||||
|
"value": "lang_cs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Danish (Dansk)",
|
||||||
|
"value": "lang_da"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Dutch (Nederlands)",
|
||||||
|
"value": "lang_nl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Esperanto (Esperanto)",
|
||||||
|
"value": "lang_eo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Estonian (Eestlane)",
|
||||||
|
"value": "lang_et"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Filipino (Pilipino)",
|
||||||
|
"value": "lang_tl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Finnish (Suomalainen)",
|
||||||
|
"value": "lang_fi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "French (Français)",
|
||||||
|
"value": "lang_fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "German (Deutsch)",
|
||||||
|
"value": "lang_de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Greek (Ελληνικά)",
|
||||||
|
"value": "lang_el"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hebrew (עִברִית)",
|
||||||
|
"value": "lang_iw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Hindi (हिंदी)",
|
||||||
|
"value": "lang_hi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Icelandic (Íslenska)",
|
||||||
|
"value": "lang_is"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Indonesian (Indonesian)",
|
||||||
|
"value": "lang_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Italian (Italiano)",
|
||||||
|
"value": "lang_it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Japanese (日本語)",
|
||||||
|
"value": "lang_ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Korean (한국어)",
|
||||||
|
"value": "lang_ko"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Kurdish (Kurdî)",
|
||||||
|
"value": "lang_ku"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Latvian (Latvietis)",
|
||||||
|
"value": "lang_lv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Lithuanian (Lietuvis)",
|
||||||
|
"value": "lang_lt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Norwegian (Norwegian)",
|
||||||
|
"value": "lang_no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Persian (فارسی)",
|
||||||
|
"value": "lang_fa"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polish (Polskie)",
|
||||||
|
"value": "lang_pl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Portuguese (Português)",
|
||||||
|
"value": "lang_pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Romanian (Română)",
|
||||||
|
"value": "lang_ro"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Russian (русский)",
|
||||||
|
"value": "lang_ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Serbian (Српски)",
|
||||||
|
"value": "lang_sr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sinhala (සිංහල)",
|
||||||
|
"value": "lang_si"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slovak (Slovák)",
|
||||||
|
"value": "lang_sk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Slovenian (Slovenščina)",
|
||||||
|
"value": "lang_sl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Spanish (Español)",
|
||||||
|
"value": "lang_es"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swahili (Kiswahili)",
|
||||||
|
"value": "lang_sw"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Swedish (Svenska)",
|
||||||
|
"value": "lang_sv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Thai (ไทย)",
|
||||||
|
"value": "lang_th"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Turkish (Türk)",
|
||||||
|
"value": "lang_tr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ukrainian (Український)",
|
||||||
|
"value": "lang_uk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Vietnamese (Tiếng Việt)",
|
||||||
|
"value": "lang_vi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Welsh (Cymraeg)",
|
||||||
|
"value": "lang_cy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Xhosa (isiXhosa)",
|
||||||
|
"value": "lang_xh"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Zulu (isiZulu)",
|
||||||
|
"value": "lang_zu"
|
||||||
|
}
|
||||||
|
]
|
5
app/static/settings/themes.json
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
[
|
||||||
|
"light",
|
||||||
|
"dark",
|
||||||
|
"system"
|
||||||
|
]
|
8
app/static/settings/time_periods.json
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
[
|
||||||
|
{"name": "Any time", "value": ""},
|
||||||
|
{"name": "Past hour", "value": "qdr:h"},
|
||||||
|
{"name": "Past 24 hours", "value": "qdr:d"},
|
||||||
|
{"name": "Past week", "value": "qdr:w"},
|
||||||
|
{"name": "Past month", "value": "qdr:m"},
|
||||||
|
{"name": "Past year", "value": "qdr:y"}
|
||||||
|
]
|
1348
app/static/settings/translations.json
Executable file
260
app/static/widgets/calculator.html
Executable file
|
@ -0,0 +1,260 @@
|
||||||
|
<!--
|
||||||
|
Calculator widget.
|
||||||
|
This file should contain all required
|
||||||
|
CSS, HTML, and JS for it.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#calc-text {
|
||||||
|
background: var(--whoogle-dark-page-bg);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: right;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--whoogle-dark-text);
|
||||||
|
}
|
||||||
|
#prev-equation {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.error-border {
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calc-btns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
grid-template-rows: repeat(5, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
#calc-btns button {
|
||||||
|
background: #313141;
|
||||||
|
color: var(--whoogle-dark-text);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#calc-btns button:hover {
|
||||||
|
background: #414151;
|
||||||
|
}
|
||||||
|
#calc-btns .common {
|
||||||
|
background: #51516a;
|
||||||
|
}
|
||||||
|
#calc-btns .common:hover {
|
||||||
|
background: #61617a;
|
||||||
|
}
|
||||||
|
#calc-btn-0 { grid-row: 5; grid-column: 3; }
|
||||||
|
#calc-btn-1 { grid-row: 4; grid-column: 3; }
|
||||||
|
#calc-btn-2 { grid-row: 4; grid-column: 4; }
|
||||||
|
#calc-btn-3 { grid-row: 4; grid-column: 5; }
|
||||||
|
#calc-btn-4 { grid-row: 3; grid-column: 3; }
|
||||||
|
#calc-btn-5 { grid-row: 3; grid-column: 4; }
|
||||||
|
#calc-btn-6 { grid-row: 3; grid-column: 5; }
|
||||||
|
#calc-btn-7 { grid-row: 2; grid-column: 3; }
|
||||||
|
#calc-btn-8 { grid-row: 2; grid-column: 4; }
|
||||||
|
#calc-btn-9 { grid-row: 2; grid-column: 5; }
|
||||||
|
#calc-btn-EQ { grid-row: 5; grid-column: 5; }
|
||||||
|
#calc-btn-PT { grid-row: 5; grid-column: 4; }
|
||||||
|
#calc-btn-BCK { grid-row: 5; grid-column: 6; }
|
||||||
|
#calc-btn-ADD { grid-row: 4; grid-column: 6; }
|
||||||
|
#calc-btn-SUB { grid-row: 3; grid-column: 6; }
|
||||||
|
#calc-btn-MLT { grid-row: 2; grid-column: 6; }
|
||||||
|
#calc-btn-DIV { grid-row: 1; grid-column: 6; }
|
||||||
|
#calc-btn-CLR { grid-row: 1; grid-column: 5; }
|
||||||
|
#calc-btn-PRC{ grid-row: 1; grid-column: 4; }
|
||||||
|
#calc-btn-RP { grid-row: 1; grid-column: 3; }
|
||||||
|
#calc-btn-LP { grid-row: 1; grid-column: 2; }
|
||||||
|
#calc-btn-ABS { grid-row: 1; grid-column: 1; }
|
||||||
|
#calc-btn-SIN { grid-row: 2; grid-column: 2; }
|
||||||
|
#calc-btn-COS { grid-row: 3; grid-column: 2; }
|
||||||
|
#calc-btn-TAN { grid-row: 4; grid-column: 2; }
|
||||||
|
#calc-btn-SQR { grid-row: 5; grid-column: 2; }
|
||||||
|
#calc-btn-EXP { grid-row: 2; grid-column: 1; }
|
||||||
|
#calc-btn-E { grid-row: 3; grid-column: 1; }
|
||||||
|
#calc-btn-PI { grid-row: 4; grid-column: 1; }
|
||||||
|
#calc-btn-LOG { grid-row: 5; grid-column: 1; }
|
||||||
|
</style>
|
||||||
|
<p id="prev-equation"></p>
|
||||||
|
<div id="calculator-widget">
|
||||||
|
<p id="calc-text">0</p>
|
||||||
|
<div id="calc-btns">
|
||||||
|
<button id="calc-btn-0" class="common">0</button>
|
||||||
|
<button id="calc-btn-1" class="common">1</button>
|
||||||
|
<button id="calc-btn-2" class="common">2</button>
|
||||||
|
<button id="calc-btn-3" class="common">3</button>
|
||||||
|
<button id="calc-btn-4" class="common">4</button>
|
||||||
|
<button id="calc-btn-5" class="common">5</button>
|
||||||
|
<button id="calc-btn-6" class="common">6</button>
|
||||||
|
<button id="calc-btn-7" class="common">7</button>
|
||||||
|
<button id="calc-btn-8" class="common">8</button>
|
||||||
|
<button id="calc-btn-9" class="common">9</button>
|
||||||
|
<button id="calc-btn-EQ" class="common">=</button>
|
||||||
|
<button id="calc-btn-PT" class="common">.</button>
|
||||||
|
<button id="calc-btn-BCK">⬅</button>
|
||||||
|
<button id="calc-btn-ADD">+</button>
|
||||||
|
<button id="calc-btn-SUB">-</button>
|
||||||
|
<button id="calc-btn-MLT">x</button>
|
||||||
|
<button id="calc-btn-DIV">/</button>
|
||||||
|
<button id="calc-btn-CLR">C</button>
|
||||||
|
<button id="calc-btn-PRC">%</button>
|
||||||
|
<button id="calc-btn-RP">)</button>
|
||||||
|
<button id="calc-btn-LP">(</button>
|
||||||
|
<button id="calc-btn-ABS">|x|</button>
|
||||||
|
<button id="calc-btn-SIN">sin</button>
|
||||||
|
<button id="calc-btn-COS">cos</button>
|
||||||
|
<button id="calc-btn-TAN">tan</button>
|
||||||
|
<button id="calc-btn-SQR">√</button>
|
||||||
|
<button id="calc-btn-EXP">^</button>
|
||||||
|
<button id="calc-btn-E">ℇ</button>
|
||||||
|
<button id="calc-btn-PI">π</button>
|
||||||
|
<button id="calc-btn-LOG">log</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// JS does not have this by default.
|
||||||
|
// from https://www.freecodecamp.org/news/how-to-factorialize-a-number-in-javascript-9263c89a4b38/
|
||||||
|
function factorial(num) {
|
||||||
|
if (num < 0)
|
||||||
|
return -1;
|
||||||
|
else if (num === 0)
|
||||||
|
return 1;
|
||||||
|
else {
|
||||||
|
return (num * factorial(num - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// returns true if the user is currently focused on the calculator widget
|
||||||
|
function usingCalculator() {
|
||||||
|
let activeElement = document.activeElement;
|
||||||
|
while (true) {
|
||||||
|
if (!activeElement) return false;
|
||||||
|
if (activeElement.id === "calculator-wrapper") return true;
|
||||||
|
activeElement = activeElement.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const $ = q => document.querySelectorAll(q);
|
||||||
|
// key bindings for commonly used buttons
|
||||||
|
const keybindings = {
|
||||||
|
"0": "0",
|
||||||
|
"1": "1",
|
||||||
|
"2": "2",
|
||||||
|
"3": "3",
|
||||||
|
"4": "4",
|
||||||
|
"5": "5",
|
||||||
|
"6": "6",
|
||||||
|
"7": "7",
|
||||||
|
"8": "8",
|
||||||
|
"9": "9",
|
||||||
|
"Enter": "EQ",
|
||||||
|
".": "PT",
|
||||||
|
"+": "ADD",
|
||||||
|
"-": "SUB",
|
||||||
|
"*": "MLT",
|
||||||
|
"/": "DIV",
|
||||||
|
"%": "PRC",
|
||||||
|
"c": "CLR",
|
||||||
|
"(": "LP",
|
||||||
|
")": "RP",
|
||||||
|
"Backspace": "BCK",
|
||||||
|
}
|
||||||
|
window.addEventListener("keydown", event => {
|
||||||
|
if (!usingCalculator()) return;
|
||||||
|
if (event.key === "Enter" && document.activeElement.id !== "search-bar")
|
||||||
|
event.preventDefault();
|
||||||
|
if (keybindings[event.key])
|
||||||
|
document.getElementById("calc-btn-" + keybindings[event.key]).click();
|
||||||
|
})
|
||||||
|
// calculates the string
|
||||||
|
const calc = () => {
|
||||||
|
var mathtext = document.getElementById("calc-text");
|
||||||
|
var statement = mathtext.innerHTML
|
||||||
|
// remove empty ()
|
||||||
|
.replace("()", "")
|
||||||
|
// special constants
|
||||||
|
.replace("π", "(Math.PI)")
|
||||||
|
.replace("ℇ", "(Math.E)")
|
||||||
|
// turns 3(1+2) into 3*(1+2) (for example)
|
||||||
|
.replace(/(?<=[0-9\)])(?<=[^+\-x*\/%^])\(/, "x(")
|
||||||
|
// same except reversed
|
||||||
|
.replace(/\)(?=[0-9\(])(?=[^+\-x*\/%^])/, ")x")
|
||||||
|
// replace human friendly x with JS *
|
||||||
|
.replace("x", "*")
|
||||||
|
// trig & misc functions
|
||||||
|
.replace("sin", "Math.sin")
|
||||||
|
.replace("cos", "Math.cos")
|
||||||
|
.replace("tan", "Math.tan")
|
||||||
|
.replace("√", "Math.sqrt")
|
||||||
|
.replace("^", "**")
|
||||||
|
.replace("abs", "Math.abs")
|
||||||
|
.replace("log", "Math.log")
|
||||||
|
;
|
||||||
|
// add any missing )s to the end
|
||||||
|
while(true) if (
|
||||||
|
(statement.match(/\(/g) || []).length >
|
||||||
|
(statement.match(/\)/g) || []).length
|
||||||
|
) statement += ")"; else break;
|
||||||
|
// evaluate the expression.
|
||||||
|
console.log("calculating [" + statement + "]");
|
||||||
|
try {
|
||||||
|
var result = eval(statement);
|
||||||
|
document.getElementById("prev-equation").innerHTML = mathtext.innerHTML + " = ";
|
||||||
|
mathtext.innerHTML = result;
|
||||||
|
mathtext.classList.remove("error-border");
|
||||||
|
} catch (e) {
|
||||||
|
mathtext.classList.add("error-border");
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updateCalc = (e) => {
|
||||||
|
// character(s) recieved from button
|
||||||
|
var c = event.target.innerHTML;
|
||||||
|
var mathtext = document.getElementById("calc-text");
|
||||||
|
if (mathtext.innerHTML === "0") mathtext.innerHTML = "";
|
||||||
|
// special cases
|
||||||
|
switch (c) {
|
||||||
|
case "C":
|
||||||
|
// Clear
|
||||||
|
mathtext.innerHTML = "0";
|
||||||
|
break;
|
||||||
|
case "⬅":
|
||||||
|
// Delete
|
||||||
|
mathtext.innerHTML = mathtext.innerHTML.slice(0, -1);
|
||||||
|
if (mathtext.innerHTML.length === 0) {
|
||||||
|
mathtext.innerHTML = "0";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "=":
|
||||||
|
calc()
|
||||||
|
break;
|
||||||
|
case "sin":
|
||||||
|
case "cos":
|
||||||
|
case "tan":
|
||||||
|
case "log":
|
||||||
|
case "√":
|
||||||
|
mathtext.innerHTML += `${c}(`;
|
||||||
|
break;
|
||||||
|
case "|x|":
|
||||||
|
mathtext.innerHTML += "abs("
|
||||||
|
break;
|
||||||
|
case "+":
|
||||||
|
case "-":
|
||||||
|
case "x":
|
||||||
|
case "/":
|
||||||
|
case "%":
|
||||||
|
case "^":
|
||||||
|
if (mathtext.innerHTML.length === 0) mathtext.innerHTML = "0";
|
||||||
|
// prevent typing 2 operators in a row
|
||||||
|
if (mathtext.innerHTML.match(/[+\-x\/%^] $/))
|
||||||
|
mathtext.innerHTML = mathtext.innerHTML.slice(0, -3);
|
||||||
|
mathtext.innerHTML += ` ${c} `;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
mathtext.innerHTML += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let i of $("#calc-btns button")) {
|
||||||
|
i.addEventListener('click', event => {
|
||||||
|
updateCalc(event);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
40
app/templates/display.html
Executable file
|
@ -0,0 +1,40 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="shortcut icon" href="static/img/favicon.ico" type="image/x-icon">
|
||||||
|
<link rel="icon" href="static/img/favicon.ico" type="image/x-icon">
|
||||||
|
{% if not search_type %}
|
||||||
|
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
|
||||||
|
{% else %}
|
||||||
|
<link rel="search" href="opensearch.xml?tbm={{ search_type }}" type="application/opensearchdescription+xml" title="RaveSearch ({{ search_name }})">
|
||||||
|
{% endif %}
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="referrer" content="no-referrer">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('input.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('search.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('header.css') }}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "{{ cb_url('dark-theme.css') }}";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<title>{{ clean_query(query) }} - RaveSearch</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{ search_header|safe }}
|
||||||
|
{% if is_translation %}
|
||||||
|
<iframe
|
||||||
|
id="lingva-iframe"
|
||||||
|
src="{{ lingva_url }}/auto/{{ translate_to }}/{{ translate_str }}">
|
||||||
|
</iframe>
|
||||||
|
{% endif %}
|
||||||
|
{{ response|safe }}
|
||||||
|
</body>
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
{% if autocomplete_enabled == '1' %}
|
||||||
|
<script src="{{ cb_url('autocomplete.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script src="{{ cb_url('utils.js') }}"></script>
|
||||||
|
<script src="{{ cb_url('keyboard.js') }}"></script>
|
||||||
|
<script src="{{ cb_url('currency.js') }}"></script>
|
||||||
|
</html>
|
106
app/templates/error.html
Executable file
|
@ -0,0 +1,106 @@
|
||||||
|
{% if config.theme %}
|
||||||
|
{% if config.theme == 'system' %}
|
||||||
|
<style>
|
||||||
|
@import "{{ cb_url('light-theme.css') }}" screen;
|
||||||
|
@import "{{ cb_url('dark-theme.css') }}" screen and (prefers-color-scheme: dark);
|
||||||
|
</style>
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url(config.theme + '-theme.css') }}"/>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url(('dark' if config.dark else 'light') + '-theme.css') }}"/>
|
||||||
|
{% endif %}
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('error.css') }}">
|
||||||
|
<style>{{ config.style }}</style>
|
||||||
|
<div>
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>
|
||||||
|
{{ error_message }}
|
||||||
|
</p>
|
||||||
|
<hr>
|
||||||
|
{% if query and translation %}
|
||||||
|
<p>
|
||||||
|
<h4><a class="link" href="https://farside.link">{{ translation['continue-search'] }}</a></h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/benbusby/whoogle-search">Whoogle</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="{{farside}}/whoogle/search?q={{query}}{{params}}">
|
||||||
|
{{farside}}/whoogle/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/searxng/searxng">SearXNG</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="{{farside}}/searxng/search?q={{query}}">
|
||||||
|
{{farside}}/searxng/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
<h4>Other options:</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://kagi.com">Kagi</a>
|
||||||
|
<ul>
|
||||||
|
<li>Requires account</li>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="https://kagi.com/search?q={{query}}">
|
||||||
|
kagi.com/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://duckduckgo.com">DuckDuckGo</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="https://duckduckgo.com/search?q={{query}}">
|
||||||
|
duckduckgo.com/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://search.brave.com">Brave Search</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="https://search.brave.com/search?q={{query}}">
|
||||||
|
search.brave.com/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://ecosia.com">Ecosia</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="https://ecosia.com/search?q={{query}}">
|
||||||
|
ecosia.com/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://google.com">Google</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a class="link-color" href="https://google.com/search?q={{query}}">
|
||||||
|
google.com/search?q={{query}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<hr>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<a class="link" href="home">Return Home</a>
|
||||||
|
</div>
|
18
app/templates/footer.html
Executable file
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<p class="footer">
|
||||||
|
A RaveSearch a Whoogle v{{ version_number }} alapjait használja ||
|
||||||
|
<a class="link" href="https://github.com/benbusby/whoogle-search"
|
||||||
|
>{{ translation['github-link'] }}</a
|
||||||
|
></p>
|
||||||
|
<!-- {% if has_update %} ||
|
||||||
|
<span class="update_available">Update Available 🟢</span>
|
||||||
|
{% endif %} -->
|
||||||
|
<span>
|
||||||
|
<p class="rp-credit" style="color: white">
|
||||||
|
Szeretnél többet megtudni a keresőről? <a class="kattide" href="https://rp1.hu/leirasok/ravepriest1-kereso/">RavePriest1</a>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</footer>
|
89
app/templates/header.html
Executable file
|
@ -0,0 +1,89 @@
|
||||||
|
|
||||||
|
<div class="header-div">
|
||||||
|
<div id="search_header">
|
||||||
|
<a id="search_logo" href="{{ home_url }}" tabindex="0">
|
||||||
|
{{ logo|safe }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="search-form header" id="search-form" method="{{ 'get' if config.get_only else 'post' }}" role="search" action="search">
|
||||||
|
<div class="header-search">
|
||||||
|
<div class="autocomplete">
|
||||||
|
{% if config.preferences %}
|
||||||
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
|
{% endif %}
|
||||||
|
<input
|
||||||
|
id="search-bar"
|
||||||
|
class="mobile-search-bar"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
name="q"
|
||||||
|
type="text"
|
||||||
|
value="{{ clean_query(query) }}"
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
<input id="search-reset" type="reset" value="x">
|
||||||
|
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||||
|
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||||
|
<input type="submit" id="search-submit" style="display: none;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="header-tab-div">
|
||||||
|
<div class="mobile-header header-tab">
|
||||||
|
{% for tab_id, tab_content in tabs.items() %}
|
||||||
|
{% if tab_content['selected'] %}
|
||||||
|
<a><button class="header-button selected-header-button">{{ tab_content['name'] }}</button></a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ tab_content['href'] }}"><button class="header-button">{{ tab_content['name'] }}</button></a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<label for="adv-search-toggle" id="adv-search-label" class="adv-search">⚙</label>
|
||||||
|
<input id="adv-search-toggle" type="checkbox">
|
||||||
|
<div class="header-tab-div-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-collapsible" id="adv-search-div">
|
||||||
|
<div class="result-config">
|
||||||
|
<label for="config-country">{{ translation['config-country'] }}: </label>
|
||||||
|
<select name="country" id="result-country">
|
||||||
|
{% for country in countries %}
|
||||||
|
<option value="{{ country.value }}"
|
||||||
|
{% if (
|
||||||
|
config.country != '' and config.country in country.value
|
||||||
|
) or (
|
||||||
|
config.country == '' and country.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ country.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<br /><br />
|
||||||
|
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
|
||||||
|
<select name="tbs" id="result-time-period">
|
||||||
|
{% for time_period in time_periods %}
|
||||||
|
<option value="{{ time_period.value }}"
|
||||||
|
{% if (
|
||||||
|
config.tbs != '' and config.tbs in time_period.value
|
||||||
|
) or (
|
||||||
|
config.tbs == '' and time_period.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ translation[time_period.value] }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
210
app/templates/header2.html
Executable file
|
@ -0,0 +1,210 @@
|
||||||
|
{% if mobile %}
|
||||||
|
<header>
|
||||||
|
<div class="header-div">
|
||||||
|
<form class="search-form header"
|
||||||
|
id="search-form"
|
||||||
|
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||||
|
<a class="logo-link mobile-logo" href="{{ home_url }}">
|
||||||
|
<div id="mobile-header-logo">
|
||||||
|
{{ logo|safe }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="H0PQec mobile-input-div">
|
||||||
|
<div class="autocomplete-mobile esbc autocomplete">
|
||||||
|
{% if config.preferences %}
|
||||||
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
|
{% endif %}
|
||||||
|
<input
|
||||||
|
id="search-bar"
|
||||||
|
class="mobile-search-bar"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
spellcheck="false"
|
||||||
|
class="search-bar-input"
|
||||||
|
name="q"
|
||||||
|
type="text"
|
||||||
|
value="{{ clean_query(query) }}"
|
||||||
|
dir="auto">
|
||||||
|
<input id="search-reset" type="reset" value="x">
|
||||||
|
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||||
|
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||||
|
<input type="submit" style="display: none;">
|
||||||
|
<div class="sc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="header-tab-div">
|
||||||
|
<div class="header-tab-div-2">
|
||||||
|
<div class="header-tab-div-3">
|
||||||
|
<div class="mobile-header header-tab">
|
||||||
|
{% for tab_id, tab_content in tabs.items() %}
|
||||||
|
{% if tab_content['selected'] %}
|
||||||
|
<span class="mobile-tab-span">{{ tab_content['name'] }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<label for="adv-search-toggle" id="adv-search-label" class="adv-search">⚙</label>
|
||||||
|
<input id="adv-search-toggle" type="checkbox">
|
||||||
|
<div class="header-tab-div-end"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="" id="s">
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{% else %}
|
||||||
|
<header>
|
||||||
|
<div class="logo-div">
|
||||||
|
<a class="logo-link" href="{{ home_url }}">
|
||||||
|
<div class="desktop-header-logo">
|
||||||
|
{{ logo|safe }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="search-div">
|
||||||
|
<form id="search-form"
|
||||||
|
class="search-form"
|
||||||
|
id="sf"
|
||||||
|
method="{{ 'GET' if config.get_only else 'POST' }}">
|
||||||
|
<div class="autocomplete header-autocomplete">
|
||||||
|
<div style="width: 100%; display: flex">
|
||||||
|
{% if config.preferences %}
|
||||||
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
|
{% endif %}
|
||||||
|
<input
|
||||||
|
id="search-bar"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="search-bar-desktop search-bar-input"
|
||||||
|
name="q"
|
||||||
|
spellcheck="false"
|
||||||
|
type="text"
|
||||||
|
value="{{ clean_query(query) }}"
|
||||||
|
dir="auto">
|
||||||
|
<input name="tbm" value="{{ search_type }}" style="display: none">
|
||||||
|
<input name="country" value="{{ config.country }}" style="display: none;">
|
||||||
|
<input name="tbs" value="{{ config.tbs }}" style="display: none;">
|
||||||
|
<input type="submit" style="display: none;">
|
||||||
|
<div class="sc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<div class="header-tab-div">
|
||||||
|
<div class="header-tab-div-2">
|
||||||
|
<div class="header-tab-div-3">
|
||||||
|
<div class="desktop-header header-tab">
|
||||||
|
{% for tab_id, tab_content in tabs.items() %}
|
||||||
|
{% if tab_content['selected'] %}
|
||||||
|
<span class="header-tab-span">{{ tab_content['name'] }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a class="header-tab-a" href="{{ tab_content['href'] }}">{{ tab_content['name'] }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<label for="adv-search-toggle" id="adv-search-label" class="adv-search">⚙</label>
|
||||||
|
<input id="adv-search-toggle" type="checkbox">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="" id="s">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="result-collapsible" id="adv-search-div">
|
||||||
|
<div class="result-config">
|
||||||
|
<label for="config-country">{{ translation['config-country'] }}: </label>
|
||||||
|
<select name="country" id="result-country">
|
||||||
|
{% for country in countries %}
|
||||||
|
<option value="{{ country.value }}"
|
||||||
|
{% if (
|
||||||
|
config.country != '' and config.country in country.value
|
||||||
|
) or (
|
||||||
|
config.country == '' and country.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ country.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<br />
|
||||||
|
<label for="config-time-period">{{ translation['config-time-period'] }}: </label>
|
||||||
|
<select name="tbs" id="result-time-period">
|
||||||
|
{% for time_period in time_periods %}
|
||||||
|
<option value="{{ time_period.value }}"
|
||||||
|
{% if (
|
||||||
|
config.tbs != '' and config.tbs in time_period.value
|
||||||
|
) or (
|
||||||
|
config.tbs == '' and time_period.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ translation[time_period.value] }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ cb_url('header.js') }}"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="search_view">
|
||||||
|
<div class="search_box">
|
||||||
|
<input id="q" name="q" type="text" placeholder="{{ _('Search for...') }}" tabindex="1" autocomplete="off" autocapitalize="none" spellcheck="false" autocorrect="off" dir="auto" value="{{ q or '' }}">
|
||||||
|
<button id="clear_search" type="reset" aria-label="{{ _('clear') }}" class="hide_if_nojs"><span>{{ icon_big('close') }}</span><span class="show_if_nojs">{{ _('clear') }}</span></button>
|
||||||
|
<button id="send_search" type="submit" {%- if search_on_category_select -%}name="category_{{ selected_categories[0]|replace(' ', '_') }}"{%- endif -%} aria-label="{{ _('search') }}"><span class="hide_if_nojs">{{ icon_big('search-outline') }}</span><span class="show_if_nojs">{{ _('search') }}</span></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% set display_tooltip = true %}
|
||||||
|
{% include 'simple/categories.html' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search_filters">
|
||||||
|
{% include 'simple/filters/languages.html' %}
|
||||||
|
{% include 'simple/filters/time_range.html' %}
|
||||||
|
{% include 'simple/filters/safesearch.html' %}
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="theme" value="{{ theme }}" >
|
||||||
|
{% if timeout_limit %}<input type="hidden" name="timeout_limit" value="{{ timeout_limit|e }}" >{% endif %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="categories" class="search_categories">{{- '' -}}
|
||||||
|
<div id="categories_container">
|
||||||
|
{%- if not search_on_category_select or not display_tooltip -%}
|
||||||
|
{%- for category in categories_as_tabs -%}
|
||||||
|
<div class="category category_checkbox">{{- '' -}}
|
||||||
|
<input type="checkbox" id="checkbox_{{ category|replace(' ', '_') }}" name="category_{{ category }}"{% if category in selected_categories %} checked="checked"{% endif %}/>
|
||||||
|
<label for="checkbox_{{ category|replace(' ', '_') }}" class="tooltips">
|
||||||
|
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
|
||||||
|
<div class="category_name">{{- _(category) -}}</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{%- endfor -%}
|
||||||
|
{%- if display_tooltip %}<div class="help">{{ _('Click on the magnifier to perform search') }}</div>{% endif -%}
|
||||||
|
{%- else -%}
|
||||||
|
{%- for category in categories_as_tabs -%}{{- '\n' -}}
|
||||||
|
<button type="submit" name="category_{{ category }}" class="category category_button {% if category in selected_categories %}selected{% endif %}">
|
||||||
|
{{- icon_big(category_icons[category]) if category in category_icons else icon_big('globe-outline') -}}
|
||||||
|
<div class="category_name">{{- _(category) -}}</div>{{- '' -}}
|
||||||
|
</button>{{- '' -}}
|
||||||
|
{%- endfor -%}
|
||||||
|
{{- '\n' -}}
|
||||||
|
{%- endif -%}
|
||||||
|
</div>{{- '' -}}
|
||||||
|
</div>
|
451
app/templates/imageresults.html
Executable file
|
@ -0,0 +1,451 @@
|
||||||
|
<div>
|
||||||
|
<!-- <style>
|
||||||
|
html {
|
||||||
|
font-family:
|
||||||
|
Roboto,
|
||||||
|
Helvetica Neue,
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
color: #3c4043;
|
||||||
|
word-wrap: break-word;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 0 8px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 900px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
a img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.FbhRzb {
|
||||||
|
border-left: thin solid #dadce0;
|
||||||
|
border-right: thin solid #dadce0;
|
||||||
|
border-top: thin solid #dadce0;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.n692Zd {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.cvifge {
|
||||||
|
height: 40px;
|
||||||
|
border-spacing: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.QvGUP {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 8px 0 8px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.O4cRJf {
|
||||||
|
height: 40px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
.O1ePr {
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.kgJEQe {
|
||||||
|
height: 36px;
|
||||||
|
width: 98px;
|
||||||
|
vertical-align: top;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.lXLRf {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.MhzMZd {
|
||||||
|
border: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-size: 14px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.xB0fq {
|
||||||
|
height: 40px;
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: #4285f4;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 16px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.xB0fq:focus {
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
.M7pB2 {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
margin: 0 0 3px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.euZec {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
table.euZec td {
|
||||||
|
padding: 0;
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
.QIqI7 {
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4285f4;
|
||||||
|
}
|
||||||
|
.EY24We {
|
||||||
|
border-bottom: 2px solid #4285f4;
|
||||||
|
}
|
||||||
|
.CsQyDc {
|
||||||
|
display: inline-block;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.TuS8Ad {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.HddGcc {
|
||||||
|
padding: 8px;
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.dzp8ae {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #3c4043;
|
||||||
|
}
|
||||||
|
.rEM8G {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.bookcf {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
.InWNIe {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.uZgmoc {
|
||||||
|
width: 350px;
|
||||||
|
display: flex;
|
||||||
|
border: none;
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
/* background-color: #29292987; */
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 35px auto;
|
||||||
|
padding: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.frGj1b {
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.BnJWBc {
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 0 13px 0;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.X6ZCif {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
display: inline-block;
|
||||||
|
padding-top: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.TwVfHd {
|
||||||
|
border-radius: 16px;
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.yekiAe {
|
||||||
|
background-color: #dadce0;
|
||||||
|
}
|
||||||
|
.svla5d {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #29292987;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
display: grid;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
.ezO2md {
|
||||||
|
border: thin solid #dadce0;
|
||||||
|
padding: 12px 16px 12px 16px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.K35ahc {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.owohpf {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.fYyStc {
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
.img_button {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 auto 10px;
|
||||||
|
border: 1px solid #84dbff;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #3c4043;
|
||||||
|
background-color: #2e97c3;
|
||||||
|
}
|
||||||
|
.ynsChf {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.Fj3V3b {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
.FrIlee {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.F9iS2e {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.WMQ2Le {
|
||||||
|
color: #70757a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.x3G5ab {
|
||||||
|
color: #202124;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.fuLhoc {
|
||||||
|
color: #1967d2;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
.epoveb {
|
||||||
|
font-size: 32px;
|
||||||
|
line-height: 40px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #202124;
|
||||||
|
}
|
||||||
|
.dXDvrc {
|
||||||
|
color: #0d652d;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.dloBPe {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.YVIcad {
|
||||||
|
color: #70757a;
|
||||||
|
}
|
||||||
|
.JkVVdd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.oXZRFd {
|
||||||
|
color: #ea4335;
|
||||||
|
}
|
||||||
|
.MQHtg {
|
||||||
|
color: #fbbc04;
|
||||||
|
}
|
||||||
|
.pyMRrb {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.EtTZid {
|
||||||
|
color: #1e8e3e;
|
||||||
|
}
|
||||||
|
.M3vVJe {
|
||||||
|
color: #1967d2;
|
||||||
|
}
|
||||||
|
.NHQNef {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.Cb8Z7c {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
a.ZWRArf {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a .CVA68e:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.results {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.result-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #29292987;
|
||||||
|
width: calc(33.333% - 16px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-title {
|
||||||
|
color: var(--primary-grad3);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-url {
|
||||||
|
color: var(--body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-url:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--grad2-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stílusok a javaslatokhoz */
|
||||||
|
.suggestions {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stílusok a lapozáshoz */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a {
|
||||||
|
margin: 0 5px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background-color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
color: #007bff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination a:hover {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.e3goi {
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TxbwNb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RAyV4b {
|
||||||
|
width: 100%;
|
||||||
|
height: 130px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t0fcAb {
|
||||||
|
scale: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Tor4Ec {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qXLe6d {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style> -->
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
{% for result in results %}
|
||||||
|
<div class="result-item">
|
||||||
|
<a href="{{ result.web_page }}">
|
||||||
|
<img src="{{ result.img_tbn }}" alt="Image thumbnail">
|
||||||
|
</a>
|
||||||
|
<a href="{{ result.web_page }}"><p class="img-title">{{ result.domain }}</p></a>
|
||||||
|
<a href="{{ result.img_url }}"><p class="img-url">{{ result.img_url }}</p></a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uZgmoc">
|
||||||
|
<!-- next page object -->
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
|
||||||
|
<div class="lIMUZd">
|
||||||
|
<div class="By0U9">
|
||||||
|
<!-- correction suggested -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
303
app/templates/index.html
Executable file
|
@ -0,0 +1,303 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="static/img/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="static/img/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="static/img/favicon/site.webmanifest">
|
||||||
|
<link rel="mask-icon" href="static/img/favicon/safari-pinned-tab.svg" color="#00566f">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
|
||||||
|
{% if autocomplete_enabled == '1' %}
|
||||||
|
<script src="{{ cb_url('autocomplete.js') }}"></script>
|
||||||
|
{% endif %}
|
||||||
|
<script type="text/javascript" src="{{ cb_url('controller.js') }}"></script>
|
||||||
|
<link rel="search" href="opensearch.xml" type="application/opensearchdescription+xml" title="RaveSearch">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('logo.css') }}">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "{{ cb_url('dark-theme.css') }}";
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ cb_url('main.css') }}">
|
||||||
|
|
||||||
|
<title>RaveSearch</title>
|
||||||
|
</head>
|
||||||
|
<body id="main_index">
|
||||||
|
<main class="search-container">
|
||||||
|
<div class="logo-container">
|
||||||
|
{{ logo|safe }}
|
||||||
|
</div>
|
||||||
|
<form id="search-form" action="search" method="{{ 'get' if config.get_only else 'post' }}">
|
||||||
|
<div class="search-fields">
|
||||||
|
<div class="autocomplete">
|
||||||
|
{% if config.preferences %}
|
||||||
|
<input type="hidden" name="preferences" value="{{ config.preferences }}" />
|
||||||
|
{% endif %}
|
||||||
|
<input
|
||||||
|
placeholder="{{ translation['input-text'] }}"
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
id="search-bar"
|
||||||
|
autofocus="autofocus"
|
||||||
|
autocapitalize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
autocomplete="off"
|
||||||
|
dir="auto" />
|
||||||
|
</div>
|
||||||
|
<input type="submit" id="search-submit" value="{{ translation['search'] }}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if not config_disabled %}
|
||||||
|
<button id="config-collapsible" class="collapsible">{{ translation['config'] }}</button>
|
||||||
|
<div class="content">
|
||||||
|
<div class="config-fields">
|
||||||
|
<form id="config-form" action="config" method="post">
|
||||||
|
<div class="config-options">
|
||||||
|
<!--<div class="config-div config-div-country">
|
||||||
|
<label for="config-country">{{ translation['config-country'] }}: </label>
|
||||||
|
<select name="country" id="config-country">
|
||||||
|
{% for country in countries %}
|
||||||
|
<option value="{{ country.value }}"
|
||||||
|
{% if (
|
||||||
|
config.country != '' and config.country in country.value
|
||||||
|
) or (
|
||||||
|
config.country == '' and country.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ country.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="config-div">
|
||||||
|
<label for="config-time-period">{{ translation['config-time-period'] }}</label>
|
||||||
|
<select name="tbs" id="config-time-period">
|
||||||
|
{% for time_period in time_periods %}
|
||||||
|
<option value="{{ time_period.value }}"
|
||||||
|
{% if (
|
||||||
|
config.tbs != '' and config.tbs in time_period.value
|
||||||
|
) or (
|
||||||
|
config.tbs == '' and time_period.value == '')
|
||||||
|
%}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ translation[time_period.value] }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div> -->
|
||||||
|
<div class="config-div config-div-lang">
|
||||||
|
<label for="config-lang-interface">{{ translation['config-lang'] }}: </label>
|
||||||
|
<select name="lang_interface" id="config-lang-interface">
|
||||||
|
{% for lang in languages %}
|
||||||
|
<option value="{{ lang.value }}"
|
||||||
|
{% if lang.value in config.lang_interface %}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ lang.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-search-lang">
|
||||||
|
<label for="config-lang-search">{{ translation['config-lang-search'] }}: </label>
|
||||||
|
<select name="lang_search" id="config-lang-search">
|
||||||
|
{% for lang in languages %}
|
||||||
|
<option value="{{ lang.value }}"
|
||||||
|
{% if lang.value in config.lang_search %}
|
||||||
|
selected
|
||||||
|
{% endif %}>
|
||||||
|
{{ lang.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="config-div config-div-autocomplete">
|
||||||
|
<label for="config-autocomplete">autocomplete</label>
|
||||||
|
<input type="checkbox" name="autocomplete" id="config-autocomplete" {{ 'checked' if config.autocomplete else '' }}>
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="config-div config-div-near">
|
||||||
|
<label for="config-near">{{ translation['config-near'] }}: </label>
|
||||||
|
<input type="text" name="near" id="config-near"
|
||||||
|
placeholder="{{ translation['config-near-help'] }}" value="{{ config.near }}">
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="config-div config-div-block">
|
||||||
|
<label for="config-block">{{ translation['config-block'] }}: </label>
|
||||||
|
<input type="text" name="block" id="config-block"
|
||||||
|
placeholder="{{ translation['config-block-help'] }}" value="{{ config.block }}">
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="config-div config-div-block">
|
||||||
|
<label for="config-block-title">{{ translation['config-block-title'] }}: </label>
|
||||||
|
<input type="text" name="block_title" id="config-block"
|
||||||
|
placeholder="{{ translation['config-block-title-help'] }}"
|
||||||
|
value="{{ config.block_title }}">
|
||||||
|
</div> -->
|
||||||
|
<!-- <div class="config-div config-div-block">
|
||||||
|
<label for="config-block-url">{{ translation['config-block-url'] }}: </label>
|
||||||
|
<input type="text" name="block_url" id="config-block"
|
||||||
|
placeholder="{{ translation['config-block-url-help'] }}" value="{{ config.block_url }}">
|
||||||
|
</div> -->
|
||||||
|
<div class="config-div config-div-anon-view">
|
||||||
|
<label for="config-anon-view">{{ translation['config-anon-view'] }}: </label>
|
||||||
|
<input type="checkbox" name="anon_view" id="config-anon-view" {{ 'checked' if config.anon_view else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-nojs">
|
||||||
|
<label for="config-nojs">{{ translation['config-nojs'] }}: </label>
|
||||||
|
<input type="checkbox" name="nojs" id="config-nojs" {{ 'checked' if config.nojs else '' }}>
|
||||||
|
</div>
|
||||||
|
<!-- DEPRECATED -->
|
||||||
|
<!--<div class="config-div config-div-dark">-->
|
||||||
|
<!--<label for="config-dark">{{ translation['config-dark'] }}: </label>-->
|
||||||
|
<!--<input type="checkbox" name="dark" id="config-dark" {{ 'checked' if config.dark else '' }}>-->
|
||||||
|
<!--</div>-->
|
||||||
|
<div class="config-div config-div-safe">
|
||||||
|
<label for="config-safe">{{ translation['config-safe'] }}: </label>
|
||||||
|
<input type="checkbox" name="safe" id="config-safe" {{ 'checked' if config.safe else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-alts">
|
||||||
|
<label class="tooltip" for="config-alts">{{ translation['config-alts'] }}: </label>
|
||||||
|
<input type="checkbox" name="alts" id="config-alts" {{ 'checked' if config.alts else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-alts-help'] }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-new-tab">
|
||||||
|
<label for="config-new-tab">{{ translation['config-new-tab'] }}: </label>
|
||||||
|
<input type="checkbox" name="new_tab"
|
||||||
|
id="config-new-tab" {{ 'checked' if config.new_tab else '' }}>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="config-div config-div-view-image">
|
||||||
|
<label for="config-view-image">{{ translation['config-images'] }}: </label>
|
||||||
|
<input checked type="checkbox" name="view_image"
|
||||||
|
id="config-view-image" {{ 'checked' if config.view_image else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-images-help'] }}</span></div>
|
||||||
|
</div> -->
|
||||||
|
<div class="config-div config-div-tor">
|
||||||
|
<label for="config-tor">{{ translation['config-tor'] }}: {{ '' if tor_available else 'Unavailable' }}</label>
|
||||||
|
<input type="checkbox" name="tor"
|
||||||
|
id="config-tor" {{ '' if tor_available else 'hidden' }} {{ 'checked' if config.tor else '' }}>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-get-only">
|
||||||
|
<label for="config-get-only">{{ translation['config-get-only'] }}: </label>
|
||||||
|
<input type="checkbox" name="get_only"
|
||||||
|
id="config-get-only" {{ 'checked' if config.get_only else '' }}>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="config-div config-div-accept-language">
|
||||||
|
<label for="config-accept-language">Set Accept-Language: </label>
|
||||||
|
<input type="checkbox" name="accept_language"
|
||||||
|
id="config-accept-language" {{ 'checked' if config.accept_language else '' }}>
|
||||||
|
</div> -->
|
||||||
|
<div class="config-div config-div-root-url">
|
||||||
|
<label for="config-url">{{ translation['config-url'] }}: </label>
|
||||||
|
<input type="text" name="url" id="config-url" value="{{ config.url }}">
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-custom-css">
|
||||||
|
<a id="css-link"
|
||||||
|
href="https://github.com/benbusby/whoogle-search/wiki/User-Contributed-CSS-Themes">
|
||||||
|
{{ translation['config-css'] }}:
|
||||||
|
</a>
|
||||||
|
<textarea
|
||||||
|
name="style_modified"
|
||||||
|
id="config-style"
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
value="">{{ config.style_modified.replace('\t', '') }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-div-pref-url">
|
||||||
|
<label for="config-pref-encryption">{{ translation['config-pref-encryption'] }}: </label>
|
||||||
|
<input type="checkbox" name="preferences_encrypted"
|
||||||
|
id="config-pref-encryption" {{ 'checked' if config.preferences_encrypted and config.preferences_key else '' }}>
|
||||||
|
<div><span class="info-text"> — {{ translation['config-pref-help'] }}</span></div>
|
||||||
|
<label for="config-pref-url">{{ translation['config-pref-url'] }}: </label>
|
||||||
|
<input type="text" name="pref-url" id="config-pref-url" value="{{ config.url }}?preferences={{ config.preferences }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="config-div config-buttons">
|
||||||
|
<input type="submit" id="config-load" value="{{ translation['load'] }}">
|
||||||
|
<input type="submit" id="config-submit" value="{{ translation['apply'] }}">
|
||||||
|
<input type="submit" id="config-save" value="{{ translation['save-as'] }}">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="social-container">
|
||||||
|
<div class="social-column">
|
||||||
|
<div class="rpkick">
|
||||||
|
<a href="https://kick.com/ravepriest1">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rpkicksearch.png"
|
||||||
|
alt="rpkick"
|
||||||
|
title="RP Kick"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<div class="rpdiscord">
|
||||||
|
<a href="https://discord.com/invite/MwUydP4gc5">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rpdiscordsearch.png"
|
||||||
|
alt="rpdiscord"
|
||||||
|
title="RP Discord"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<div class="rptwitch">
|
||||||
|
<a href="https://www.twitch.tv/ravepriest1">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rptwitchsearch.png"
|
||||||
|
alt="rptwitch"
|
||||||
|
title="RP Twitch"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<div class="rpgit">
|
||||||
|
<a href="https://git.rp1.hu/explore/repos">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rpgiteasearch.png"
|
||||||
|
alt="rpgit"
|
||||||
|
title="RP Gitea"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<div class="rpyoutube">
|
||||||
|
<a href="https://www.youtube.com/@RPslair">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rpyoutubesearch.png"
|
||||||
|
alt="rpyoutube"
|
||||||
|
title="RP YouTube"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
<div class="rpweb">
|
||||||
|
<a href="https://rp1.hu">
|
||||||
|
<img
|
||||||
|
src="static/img/rp/rpwebsearch.png"
|
||||||
|
alt="rpweb"
|
||||||
|
title="RP Website"
|
||||||
|
width="200px"
|
||||||
|
height="50px"
|
||||||
|
/></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% include 'footer.html' %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
8
app/templates/logo.html
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="rplogo">
|
||||||
|
<img
|
||||||
|
src="static/img/rplogo.webp"
|
||||||
|
alt="rplogo"
|
||||||
|
title="RP Kereső"
|
||||||
|
style="object-fit: contain; height: auto; width: 100%;"
|
||||||
|
/>
|
||||||
|
</div>
|
25
app/templates/opensearch.xml
Executable file
15
app/templates/search.html
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
<form id="search-form" action="search" method="post">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
style="width: 90%"
|
||||||
|
autofocus="autofocus"
|
||||||
|
autocapitalize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
autocorrect="off"
|
||||||
|
placeholder="RaveSearch"
|
||||||
|
autocomplete="off"
|
||||||
|
dir="auto"
|
||||||
|
/>
|
||||||
|
<input type="submit" style="width: 9%" id="search-submit" value="Search" />
|
||||||
|
</form>
|
BIN
app/utils/.DS_Store
vendored
Executable file
0
app/utils/__init__.py
Executable file
146
app/utils/bangs.py
Executable file
|
@ -0,0 +1,146 @@
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
bangs_dict = {}
|
||||||
|
DDG_BANGS = 'https://duckduckgo.com/bang.js'
|
||||||
|
|
||||||
|
|
||||||
|
def load_all_bangs(ddg_bangs_file: str, ddg_bangs: dict = {}):
|
||||||
|
"""Loads all the bang files in alphabetical order
|
||||||
|
Args:
|
||||||
|
ddg_bangs_file: The str path to the new DDG bangs json file
|
||||||
|
ddg_bangs: The dict of ddg bangs. If this is empty, it will load the
|
||||||
|
bangs from the file
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
global bangs_dict
|
||||||
|
ddg_bangs_file = os.path.normpath(ddg_bangs_file)
|
||||||
|
|
||||||
|
if (bangs_dict and not ddg_bangs) or os.path.getsize(ddg_bangs_file) <= 4:
|
||||||
|
return
|
||||||
|
|
||||||
|
bangs = {}
|
||||||
|
bangs_dir = os.path.dirname(ddg_bangs_file)
|
||||||
|
bang_files = glob.glob(os.path.join(bangs_dir, '*.json'))
|
||||||
|
|
||||||
|
# Normalize the paths
|
||||||
|
bang_files = [os.path.normpath(f) for f in bang_files]
|
||||||
|
|
||||||
|
# Move the ddg bangs file to the beginning
|
||||||
|
bang_files = sorted([f for f in bang_files if f != ddg_bangs_file])
|
||||||
|
|
||||||
|
if ddg_bangs:
|
||||||
|
bangs |= ddg_bangs
|
||||||
|
else:
|
||||||
|
bang_files.insert(0, ddg_bangs_file)
|
||||||
|
|
||||||
|
for i, bang_file in enumerate(bang_files):
|
||||||
|
try:
|
||||||
|
bangs |= json.load(open(bang_file))
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
# Ignore decoding error only for the ddg bangs file, since this can
|
||||||
|
# occur if file is still being written
|
||||||
|
if i != 0:
|
||||||
|
raise
|
||||||
|
|
||||||
|
bangs_dict = dict(sorted(bangs.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def gen_bangs_json(bangs_file: str) -> None:
|
||||||
|
"""Generates a json file from the DDG bangs list
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bangs_file: The str path to the new DDG bangs json file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Request full list from DDG
|
||||||
|
r = requests.get(DDG_BANGS)
|
||||||
|
r.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as err:
|
||||||
|
raise SystemExit(err)
|
||||||
|
|
||||||
|
# Convert to json
|
||||||
|
data = json.loads(r.text)
|
||||||
|
|
||||||
|
# Set up a json object (with better formatting) for all available bangs
|
||||||
|
bangs_data = {}
|
||||||
|
|
||||||
|
for row in data:
|
||||||
|
bang_command = '!' + row['t']
|
||||||
|
bangs_data[bang_command] = {
|
||||||
|
'url': row['u'].replace('{{{s}}}', '{}'),
|
||||||
|
'suggestion': bang_command + ' (' + row['s'] + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
json.dump(bangs_data, open(bangs_file, 'w'))
|
||||||
|
print('* Finished creating ddg bangs json')
|
||||||
|
load_all_bangs(bangs_file, bangs_data)
|
||||||
|
|
||||||
|
|
||||||
|
def suggest_bang(query: str) -> list[str]:
|
||||||
|
"""Suggests bangs for a user's query
|
||||||
|
Args:
|
||||||
|
query: The search query
|
||||||
|
Returns:
|
||||||
|
list[str]: A list of bang suggestions
|
||||||
|
"""
|
||||||
|
global bangs_dict
|
||||||
|
return [bangs_dict[_]['suggestion'] for _ in bangs_dict if _.startswith(query)]
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_bang(query: str) -> str:
|
||||||
|
"""Transform's a user's query to a bang search, if an operator is found
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: The search query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A formatted redirect for a bang search, or an empty str if there
|
||||||
|
wasn't a match or didn't contain a bang operator
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
global bangs_dict
|
||||||
|
|
||||||
|
#if ! not in query simply return (speed up processing)
|
||||||
|
if '!' not in query:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
split_query = query.strip().split(' ')
|
||||||
|
|
||||||
|
# look for operator in query if one is found, list operator should be of
|
||||||
|
# length 1, operator should not be case-sensitive here to remove it later
|
||||||
|
operator = [
|
||||||
|
word
|
||||||
|
for word in split_query
|
||||||
|
if word.lower() in bangs_dict
|
||||||
|
]
|
||||||
|
if len(operator) == 1:
|
||||||
|
# get operator
|
||||||
|
operator = operator[0]
|
||||||
|
|
||||||
|
# removes operator from query
|
||||||
|
split_query.remove(operator)
|
||||||
|
|
||||||
|
# rebuild the query string
|
||||||
|
bang_query = ' '.join(split_query).strip()
|
||||||
|
|
||||||
|
# Check if operator is a key in bangs and get bang if exists
|
||||||
|
bang = bangs_dict.get(operator.lower(), None)
|
||||||
|
if bang:
|
||||||
|
bang_url = bang['url']
|
||||||
|
|
||||||
|
if bang_query:
|
||||||
|
return bang_url.replace('{}', bang_query, 1)
|
||||||
|
else:
|
||||||
|
parsed_url = urlparse.urlparse(bang_url)
|
||||||
|
return f'{parsed_url.scheme}://{parsed_url.netloc}'
|
||||||
|
return ''
|
137
app/utils/misc.py
Executable file
|
@ -0,0 +1,137 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import contextlib
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from requests import exceptions, get
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from bs4 import BeautifulSoup as bsoup
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from flask import Request
|
||||||
|
|
||||||
|
ddg_favicon_site = 'http://icons.duckduckgo.com/ip2'
|
||||||
|
|
||||||
|
empty_gif = base64.b64decode(
|
||||||
|
'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==')
|
||||||
|
|
||||||
|
placeholder_img = base64.b64decode(
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAABF0lEQVRIS8XWPw9EMBQA8Eok' \
|
||||||
|
'JBKrMFqMBt//GzAYLTZ/VomExPDu6uLiaPteqVynBn0/75W2Vp7nEIYhe6p1XcespmmAd7Is' \
|
||||||
|
'M+4URcGiKPogvMMvmIS2eN9MOMKbKWgf54SYgI4vKkTuQKJKSJErkKzUSkQHUs0lilAg7GMh' \
|
||||||
|
'ISoIA/hYMiKCKIA2soeowCWEMkfHtUmrXLcyGYYBfN9HF8djiaglWzNZlgVs21YisoAUaEXG' \
|
||||||
|
'cQTP86QIFgi7vyLzPIPjOEIEC7ANQv/4aZrAdd0TUtc1i+MYnSsMWjPp+x6CIPgJVlUVS5KE' \
|
||||||
|
'DKig/+wnVzM4pnzaGeHd+ENlWbI0TbVLJBtw2uMfP63wc9d2kDCWxi5Q27bsBerSJ9afJbeL' \
|
||||||
|
'AAAAAElFTkSuQmCC'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_favicon(url: str) -> bytes:
|
||||||
|
"""Fetches a favicon using DuckDuckGo's favicon retriever
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The url to fetch the favicon from
|
||||||
|
Returns:
|
||||||
|
bytes - the favicon bytes, or a placeholder image if one
|
||||||
|
was not returned
|
||||||
|
"""
|
||||||
|
response = get(f'{ddg_favicon_site}/{urlparse(url).netloc}.ico')
|
||||||
|
|
||||||
|
if response.status_code == 200 and len(response.content) > 0:
|
||||||
|
tmp_mem = io.BytesIO()
|
||||||
|
tmp_mem.write(response.content)
|
||||||
|
tmp_mem.seek(0)
|
||||||
|
|
||||||
|
return tmp_mem.read()
|
||||||
|
return placeholder_img
|
||||||
|
|
||||||
|
|
||||||
|
def gen_file_hash(path: str, static_file: str) -> str:
|
||||||
|
file_contents = open(os.path.join(path, static_file), 'rb').read()
|
||||||
|
file_hash = hashlib.md5(file_contents).hexdigest()[:8]
|
||||||
|
filename_split = os.path.splitext(static_file)
|
||||||
|
|
||||||
|
return f'{filename_split[0]}.{file_hash}{filename_split[-1]}'
|
||||||
|
|
||||||
|
|
||||||
|
def read_config_bool(var: str, default: bool=False) -> bool:
|
||||||
|
val = os.getenv(var, '1' if default else '0')
|
||||||
|
# user can specify one of the following values as 'true' inputs (all
|
||||||
|
# variants with upper case letters will also work):
|
||||||
|
# ('true', 't', '1', 'yes', 'y')
|
||||||
|
return val.lower() in ('true', 't', '1', 'yes', 'y')
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(r: Request) -> str:
|
||||||
|
if r.environ.get('HTTP_X_FORWARDED_FOR') is None:
|
||||||
|
return r.environ['REMOTE_ADDR']
|
||||||
|
return r.environ['HTTP_X_FORWARDED_FOR']
|
||||||
|
|
||||||
|
|
||||||
|
def get_request_url(url: str) -> str:
|
||||||
|
if os.getenv('HTTPS_ONLY', False):
|
||||||
|
return url.replace('http://', 'https://', 1)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def get_proxy_host_url(r: Request, default: str, root=False) -> str:
|
||||||
|
scheme = r.headers.get('X-Forwarded-Proto', 'https')
|
||||||
|
http_host = r.headers.get('X-Forwarded-Host')
|
||||||
|
|
||||||
|
full_path = r.full_path if not root else ''
|
||||||
|
if full_path.startswith('/'):
|
||||||
|
full_path = f'/{full_path}'
|
||||||
|
|
||||||
|
if http_host:
|
||||||
|
prefix = os.environ.get('WHOOGLE_URL_PREFIX', '')
|
||||||
|
if prefix:
|
||||||
|
prefix = f'/{re.sub("[^0-9a-zA-Z]+", "", prefix)}'
|
||||||
|
return f'{scheme}://{http_host}{prefix}{full_path}'
|
||||||
|
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_update(version_url: str, current: str) -> int:
|
||||||
|
# Check for the latest version of Whoogle
|
||||||
|
has_update = ''
|
||||||
|
with contextlib.suppress(exceptions.ConnectionError, AttributeError):
|
||||||
|
update = bsoup(get(version_url).text, 'html.parser')
|
||||||
|
latest = update.select_one('[class="Link--primary"]').string[1:]
|
||||||
|
current = int(''.join(filter(str.isdigit, current)))
|
||||||
|
latest = int(''.join(filter(str.isdigit, latest)))
|
||||||
|
has_update = '' if current >= latest else latest
|
||||||
|
|
||||||
|
return has_update
|
||||||
|
|
||||||
|
|
||||||
|
def get_abs_url(url, page_url):
|
||||||
|
# Creates a valid absolute URL using a partial or relative URL
|
||||||
|
urls = {
|
||||||
|
"//": f"https:{url}",
|
||||||
|
"/": f"{urlparse(page_url).netloc}{url}",
|
||||||
|
"./": f"{page_url}{url[2:]}"
|
||||||
|
}
|
||||||
|
for start in urls:
|
||||||
|
if url.startswith(start):
|
||||||
|
return urls[start]
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def list_to_dict(lst: list) -> dict:
|
||||||
|
if len(lst) < 2:
|
||||||
|
return {}
|
||||||
|
return {lst[i].replace(' ', ''): lst[i+1].replace(' ', '')
|
||||||
|
for i in range(0, len(lst), 2)}
|
||||||
|
|
||||||
|
|
||||||
|
def encrypt_string(key: bytes, string: str) -> str:
|
||||||
|
cipher_suite = Fernet(key)
|
||||||
|
return cipher_suite.encrypt(string.encode()).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def decrypt_string(key: bytes, string: str) -> str:
|
||||||
|
cipher_suite = Fernet(g.session_key)
|
||||||
|
return cipher_suite.decrypt(string.encode()).decode()
|
466
app/utils/results.py
Executable file
|
@ -0,0 +1,466 @@
|
||||||
|
from app.models.config import Config
|
||||||
|
from app.models.endpoint import Endpoint
|
||||||
|
from app.utils.misc import list_to_dict
|
||||||
|
from bs4 import BeautifulSoup, NavigableString
|
||||||
|
import copy
|
||||||
|
from flask import current_app
|
||||||
|
import html
|
||||||
|
import os
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
from urllib.parse import parse_qs
|
||||||
|
import re
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
SKIP_ARGS = ['ref_src', 'utm']
|
||||||
|
SKIP_PREFIX = ['//www.', '//mobile.', '//m.']
|
||||||
|
GOOG_STATIC = 'www.gstatic.com'
|
||||||
|
G_M_LOGO_URL = 'https://www.gstatic.com/m/images/icons/googleg.gif'
|
||||||
|
GOOG_IMG = '/images/branding/searchlogo/1x/googlelogo'
|
||||||
|
LOGO_URL = GOOG_IMG + '_desk'
|
||||||
|
BLANK_B64 = ('data:image/png;base64,'
|
||||||
|
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAD0lEQVR42mNkw'
|
||||||
|
'AIYh7IgAAVVAAuInjI5AAAAAElFTkSuQmCC')
|
||||||
|
|
||||||
|
# Ad keywords
|
||||||
|
BLACKLIST = [
|
||||||
|
'ad', 'ads', 'anuncio', 'annuncio', 'annonce', 'Anzeige', '广告', '廣告',
|
||||||
|
'Reklama', 'Реклама', 'Anunț', '광고', 'annons', 'Annonse', 'Iklan',
|
||||||
|
'広告', 'Augl.', 'Mainos', 'Advertentie', 'إعلان', 'Գովազդ', 'विज्ञापन',
|
||||||
|
'Reklam', 'آگهی', 'Reklāma', 'Reklaam', 'Διαφήμιση', 'מודעה', 'Hirdetés', 'Szponzorált',
|
||||||
|
'Anúncio', 'Quảng cáo','โฆษณา', 'sponsored', 'patrocinado', 'gesponsert', 'Sponzorováno', '스폰서', 'Gesponsord'
|
||||||
|
]
|
||||||
|
|
||||||
|
SITE_ALTS = {
|
||||||
|
'twitter.com': os.getenv('WHOOGLE_ALT_TW', 'farside.link/nitter'),
|
||||||
|
'youtube.com': os.getenv('WHOOGLE_ALT_YT', 'farside.link/invidious'),
|
||||||
|
'reddit.com': os.getenv('WHOOGLE_ALT_RD', 'farside.link/libreddit'),
|
||||||
|
**dict.fromkeys([
|
||||||
|
'medium.com',
|
||||||
|
'levelup.gitconnected.com'
|
||||||
|
], os.getenv('WHOOGLE_ALT_MD', 'farside.link/scribe')),
|
||||||
|
'imgur.com': os.getenv('WHOOGLE_ALT_IMG', 'farside.link/rimgo'),
|
||||||
|
'wikipedia.org': os.getenv('WHOOGLE_ALT_WIKI', 'farside.link/wikiless'),
|
||||||
|
'imdb.com': os.getenv('WHOOGLE_ALT_IMDB', 'farside.link/libremdb'),
|
||||||
|
'quora.com': os.getenv('WHOOGLE_ALT_QUORA', 'farside.link/quetre'),
|
||||||
|
'stackoverflow.com': os.getenv('WHOOGLE_ALT_SO', 'farside.link/anonymousoverflow')
|
||||||
|
}
|
||||||
|
|
||||||
|
# Include custom site redirects from WHOOGLE_REDIRECTS
|
||||||
|
SITE_ALTS.update(list_to_dict(re.split(',|:', os.getenv('WHOOGLE_REDIRECTS', ''))))
|
||||||
|
|
||||||
|
|
||||||
|
def contains_cjko(s: str) -> bool:
|
||||||
|
"""This function check whether or not a string contains Chinese, Japanese,
|
||||||
|
or Korean characters. It employs regex and uses the u escape sequence to
|
||||||
|
match any character in a set of Unicode ranges.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s (str): string to be checked
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the input s contains the characters and False otherwise
|
||||||
|
"""
|
||||||
|
unicode_ranges = ('\u4e00-\u9fff' # Chinese characters
|
||||||
|
'\u3040-\u309f' # Japanese hiragana
|
||||||
|
'\u30a0-\u30ff' # Japanese katakana
|
||||||
|
'\u4e00-\u9faf' # Japanese kanji
|
||||||
|
'\uac00-\ud7af' # Korean hangul syllables
|
||||||
|
'\u1100-\u11ff' # Korean hangul jamo
|
||||||
|
)
|
||||||
|
return bool(re.search(fr'[{unicode_ranges}]', s))
|
||||||
|
|
||||||
|
|
||||||
|
def bold_search_terms(response: str, query: str) -> BeautifulSoup:
|
||||||
|
"""Wraps all search terms in bold tags (<b>). If any terms are wrapped
|
||||||
|
in quotes, only that exact phrase will be made bold.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: The initial response body for the query
|
||||||
|
query: The original search query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup: modified soup object with bold items
|
||||||
|
"""
|
||||||
|
response = BeautifulSoup(response, 'html.parser')
|
||||||
|
|
||||||
|
def replace_any_case(element: NavigableString, target_word: str) -> None:
|
||||||
|
# Replace all instances of the word, but maintaining the same case in
|
||||||
|
# the replacement
|
||||||
|
if len(element) == len(target_word):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure target word is escaped for regex
|
||||||
|
target_word = re.escape(target_word)
|
||||||
|
|
||||||
|
# Check if the word contains Chinese, Japanese, or Korean characters
|
||||||
|
if contains_cjko(target_word):
|
||||||
|
reg_pattern = fr'((?![{{}}<>-]){target_word}(?![{{}}<>-]))'
|
||||||
|
else:
|
||||||
|
reg_pattern = fr'\b((?![{{}}<>-]){target_word}(?![{{}}<>-]))\b'
|
||||||
|
|
||||||
|
if re.match(r'.*[@_!#$%^&*()<>?/\|}{~:].*', target_word) or (
|
||||||
|
element.parent and element.parent.name == 'style'):
|
||||||
|
return
|
||||||
|
|
||||||
|
element.replace_with(BeautifulSoup(
|
||||||
|
re.sub(reg_pattern,
|
||||||
|
r'<b>\1</b>',
|
||||||
|
element,
|
||||||
|
flags=re.I), 'html.parser')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Split all words out of query, grouping the ones wrapped in quotes
|
||||||
|
for word in re.split(r'\s+(?=[^"]*(?:"[^"]*"[^"]*)*$)', query):
|
||||||
|
word = re.sub(r'[@_!#$%^&*()<>?/\|}{~:]+', '', word)
|
||||||
|
target = response.find_all(
|
||||||
|
text=re.compile(r'' + re.escape(word), re.I))
|
||||||
|
for nav_str in target:
|
||||||
|
replace_any_case(nav_str, word)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def has_ad_content(element: str) -> bool:
|
||||||
|
"""Inspects an HTML element for ad related content
|
||||||
|
|
||||||
|
Args:
|
||||||
|
element: The HTML element to inspect
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True/False for the element containing an ad
|
||||||
|
|
||||||
|
"""
|
||||||
|
element_str = ''.join(filter(str.isalpha, element))
|
||||||
|
return (element_str.upper() in (value.upper() for value in BLACKLIST)
|
||||||
|
or 'ⓘ' in element)
|
||||||
|
|
||||||
|
|
||||||
|
def get_first_link(soup: BeautifulSoup) -> str:
|
||||||
|
"""Retrieves the first result link from the query response
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: The BeautifulSoup response body
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A str link to the first result
|
||||||
|
|
||||||
|
"""
|
||||||
|
first_link = ''
|
||||||
|
orig_details = []
|
||||||
|
|
||||||
|
# Temporarily remove details so we don't grab those links
|
||||||
|
for details in soup.find_all('details'):
|
||||||
|
temp_details = soup.new_tag('removed_details')
|
||||||
|
orig_details.append(details.replace_with(temp_details))
|
||||||
|
|
||||||
|
# Replace hrefs with only the intended destination (no "utm" type tags)
|
||||||
|
for a in soup.find_all('a', href=True):
|
||||||
|
# Return the first search result URL
|
||||||
|
if a['href'].startswith('http://') or a['href'].startswith('https://'):
|
||||||
|
first_link = a['href']
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add the details back
|
||||||
|
for orig_detail, details in zip(orig_details, soup.find_all('removed_details')):
|
||||||
|
details.replace_with(orig_detail)
|
||||||
|
|
||||||
|
return first_link
|
||||||
|
|
||||||
|
|
||||||
|
def get_site_alt(link: str, site_alts: dict = SITE_ALTS) -> str:
|
||||||
|
"""Returns an alternative to a particular site, if one is configured
|
||||||
|
|
||||||
|
Args:
|
||||||
|
link: A string result URL to check against the site_alts map
|
||||||
|
site_alts: A map of site alternatives to replace with. defaults to SITE_ALTS
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: An updated (or ignored) result link
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Need to replace full hostname with alternative to encapsulate
|
||||||
|
# subdomains as well
|
||||||
|
parsed_link = urlparse.urlparse(link)
|
||||||
|
|
||||||
|
# Extract subdomain separately from the domain+tld. The subdomain
|
||||||
|
# is used for wikiless translations.
|
||||||
|
split_host = parsed_link.netloc.split('.')
|
||||||
|
subdomain = split_host[0] if len(split_host) > 2 else ''
|
||||||
|
hostname = '.'.join(split_host[-2:])
|
||||||
|
|
||||||
|
# The full scheme + hostname is used when comparing against the list of
|
||||||
|
# available alternative services, due to how Medium links are constructed.
|
||||||
|
# (i.e. for medium.com: "https://something.medium.com" should match,
|
||||||
|
# "https://medium.com/..." should match, but "philomedium.com" should not)
|
||||||
|
hostcomp = f'{parsed_link.scheme}://{hostname}'
|
||||||
|
|
||||||
|
for site_key in site_alts.keys():
|
||||||
|
site_alt = f'{parsed_link.scheme}://{site_key}'
|
||||||
|
if not hostname or site_alt not in hostcomp or not site_alts[site_key]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Wikipedia -> Wikiless replacements require the subdomain (if it's
|
||||||
|
# a 2-char language code) to be passed as a URL param to Wikiless
|
||||||
|
# in order to preserve the language setting.
|
||||||
|
params = ''
|
||||||
|
if 'wikipedia' in hostname and len(subdomain) == 2:
|
||||||
|
hostname = f'{subdomain}.{hostname}'
|
||||||
|
params = f'?lang={subdomain}'
|
||||||
|
elif 'medium' in hostname and len(subdomain) > 0:
|
||||||
|
hostname = f'{subdomain}.{hostname}'
|
||||||
|
|
||||||
|
parsed_alt = urlparse.urlparse(site_alts[site_key])
|
||||||
|
link = link.replace(hostname, site_alts[site_key]) + params
|
||||||
|
# If a scheme is specified in the alternative, this results in a
|
||||||
|
# replaced link that looks like "https://http://altservice.tld".
|
||||||
|
# In this case, we can remove the original scheme from the result
|
||||||
|
# and use the one specified for the alt.
|
||||||
|
if parsed_alt.scheme:
|
||||||
|
link = '//'.join(link.split('//')[1:])
|
||||||
|
|
||||||
|
for prefix in SKIP_PREFIX:
|
||||||
|
if parsed_alt.scheme:
|
||||||
|
# If a scheme is specified, remove everything before the
|
||||||
|
# first occurence of it
|
||||||
|
link = f'{parsed_alt.scheme}{link.split(parsed_alt.scheme, 1)[-1]}'
|
||||||
|
else:
|
||||||
|
# Otherwise, replace the first occurrence of the prefix
|
||||||
|
link = link.replace(prefix, '//', 1)
|
||||||
|
break
|
||||||
|
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def filter_link_args(link: str) -> str:
|
||||||
|
"""Filters out unnecessary URL args from a result link
|
||||||
|
|
||||||
|
Args:
|
||||||
|
link: The string result link to check for extraneous URL params
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: An updated (or ignored) result link
|
||||||
|
|
||||||
|
"""
|
||||||
|
parsed_link = urlparse.urlparse(link)
|
||||||
|
link_args = parse_qs(parsed_link.query)
|
||||||
|
safe_args = {}
|
||||||
|
|
||||||
|
if len(link_args) == 0 and len(parsed_link) > 0:
|
||||||
|
return link
|
||||||
|
|
||||||
|
for arg in link_args.keys():
|
||||||
|
if arg in SKIP_ARGS:
|
||||||
|
continue
|
||||||
|
|
||||||
|
safe_args[arg] = link_args[arg]
|
||||||
|
|
||||||
|
# Remove original link query and replace with filtered args
|
||||||
|
link = link.replace(parsed_link.query, '')
|
||||||
|
if len(safe_args) > 0:
|
||||||
|
link = link + urlparse.urlencode(safe_args, doseq=True)
|
||||||
|
else:
|
||||||
|
link = link.replace('?', '')
|
||||||
|
|
||||||
|
return link
|
||||||
|
|
||||||
|
|
||||||
|
def append_nojs(result: BeautifulSoup) -> None:
|
||||||
|
"""Appends a no-Javascript alternative for a search result
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: The search result to append a no-JS link to
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
"""
|
||||||
|
nojs_link = BeautifulSoup(features='html.parser').new_tag('a')
|
||||||
|
nojs_link['href'] = f'{Endpoint.window}?nojs=1&location=' + result['href']
|
||||||
|
nojs_link.string = ' NoJS Link'
|
||||||
|
result.append(nojs_link)
|
||||||
|
|
||||||
|
|
||||||
|
def append_anon_view(result: BeautifulSoup, config: Config) -> None:
|
||||||
|
"""Appends an 'anonymous view' for a search result, where all site
|
||||||
|
contents are viewed through Whoogle as a proxy.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result: The search result to append an anon view link to
|
||||||
|
nojs: Remove Javascript from Anonymous View
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
|
||||||
|
"""
|
||||||
|
av_link = BeautifulSoup(features='html.parser').new_tag('a')
|
||||||
|
nojs = 'nojs=1' if config.nojs else 'nojs=0'
|
||||||
|
location = f'location={result["href"]}'
|
||||||
|
av_link['href'] = f'{Endpoint.window}?{nojs}&{location}'
|
||||||
|
translation = current_app.config['TRANSLATIONS'][
|
||||||
|
config.get_localization_lang()
|
||||||
|
]
|
||||||
|
av_link.string = f'{translation["anon-view"]}'
|
||||||
|
av_link['class'] = 'anon-view'
|
||||||
|
result.append(av_link)
|
||||||
|
|
||||||
|
def check_currency(response: str) -> dict:
|
||||||
|
"""Check whether the results have currency conversion
|
||||||
|
|
||||||
|
Args:
|
||||||
|
response: Search query Result
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Consists of currency names and values
|
||||||
|
|
||||||
|
"""
|
||||||
|
soup = BeautifulSoup(response, 'html.parser')
|
||||||
|
currency_link = soup.find('a', {'href': 'https://g.co/gfd'})
|
||||||
|
if currency_link:
|
||||||
|
while 'class' not in currency_link.attrs or \
|
||||||
|
'ZINbbc' not in currency_link.attrs['class']:
|
||||||
|
if currency_link.parent:
|
||||||
|
currency_link = currency_link.parent
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
currency_link = currency_link.find_all(class_='BNeawe')
|
||||||
|
currency1 = currency_link[0].text
|
||||||
|
currency2 = currency_link[1].text
|
||||||
|
currency1 = currency1.rstrip('=').split(' ', 1)
|
||||||
|
currency2 = currency2.split(' ', 1)
|
||||||
|
|
||||||
|
# Handle differences in currency formatting
|
||||||
|
# i.e. "5.000" vs "5,000"
|
||||||
|
if currency2[0][-3] == ',':
|
||||||
|
currency1[0] = currency1[0].replace('.', '')
|
||||||
|
currency1[0] = currency1[0].replace(',', '.')
|
||||||
|
currency2[0] = currency2[0].replace('.', '')
|
||||||
|
currency2[0] = currency2[0].replace(',', '.')
|
||||||
|
else:
|
||||||
|
currency1[0] = currency1[0].replace(',', '')
|
||||||
|
currency2[0] = currency2[0].replace(',', '')
|
||||||
|
|
||||||
|
currency1_value = float(re.sub(r'[^\d\.]', '', currency1[0]))
|
||||||
|
currency1_label = currency1[1]
|
||||||
|
|
||||||
|
currency2_value = float(re.sub(r'[^\d\.]', '', currency2[0]))
|
||||||
|
currency2_label = currency2[1]
|
||||||
|
|
||||||
|
return {'currencyValue1': currency1_value,
|
||||||
|
'currencyLabel1': currency1_label,
|
||||||
|
'currencyValue2': currency2_value,
|
||||||
|
'currencyLabel2': currency2_label
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def add_currency_card(soup: BeautifulSoup,
|
||||||
|
conversion_details: dict) -> BeautifulSoup:
|
||||||
|
"""Adds the currency conversion boxes
|
||||||
|
to response of the search query
|
||||||
|
|
||||||
|
Args:
|
||||||
|
soup: Parsed search result
|
||||||
|
conversion_details: Dictionary of currency
|
||||||
|
related information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup
|
||||||
|
"""
|
||||||
|
# Element before which the code will be changed
|
||||||
|
# (This is the 'disclaimer' link)
|
||||||
|
element1 = soup.find('a', {'href': 'https://g.co/gfd'})
|
||||||
|
|
||||||
|
while 'class' not in element1.attrs or \
|
||||||
|
'nXE3Ob' not in element1.attrs['class']:
|
||||||
|
element1 = element1.parent
|
||||||
|
|
||||||
|
# Creating the conversion factor
|
||||||
|
conversion_factor = (conversion_details['currencyValue1'] /
|
||||||
|
conversion_details['currencyValue2'])
|
||||||
|
|
||||||
|
# Creating a new div for the input boxes
|
||||||
|
conversion_box = soup.new_tag('div')
|
||||||
|
conversion_box['class'] = 'conversion_box'
|
||||||
|
|
||||||
|
# Currency to be converted from
|
||||||
|
input_box1 = soup.new_tag('input')
|
||||||
|
input_box1['id'] = 'cb1'
|
||||||
|
input_box1['type'] = 'number'
|
||||||
|
input_box1['class'] = 'cb'
|
||||||
|
input_box1['value'] = conversion_details['currencyValue1']
|
||||||
|
input_box1['oninput'] = f'convert(1, 2, {1 / conversion_factor})'
|
||||||
|
|
||||||
|
label_box1 = soup.new_tag('label')
|
||||||
|
label_box1['for'] = 'cb1'
|
||||||
|
label_box1['class'] = 'cb_label'
|
||||||
|
label_box1.append(conversion_details['currencyLabel1'])
|
||||||
|
|
||||||
|
br = soup.new_tag('br')
|
||||||
|
|
||||||
|
# Currency to be converted to
|
||||||
|
input_box2 = soup.new_tag('input')
|
||||||
|
input_box2['id'] = 'cb2'
|
||||||
|
input_box2['type'] = 'number'
|
||||||
|
input_box2['class'] = 'cb'
|
||||||
|
input_box2['value'] = conversion_details['currencyValue2']
|
||||||
|
input_box2['oninput'] = f'convert(2, 1, {conversion_factor})'
|
||||||
|
|
||||||
|
label_box2 = soup.new_tag('label')
|
||||||
|
label_box2['for'] = 'cb2'
|
||||||
|
label_box2['class'] = 'cb_label'
|
||||||
|
label_box2.append(conversion_details['currencyLabel2'])
|
||||||
|
|
||||||
|
conversion_box.append(input_box1)
|
||||||
|
conversion_box.append(label_box1)
|
||||||
|
conversion_box.append(br)
|
||||||
|
conversion_box.append(input_box2)
|
||||||
|
conversion_box.append(label_box2)
|
||||||
|
|
||||||
|
element1.insert_before(conversion_box)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
|
||||||
|
def get_tabs_content(tabs: dict,
|
||||||
|
full_query: str,
|
||||||
|
search_type: str,
|
||||||
|
preferences: 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
|
||||||
|
"""
|
||||||
|
map_query = full_query
|
||||||
|
if '-site:' in full_query:
|
||||||
|
block_idx = full_query.index('-site:')
|
||||||
|
map_query = map_query[:block_idx]
|
||||||
|
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']}"
|
||||||
|
|
||||||
|
if preferences:
|
||||||
|
query = f"{query}&preferences={preferences}"
|
||||||
|
|
||||||
|
tab_content['href'] = tab_content['href'].format(
|
||||||
|
query=query,
|
||||||
|
map_query=map_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
|
194
app/utils/search.py
Executable file
|
@ -0,0 +1,194 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
from app.filter import Filter
|
||||||
|
from app.request import gen_query
|
||||||
|
from app.utils.misc import get_proxy_host_url
|
||||||
|
from app.utils.results import get_first_link
|
||||||
|
from bs4 import BeautifulSoup as bsoup
|
||||||
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
|
from flask import g
|
||||||
|
|
||||||
|
TOR_BANNER = '<hr><h1 style="text-align: center">You are using Tor</h1><hr>'
|
||||||
|
CAPTCHA = 'div class="g-recaptcha"'
|
||||||
|
|
||||||
|
|
||||||
|
def needs_https(url: str) -> bool:
|
||||||
|
"""Checks if the current instance needs to be upgraded to HTTPS
|
||||||
|
|
||||||
|
Note that all Heroku instances are available by default over HTTPS, but
|
||||||
|
do not automatically set up a redirect when visited over HTTP.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The instance url
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True/False representing the need to upgrade
|
||||||
|
|
||||||
|
"""
|
||||||
|
https_only = bool(os.getenv('HTTPS_ONLY', 0))
|
||||||
|
is_heroku = url.endswith('.herokuapp.com')
|
||||||
|
is_http = url.startswith('http://')
|
||||||
|
|
||||||
|
return (is_heroku and is_http) or (https_only and is_http)
|
||||||
|
|
||||||
|
|
||||||
|
def has_captcha(results: str) -> bool:
|
||||||
|
"""Checks to see if the search results are blocked by a captcha
|
||||||
|
|
||||||
|
Args:
|
||||||
|
results: The search page html as a string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True/False indicating if a captcha element was found
|
||||||
|
|
||||||
|
"""
|
||||||
|
return CAPTCHA in results
|
||||||
|
|
||||||
|
|
||||||
|
class Search:
|
||||||
|
"""Search query preprocessor - used before submitting the query or
|
||||||
|
redirecting to another site
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
request: the incoming flask request
|
||||||
|
config: the current user config settings
|
||||||
|
session_key: the flask user fernet key
|
||||||
|
"""
|
||||||
|
def __init__(self, request, config, session_key, cookies_disabled=False):
|
||||||
|
method = request.method
|
||||||
|
self.request = request
|
||||||
|
self.request_params = request.args if method == 'GET' else request.form
|
||||||
|
self.user_agent = request.headers.get('User-Agent')
|
||||||
|
self.feeling_lucky = False
|
||||||
|
self.config = config
|
||||||
|
self.session_key = session_key
|
||||||
|
self.query = ''
|
||||||
|
self.widget = ''
|
||||||
|
self.view_image = True
|
||||||
|
self.cookies_disabled = cookies_disabled
|
||||||
|
self.search_type = self.request_params.get(
|
||||||
|
'tbm') if 'tbm' in self.request_params else ''
|
||||||
|
|
||||||
|
def __getitem__(self, name) -> Any:
|
||||||
|
return getattr(self, name)
|
||||||
|
|
||||||
|
def __setitem__(self, name, value) -> None:
|
||||||
|
return setattr(self, name, value)
|
||||||
|
|
||||||
|
def __delitem__(self, name) -> None:
|
||||||
|
return delattr(self, name)
|
||||||
|
|
||||||
|
def __contains__(self, name) -> bool:
|
||||||
|
return hasattr(self, name)
|
||||||
|
|
||||||
|
def new_search_query(self) -> str:
|
||||||
|
"""Parses a plaintext query into a valid string for submission
|
||||||
|
|
||||||
|
Also decrypts the query string, if encrypted (in the case of
|
||||||
|
paginated results).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A valid query string
|
||||||
|
|
||||||
|
"""
|
||||||
|
q = self.request_params.get('q')
|
||||||
|
|
||||||
|
if q is None or len(q) == 0:
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
# Attempt to decrypt if this is an internal link
|
||||||
|
try:
|
||||||
|
q = Fernet(self.session_key).decrypt(q.encode()).decode()
|
||||||
|
except InvalidToken:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Strip '!' for "feeling lucky" queries
|
||||||
|
if match := re.search(r"(^|\s)!($|\s)", q):
|
||||||
|
self.feeling_lucky = True
|
||||||
|
start, end = match.span()
|
||||||
|
self.query = " ".join([seg for seg in [q[:start], q[end:]] if seg])
|
||||||
|
else:
|
||||||
|
self.feeling_lucky = False
|
||||||
|
self.query = q
|
||||||
|
|
||||||
|
# Check for possible widgets
|
||||||
|
self.widget = "ip" if re.search("([^a-z0-9]|^)my *[^a-z0-9] *(ip|internet protocol)" +
|
||||||
|
"($|( *[^a-z0-9] *(((addres|address|adres|" +
|
||||||
|
"adress)|a)? *$)))", self.query.lower()) else self.widget
|
||||||
|
self.widget = 'calculator' if re.search(
|
||||||
|
r"\bcalculator\b|\bcalc\b|\bcalclator\b|\bmath\b",
|
||||||
|
self.query.lower()) else self.widget
|
||||||
|
return self.query
|
||||||
|
|
||||||
|
def generate_response(self) -> str:
|
||||||
|
"""Generates a response for the user's query
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A string response to the search query, in the form of a URL
|
||||||
|
or string representation of HTML content.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mobile = 'Android' in self.user_agent or 'iPhone' in self.user_agent
|
||||||
|
# reconstruct url if X-Forwarded-Host header present
|
||||||
|
root_url = get_proxy_host_url(
|
||||||
|
self.request,
|
||||||
|
self.request.url_root,
|
||||||
|
root=True)
|
||||||
|
|
||||||
|
content_filter = Filter(self.session_key,
|
||||||
|
root_url=root_url,
|
||||||
|
mobile=mobile,
|
||||||
|
config=self.config,
|
||||||
|
query=self.query)
|
||||||
|
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
|
||||||
|
view_image = ('tbm=isch' in full_query
|
||||||
|
# and self.config.view_image
|
||||||
|
#and not g.user_request.mobile
|
||||||
|
)
|
||||||
|
|
||||||
|
get_body = g.user_request.send(query=full_query,
|
||||||
|
force_mobile=view_image,
|
||||||
|
user_agent=self.user_agent)
|
||||||
|
|
||||||
|
|
||||||
|
# Produce cleanable html soup from response
|
||||||
|
get_body_safed = get_body.text.replace("<","andlt;").replace(">","andgt;")
|
||||||
|
html_soup = bsoup(get_body_safed, 'html.parser')
|
||||||
|
|
||||||
|
# Replace current soup if view_image is active
|
||||||
|
if view_image:
|
||||||
|
html_soup = content_filter.view_image(html_soup)
|
||||||
|
|
||||||
|
# Indicate whether or not a Tor connection is active
|
||||||
|
if g.user_request.tor_valid:
|
||||||
|
html_soup.insert(0, bsoup(TOR_BANNER, 'html.parser'))
|
||||||
|
|
||||||
|
formatted_results = content_filter.clean(html_soup)
|
||||||
|
if self.feeling_lucky:
|
||||||
|
if lucky_link := get_first_link(formatted_results):
|
||||||
|
return lucky_link
|
||||||
|
|
||||||
|
# Fall through to regular search if unable to find link
|
||||||
|
self.feeling_lucky = False
|
||||||
|
|
||||||
|
# Append user config to all search links, if available
|
||||||
|
param_str = ''.join('&{}={}'.format(k, v)
|
||||||
|
for k, v in
|
||||||
|
self.request_params.to_dict(flat=True).items()
|
||||||
|
if self.config.is_safe_key(k))
|
||||||
|
for link in formatted_results.find_all('a', href=True):
|
||||||
|
link['rel'] = "nofollow noopener noreferrer"
|
||||||
|
if 'search?' not in link['href'] or link['href'].index(
|
||||||
|
'search?') > 1:
|
||||||
|
continue
|
||||||
|
link['href'] += param_str
|
||||||
|
|
||||||
|
return str(formatted_results)
|
||||||
|
|
39
app/utils/session.py
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
from flask import current_app as app
|
||||||
|
|
||||||
|
REQUIRED_SESSION_VALUES = ['uuid', 'config', 'key', 'auth']
|
||||||
|
|
||||||
|
|
||||||
|
def generate_key() -> bytes:
|
||||||
|
"""Generates a key for encrypting searches and element URLs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cookies_disabled: Flag for whether or not cookies are disabled by the
|
||||||
|
user. If so, the user can only use the default key
|
||||||
|
generated on app init for queries.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A unique Fernet key
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Generate/regenerate unique key per user
|
||||||
|
return Fernet.generate_key()
|
||||||
|
|
||||||
|
|
||||||
|
def valid_user_session(session: dict) -> bool:
|
||||||
|
"""Validates the current user session
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session: The current Flask user session
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True/False indicating that all required session values are
|
||||||
|
available
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Generate secret key for user if unavailable
|
||||||
|
for value in REQUIRED_SESSION_VALUES:
|
||||||
|
if value not in session:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
71
app/utils/widgets.py
Executable file
|
@ -0,0 +1,71 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
|
||||||
|
# root
|
||||||
|
BASE_DIR = Path(__file__).parent.parent.parent
|
||||||
|
|
||||||
|
def add_ip_card(html_soup: BeautifulSoup, ip: str) -> BeautifulSoup:
|
||||||
|
"""Adds the client's IP address to the search results
|
||||||
|
if query contains keywords
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_soup: The parsed search result containing the keywords
|
||||||
|
ip: ip address of the client
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup
|
||||||
|
|
||||||
|
"""
|
||||||
|
main_div = html_soup.select_one('#main')
|
||||||
|
if main_div:
|
||||||
|
# HTML IP card tag
|
||||||
|
ip_tag = html_soup.new_tag('div')
|
||||||
|
ip_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
||||||
|
|
||||||
|
# For IP Address html tag
|
||||||
|
ip_address = html_soup.new_tag('div')
|
||||||
|
ip_address['class'] = 'kCrYT ip-address-div'
|
||||||
|
ip_address.string = ip
|
||||||
|
|
||||||
|
# Text below the IP address
|
||||||
|
ip_text = html_soup.new_tag('div')
|
||||||
|
ip_text.string = 'Your public IP address'
|
||||||
|
ip_text['class'] = 'kCrYT ip-text-div'
|
||||||
|
|
||||||
|
# Adding all the above html tags to the IP card
|
||||||
|
ip_tag.append(ip_address)
|
||||||
|
ip_tag.append(ip_text)
|
||||||
|
|
||||||
|
# Insert the element at the top of the result list
|
||||||
|
main_div.insert_before(ip_tag)
|
||||||
|
return html_soup
|
||||||
|
|
||||||
|
def add_calculator_card(html_soup: BeautifulSoup) -> BeautifulSoup:
|
||||||
|
"""Adds the a calculator widget to the search results
|
||||||
|
if query contains keywords
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html_soup: The parsed search result containing the keywords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BeautifulSoup
|
||||||
|
"""
|
||||||
|
main_div = html_soup.select_one('#main')
|
||||||
|
if main_div:
|
||||||
|
# absolute path
|
||||||
|
widget_file = open(BASE_DIR / 'app/static/widgets/calculator.html', encoding="utf8")
|
||||||
|
widget_tag = html_soup.new_tag('div')
|
||||||
|
widget_tag['class'] = 'ZINbbc xpd O9g5cc uUPGi'
|
||||||
|
widget_tag['id'] = 'calculator-wrapper'
|
||||||
|
calculator_text = html_soup.new_tag('div')
|
||||||
|
calculator_text['class'] = 'kCrYT ip-address-div'
|
||||||
|
calculator_text.string = 'Calculator'
|
||||||
|
calculator_widget = html_soup.new_tag('div')
|
||||||
|
calculator_widget.append(BeautifulSoup(widget_file, 'html.parser'))
|
||||||
|
calculator_widget['class'] = 'kCrYT ip-text-div'
|
||||||
|
widget_tag.append(calculator_text)
|
||||||
|
widget_tag.append(calculator_widget)
|
||||||
|
main_div.insert_before(widget_tag)
|
||||||
|
widget_file.close()
|
||||||
|
return html_soup
|
7
app/version.py
Executable file
|
@ -0,0 +1,7 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
optional_dev_tag = ''
|
||||||
|
if os.getenv('DEV_BUILD'):
|
||||||
|
optional_dev_tag = '.dev' + os.getenv('DEV_BUILD')
|
||||||
|
|
||||||
|
__version__ = '0.9.1' + optional_dev_tag
|
23
charts/whoogle/.helmignore
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
23
charts/whoogle/Chart.yaml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
apiVersion: v2
|
||||||
|
name: whoogle
|
||||||
|
description: A self hosted search engine on Kubernetes
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: 0.9.1
|
||||||
|
|
||||||
|
icon: https://github.com/benbusby/whoogle-search/raw/main/app/static/img/favicon/favicon-96x96.png
|
||||||
|
|
||||||
|
sources:
|
||||||
|
- https://github.com/benbusby/whoogle-search
|
||||||
|
- https://gitlab.com/benbusby/whoogle-search
|
||||||
|
- https://gogs.benbusby.com/benbusby/whoogle-search
|
||||||
|
|
||||||
|
keywords:
|
||||||
|
- whoogle
|
||||||
|
- degoogle
|
||||||
|
- search
|
||||||
|
- google
|
||||||
|
- search-engine
|
||||||
|
- privacy
|
||||||
|
- tor
|
||||||
|
- python
|
22
charts/whoogle/templates/NOTES.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "whoogle.fullname" . }})
|
||||||
|
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||||
|
echo http://$NODE_IP:$NODE_PORT
|
||||||
|
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||||
|
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||||
|
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "whoogle.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "whoogle.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "whoogle.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||||
|
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||||
|
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||||
|
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||||
|
{{- end }}
|
62
charts/whoogle/templates/_helpers.tpl
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
If release name contains chart name it will be used as a full name.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.fullname" -}}
|
||||||
|
{{- if .Values.fullnameOverride }}
|
||||||
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||||
|
{{- if contains $name .Release.Name }}
|
||||||
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- else }}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create chart name and version as used by the chart label.
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "whoogle.chart" . }}
|
||||||
|
{{ include "whoogle.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "whoogle.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "whoogle.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "whoogle.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|