A privacy-respecting, ad-free, self-hosted metasearch engine.
11 |
12 | [](https://github.com/Extravi/araa-search)
13 | [](https://github.com/Extravi/araa-search/blob/main/LICENSE)
14 | [](https://github.com/Extravi/araa-search/stargazers)
15 |
16 | If you're looking to [install](https://extravi.dev/araa) Araa, here is a how-to guide for it.
17 |
18 | ### Instances
19 |
20 | | Clearnet | Tor | SSL | Country | Status |
21 | |-|-|-|-|-|
22 | | [araa.extravi.dev](https://araa.extravi.dev/) | N/A | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=araa.extravi.dev)✅| United States 🇺🇸 | Official instance |
23 | | [tailsx.com](https://tailsx.com/) | [Link](http://inbbfryz7elofjk23pi7txnibttnuyz3rg2vwqmfengteeyhrmvex4id.onion/) | [www.ssllabs.com](https://www.ssllabs.com/ssltest/analyze.html?d=tailsx.com)✅| Canada 🇨🇦 | Unofficial instance |
24 |
25 | ### Contributors
26 | 
27 |
28 | ## Features
29 | Here are some of the features that Araa a privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security provides:
30 |
31 | * Full API support for easy integration into third-party apps and services
32 | * Utilizes Qwant for image search, which is known for its strong privacy protections
33 | * DuckDuckGo is used for auto-complete, offering privacy-enhanced search suggestions
34 | * Hosted on your own server, providing complete control over your data and ensuring privacy
35 | * Strong security measures implemented, including SSL encryption and firewalls
36 | * Ad-free search results, with no tracking or data collection for advertising purposes
37 |
38 | ## Screenshots
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | ## Contact
50 | Email: dante@extravi.dev
51 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 | Please report any potential security issues to security@extravi.dev
3 |
--------------------------------------------------------------------------------
/_config.py:
--------------------------------------------------------------------------------
1 | ARAA_NAME = "Araa"
2 |
3 | # The char used to denote bangs (see below).
4 | # EG BANG='!': "!ddg cats" will search "cats" on DuckDuckGo.
5 | BANG = '!'
6 |
7 | # Search engine bangs for ppl who want to use another engine through
8 | # Araa's search bar.
9 | # Bangs with their assosiated URLs can be found in /bangs.json.
10 |
11 | # The repository this instance is based off on.
12 | REPO = 'https://github.com/Extravi/araa-search'
13 | DONATE = 'https://github.com/sponsors/Extravi'
14 |
15 | DEFAULT_ENGINE = "google"
16 |
17 |
18 | # Default theme
19 | DEFAULT_THEME = 'dark_default'
20 |
21 | # Default method
22 | DEFAULT_METHOD = "GET"
23 |
24 | # Default autocomplete "google" will use Google, and "ddg" will use DuckDuckGo
25 | DEFAULT_AUTOCOMPLETE = "google"
26 |
27 | # The port for this server to listen on
28 | PORT = 8000
29 |
30 | # Torrent domains
31 | TORRENTGALAXY_DOMAIN = "torrentgalaxy.to"
32 | NYAA_DOMAIN = "nyaa.si"
33 | # apibay is the api for thepiratebay.org
34 | API_BAY_DOMAIN = "apibay.org"
35 | RUTOR_DOMAIN = "rutor.info"
36 |
37 | # Domain of the Piped instance to use
38 | PIPED_INSTANCE_API = "ytapi.ttj.dev"
39 | PIPED_INSTANCE = "yt.ttj.dev"
40 | PIPED_INSTANCE_PROXY = "ytproxy.ttj.dev"
41 |
42 | # Useragents to use in the request.
43 | user_agents = [
44 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.3",
45 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
46 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14.1; rv:109.0) Gecko/20100101 Firefox/121.0",
47 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.2210.89",
48 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0",
49 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15",
50 | ]
51 |
52 | # prompts for user agent & ip queries
53 | VALID_IP_PROMPTS = [
54 | "what is my ip",
55 | "what is my ip address",
56 | "what's my ip",
57 | "whats my ip"
58 | ]
59 | VALID_UA_PROMPTS = [
60 | "what is my user agent",
61 | "what is my useragent",
62 | "whats my useragent",
63 | "whats my user agent",
64 | "what's my useragent",
65 | "what's my user agent",
66 | ]
67 |
68 |
69 | WHITELISTED_DOMAINS = [
70 | "www.google.com",
71 | "wikipedia.org",
72 | PIPED_INSTANCE,
73 | PIPED_INSTANCE_API,
74 | PIPED_INSTANCE_PROXY,
75 | "api.qwant.com",
76 | TORRENTGALAXY_DOMAIN,
77 | NYAA_DOMAIN,
78 | API_BAY_DOMAIN,
79 | RUTOR_DOMAIN,
80 | ]
81 |
82 | ENABLED_TORRENT_SITES = [
83 | "nyaa",
84 | "torrentgalaxy",
85 | "tpb",
86 | "rutor",
87 | ]
88 |
89 | TORRENT_TRACKERS = [
90 | 'http://nyaa.tracker.wf:7777/announce',
91 | 'udp://open.stealth.si:80/announce',
92 | 'udp://tracker.opentrackr.org:1337/announce',
93 | 'udp://exodus.desync.com:6969/announce',
94 | 'udp://tracker.torrent.eu.org:451/announce'
95 | ]
96 |
97 | COOKIE_AGE = 2147483647
98 |
99 | # set to true to enable api support
100 | API_ENABLED = False
101 |
102 | # set to false to disable torrent search
103 | TORRENTSEARCH_ENABLED = True
104 |
105 | UX_LANGUAGES = [
106 | {'lang_lower': 'english', 'lang_fancy': 'English'},
107 | {'lang_lower': 'danish', 'lang_fancy': 'Danish (Dansk)'},
108 | {'lang_lower': 'dutch', 'lang_fancy': 'Dutch (Nederlands)'},
109 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'},
110 | {'lang_lower': 'french_canadian', 'lang_fancy': 'French Canadian (Français canadien)'},
111 | {'lang_lower': 'german', 'lang_fancy': 'German (Deutsch)'},
112 | {'lang_lower': 'greek', 'lang_fancy': 'Greek (Ελληνικά)'},
113 | {'lang_lower': 'italian', 'lang_fancy': 'Italian (Italiano)'},
114 | {'lang_lower': 'japanese', 'lang_fancy': 'Japanese (日本語)'},
115 | {'lang_lower': 'korean', 'lang_fancy': 'Korean (한국어)'},
116 | {'lang_lower': 'mandarin_chinese', 'lang_fancy': 'Mandarin Chinese (普通话 or 中文)'},
117 | {'lang_lower': 'norwegian', 'lang_fancy': 'Norwegian (Norsk)'},
118 | {'lang_lower': 'polish', 'lang_fancy': 'Polish (Polski)'},
119 | {'lang_lower': 'portuguese', 'lang_fancy': 'Portuguese (Português)'},
120 | {'lang_lower': 'russian', 'lang_fancy': 'Russian (Русский)'},
121 | {'lang_lower': 'spanish', 'lang_fancy': 'Spanish (Español)'},
122 | {'lang_lower': 'swedish', 'lang_fancy': 'Swedish (Svenska)'},
123 | {'lang_lower': 'turkish', 'lang_fancy': 'Turkish (Türkçe)'},
124 | {'lang_lower': 'ukrainian', 'lang_fancy': 'Ukrainian (Українська)'},
125 | {'lang_lower': 'romanian', 'lang_fancy': 'Romanian (Română)'},
126 | ]
127 |
128 | # See all the 'lang_lower' values in UX_LANGUAGES
129 | DEFAULT_UX_LANG = "english"
130 |
131 | DEFAULT_GOOGLE_DOMAIN = "/search?gl=us"
132 |
133 | ENGINE_RATELIMIT_COOLDOWN_MINUTES = 28
134 |
--------------------------------------------------------------------------------
/alpine-based.dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:alpine
2 |
3 | # LABEL can be used to attach metadata to the container.
4 | LABEL title="Araa Search" \
5 | description="A privacy-respecting, ad-free, self-hosted Google metasearch engine with strong security and full API support." \
6 | git_repo="https://github.com/TEMtheLEM/araa-search" \
7 | authors="https://github.com/Extravi/araa-search/contributors" \
8 | maintainer="TEMtheLEM " \
9 | image="https://hub.docker.com/r/temthelem/araa-search"
10 |
11 | WORKDIR /app
12 |
13 | COPY requirements.txt /app/
14 |
15 | # We will only be running our own python app in a container,
16 | # so this shouldn't be terrible.
17 | RUN pip install --break-system-packages -r requirements.txt
18 |
19 | COPY . .
20 |
21 | ENV ORIGIN_REPO=https://github.com/TEMtheLEM/araa-search
22 |
23 | CMD [ "sh", "scripts/docker-cmd.sh" ]
24 |
--------------------------------------------------------------------------------
/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | araa-search:
3 | container_name: Araa
4 | image: docker.io/temthelem/araa-search:latest
5 | env_file:
6 | - .env # May be redundant. Who cares ¯\_(ツ)_/¯
7 | ports:
8 | - "${PORT}:${PORT}"
9 | watchtower: # Not required. Keeps containers up-to-date.
10 | container_name: watchtower
11 | image: docker.io/containrrr/watchtower
12 | volumes:
13 | - /var/run/docker.sock:/var/run/docker.sock
14 | command: --interval 60
15 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | lxml
3 | bs4
4 | gunicorn
5 | requests
6 | thefuzz
7 | httpx[http2]
8 | trio
9 | werkzeug>=3.0.3 # not directly required, pinned by Snyk to avoid a vulnerability
10 | urllib3>=2.2.2 # not directly required, pinned by Snyk to avoid a vulnerability
11 | anyio>=4.4.0 # not directly required, pinned by Snyk to avoid a vulnerability
12 |
--------------------------------------------------------------------------------
/resources/_config.py.gen.template:
--------------------------------------------------------------------------------
1 | # Additional variables that cannot be configured with the generator.
2 | user_agents=[
3 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
4 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0",
6 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
7 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:106.0) Gecko/20100101 Firefox/106.0",
8 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
9 | ]
10 | VALID_IP_PROMPTS=[
11 | "what is my ip",
12 | "what is my ip address",
13 | "what's my ip",
14 | "whats my ip"
15 | ]
16 | VALID_UA_PROMPTS=[
17 | "what is my user agent",
18 | "what is my useragent",
19 | "whats my useragent",
20 | "whats my user agent",
21 | "what's my useragent",
22 | "what's my user agent",
23 | ]
24 | WHITELISTED_DOMAINS=[
25 | "www.google.com",
26 | "wikipedia.org",
27 | PIPED_INSTANCE,
28 | PIPED_INSTANCE_API,
29 | PIPED_INSTANCE_PROXY,
30 | "api.qwant.com",
31 | TORRENTGALAXY_DOMAIN,
32 | NYAA_DOMAIN,
33 | API_BAY_DOMAIN,
34 | RUTOR_DOMAIN,
35 | ]
36 | TORRENT_TRACKERS = [
37 | 'http://nyaa.tracker.wf:7777/announce',
38 | 'udp://open.stealth.si:80/announce',
39 | 'udp://tracker.opentrackr.org:1337/announce',
40 | 'udp://exodus.desync.com:6969/announce',
41 | 'udp://tracker.torrent.eu.org:451/announce'
42 | ]
43 | COOKIE_AGE=2147483647
44 | UX_LANGUAGES=[
45 | {'lang_lower': 'english', 'lang_fancy': 'English'},
46 | {'lang_lower': 'danish', 'lang_fancy': 'Danish (Dansk)'},
47 | {'lang_lower': 'dutch', 'lang_fancy': 'Dutch (Nederlands)'},
48 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'},
49 | {'lang_lower': 'french_canadian', 'lang_fancy': 'French Canadian (Français canadien)'},
50 | {'lang_lower': 'german', 'lang_fancy': 'German (Deutsch)'},
51 | {'lang_lower': 'greek', 'lang_fancy': 'Greek (Ελληνικά)'},
52 | {'lang_lower': 'italian', 'lang_fancy': 'Italian (Italiano)'},
53 | {'lang_lower': 'japanese', 'lang_fancy': 'Japanese (日本語)'},
54 | {'lang_lower': 'korean', 'lang_fancy': 'Korean (한국어)'},
55 | {'lang_lower': 'mandarin_chinese', 'lang_fancy': 'Mandarin Chinese (普通话 or 中文)'},
56 | {'lang_lower': 'norwegian', 'lang_fancy': 'Norwegian (Norsk)'},
57 | {'lang_lower': 'polish', 'lang_fancy': 'Polish (Polski)'},
58 | {'lang_lower': 'portuguese', 'lang_fancy': 'Portuguese (Português)'},
59 | {'lang_lower': 'russian', 'lang_fancy': 'Russian (Русский)'},
60 | {'lang_lower': 'spanish', 'lang_fancy': 'Spanish (Español)'},
61 | {'lang_lower': 'swedish', 'lang_fancy': 'Swedish (Svenska)'},
62 | {'lang_lower': 'turkish', 'lang_fancy': 'Turkish (Türkçe)'},
63 | {'lang_lower': 'ukrainian', 'lang_fancy': 'Ukrainian (Українська)'},
64 | {'lang_lower': 'romanian', 'lang_fancy': 'Romanian (Română)'},
65 | ]
66 | DEFAULT_GOOGLE_DOMAIN='/search?gl=us'
67 |
--------------------------------------------------------------------------------
/scripts/docker-cmd.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This script is meant to be ran solely in a Docker container.
3 | # Do not run it manually, it's not meant for that.
4 |
5 | sh scripts/generate-opensearch.sh || exit $?
6 | python3 scripts/generate-pyconfig.py || exit $?
7 |
8 | [ "$WORKERS" ] || WORKERS=2
9 | [ "$THREADS" ] || THREADS=8
10 | [ "$PORT" ] || PORT=8000
11 |
12 | exec gunicorn --workers $WORKERS --threads $THREADS --bind="0.0.0.0:$PORT" __init__:app
13 |
--------------------------------------------------------------------------------
/scripts/generate-opensearch.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Generates the opensearch.xml file based off of a $DOMAIN env. var.
3 | # Fails if $DOMAIN is blank or not set.
4 |
5 | if [ $DOMAIN ]; then
6 | echo "
7 |
8 | Araa
9 |
10 |
11 | UTF-8
12 | UTF-8
13 | en-us
14 | " > ./static/opensearch.xml;
15 | else
16 | echo "Make a DOMAIN env. variable & set it to your domain!
17 | (Ex; DOMAIN=www.yourdomain.com)";
18 | exit 1;
19 | fi
20 |
--------------------------------------------------------------------------------
/scripts/generate-pyconfig.py:
--------------------------------------------------------------------------------
1 | #!/bin/python
2 | # This python script was made to generate a suitable `/_config.py`
3 | # using the current environment variables OR the defaults specified in
4 | # the dictionary below.
5 | #
6 | # This script uses `/resource/_config.py.gen.template` as a template
7 | # for the generated `/_config.py`
8 | #
9 | # Also, it's meant to be ran with the cwd at the root of this repository.
10 |
11 | # A reference for acceptable environment variables & how to use them
12 | # for `/_config.py`
13 | ENV_VARS = {
14 | 'PORT': { # Name of the env. var.
15 | 'default_val': 8000, # Default value (obvious)
16 | 'pyname': 'PORT', # The name of the variable in Python (_config.py)
17 | 'type': int, # Type of the variable
18 | },
19 | 'SHEBANG': {
20 | 'default_val': '!',
21 | 'pyname': 'BANG',
22 | 'type': str,
23 | },
24 | 'ORIGIN_REPO': {
25 | 'default_val': 'https://github.com/Extravi/araa-search',
26 | 'pyname': 'REPO',
27 | 'type': str,
28 | },
29 | 'DONATE_URL': {
30 | 'default_val': 'https://github.com/sponsors/Extravi',
31 | 'pyname': 'DONATE',
32 | 'type': str,
33 | },
34 | 'DEFAULT_THEME': {
35 | 'default_val': 'dark_blur',
36 | 'pyname': 'DEFAULT_THEME',
37 | 'type': str,
38 | },
39 | 'ENABLE_API': {
40 | 'default_val': False,
41 | 'pyname': 'API_ENABLED',
42 | 'type': bool,
43 | },
44 | 'ENABLE_TORRENTS': {
45 | 'default_val': True,
46 | 'pyname': 'TORRENTSEARCH_ENABLED',
47 | 'type': bool,
48 | },
49 | 'PIPED_INSTANCE': {
50 | 'default_val': 'yt.extravi.dev',
51 | 'pyname': 'PIPED_INSTANCE',
52 | 'type': str,
53 | },
54 | 'PIPED_API': {
55 | 'default_val': 'ytapi.extravi.dev',
56 | 'pyname': 'PIPED_INSTANCE_API',
57 | 'type': str,
58 | },
59 | 'PIPED_PROXY': {
60 | 'default_val': 'ytproxy.extravi.dev',
61 | 'pyname': 'PIPED_INSTANCE_PROXY',
62 | 'type': str,
63 | },
64 | 'TORRENTGALAXY_DOMAIN': {
65 | 'default_val': 'torrentgalaxy.to',
66 | 'pyname': 'TORRENTGALAXY_DOMAIN',
67 | 'type': str,
68 | },
69 | 'NYAA_DOMAIN': {
70 | 'default_val': 'nyaa.si',
71 | 'pyname': 'NYAA_DOMAIN',
72 | 'type': str,
73 | },
74 | 'APIBAY_DOMAIN': {
75 | 'default_val': 'apibay.org',
76 | 'pyname': 'API_BAY_DOMAIN',
77 | 'type': str,
78 | },
79 | 'RUTOR_DOMAIN': {
80 | 'default_val': 'rutor.info',
81 | 'pyname': 'RUTOR_DOMAIN',
82 | 'type': str,
83 | },
84 | 'TORRENT_SITES': {
85 | 'default_val': [
86 | 'nyaa',
87 | 'torrentgalaxy',
88 | 'tpb',
89 | 'rutor',
90 | ],
91 | 'pyname': 'ENABLED_TORRENT_SITES',
92 | 'type': list,
93 | },
94 | 'DEFAULT_METHOD': {
95 | 'default_val': 'GET',
96 | 'pyname': 'DEFAULT_METHOD',
97 | 'type': str,
98 | },
99 | 'DEFAULT_AC_ENGINE': {
100 | 'default_val': 'google',
101 | 'pyname': 'DEFAULT_AUTOCOMPLETE',
102 | 'type': str,
103 | },
104 | 'DEFAULT_LANG': {
105 | 'default_val': 'english',
106 | 'pyname': 'DEFAULT_UX_LANG',
107 | 'type': str,
108 | },
109 | 'ENGINE_RATELIMIT_COOLDOWN': {
110 | 'default_val': 10,
111 | 'pyname': 'ENGINE_RATELIMIT_COOLDOWN_MINUTES',
112 | 'type': int,
113 | },
114 | }
115 |
116 | import os
117 |
118 | config_py = open('_config.py', 'w')
119 |
120 | # Write a disclaimer saying that this file was automatically generated.
121 | config_py.write(
122 | '# This _config.py was automatically generated using scripts/generate-pyconfig.py.\n'
123 | )
124 |
125 | for env_var in ENV_VARS.keys():
126 | val = os.environ.get(env_var)
127 |
128 | # If environ.get() returns None (the env. var. wasn't supplied), or
129 | # the value is blank, then fall back on the default.
130 | if val == None or val == "":
131 | val = ENV_VARS[env_var]['default_val']
132 | # Wrap strings with quotes
133 | pretty_val = f"'{val}'" if ENV_VARS[env_var]['type'] == str else val
134 | print(f"Config var. '{env_var}' not specified. Defaulting to {pretty_val}.")
135 |
136 | # Put quotes around each variable if it's a string.
137 | if ENV_VARS[env_var]['type'] == str:
138 | val = f"'{val}'"
139 |
140 | config_py.write(f"{ENV_VARS[env_var]['pyname']}={val}\n")
141 |
142 | # Write the rest of the template's variables.
143 | # These variables are not yet configurable with this generator.
144 | conf_template = open('resources/_config.py.gen.template', 'r')
145 | config_py.write(conf_template.read())
146 | conf_template.close()
147 |
148 | config_py.close()
149 |
--------------------------------------------------------------------------------
/src/helpers.py:
--------------------------------------------------------------------------------
1 | import random
2 | import requests
3 | import httpx
4 | import trio
5 | import re
6 | import json
7 | from bs4 import BeautifulSoup
8 | from urllib.parse import unquote, urlparse
9 | from _config import *
10 | from markupsafe import escape, Markup
11 | from os.path import exists
12 | from thefuzz import fuzz
13 | from flask import request
14 |
15 | # Debug code uncomment when needed
16 | #import logging, timeit
17 | #logging.basicConfig(level=logging.DEBUG, format="%(message)s")
18 |
19 | # Force all requests to only use IPv4
20 | requests.packages.urllib3.util.connection.HAS_IPV6 = False
21 |
22 | # Force all HTTPX requests to only use IPv4
23 | transport = httpx.HTTPTransport(local_address="0.0.0.0")
24 |
25 | # Pool limit configuration
26 | limits = httpx.Limits(max_keepalive_connections=None, max_connections=None, keepalive_expiry=None)
27 |
28 | # Make persistent request sessions
29 | s = requests.Session() # generic
30 | google = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # google
31 | wiki = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # wikipedia
32 | piped = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # piped
33 | qwant = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits) # qwant
34 |
35 | def makeHTMLRequest(url: str, is_google=False, is_wiki=False, is_piped=False):
36 | # block unwanted request from an edited cookie
37 | domain = unquote(url).split('/')[2]
38 | if domain not in WHITELISTED_DOMAINS:
39 | raise Exception(f"The domain '{domain}' is not whitelisted.")
40 |
41 | headers = {
42 | "User-Agent": random.choice(user_agents), # Choose a user-agent at random
43 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
44 | "Accept-Encoding": "gzip, deflate",
45 | "Accept-Language": "en-US,en;q=0.5",
46 | "Dnt": "1",
47 | "Sec-Fetch-Dest": "document",
48 | "Sec-Fetch-Mode": "navigate",
49 | "Sec-Fetch-Site": "none",
50 | "Sec-Fetch-User": "?1",
51 | "Upgrade-Insecure-Requests": "1"
52 | }
53 |
54 | # Grab HTML content with the specific cookie
55 | if is_google:
56 | html = google.get(url, headers=headers) # persistent session for google
57 | elif is_wiki:
58 | html = wiki.get(url, headers=headers) # persistent session for wikipedia
59 | elif is_piped:
60 | html = piped.get(url, headers=headers) # persistent session for piped
61 | else:
62 | html = s.get(url, headers=headers) # generic persistent session
63 |
64 | # Allow for callers to handle errors better
65 | content = None if html.status_code != 200 else BeautifulSoup(html.text, "lxml")
66 |
67 | # Return the BeautifulSoup object
68 | return (content, html.status_code)
69 |
70 | # search highlights
71 | def highlight_query_words(string, query):
72 | string = escape(string)
73 | query_words = query.lower().split()
74 | highlighted_words = []
75 | for word in string.split():
76 | for query_word in query_words:
77 | if fuzz.ratio(word.lower(), query_word) >= 80:
78 | highlighted_word = Markup(f'{word}')
79 | highlighted_words.append(highlighted_word)
80 | break
81 | else:
82 | highlighted_words.append(word)
83 | highlighted = ' '.join(highlighted_words)
84 | return Markup(highlighted)
85 |
86 |
87 | def latest_commit():
88 | if exists(".git/refs/heads/main"):
89 | with open('./.git/refs/heads/main') as f:
90 | return f.readline()
91 | return "Not in main branch"
92 |
93 | def makeJSONRequest(url: str, is_qwant=False):
94 | # block unwanted request from an edited cookie
95 | domain = unquote(url).split('/')[2]
96 | if domain not in WHITELISTED_DOMAINS:
97 | raise Exception(f"The domain '{domain}' is not whitelisted.")
98 |
99 | # Choose a user-agent at random
100 | user_agent = random.choice(user_agents)
101 | headers = {"User-Agent": user_agent}
102 | # Grab json content
103 | if is_qwant:
104 | response = qwant.get(url, headers=headers) # persistent session for qwant
105 | else:
106 | response = s.get(url, headers=headers) # generic persistent session
107 |
108 | # Return the JSON object
109 | return (json.loads(response.text), response.status_code)
110 |
111 | def get_magnet_hash(magnet):
112 | return magnet.split("btih:")[1].split("&")[0]
113 |
114 | def get_magnet_name(magnet):
115 | return magnet.split("&dn=")[1].split("&tr")[0]
116 |
117 |
118 | def apply_trackers(hash, name="", magnet=True):
119 | if magnet:
120 | name = get_magnet_name(hash)
121 | hash = get_magnet_hash(hash)
122 |
123 | return f"magnet:?xt=urn:btih:{hash}&dn={name}&tr={'&tr='.join(TORRENT_TRACKERS)}"
124 |
125 | def string_to_bytes(file_size):
126 | units = {
127 | 'bytes': 1,
128 | 'kb': 1024,
129 | 'mb': 1024 ** 2,
130 | 'gb': 1024 ** 3,
131 | 'tb': 1024 ** 4,
132 | 'kib': 1024,
133 | 'mib': 1024 ** 2,
134 | 'gib': 1024 ** 3,
135 | 'tib': 1024 ** 4
136 | }
137 |
138 | size, unit = file_size.lower().split()
139 | return float(size) * units[unit]
140 |
141 | def bytes_to_string(size):
142 | units = ['bytes', 'KB', 'MB', 'GB', 'TB']
143 | index = 0
144 | while size >= 1024 and index < len(units) - 1:
145 | size /= 1024
146 | index += 1
147 | return f"{size:.2f} {units[index]}"
148 |
149 |
150 | class Settings():
151 | def __init__(self):
152 | self.domain = request.cookies.get("domain", DEFAULT_GOOGLE_DOMAIN)
153 | self.javascript = request.cookies.get("javascript", "enabled")
154 | self.lang = request.cookies.get("lang", "")
155 | self.new_tab = request.cookies.get("new_tab", "")
156 | self.safe = request.cookies.get("safe", "active")
157 | self.ux_lang = request.cookies.get("ux_lang", DEFAULT_UX_LANG)
158 | self.theme = request.cookies.get("theme", DEFAULT_THEME)
159 | self.method = request.cookies.get("method", DEFAULT_METHOD)
160 | self.ac = request.cookies.get("ac", DEFAULT_AUTOCOMPLETE)
161 | self.engine = request.cookies.get("engine", DEFAULT_ENGINE)
162 | self.torrent = request.cookies.get("torrent", "enabled" if TORRENTSEARCH_ENABLED else "disabled")
163 |
164 |
165 | # Returns a tuple of two ellements.
166 | # The first is the wikipedia proxy's URL (used to load an wiki page's image after page load),
167 | # and the second is an image proxy link for the very image of the page itself.
168 | #
169 | # Either the first or second ellement will be a string, but not both (at least one ellement
170 | # will be None).
171 | #
172 | # NOTE: This function may return (None, None) in cases of failure.
173 | def grab_wiki_image_from_url(wikipedia_url: str, user_settings: Settings) -> tuple[str | None]:
174 | kno_title = None
175 | kno_image = None
176 |
177 | if user_settings.javascript == "enabled":
178 | kno_title = wikipedia_url.split("/")[-1]
179 | kno_title = f"/wikipedia?q={kno_title}"
180 | else:
181 | try:
182 | _kno_title = wikipedia_url.split("/")[-1]
183 | soup = makeHTMLRequest(f"https://wikipedia.org/w/api.php?action=query&format=json&prop=pageimages&titles={_kno_title}&pithumbsize=500", is_wiki=True)
184 | data = json.loads(soup.text)
185 | img_src = data['query']['pages'][list(data['query']['pages'].keys())[0]]['thumbnail']['source']
186 | _kno_image = [f"/img_proxy?url={img_src}"]
187 | _kno_image = ''.join(_kno_image)
188 | finally:
189 | kno_image = _kno_image
190 |
191 | return kno_title, kno_image
192 |
193 |
194 | def format_araa_name(json_obj):
195 | # Recursively format araa_name=ARAA_NAME
196 | if isinstance(json_obj, dict):
197 | return {key: format_araa_name(value) for key, value in json_obj.items()}
198 | elif isinstance(json_obj, list):
199 | return [format_araa_name(item) for item in json_obj]
200 | elif isinstance(json_obj, str):
201 | return json_obj.format(araa_name=ARAA_NAME)
202 | else:
203 | return json_obj
204 |
--------------------------------------------------------------------------------
/src/images.py:
--------------------------------------------------------------------------------
1 | from src import helpers
2 | from urllib.parse import unquote, quote, urlparse
3 | from _config import *
4 | from flask import request, render_template, jsonify, Response, redirect
5 | import time
6 | import json
7 | import requests
8 | import httpx
9 | import trio
10 | import random
11 |
12 | # Debug code uncomment when needed
13 | #import logging, timeit
14 | #logging.basicConfig(level=logging.DEBUG, format="%(message)s")
15 |
16 | # Force all requests to only use IPv4
17 | requests.packages.urllib3.util.connection.HAS_IPV6 = False
18 |
19 | # Force all HTTPX requests to only use IPv4
20 | transport = httpx.HTTPTransport(local_address="0.0.0.0")
21 |
22 | # Pool limit configuration
23 | limits = httpx.Limits(max_keepalive_connections=None, max_connections=None, keepalive_expiry=None)
24 |
25 | # Make a persistent session
26 | qwant = httpx.Client(http2=True, follow_redirects=True, transport=transport, limits=limits)
27 |
28 | def imageResults(query) -> Response:
29 | # get user language settings
30 | settings = helpers.Settings()
31 |
32 | if request.method == "GET":
33 | args = request.args
34 | else:
35 | args = request.form
36 |
37 | json_path = f'static/lang/{settings.ux_lang}.json'
38 | with open(json_path, 'r') as file:
39 | lang_data = helpers.format_araa_name(json.load(file))
40 |
41 | # remember time we started
42 | start_time = time.time()
43 |
44 | api = args.get("api", "false")
45 |
46 | p = args.get('p', '1')
47 | if not p.isdigit():
48 | return redirect('/search')
49 |
50 | # returns 1 if active, else 0
51 | safe_search = int(settings.safe == "active")
52 |
53 | # grab & format webpage
54 | user_agent = random.choice(user_agents)
55 | headers = {"User-Agent": user_agent}
56 | response = qwant.get(f"https://api.qwant.com/v3/search/images?t=images&q={quote(query)}&count=50&locale=en_CA&offset={p}&device=desktop&tgp=2&safesearch={safe_search}", headers=headers)
57 | json_data = response.json()
58 |
59 | # Get all the images from the response, while avoiding any errors.
60 | images = json_data.get("data", {}).get("result", {}).get("items", None)
61 | if images is None:
62 | return redirect('/search')
63 |
64 | results = []
65 | for image in images:
66 | # Get original bing image URL
67 | bing_url = unquote(urlparse(image['thumbnail']).query).split("u=")[1].split("&")[0]
68 |
69 | image['thumb_proxy'] = f"/img_proxy?url={quote(bing_url)}"
70 |
71 | # Get domain name
72 | image['source'] = urlparse(image['url']).netloc
73 |
74 | results.append(image)
75 |
76 | # calc. time spent
77 | end_time = time.time()
78 | elapsed_time = end_time - start_time
79 |
80 | # render
81 | if api == "true" and API_ENABLED:
82 | # return the results list as a JSON response
83 | return jsonify(results)
84 | else:
85 | return render_template("images.html", results=results, title=f"{query} - {ARAA_NAME}",
86 | q=f"{query}", fetched=f"{elapsed_time:.2f}",
87 | type="image",
88 | repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED,
89 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data,
90 | commit=helpers.latest_commit(), settings=settings, araa_name=ARAA_NAME)
91 |
--------------------------------------------------------------------------------
/src/textResults.py:
--------------------------------------------------------------------------------
1 | from src import helpers
2 | from _config import *
3 | from flask import request, render_template, jsonify, Response
4 | import time
5 | import json
6 | import re
7 | from math import isclose # For float comparisons
8 | from src.text_engines import google, qwant
9 |
10 | ENGINES = [
11 | google,
12 | qwant,
13 | ]
14 | ratelimited_timestamps = {}
15 |
16 |
17 | def handleUserInfoQueries(query: str) -> str | None:
18 | if any(query.lower().find(valid_ip_prompt) != -1 for valid_ip_prompt in VALID_IP_PROMPTS):
19 | xff = request.headers.get("X-Forwarded-For")
20 | if xff:
21 | return xff.split(",")[-1].strip()
22 | return request.remote_addr or "unknown"
23 | elif any(query.lower().find(valid_ua_prompt) != -1 for valid_ua_prompt in VALID_UA_PROMPTS):
24 | return request.headers.get("User-Agent") or "unknown"
25 | return None
26 |
27 |
28 | def textResults(query: str) -> Response:
29 | global ratelimited_engines
30 | # get user language settings
31 | settings = helpers.Settings()
32 |
33 | # Define where to get request args from. If the request is using GET,
34 | # use request.args. Otherwise (POST), use request.form
35 | if request.method == "GET":
36 | args = request.args
37 | else:
38 | args = request.form
39 |
40 | with open(f'static/lang/{settings.ux_lang}.json', 'r') as file:
41 | lang_data = helpers.format_araa_name(json.load(file))
42 |
43 | # used to measure time spent
44 | start_time = time.time()
45 |
46 | api = args.get("api", "false")
47 | search_type = args.get("t", "text")
48 | p = args.get("p", 0)
49 |
50 | results = None
51 | ratelimited = True # Used to determine if complete engine failure is due to a bug or due to
52 | # the server getting completely ratelimited from every supported engine.
53 |
54 | engine_list = []
55 | for engine in ENGINES:
56 | if engine.NAME == settings.engine:
57 | # Put prefered engine at the top of the list
58 | engine_list = [engine] + engine_list
59 | else:
60 | engine_list.append(engine)
61 |
62 |
63 | try:
64 | for ENGINE in engine_list:
65 | if (t := ratelimited_timestamps.get(ENGINE.__name__)) is not None and t + ENGINE_RATELIMIT_COOLDOWN_MINUTES * 60 >= time.time():
66 | # Current engine is ratelimited. Skip it.
67 | continue
68 | results = ENGINE.search(query, p, search_type, settings)
69 | if results.code == 429:
70 | t = time.time()
71 | print(f"Text engine {results.engine} was just ratelimited (time={t})")
72 | ratelimited_timestamps[ENGINE.__name__] = t
73 | else: # Server *likely* isn't ratelimited.
74 | ratelimited = False
75 | if results.ok:
76 | break
77 | print(f"WARN: Text engine {results.engine} failed with code {results.code}.")
78 | if results.code == 429:
79 | print("NOTE: this engine just got ratelimited.")
80 | else:
81 | print(f"Response: {results}")
82 | results = None
83 | except Exception as e:
84 | return jsonify({"error": str(e)}), 500
85 |
86 | if results is None:
87 | if ratelimited: # Server is completely ratelimited :(.
88 | return jsonify({"instance_rate_limited": "The instance you are using is rate limited for every supported engine. Try again later."}), 429
89 | else: # *Likely* not ratelimited. Something probably went wrong.
90 | return jsonify({"error": "Complete engine failure. If this occurs multiple times, then " \
91 | "this is *likely* an extremely unfortanute bug. Some additional " \
92 | "information is provided with this error.", "query": query, "type": search_type}), 500
93 |
94 | elapsed_time = time.time() - start_time
95 |
96 | # gets users ip or user agent
97 | info = handleUserInfoQueries(query)
98 | calc = ""
99 | exported_math_expression = ""
100 | # calculator (TODO: Maybe remove expression parsing. It behaves in odd ways, and in general people who need a calculator can just search calculator)
101 | if info == None:
102 | info = ""
103 | math_expression = re.search(r'(\d+(\.\d+)?)\s*([\+\-\*/x])\s*(\d+(\.\d+)?)', query)
104 | if math_expression:
105 | exported_math_expression = math_expression.group(0)
106 | num1 = float(math_expression.group(1))
107 | operator = math_expression.group(3)
108 | num2 = float(math_expression.group(4))
109 |
110 | if operator == '+':
111 | result = num1 + num2
112 | elif operator == '-':
113 | result = num1 - num2
114 | elif operator == '*':
115 | result = num1 * num2
116 | elif operator == 'x':
117 | result = num1 * num2
118 | elif operator == '/':
119 | result = num1 / num2 if not isclose(num2, 0) else "Err; cannot divide by 0."
120 |
121 | try:
122 | result = float(result)
123 | if result.is_integer():
124 | result = int(result)
125 | except:
126 | pass
127 |
128 | calc = result
129 | elif "calculator" in query.lower():
130 | calc = "0"
131 | else:
132 | calc = ""
133 |
134 | if api == "true" and API_ENABLED == True:
135 | # return the results as a JSON response
136 | return jsonify(results.asDICT())
137 | else:
138 | check = "" if results.correction is None else results.correction
139 | snip = "" if results.featured is None else results.featured
140 |
141 | return render_template("results.html",
142 | engine=results.engine,
143 | results=results.results, sublink=results.top_result_sublinks, p=p, title=f"{query} - {ARAA_NAME}",
144 | q=f"{query}", fetched=f"{elapsed_time:.2f}",
145 | snip=f"{snip}",
146 | user_info=f"{info}", calc=f"{calc}", check=check, current_url=request.url,
147 | type=search_type, repo_url=REPO, donate_url=DONATE, commit=helpers.latest_commit(),
148 | exported_math_expression=exported_math_expression, API_ENABLED=API_ENABLED,
149 | TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED, lang_data=lang_data,
150 | settings=settings, wiki=results.wiki, araa_name=ARAA_NAME,
151 | before=args.get("before", ""), after=args.get("after", "")
152 | )
153 |
--------------------------------------------------------------------------------
/src/text_engines/google.py:
--------------------------------------------------------------------------------
1 | from src import helpers
2 | from urllib.parse import unquote, urlparse, parse_qs, urlencode
3 | from _config import *
4 | from src.text_engines.objects.fullEngineResults import FullEngineResults
5 | from src.text_engines.objects.wikiSnippet import WikiSnippet
6 | from src.text_engines.objects.textResult import TextResult
7 | from flask import request
8 |
9 |
10 | NAME = "google"
11 |
12 |
13 | def __local_href__(url):
14 | url_parsed = parse_qs(urlparse(url).query)
15 | if "q" not in url_parsed:
16 | return ""
17 | return f"/search?q={url_parsed['q'][0]}&p=0&t=text"
18 |
19 |
20 | def search(query: str, page: int, search_type: str, user_settings: helpers.Settings) -> FullEngineResults:
21 | if search_type == "reddit":
22 | query += " site:reddit.com"
23 |
24 | after_date = request.args.get("after", "")
25 | before_date = request.args.get("before", "")
26 | if after_date != "":
27 | query += f" after:{after_date}"
28 | if before_date != "":
29 | query += f" before:{before_date}"
30 |
31 | # Random characters are to trick google into thinking it's a mobile phone
32 | # loading more results
33 | # -> https://github.com/searxng/searxng/issues/159
34 | link_args = {
35 | "q": query,
36 | "start": page,
37 | "lr": user_settings.lang,
38 | "num": 20,
39 | "safe": user_settings.safe,
40 | "vet": "12ahUKEwjE4O6xoajxAhWL_KQKHVCLBKoQxK8CegQIAhAG..i",
41 | "ved": "2ahUKEwjE4O6xoajxAhWL_KQKHVCLBKoQqq4CegQIAhAI",
42 | "yv": 3,
43 | "prmd": "vmin",
44 | "ei": "c0fQYITbBIv5kwXQlpLQCg",
45 | "sa": "N",
46 | "asearch": "arc",
47 | "async": "arc_id:srp_510,ffilt:all,ve_name:MoreResultsContainer,next_id:srp_5,use_ac:true,_id:arc-srp_510,_pms:qs,_fmt:pc"
48 | }
49 | link = f"https://www.google.com{user_settings.domain}&" + urlencode(link_args)
50 |
51 | soup, response_code = helpers.makeHTMLRequest(link, is_google=True)
52 |
53 | if response_code != 200:
54 | return FullEngineResults(engine="google", search_type=search_type, ok=False, code=response_code)
55 |
56 | # retrieve links
57 | result_divs = soup.findAll("div", {"class": "yuRUbf"})
58 | links = [div.find("a") for div in result_divs]
59 | hrefs = [link.get("href") for link in links]
60 |
61 | # retrieve title
62 | h3 = [div.find("h3") for div in result_divs]
63 | titles = [titles.text.strip() for titles in h3]
64 |
65 | # retrieve description
66 | result_desc = soup.findAll("div", {"class": "VwiC3b"})
67 | descriptions = [descs.text.strip() for descs in result_desc]
68 |
69 | # retrieve sublinks
70 | try:
71 | result_sublinks = soup.findAll("tr", {"class": lambda x: x and x.startswith("mslg")})
72 | sublinks_divs = [sublink.find("div", {"class": "zz3gNc"}) for sublink in result_sublinks]
73 | sublinks = [sublink.text.strip() for sublink in sublinks_divs]
74 | sublinks_links = [sublink.find("a") for sublink in result_sublinks]
75 | sublinks_hrefs = [link.get("href") for link in sublinks_links]
76 | sublinks_titles = [title.text.strip() for title in sublinks_links]
77 | except:
78 | sublinks = ""
79 | sublinks_hrefs = ""
80 | sublinks_titles = ""
81 |
82 | # retrieve kno-rdesc
83 | try:
84 | rdesc = soup.find("div", {"class": "CYJS5e"})
85 | span_element = rdesc.find("span", {"class": "QoPDcf"})
86 | desc_link = rdesc.find("a", {"class": "y171A"})
87 | kno_link = desc_link.get("href")
88 | kno = span_element.find("span").get_text()
89 | except:
90 | kno = ""
91 | kno_link = ""
92 |
93 | # retrieve kno-title
94 | try: # look for the title inside of a span in div.SPZz6b
95 | rtitle = soup.find("div", {"class": "SPZz6b"})
96 | rt_span = rtitle.find("span")
97 | rkno_title = rt_span.text.strip()
98 | # if we didn't find anyhing useful, move to next tests
99 | if rkno_title in ["", "See results about"]:
100 | raise
101 | except:
102 | for ellement, class_name in zip(["div", "span", "div"], ["DoxwDb", "yKMVIe", "DoxwDb"]):
103 | try:
104 | rtitle = soup.find(ellement, {"class": class_name})
105 | rkno_title = rtitle.text.strip()
106 | except:
107 | continue # couldn't scrape anything. continue if we can.
108 | else:
109 | if rkno_title not in ["", "See results about"]:
110 | break # we got one
111 | else:
112 | rkno_title = ""
113 |
114 | for div in soup.find_all("div", {'class': 'nnFGuf'}):
115 | div.decompose()
116 |
117 | # retrieve featured snippet
118 | try:
119 | featured_snip = soup.find("span", {"class": "hgKElc"})
120 | snip = featured_snip.text.strip()
121 | except:
122 | snip = ""
123 |
124 | # retrieve spell check
125 | try:
126 | spell = soup.find("a", {"class": "gL9Hy"})
127 | check = spell.text.strip()
128 | except:
129 | check = ""
130 | if search_type == "reddit":
131 | check = check.replace("site:reddit.com", "").strip()
132 |
133 | kno_image = None
134 | kno_title = None
135 |
136 | # get wiki image
137 | if kno_link != "":
138 | kno_title, kno_image = helpers.grab_wiki_image_from_url(kno_link, user_settings)
139 |
140 | wiki_known_for = soup.find("div", {'class': 'loJjTe'})
141 | if wiki_known_for is not None:
142 | wiki_known_for = wiki_known_for.get_text().strip()
143 |
144 | wiki_info = {}
145 | wiki_info_divs = soup.find_all("div", {"class": "rVusze"})
146 | for info in wiki_info_divs:
147 | spans = info.findChildren("span" , recursive=False)
148 | for a in spans[1].find_all("a"):
149 | # Delete all non-href attributes
150 | a.attrs = {"href": a.get("href", "")}
151 | if "sca_esv=" in a['href']:
152 | # Remove any trackers for google domains
153 | a['href'] = __local_href__(a.get("href", ""))
154 |
155 | wiki_info[spans[0].get_text()] = spans[1]
156 |
157 | results = []
158 | for href, title, desc in zip(hrefs, titles, descriptions):
159 | results.append(TextResult(
160 | url = unquote(href),
161 | title = title,
162 | desc = desc,
163 | sublinks=[]
164 | ))
165 | sublink = []
166 | for sublink_href, sublink_title, sublink_desc in zip(sublinks_hrefs, sublinks_titles, sublinks):
167 | sublink.append(TextResult(
168 | url = unquote(sublink_href),
169 | title = sublink_title,
170 | desc = sublink_desc,
171 | sublinks=[]
172 | ))
173 |
174 | wiki = None if kno == "" else WikiSnippet(
175 | title = rkno_title,
176 | image = kno_image,
177 | desc = kno,
178 | link = unquote(kno_link),
179 | wiki_thumb_proxy_link = kno_title,
180 | known_for = wiki_known_for,
181 | info = wiki_info,
182 | )
183 |
184 | return FullEngineResults(
185 | engine = "google",
186 | search_type = search_type,
187 | ok = True,
188 | code = 200,
189 | results = results,
190 | wiki = wiki,
191 | featured = None if snip == "" else snip,
192 | correction = None if check == "" else check,
193 | top_result_sublinks = sublink,
194 | )
195 |
--------------------------------------------------------------------------------
/src/text_engines/objects/fullEngineResults.py:
--------------------------------------------------------------------------------
1 | from src.text_engines.objects.textResult import TextResult
2 | from src.text_engines.objects.wikiSnippet import WikiSnippet
3 | from dataclasses import dataclass, field
4 |
5 | @dataclass
6 | class FullEngineResults:
7 | engine: str
8 | search_type: str
9 | ok: bool
10 | code: int
11 | results: list[TextResult] = field(default_factory = list)
12 | wiki: WikiSnippet | None = None
13 | featured: str | None = None
14 | correction: str | None = None
15 | top_result_sublinks: list[TextResult] = field(default_factory = list)
16 |
17 | def asDICT(self):
18 | results_asdict = [result.asDICT() for result in self.results]
19 |
20 | return {
21 | "engine": self.engine,
22 | "type": self.search_type,
23 | "ok": self.ok,
24 | "code": self.code,
25 | "results": results_asdict,
26 | "results.len": len(results_asdict),
27 | "wiki": self.wiki.asDICT() if self.wiki != None else None,
28 | "featured": self.featured,
29 | "correction": self.correction,
30 | "sublinks": self.top_result_sublinks,
31 | "sublinks.len": len(self.top_result_sublinks)
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/src/text_engines/objects/textResult.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | # Provided as a 'blueprint' for a singular result from the text engine.
4 | @dataclass
5 | class TextResult:
6 | title: str
7 | desc: str
8 | url: str
9 | sublinks: list
10 |
11 | def asDICT(self):
12 | return {
13 | "title": self.title,
14 | "desc": self.desc,
15 | "url": self.url,
16 | "sublinks": self.sublinks,
17 | }
18 |
--------------------------------------------------------------------------------
/src/text_engines/objects/wikiSnippet.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 |
3 | @dataclass
4 | class WikiSnippet:
5 | title: str
6 | desc: str
7 | link: str
8 | image: str | None = None
9 | wiki_thumb_proxy_link: str | None = None
10 | known_for: str | None = None
11 | info: dict = field(default_factory = dict)
12 |
13 | def asDICT(self):
14 | # self.info is a dict[str, Tag] => `Tag`s are used because of links.
15 | # we just want a dict[str, str] => we don't want to perserve links, just text.
16 | info_cleaned = {}
17 | for info in self.info.items():
18 | keypoint = info[0][:len(info[0]) - 2] # remove a trailing ": "
19 | info_cleaned[keypoint] = info[1].get_text()
20 |
21 | return {
22 | "title": self.title,
23 | "image": self.image,
24 | "desc": self.desc,
25 | "link": self.link,
26 | "wiki_thumb_proxy_link": self.wiki_thumb_proxy_link,
27 | "known_for": self.known_for,
28 | "info": info_cleaned,
29 | }
30 |
--------------------------------------------------------------------------------
/src/text_engines/qwant.py:
--------------------------------------------------------------------------------
1 | import re
2 | from src.text_engines.objects.fullEngineResults import FullEngineResults
3 | from src.text_engines.objects.textResult import TextResult
4 | from src.text_engines.objects.wikiSnippet import WikiSnippet
5 | from urllib.parse import urlparse, urlencode, unquote
6 | from src import helpers
7 |
8 | NAME = "qwant"
9 |
10 |
11 | def sanitize_wiki(desc):
12 | desc = re.sub(r"\[\d{1,}\]", "", desc)
13 | return desc
14 |
15 |
16 | # NOTE: Qwant engine made by amongusussy. Taken from https://github.com/Extravi/araa-search/pull/106
17 | # Slightly modified to adapt different text results engine.
18 | def search(query: str, page: int, search_type: str, user_settings: helpers.Settings) -> FullEngineResults:
19 | if search_type == "reddit":
20 | query += " site:reddit.com"
21 |
22 | url_args = {
23 | "t": "web",
24 | "q": query,
25 | "count": 10,
26 | "locale": "en_us",
27 | "offset": page,
28 | "device": "desktop",
29 | "safesearch": 2 if user_settings.safe == "active" else 0,
30 | "tgp": 1,
31 | }
32 |
33 | json_data, code = helpers.makeJSONRequest(
34 | "https://api.qwant.com/v3/search/web?{}".format(urlencode(url_args)),
35 | is_qwant=True
36 | )
37 | print(
38 | "https://api.qwant.com/v3/search/web?{}".format(urlencode(url_args)),
39 | )
40 |
41 | if code == 403 and user_settings.safe == "active":
42 | # Qwant returns 403 when safesearch restricted all content.
43 | # This is just to prevent an 'engine failure' error.
44 | return FullEngineResults(
45 | engine="qwant",
46 | search_type=search_type,
47 | ok=True,
48 | code=code,
49 | )
50 |
51 | if json_data['status'] != "success":
52 | # Add error handling later
53 | return FullEngineResults(
54 | engine="qwant",
55 | search_type=search_type,
56 | ok=False,
57 | code=code,
58 | )
59 |
60 | try:
61 | resp_results = json_data["data"]["result"]["items"]["mainline"]
62 | except KeyError:
63 | return FullEngineResults(
64 | engine="qwant",
65 | search_type=search_type,
66 | ok=False,
67 | code=code,
68 | )
69 |
70 | web_results = []
71 | for group in resp_results:
72 | if group.get("type") == "web":
73 | # Only get web results. No images/ads.
74 | web_results += group.get("items", [])
75 |
76 | results = []
77 | wiki = None
78 | for result in web_results:
79 | if len(result['desc']) > 166:
80 | short_desc = result['desc'][:166] + "..."
81 | else:
82 | short_desc = result['desc']
83 |
84 | if result.get("links") is not None:
85 | sublinks = result.get("links")
86 | else:
87 | sublinks = []
88 |
89 | results.append(TextResult(
90 | title=result['title'],
91 | desc=short_desc,
92 | url=unquote(result['url']),
93 | sublinks=sublinks
94 | ))
95 |
96 | # wikipedia snippet scraper
97 | if wiki is None and 'wikipedia.org' in urlparse(result['source']).netloc:
98 | wiki_proxy_link, wiki_image = helpers.grab_wiki_image_from_url(result['source'], user_settings)
99 |
100 | wiki = WikiSnippet(
101 | title=result['title'],
102 | desc=sanitize_wiki(result['desc']),
103 | link=result['source'],
104 | image=wiki_image,
105 | wiki_thumb_proxy_link=wiki_proxy_link,
106 | )
107 |
108 | spell = json_data['data']['query']['queryContext'].get('alteredQuery', '')
109 |
110 | return FullEngineResults(
111 | engine="qwant",
112 | search_type=search_type,
113 | ok=True,
114 | code=200,
115 | results=results,
116 | wiki=wiki,
117 | correction=spell,
118 | )
119 |
--------------------------------------------------------------------------------
/src/torrent_sites/nyaa.py:
--------------------------------------------------------------------------------
1 | from _config import *
2 | from src import helpers
3 | from urllib.parse import quote
4 |
5 | def name():
6 | return "nyaa"
7 |
8 | def get_catagory_code(cat):
9 | match cat:
10 | case "all":
11 | return ""
12 | case "anime":
13 | return "&c=1_0"
14 | case "music":
15 | return "&c=2_0"
16 | case "game":
17 | return "&c=6_2"
18 | case "software":
19 | return "&c=6_1"
20 | case _:
21 | return "ignore"
22 |
23 |
24 | def search(query, catagory="all"):
25 | catagory = get_catagory_code(catagory)
26 | if catagory == "ignore":
27 | return []
28 |
29 | soup, response_code = helpers.makeHTMLRequest(f"https://{NYAA_DOMAIN}/?f=0&q={quote(query)}{catagory}")
30 | results = []
31 | for torrent in soup.select(".default, .success, .danger"):
32 | list_of_tds = torrent.find_all("td")
33 | byte_size = helpers.string_to_bytes(list_of_tds[3].get_text().strip())
34 |
35 | results.append({
36 | "href": NYAA_DOMAIN,
37 | "title": list_of_tds[1].find_all("a")[-1].get_text(),
38 | "magnet": helpers.apply_trackers(list_of_tds[2].find_all("a")[-1]["href"]),
39 | "bytes": byte_size,
40 | "size": helpers.bytes_to_string(byte_size),
41 | "views": None,
42 | "seeders": int(list_of_tds[5].get_text().strip()),
43 | "leechers": int(list_of_tds[6].get_text().strip())
44 | })
45 | return results
46 |
--------------------------------------------------------------------------------
/src/torrent_sites/rutor.py:
--------------------------------------------------------------------------------
1 | from _config import *
2 | from src import helpers
3 | from urllib.parse import quote
4 |
5 | def name():
6 | return "rutor"
7 |
8 | def get_catagory_code(cat):
9 | match cat:
10 | case "all":
11 | return ""
12 | case "audiobook":
13 | return "ignore"
14 | case "movie":
15 | return "&category=1"
16 | case "tv":
17 | return "&category=6"
18 | case "games":
19 | return "&category=8"
20 | case "software":
21 | return "&category=9"
22 | case "anime":
23 | return "&category=10"
24 | case "music":
25 | return "&category=2"
26 | case "xxx":
27 | return "ignore"
28 | case _:
29 | return ""
30 |
31 | def search(query, catagory="all"):
32 | catagory = get_catagory_code(catagory)
33 | if catagory == "ignore":
34 | return []
35 |
36 |
37 | url = f"https://{RUTOR_DOMAIN}/search/{quote(query)}{catagory}"
38 | html, response_code = helpers.makeHTMLRequest(url)
39 | results = []
40 |
41 | for torrent in html.select(".gai, .tum"):
42 | tds = torrent.find_all("td")
43 | spans = torrent.find_all("span")
44 | byte_size = helpers.string_to_bytes(tds[-2].get_text())
45 |
46 | results.append({
47 | "href": RUTOR_DOMAIN,
48 | "title": tds[1].get_text(),
49 | "magnet": helpers.apply_trackers(tds[1].find_all("a")[1]["href"]),
50 | "bytes": byte_size,
51 | "size": helpers.bytes_to_string(byte_size),
52 | "views": None,
53 | "seeders": int(spans[0].get_text()),
54 | "leechers": int(spans[1].get_text()),
55 | })
56 |
57 | return results
58 |
--------------------------------------------------------------------------------
/src/torrent_sites/thepiratebay.py:
--------------------------------------------------------------------------------
1 | from _config import *
2 | from src import helpers
3 | from urllib.parse import quote
4 |
5 | def name():
6 | return "tpb"
7 |
8 | def get_catagory_code(cat):
9 | match cat:
10 | case "all":
11 | return ""
12 | case "audiobook":
13 | return "102"
14 | case "movie":
15 | return "201"
16 | case "tv":
17 | return "205"
18 | case "games":
19 | return "400"
20 | case "software":
21 | return "300"
22 | case "anime":
23 | # TPB has no anime catagory.
24 | return "ignore"
25 | case "music":
26 | return "100"
27 | case "xxx":
28 | safesearch = (request.cookies.get("safe", "active") == "active")
29 | if safesearch:
30 | return "ignore"
31 | return "500"
32 | case _:
33 | return ""
34 |
35 | def search(query, catagory="all"):
36 | catagory = get_catagory_code(catagory)
37 | if catagory == "ignore":
38 | return []
39 |
40 | url = f"https://{API_BAY_DOMAIN}/q.php?q={quote(query)}&cat={catagory}"
41 | torrent_data = helpers.makeJSONRequest(url)
42 | results = []
43 |
44 | for torrent in torrent_data:
45 | byte_size = int(torrent["size"])
46 | results.append({
47 | "href": "thepiratebay.org",
48 | "title": torrent["name"],
49 | "magnet": helpers.apply_trackers(
50 | torrent["info_hash"],
51 | name=torrent["name"],
52 | magnet=False
53 | ),
54 | "bytes": byte_size,
55 | "size": helpers.bytes_to_string(byte_size),
56 | "views": None,
57 | "seeders": int(torrent["seeders"]),
58 | "leechers": int(torrent["leechers"])
59 | })
60 |
61 | return results
62 |
--------------------------------------------------------------------------------
/src/torrent_sites/torrentgalaxy.py:
--------------------------------------------------------------------------------
1 | from _config import *
2 | from src import helpers
3 | from urllib.parse import quote
4 |
5 | def name():
6 | return "torrentgalaxy"
7 |
8 | def get_catagory_code(cat):
9 | match cat:
10 | case "all":
11 | return ""
12 | case "audiobook":
13 | return "&c13=1"
14 | case "movie":
15 | return "&c3=1&c46=1&c45=1&c42=1&c4=1&c1=1"
16 | case "tv":
17 | return "&c41=1&c5=1&c11=1&c6=1&c7=1"
18 | case "games":
19 | return "&c43=1&c10=1"
20 | case "software":
21 | return "&c20=1&c21=1&c18=1"
22 | case "anime":
23 | return "&c28=1"
24 | case "music":
25 | return "&c28=1&c22=1&c26=1&c23=1&c25=1&c24=1"
26 | case "xxx":
27 | safesearch = (request.cookies.get("safe", "active") == "active")
28 | if safesearch:
29 | return "ignore"
30 | return "&c48=1&c35=1&c47=1&c34=1"
31 | case _:
32 | return ""
33 |
34 |
35 | def search(query, catagory="all"):
36 | catagory = get_catagory_code(catagory)
37 | if catagory == "ignore":
38 | return []
39 | soup, response_code = helpers.makeHTMLRequest(f"https://{TORRENTGALAXY_DOMAIN}/torrents.php?search={quote(query)}{catagory}#results")
40 |
41 | result_divs = soup.findAll("div", {"class": "tgxtablerow"})
42 | title = [div.find("div", {"id": "click"}) for div in result_divs]
43 | title = [title.text.strip() for title in title]
44 | magnet_links = [
45 | div.find("a", href=lambda href: href and href.startswith("magnet")).get("href")
46 | for div in result_divs
47 | ]
48 | byte_sizes = [
49 | helpers.string_to_bytes(div.find("span", {"class": "badge-secondary"}).text.strip())
50 | for div in result_divs
51 | ]
52 | view_counts = [int(div.find("font", {"color": "orange"}).text.replace(',', '')) for div in result_divs]
53 | seeders = [int(div.find("font", {"color": "green"}).text.replace(',', '')) for div in result_divs]
54 | leechers = [int(div.find("font", {"color": "#ff0000"}).text.replace(',', '')) for div in result_divs]
55 |
56 | # list
57 | results = []
58 | for title, magnet_link, byte_size, view_count, seeder, leecher in zip(
59 | title, magnet_links, byte_sizes, view_counts, seeders, leechers):
60 | results.append({
61 | "href": TORRENTGALAXY_DOMAIN,
62 | "title": title,
63 | "magnet": helpers.apply_trackers(magnet_link),
64 | "bytes": byte_size,
65 | "size": helpers.bytes_to_string(byte_size),
66 | "views": view_count,
67 | "seeders": seeder,
68 | "leechers": leecher
69 | })
70 |
71 | return results
72 |
--------------------------------------------------------------------------------
/src/torrents.py:
--------------------------------------------------------------------------------
1 | import time
2 | import json
3 | from src import helpers
4 | from _config import *
5 | from flask import request, render_template, jsonify, Response
6 | from src.torrent_sites import torrentgalaxy, nyaa, thepiratebay, rutor
7 |
8 | def torrentResults(query) -> Response:
9 | settings = helpers.Settings()
10 |
11 | if not TORRENTSEARCH_ENABLED:
12 | return jsonify({"error": "Torrent search disabled by instance operator"}), 503
13 |
14 | # Define where to get request args from. If the request is using GET,
15 | # use request.args. Otherwise (POST), use request.form
16 | if request.method == "GET":
17 | args = request.args
18 | else:
19 | args = request.form
20 |
21 | # get user language settings
22 | json_path = f'static/lang/{settings.ux_lang}.json'
23 | with open(json_path, 'r') as file:
24 | lang_data = helpers.format_araa_name(json.load(file))
25 |
26 | # remember time we started
27 | start_time = time.time()
28 |
29 | api = args.get("api", "false")
30 | catagory = args.get("cat", "all")
31 | query = args.get("q", " ").strip()
32 | sort = args.get("sort", "seed")
33 | if sort not in ["seed", "leech", "lth", "htl"]:
34 | sort = "seed"
35 |
36 | sites = [
37 | torrentgalaxy,
38 | nyaa,
39 | thepiratebay,
40 | rutor,
41 | ]
42 |
43 | results = []
44 | for site in sites:
45 | if site.name() in ENABLED_TORRENT_SITES:
46 | try:
47 | # For some reason, rutor doesn't give reliable catagories.
48 | if catagory != "all" and site.name() == "rutor":
49 | continue
50 | results += site.search(query, catagory=catagory)
51 | except:
52 | continue
53 |
54 | # Allow user to decide how the results are sorted
55 | match sort:
56 | case "leech":
57 | results = sorted(results, key=lambda x: x["leechers"])[::-1]
58 | case "lth": # Low to High file size
59 | results = sorted(results, key=lambda x: x["bytes"])
60 | case "htl": # High to low file size
61 | results = sorted(results, key=lambda x: x["bytes"])[::-1]
62 | case _: # Defaults to seeders
63 | results = sorted(results, key=lambda x: x["seeders"])[::-1]
64 |
65 |
66 | # calc. time spent
67 | end_time = time.time()
68 | elapsed_time = end_time - start_time
69 |
70 | if api == "true" and API_ENABLED:
71 | # return the results list as a JSON response
72 | return jsonify(results)
73 |
74 | return render_template("torrents.html",
75 | results=results, title=f"{query} - {ARAA_NAME}",
76 | q=f"{query}", fetched=f"{elapsed_time:.2f}",
77 | cat=catagory, type="torrent", repo_url=REPO, donate_url=DONATE,
78 | API_ENABLED=API_ENABLED, TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED,
79 | lang_data=lang_data, commit=helpers.latest_commit(), sort=sort, settings=settings,
80 | araa_name=ARAA_NAME
81 | )
82 |
--------------------------------------------------------------------------------
/src/video.py:
--------------------------------------------------------------------------------
1 | from src.helpers import makeHTMLRequest
2 | from src import helpers
3 | from _config import *
4 | from flask import request, render_template, jsonify, Response
5 | import time
6 | import json
7 | from src.helpers import latest_commit
8 | from urllib.parse import quote
9 |
10 |
11 | def parse_time(time):
12 | hours = time // 3600
13 | minutes = (time % 3600) // 60
14 | seconds = time % 60
15 | time_string = ""
16 | if hours != 0:
17 | time_string += f"{hours:02d}:"
18 | return f"{time_string}{minutes:02d}:{seconds:02d}"
19 |
20 |
21 | def videoResults(query) -> Response:
22 | settings = helpers.Settings()
23 |
24 | # Define where to get request args from. If the request is using GET,
25 | # use request.args. Otherwise (POST), use request.form
26 | if request.method == "GET":
27 | args = request.args
28 | else:
29 | args = request.form
30 |
31 | json_path = f'static/lang/{settings.ux_lang}.json'
32 | with open(json_path, 'r') as file:
33 | lang_data = helpers.format_araa_name(json.load(file))
34 |
35 | # remember time we started
36 | start_time = time.time()
37 |
38 | api = args.get("api", "false")
39 |
40 | # grab & format webpage
41 | soup, response_code = makeHTMLRequest(f"https://{PIPED_INSTANCE_API}/search?q={quote(query)}&filter=all", is_piped=True)
42 | data = json.loads(soup.text)
43 |
44 | # retrieve links
45 | ytIds = [item["url"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
46 | hrefs = [f"https://{PIPED_INSTANCE}{ytId}" for ytId in ytIds]
47 |
48 | # retrieve title
49 | title = [item["title"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
50 |
51 | # retrieve date
52 | date_span = [item["uploadedDate"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
53 |
54 | # retrieve views
55 | views = [f"{views//1000000000}B views" if views >= 1000000000 else f"{views//1000000}M views" if views >= 1000000 else f"{views/1000:.1f}K views" if 1000 < views < 10000 else f"{views//1000}K views" if views >= 10000 else f"{views} views" for views in [item["views"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]]
56 |
57 | # retrieve creator
58 | creator_text = [item["uploaderName"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
59 |
60 | # retrieve publisher
61 | publisher_text = ["Piped" for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
62 |
63 | # retrieve images
64 | filtered_urls = [item["thumbnail"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
65 | filtered_urls = [f'/img_proxy?url={filtered_url}' for filtered_url in filtered_urls]
66 |
67 | # retrieve time
68 | duration = [item["duration"] for item in data["items"] if item.get("type") not in ["channel", "playlist"]]
69 | formatted_durations = [parse_time(duration) for duration in duration]
70 |
71 | # list
72 | results = []
73 | for href, title, date, view, creator, publisher, image, duration in zip(hrefs, title, date_span, views, creator_text, publisher_text, filtered_urls, formatted_durations):
74 | results.append([href, title, date, view, creator, publisher, image, duration])
75 |
76 | # calc. time spent
77 | end_time = time.time()
78 | elapsed_time = end_time - start_time
79 |
80 | if api == "true" and API_ENABLED == True:
81 | # return the results list as a JSON response
82 | return jsonify(results)
83 | else:
84 | return render_template("videos.html",
85 | results=results, title=f"{query} - {ARAA_NAME}",
86 | q=f"{query}", fetched=f"{elapsed_time:.2f}",
87 | type="video", repo_url=REPO, donate_url=DONATE, API_ENABLED=API_ENABLED, TORRENTSEARCH_ENABLED=TORRENTSEARCH_ENABLED,
88 | lang_data=lang_data, commit=latest_commit(), settings=settings, araa_name=ARAA_NAME
89 | )
90 |
--------------------------------------------------------------------------------
/static/calculator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./calculator.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | const calcInput = document.getElementById('current_calc');
30 | const numberButtons = document.querySelectorAll('.calc-btn-style:not(#ce):not(#backspace)');
31 | const addBtn = document.getElementById('add');
32 | const subtractBtn = document.getElementById('subtract');
33 | const multiplyBtn = document.getElementById('multiply');
34 | const divideBtn = document.getElementById('divide');
35 | const equalsBtn = document.getElementById('equals');
36 | const clearBtn = document.getElementById('ce');
37 | const backspaceBtn = document.getElementById('backspace');
38 |
39 | document.body.addEventListener('keydown', (key) => {
40 | if (key.target.tagName.toLowerCase() == "input") {
41 | return;
42 | }
43 |
44 | if ('0123456789().'.includes(key.key)) {
45 | numberButtonHandle(key.key);
46 | }
47 | else if ('+-*/'.includes(key.key)) {
48 | operatorButtonHandle(key.key);
49 | }
50 | else switch (key.key) {
51 | case 'Backspace':
52 | doBackspace();
53 | break;
54 | case 'Enter':
55 | evaluateExpression();
56 | break;
57 | }
58 | })
59 |
60 | numberButtons.forEach(button => {
61 | button.addEventListener('click', () => numberButtonHandle(button.textContent));
62 | });
63 |
64 | addBtn.addEventListener('click', () => operatorButtonHandle('+'));
65 | subtractBtn.addEventListener('click', () => operatorButtonHandle('-'));
66 | multiplyBtn.addEventListener('click', () => operatorButtonHandle('*'));
67 | divideBtn.addEventListener('click', () => operatorButtonHandle('/'));
68 |
69 | clearBtn.addEventListener('click', () => {
70 | calcInput.textContent = '0';
71 | });
72 |
73 | backspaceBtn.addEventListener('click', doBackspace);
74 |
75 | equalsBtn.addEventListener('click', evaluateExpression);
76 |
77 | // Executes a 'backspace' on the calculator's expression.
78 | // Made to reduce repetitive code.
79 | function doBackspace() {
80 | do {
81 | calcInput.textContent = calcInput.textContent.slice(0, -1);
82 | } while (calcInput.textContent.endsWith(' '));
83 | // ^^^ Sometimes there are spaces in the expression (see above listeners).
84 | // Remove them aswell.
85 |
86 | // If the contents of calcInput have been completely cleared, output 0
87 | // to the textbox to match the clear button's behaviour.
88 | if (calcInput.textContent.length === 0) {
89 | calcInput.textContent = '0';
90 | }
91 | }
92 |
93 | // Handles the presses to all operator buttons.
94 | function operatorButtonHandle(operator) {
95 | // Avoid multiple operators being added.
96 | // This will override the current operator with the new operator being added.
97 | if (/\+|\-|\*|\//.test(calcInput.textContent.split(' ').pop())) {
98 | calcInput.textContent = calcInput.textContent.substring(0, calcInput.textContent.length - 1);
99 | }
100 |
101 | // Make a decimal that's '1234.' into '1234.0' before adding an operator.
102 | if (calcInput.textContent[calcInput.textContent.length - 1] === '.') {
103 | calcInput.textContent += '0';
104 | }
105 |
106 | calcInput.textContent += ` ${operator}`;
107 | }
108 |
109 | // Handles the presses to all numberButtons.
110 | function numberButtonHandle(button) {
111 | // The absolute last char of the expression; i.e. '3' in '4 + 5 * 6 + 2.3'
112 | const lastChar = calcInput.textContent[calcInput.textContent.length - 1];
113 |
114 | // If the end of calcInput has an operator, append an extra space for
115 | // the number to make the expression look better.
116 | if (/\+|\-|\*|\//.test(lastChar)) {
117 | calcInput.textContent += ' ';
118 | }
119 | // Add a multiplication operator around brackets.
120 | else if ((lastChar === ")" || button === "(") && calcInput.textContent !== '0') {
121 | operatorButtonHandle('* ');
122 | }
123 |
124 | // The 'trailing' substring in an expression; i.e. 2.3 in '4 + 5 * 6 + 2.3'
125 | // Collected here as the expression may have been modified by the above code
126 | const trailing = calcInput.textContent.split(' ').pop();
127 |
128 | // Do some specific things for decimals.
129 | if (button === '.') {
130 | // Prevent multiple dots in a number.
131 | if (trailing.includes('.')) {
132 | return;
133 | }
134 |
135 | // Add an extra 0 if the trailing substring is blank or opening parenthesis
136 | // Makes thinks look nicer (0.3 instead of .3).
137 | if (trailing.length === 0 || trailing === '(') {
138 | calcInput.textContent += '0';
139 | }
140 | }
141 | // Remove any 0 output if a dot is not being added.
142 | // i.e if 9 is input and the expression is '9 + 0', it'll change
143 | // to '9 + 9' because of this if statement.
144 | else if (trailing === '0') {
145 | calcInput.textContent = calcInput.textContent.substring(0, calcInput.textContent.length - 1);
146 | }
147 |
148 | calcInput.textContent += button;
149 | }
150 |
151 | // Implementation from https://github.com/TommyPang/SimpleCalculator.
152 | // Slightly modified.
153 |
154 | function evaluateExpression() {
155 | let expression = calcInput.textContent;
156 | document.querySelector(".prev_calculation").textContent = expression;
157 | expression = expression.replace(/\s/g, '');
158 | calcInput.textContent = helper(Array.from(expression));
159 | }
160 |
161 | function helper(s, idx = 0) {
162 | var stk = [];
163 | let sign = '+';
164 | let num = 0;
165 | let decimalFlag = false;
166 | let decimalDivider = 1;
167 |
168 | for (let i = idx; i < s.length; i++) {
169 | let c = s[i];
170 |
171 | if (c >= '0' && c <= '9') {
172 | if (decimalFlag) {
173 | // Handle numbers after decimal point
174 | decimalDivider *= 10;
175 | num = num + (parseInt(c) / decimalDivider);
176 | } else {
177 | // Handle whole numbers
178 | num = num * 10 + (c - '0');
179 | }
180 | } else if (c === '.') {
181 | decimalFlag = true;
182 | }
183 |
184 | if ((!(c >= '0' && c <= '9') && c !== '.') || i === s.length - 1) {
185 | if (c === '(') {
186 | num = helper(s, i + 1);
187 | let l = 1,
188 | r = 0;
189 | for (let j = i + 1; j < s.length; j++) {
190 | if (s[j] === ')') {
191 | r++;
192 | if (r === l) {
193 | i = j;
194 | break;
195 | }
196 | } else if (s[j] === '(') l++;
197 | }
198 | }
199 |
200 | let pre = -1;
201 | switch (sign) {
202 | case '+':
203 | stk.push(num);
204 | break;
205 | case '-':
206 | stk.push(num * -1);
207 | break;
208 | case '*':
209 | pre = stk.pop();
210 | stk.push(pre * num);
211 | break;
212 | case '/':
213 | pre = stk.pop();
214 | stk.push(pre / num);
215 | break;
216 | }
217 | sign = c;
218 | num = 0;
219 | decimalFlag = false;
220 | decimalDivider = 1;
221 |
222 | if (c === ')') {
223 | break;
224 | }
225 | }
226 | }
227 |
228 | let ans = 0;
229 | while (stk.length > 0) {
230 | ans += stk.pop();
231 | }
232 | return ans;
233 | }
234 |
--------------------------------------------------------------------------------
/static/cookies-settings-version.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./cookies-settings-version.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | function setCookie(name, value) {
30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`;
31 | }
32 |
33 | document.addEventListener("DOMContentLoaded", function () {
34 | const saveButton = document.querySelector(".save-settings-page");
35 |
36 | if (saveButton === null) {
37 | return;
38 | }
39 |
40 | saveButton.addEventListener("click", function () {
41 | let setting_list = ["lang", "domain", "theme", "safe", "open-new-tab", "ux_lang", "ac"];
42 | for (let i = 0; i < setting_list.length; i++) {
43 | setting = setting_list[i];
44 | settingSelect = document.getElementById(setting);
45 | if (settingSelect) {
46 | const selectedOption = settingSelect.options[settingSelect.selectedIndex];
47 | const selectedValue = selectedOption.value;
48 | setCookie(setting, selectedValue);
49 | }
50 | }
51 | });
52 | });
53 |
54 | document.getElementById("discoverButton").addEventListener("click", function (event) {
55 | event.preventDefault();
56 | window.location.href = "/discover";
57 | });
58 |
--------------------------------------------------------------------------------
/static/cookies.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./cookies.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | function setCookie(name, value) {
30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`;
31 | }
32 |
33 | function reloadPageForTheme() {
34 | const themeCookie = document.cookie.split(";").find((cookie) => cookie.trim().startsWith("theme="));
35 |
36 | if (themeCookie) {
37 | window.location.reload();
38 | }
39 | }
40 |
41 | document.addEventListener("DOMContentLoaded", function () {
42 | const langSelect = document.querySelector(".lang");
43 |
44 | if (langSelect) {
45 | langSelect.addEventListener("change", function () {
46 | const selectedOption = langSelect.options[langSelect.selectedIndex];
47 | const selectedValue = selectedOption.value;
48 | setCookie("lang", selectedValue);
49 | window.location.reload();
50 | });
51 | }
52 |
53 | const domainSelect = document.querySelector(".domain");
54 |
55 | if (domainSelect) {
56 | domainSelect.addEventListener("change", function () {
57 | const selectedOption = domainSelect.options[domainSelect.selectedIndex];
58 | const selectedValue = selectedOption.value;
59 | setCookie("domain", selectedValue);
60 | window.location.reload();
61 | });
62 | }
63 |
64 | const engineSelect = document.querySelector("#engine_select")
65 | if (engineSelect) {
66 | engineSelect.addEventListener("change", function () {
67 | const selectedOption = engineSelect.options[engineSelect.selectedIndex];
68 | const selectedValue = selectedOption.value;
69 | setCookie("engine", selectedValue);
70 | window.location.reload();
71 | });
72 | }
73 |
74 | const themeDivs = document.querySelectorAll(".themes-settings-menu div");
75 |
76 | themeDivs.forEach(function (div) {
77 | div.addEventListener("click", function () {
78 | const clickedDivId = div.firstElementChild.id;
79 | setCookie("theme", clickedDivId);
80 | reloadPageForTheme();
81 | });
82 | });
83 |
84 | const safeSearchSelect = document.getElementById("safeSearchSelect");
85 |
86 | if (safeSearchSelect) {
87 | safeSearchSelect.addEventListener("change", function () {
88 | const selectedValue = safeSearchSelect.value;
89 | setCookie("safe", selectedValue);
90 | window.location.reload();
91 | });
92 | }
93 |
94 | const languageSelect = document.getElementById("languageSelect");
95 |
96 | if (languageSelect) {
97 | languageSelect.addEventListener("change", function () {
98 | const selectedValue = languageSelect.value;
99 | setCookie("lang", selectedValue);
100 | window.location.reload();
101 | });
102 | }
103 | });
104 |
--------------------------------------------------------------------------------
/static/css/ancient.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #306763;
3 | --font-fg: #000000;
4 | --fg: #000000;
5 |
6 | --search-bg: #c0c0c0;
7 | --search-bg-input: #c0c0c0;
8 | --search-bg-input-border: #303030;
9 | --search-select: #c0c0c0;
10 |
11 | --border: #303030;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #303030;
17 | --snip-background: #c0c0c0;
18 | --snip-text: #ffffff;
19 |
20 | --settings-border: #303030;
21 | --button: #c0c0c0;
22 |
23 | --footer-bg: #c0c0c0;
24 | --footer-font: #000000;
25 |
26 | --highlight: #000000;
27 | --link: #1a0dab;
28 | --link-visited: #681da8;
29 |
30 | --blue: #306763;
31 |
32 | --green: #31b06e;
33 |
34 | --image-view: #c0c0c0;
35 | --image-view-titlebar: #c0c0c0;
36 | --view-image-color: #000000;
37 | --image-select: #303030;
38 | --fff: #fff;
39 |
40 | --search-button: #000000;
41 |
42 | --publish-info: #7f869e;
43 |
44 | color-scheme: light;
45 | }
46 |
47 | .results-settings {
48 | background: #c0c0c0;
49 | border: 8px solid #c0c0c0;
50 | box-shadow: 2px 2px #808080;
51 | color: #000000;
52 | }
53 |
54 | button {
55 | background: #c0c0c0;
56 | border: 8px solid #c0c0c0;
57 | box-shadow: 2px 2px #808080;
58 | color: #000000;
59 | }
60 |
61 | .results {
62 | background: #c0c0c0;
63 | border: 8px solid #c0c0c0;
64 | box-shadow: 2px 2px #808080;
65 | color: #000000;
66 | }
67 |
68 |
69 | .result_sublink {
70 | background: #c0c0c0;
71 | border: 8px solid #c0c0c0;
72 | box-shadow: 2px 2px #808080;
73 | color: #000000;
74 | }
75 |
76 | .calc-btn:hover {
77 | background: #c0c0c0;
78 | border: 1px solid #c0c0c0;
79 | box-shadow: 2px 2px #808080;
80 | color: #000000;
81 | }
82 |
83 | .calc-btn-2:hover {
84 | background: #c0c0c0;
85 | }
86 |
87 | .calc-btn {
88 | background: #c0c0c0;
89 | }
90 |
91 | .calc {
92 | background: #c0c0c0;
93 | box-shadow: -2px -2px #c0c0c0;
94 | color: #000000;
95 | }
96 |
97 | .image {
98 | background: #c0c0c0;
99 | border: 8px solid #c0c0c0;
100 | box-shadow: 2px 2px #808080;
101 | color: #000000;
102 | }
103 |
104 | .view-image-search {
105 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
106 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
107 | }
108 |
109 | .view-image-search:hover {
110 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
111 | }
--------------------------------------------------------------------------------
/static/css/catppuccin_mocha_blue.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #1e1e2e;
3 | --font-fg: #cdd6f4;
4 | --fg: #cdd6f4;
5 |
6 | --search-bg: #11111b;
7 | --search-bg-input: #1e1e2e;
8 | --search-bg-input-border: #303030;
9 | --search-select: #11111b;
10 |
11 | --border: #303030;
12 |
13 | --link: #89b4fa;
14 | --link-visited: #cba6f7;
15 |
16 | --snip-border: #303030;
17 | --snip-background: #11111b;
18 | --snip-text: #cdd6f4;
19 |
20 | --settings-border: #303030;
21 | --button: #11111b;
22 |
23 | --footer-bg: #11111b;
24 | --footer-font: #cdd6f4;
25 |
26 | --highlight: #ofofof;
27 |
28 | --blue: #89b4fa;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #11111b;
33 | --image-view-titlebar: #11111b;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #cdd6f4;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/catppuccin_mocha_green.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #1e1e2e;
3 | --font-fg: #cdd6f4;
4 | --fg: #cdd6f4;
5 |
6 | --search-bg: #11111b;
7 | --search-bg-input: #1e1e2e;
8 | --search-bg-input-border: #303030;
9 | --search-select: #11111b;
10 |
11 | --border: #303030;
12 |
13 | --link: #a6e3a1;
14 | --link-visited: #cba6f7;
15 |
16 | --snip-border: #303030;
17 | --snip-background: #11111b;
18 | --snip-text: #cdd6f4;
19 |
20 | --settings-border: #303030;
21 | --button: #11111b;
22 |
23 | --footer-bg: #11111b;
24 | --footer-font: #cdd6f4;
25 |
26 | --highlight: #ofofof;
27 |
28 | --blue: #a6e3a1;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #11111b;
33 | --image-view-titlebar: #11111b;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #cdd6f4;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/classic.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #202124;
3 | --font-fg: #f1f3f4;
4 | --fg: #BABCBE;
5 |
6 | --search-bg: #202124;
7 | --search-bg-input: #303134;
8 | --search-bg-input-border: #3C4043;
9 | --search-select: #3c4043;
10 |
11 | --border: #3c4043;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #3c4043;
17 | --snip-background: #202124;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #5f6368;
21 | --button: #303134;
22 |
23 | --footer-bg: #171717;
24 | --footer-font: #BABCBE;
25 |
26 | --highlight: #bcc0c3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --search-button: #BABCBE;
33 |
34 | --image-view: #171717;
35 | --image-view-titlebar: #171717;
36 | --view-image-color: #000000;
37 | --image-select: #303030;
38 | --fff: #fff;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/dark.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #1c1c1c;
3 | --font-fg: #f1f3f4;
4 | --fg: #BABCBE;
5 |
6 | --search-bg: #161616;
7 | --search-bg-input: #333333;
8 | --search-bg-input-border: #3C4043;
9 | --search-select: #282828;
10 |
11 | --border: #303134;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #303134;
17 | --snip-background: #282828;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #5f6368;
21 | --button: #333333;
22 |
23 | --footer-bg: #161616;
24 | --footer-font: #999da2;
25 |
26 | --highlight: #bcc0c3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --search-button: #BABCBE;
33 |
34 | --image-view: #161616;
35 | --image-view-titlebar: #161616;
36 | --view-image-color: #000000;
37 | --image-select: #303030;
38 | --fff: #fff;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/dark_blur.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #171717;
3 | --font-fg: #f1f3f4;
4 | --fg: #BABCBE;
5 |
6 | --search-bg: #101010;
7 | --search-bg-input: #202020;
8 | --search-bg-input-border: #2a2a2a;
9 | --search-select: #303030;
10 |
11 | --border: #2a2a2a;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #282828;
17 | --snip-background: #202020;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #5f6368;
21 | --button: #202020;
22 |
23 | --footer-bg: #101010;
24 | --footer-font: #999da2;
25 |
26 | --highlight: #bcc0c3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --search-button: #BABCBE;
33 |
34 | --image-view: #101010;
35 | --image-view-titlebar: #101010;
36 | --view-image-color: #000000;
37 | --image-select: #303030;
38 | --fff: #fff;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .snip {
46 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
47 | }
48 |
49 | .calc-btn:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2:hover {
54 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
55 | }
56 |
57 | .calc-btn-2 {
58 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
59 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
60 | }
61 |
62 | .calc-btn {
63 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
64 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
65 | }
66 |
67 | .calc {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
69 | }
70 |
71 | .view-image-search {
72 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
73 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
74 | }
75 |
76 | .view-image-search:hover {
77 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
78 | }
79 |
--------------------------------------------------------------------------------
/static/css/dark_blur_beta.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | height: 100%;
4 | }
5 |
6 | body {
7 | min-height: 100%;
8 | overflow-x: hidden;
9 | }
10 |
11 | html::before {
12 | content: "";
13 | position: absolute;
14 | top: -10px;
15 | left: -10px;
16 | width: calc(100% + 20px);
17 | height: calc(100% + 20px);
18 | background: url("/sheng-l-q2dUSl9S4Xg-unsplash.webp") no-repeat center;
19 | background-size: cover;
20 | filter: blur(5px) brightness(67%);
21 | z-index: -1;
22 | }
23 |
24 | .search-container input {
25 | background: rgba(21, 21, 21, 0);
26 | }
27 |
28 | .wrapper {
29 | background: rgba(21, 21, 21, 0.7);
30 | -webkit-backdrop-filter: blur(20px);
31 | backdrop-filter: blur(20px);
32 | box-shadow: 0 4px 8px rgba(0, 0, 0, .2);
33 | --search-bg-input-border: rgba(60, 64, 67, 0.7);
34 | }
35 |
36 | .autocomplete ul li:hover {
37 | background: rgba(255, 255, 255, 0.2);
38 | }
39 |
40 | .selected {
41 | background: rgba(255, 255, 255, 0.2);
42 | }
43 |
44 | .search-button-wrapper button {
45 | background: rgba(21, 21, 21, 0.7);
46 | -webkit-backdrop-filter: blur(20px);
47 | backdrop-filter: blur(20px);
48 | box-shadow: 0 4px 8px rgba(0, 0, 0, .2);
49 | }
--------------------------------------------------------------------------------
/static/css/dark_default.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #171717;
3 | --font-fg: #f1f3f4;
4 | --fg: #BABCBE;
5 |
6 | --search-bg: #101010;
7 | --search-bg-input: #202020;
8 | --search-bg-input-border: #2a2a2a;
9 | --search-select: #303030;
10 |
11 | --border: #2a2a2a;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #282828;
17 | --snip-background: #202020;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #5f6368;
21 | --button: #202020;
22 |
23 | --footer-bg: #101010;
24 | --footer-font: #999da2;
25 |
26 | --highlight: #bcc0c3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --search-button: #BABCBE;
33 |
34 | --image-view: #101010;
35 | --image-view-titlebar: #101010;
36 | --view-image-color: #000000;
37 | --image-select: #303030;
38 | --fff: #fff;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .snip {
46 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
47 | }
48 |
49 | .calc-btn:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2:hover {
54 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
55 | }
56 |
57 | .calc-btn-2 {
58 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
59 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
60 | }
61 |
62 | .calc-btn {
63 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
64 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
65 | }
66 |
67 | .calc {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
69 | }
70 |
71 | .view-image-search {
72 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
73 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
74 | }
75 |
76 | .view-image-search:hover {
77 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
78 | }
--------------------------------------------------------------------------------
/static/css/darker.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #0f0f0f;
3 | --font-fg: #f1f3f4;
4 | --fg: #BABCBE;
5 |
6 | --search-bg: #0f0f0f;
7 | --search-bg-input: #121212;
8 | --search-bg-input-border: #303030;
9 | --search-select: #272727;
10 |
11 | --border: #303030;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #303030;
17 | --snip-background: #0f0f0f;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #303030;
21 | --button: #272727;
22 |
23 | --footer-bg: #0f0f0f;
24 | --footer-font: #BABCBE;
25 |
26 | --highlight: #bcc0c3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #0f0f0f;
33 | --image-view-titlebar: #0f0f0f;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #BABCBE;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/gentoo_lavender.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #dddaec;
3 | --font-fg: #000000;
4 | --fg: #202124;
5 |
6 | --search-bg: #ffffff;
7 | --search-bg-input: #f6f6f6;
8 | --search-bg-input-border: #dadce0;
9 | --search-select: #eeeeee;
10 |
11 | --border: #dadce0;
12 |
13 | --link: #1a0dab;
14 | --link-visited: #681da8;
15 |
16 | --snip-border: #dadce0;
17 | --snip-background: #ffffff;
18 | --snip-text: #000000;
19 |
20 | --settings-border: #5f6368;
21 | --button: #f6f6f6;
22 |
23 | --footer-bg: #e1e1e1;
24 | --footer-font: #4c416e;
25 |
26 | --highlight: #202124;
27 |
28 | --blue: #4c416e;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #ffffff;
33 | --image-view-titlebar: #ffffff;
34 | --view-image-color: #f1f3f4;
35 | --image-select: #f6f6f6;
36 | --fff: #fff;
37 |
38 | --publish-info: #7f869e;
39 |
40 | --search-button: #202124;
41 | }
42 |
43 | .wrapper-results:hover,
44 | .wrapper-results:focus-within,
45 | .wrapper:hover,
46 | .wrapper:focus-within {
47 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
48 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
49 | }
50 |
51 | .check p {
52 | color: var(--highlight) !important;
53 | }
54 |
55 | .image_view {
56 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
57 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
58 | }
59 |
60 | .view-image-search {
61 | box-shadow: none;
62 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
63 | }
64 |
65 | .view-image-search:hover {
66 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
67 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
68 | }
69 |
--------------------------------------------------------------------------------
/static/css/github_night.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #0d1117;
3 | --font-fg: #f0f6fc;
4 | --fg: #8b949e;
5 |
6 | --search-bg: #161b22;
7 | --search-bg-input: #1f242b;
8 | --search-bg-input-border: #303842;
9 | --search-select: #316dca;
10 |
11 | --border: #303842;
12 |
13 | --link: #58a6ff;
14 | --link-visited: #bd93f9;
15 |
16 | --snip-border: #303842;
17 | --snip-background: #161b22;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #303842;
21 | --button: #2b3036;
22 |
23 | --footer-bg: #161b22;
24 | --footer-font: #8b949e;
25 |
26 | --highlight: #e6edf3;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #161b22;
33 | --image-view-titlebar: #161b22;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #BABCBE;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/light.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #ffffff;
3 | --font-fg: #000000;
4 | --fg: #202124;
5 |
6 | --search-bg: #ffffff;
7 | --search-bg-input: #f6f6f6;
8 | --search-bg-input-border: #dadce0;
9 | --search-select: #eeeeee;
10 |
11 | --border: #dadce0;
12 |
13 | --link: #1a0dab;
14 | --link-visited: #681da8;
15 |
16 | --snip-border: #dadce0;
17 | --snip-background: #ffffff;
18 | --snip-text: #000000;
19 |
20 | --settings-border: #5f6368;
21 | --button: #f6f6f6;
22 |
23 | --footer-bg: #f6f6f6;
24 | --footer-font: #353535;
25 |
26 | --highlight: #202124;
27 |
28 | --blue: #4285f4;
29 |
30 | --green: #202124;
31 |
32 | --image-view: #ffffff;
33 | --image-view-titlebar: #ffffff;
34 | --view-image-color: #f1f3f4;
35 | --image-select: #f6f6f6;
36 | --fff: #fff;
37 |
38 | --publish-info: #202124;
39 |
40 | --search-button: #202124;
41 | }
42 |
43 | .wrapper-results:hover,
44 | .wrapper-results:focus-within,
45 | .wrapper:hover,
46 | .wrapper:focus-within {
47 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
48 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
49 | }
50 |
51 | .check p {
52 | color: var(--highlight) !important;
53 | }
54 |
55 | .image_view {
56 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
57 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
58 | }
59 |
60 | .search-menu {
61 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
62 | }
63 |
64 | .view-image-search {
65 | box-shadow: none;
66 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
67 | }
68 |
69 | .view-image-search:hover {
70 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.08) !important;
71 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1) !important;
72 | }
73 |
--------------------------------------------------------------------------------
/static/css/menu.css:
--------------------------------------------------------------------------------
1 | .search-menu {
2 | margin-top: -50px !important;
3 | }
--------------------------------------------------------------------------------
/static/css/night.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #171b25;
3 | --font-fg: #ebecf7;
4 | --fg: #ebecf7;
5 |
6 | --search-bg: #0c0d0f;
7 | --search-bg-input: #2e3443;
8 | --search-bg-input-border: rgb(46, 52, 67);
9 | --search-select: #3a445c;
10 |
11 | --border: rgb(46, 52, 67);
12 |
13 | --link: #a7b1fc;
14 | --link-visited: #ad71bc;
15 |
16 | --snip-border: rgb(46, 52, 67);
17 | --snip-background: #1e222d;
18 | --snip-text: #f1f3f4;
19 |
20 | --settings-border: #5f6368;
21 | --button: #0c0d0f;
22 |
23 | --footer-bg: #0c0d0f;
24 | --footer-font: #ebecf7;
25 |
26 | --highlight: #ebecf7;
27 |
28 | --blue: #8ab4f8;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #0c0d0f;
33 | --image-view-titlebar: #0c0d0f;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #BABCBE;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .calc-btn:hover {
46 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
47 | }
48 |
49 | .calc-btn-2:hover {
50 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
51 | }
52 |
53 | .calc-btn-2 {
54 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
55 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
56 | }
57 |
58 | .calc-btn {
59 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
60 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
61 | }
62 |
63 | .calc {
64 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
65 | }
66 |
67 | .view-image-search {
68 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
69 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
70 | }
71 |
72 | .view-image-search:hover {
73 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
74 | }
75 |
--------------------------------------------------------------------------------
/static/css/red.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --html-bg: #7d0011;
3 | --font-fg: #ffffff;
4 | --fg: #ffffff;
5 |
6 | --search-bg: #0f0f0f;
7 | --search-bg-input: #121212;
8 | --search-bg-input-border: #303030;
9 | --search-select: #111111;
10 |
11 | --border: #303030;
12 |
13 | --link: #8ab4f8;
14 | --link-visited: #c58af9;
15 |
16 | --snip-border: #303030;
17 | --snip-background: #0f0f0f;
18 | --snip-text: #ffffff;
19 |
20 | --settings-border: #303030;
21 | --button: #111111;
22 |
23 | --footer-bg: #0f0f0f;
24 | --footer-font: #ffffff;
25 |
26 | --highlight: #ofofof;
27 |
28 | --blue: #007032;
29 |
30 | --green: #31b06e;
31 |
32 | --image-view: #0f0f0f;
33 | --image-view-titlebar: #0f0f0f;
34 | --view-image-color: #000000;
35 | --image-select: #303030;
36 | --fff: #fff;
37 |
38 | --search-button: #ffffff;
39 |
40 | --publish-info: #7f869e;
41 |
42 | color-scheme: dark;
43 | }
44 |
45 | .results {
46 | background: #0f0f0f;
47 | border: 7px solid #0f0f0f;
48 |
49 | }
50 |
51 | .result_sublink {
52 | background: #0f0f0f;
53 | border: 8px solid #0f0f0f;
54 | }
55 |
56 | .calc-btn:hover {
57 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
58 | }
59 |
60 | .calc-btn-2:hover {
61 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
62 | }
63 |
64 | .calc-btn-2 {
65 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
66 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
67 | }
68 |
69 | .calc-btn {
70 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
71 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
72 | }
73 |
74 | .calc {
75 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
76 | }
77 |
78 | .view-image-search {
79 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
80 | transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
81 | }
82 |
83 | .view-image-search:hover {
84 | box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
85 | }
86 |
--------------------------------------------------------------------------------
/static/css/search.css:
--------------------------------------------------------------------------------
1 | html {
2 | overflow: hidden;
3 | height: 100%;
4 | }
5 |
6 | body {
7 | min-height: 100%;
8 | overflow-x: hidden;
9 | }
--------------------------------------------------------------------------------
/static/css/settings-style.css:
--------------------------------------------------------------------------------
1 | .logomobile {
2 | top: 13px !important;
3 | font-size: 23px;
4 | }
5 |
6 | .no-decoration {
7 | color: var(--fg) !important;
8 | transition: all .3s ease;
9 | font-weight: 700;
10 | text-decoration: none;
11 | }
12 |
13 | .no-decoration:hover {
14 | color: var(--blue) !important;
15 | text-decoration: none;
16 | }
17 |
18 | .theme-settings {
19 | width: 100% !important;
20 | }
21 |
22 | html {
23 | height: 100%;
24 | }
25 |
26 | body {
27 | min-height: 100%;
28 | }
29 |
30 | .themes-settings-menu>div {
31 | width: calc(32%) !important;
32 | margin: 5px;
33 | }
34 |
35 | p {
36 | max-width: 620px !important;
37 | }
38 |
39 | #discoverButton {
40 | border-radius: 4px;
41 | padding: 6px;
42 | font-size: 15px;
43 | border: 1px solid var(--border);
44 | color: var(--font-fg);
45 | width: 160px;
46 | background: var(--button);
47 | float: right;
48 | transition: all .3s ease;
49 | }
50 |
51 | #discoverButton:hover {
52 | border: 1px solid #5f6368;
53 | cursor: pointer;
54 | }
55 |
56 | @media only screen and (max-width: 950px) {
57 | .themes-settings-menu>div {
58 | width: calc(20% - -60px) !important;
59 | margin: 5px;
60 | }
61 | }
62 |
63 | @media only screen and (max-width: 750px) {
64 | .themes-settings-menu>div {
65 | width: calc(50% - 10px) !important;
66 | margin: 5px;
67 | }
68 |
69 | .logomobile {
70 | top: -5px !important;
71 | }
72 | }
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/favicon.ico
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/favicon.png
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-300.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-300.woff
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-300.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-300.woff2
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-700.woff
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-700.woff2
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-regular.woff
--------------------------------------------------------------------------------
/static/fonts/inter-v12-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/inter-v12-latin-regular.woff2
--------------------------------------------------------------------------------
/static/fonts/material-icons-round-v108-latin-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/fonts/material-icons-round-v108-latin-regular.woff2
--------------------------------------------------------------------------------
/static/imagesearch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/imagesearch.png
--------------------------------------------------------------------------------
/static/lang/README.md:
--------------------------------------------------------------------------------
1 | The file should be saved to a file in `static/lang/` and should be titled with the english name of the language in all lower case, followed by '.json'.
2 |
3 | Make sure to add an entry to `UX_LANGUAGES` in _config.py
4 |
5 | It should be formatted like this:
6 |
7 | {'lang_lower': 'french', 'lang_fancy': 'French (Français)'},
8 |
9 | `lang_lower` should match the first part of the json file (e.g. 'english' for 'english.json').
10 | `lang_fancy` should be the name of the language in english with the first letter capitalized, followed by the name of the language in its own language in brackets.
11 |
--------------------------------------------------------------------------------
/static/lang/danish.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Billeder",
5 | "video": "Videoer",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Næste",
9 | "previous": "Forrige"
10 | },
11 | "results": {
12 | "results": "Resultater hentet på",
13 | "seconds": "sekunder"
14 | },
15 | "footer": {
16 | "settings": "Indstillinger",
17 | "source_code": "Kildekode",
18 | "commit": "Godkendelse",
19 | "donate": "Doner"
20 | },
21 | "search_buttons": {
22 | "search_text": "Søg med {araa_name}",
23 | "search_images": "Søg billeder med {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Brugertema",
28 | "all_settings": "Alle indstillinger",
29 | "settings_header": "Indstillinger",
30 | "discover_themes": "Gennemse temaer lavet af fællesskabet",
31 | "discover_themes_button": "Find temaer",
32 | "preferred_language": "Foretrukken sprog til dine søgeresultater",
33 | "google_domain": "Google domæne",
34 | "safe_search": "Sikker søgning",
35 | "open_links_new_tab": "Åbn links i en ny fane",
36 | "disable_javascript": "Deaktivering af JavaScript (ikke anbefalet) påvirker websidefunktioner, deaktiverer autoudfyldning og bremser visse funktioner. At holde JavaScript aktiveret vil ikke kompromittere dit privatliv, da anmodninger behandles gennem serverbaserede slutpunkter.",
37 | "save_settings": "Gem dine indstillinger",
38 | "return_to_settings": "Tilbage til indstillinger",
39 | "preferredux_language": "Foretrukken Sprog til Overskrifter, Knapper og Anden Tekst fra {araa_name}",
40 | "on": "på",
41 | "off": "fra",
42 | "any_language": "Enhver sprog"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Forslag API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/dutch.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Afbeeldingen",
5 | "video": "Video's",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Volgende",
9 | "previous": "Vorige"
10 | },
11 | "results": {
12 | "results": "Resultaten opgehaald in",
13 | "seconds": "seconden"
14 | },
15 | "footer": {
16 | "settings": "Instellingen",
17 | "source_code": "Broncode",
18 | "commit": "Commit",
19 | "donate": "Doneren"
20 | },
21 | "search_buttons": {
22 | "search_text": "Zoeken met {araa_name}",
23 | "search_images": "Zoek afbeeldingen met {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Thema",
27 | "user_theme": "Gebruikersthema",
28 | "all_settings": "Alle instellingen",
29 | "settings_header": "Instellingen",
30 | "discover_themes": "Blader door thema's gemaakt door de gemeenschap",
31 | "discover_themes_button": "Thema's ontdekken",
32 | "preferred_language": "Voorkeurstaal voor uw zoekresultaten",
33 | "google_domain": "Google-domein",
34 | "safe_search": "Veilig zoeken",
35 | "open_links_new_tab": "Links in een nieuw tabblad openen",
36 | "disable_javascript": "Het uitschakelen van JavaScript (niet aanbevolen) heeft invloed op websitefunctionaliteiten, schakelt automatisch aanvullen uit en vertraagt bepaalde functies. Het ingeschakeld houden van JavaScript compromitteert uw privacy niet, aangezien verzoeken worden verwerkt via serverzijde endpoints.",
37 | "save_settings": "Instellingen opslaan",
38 | "return_to_settings": "Terug naar instellingen",
39 | "preferredux_language": "Voorkeurstaal voor Koppen, Knoppen en Andere Tekst van {araa_name}",
40 | "on": "aan",
41 | "off": "uit",
42 | "any_language": "Elke taal"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Suggesties API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/english.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Images",
5 | "video": "Videos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Next",
9 | "previous": "Previous"
10 | },
11 | "results": {
12 | "results": "Fetched the results in",
13 | "seconds": "seconds"
14 | },
15 | "footer": {
16 | "settings": "Settings",
17 | "source_code": "Source code",
18 | "commit": "Commit",
19 | "donate": "Donate"
20 | },
21 | "search_buttons": {
22 | "search_text": "Search with {araa_name}",
23 | "search_images": "Search images with {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Theme",
27 | "user_theme": "User Theme",
28 | "all_settings": "All settings",
29 | "settings_header": "Settings",
30 | "discover_themes": "Browse themes made by the community",
31 | "discover_themes_button": "Discover Themes",
32 | "preferred_language": "Preferred language for your search results",
33 | "google_domain": "Google domain",
34 | "safe_search": "SafeSearch",
35 | "open_links_new_tab": "Open links in a new tab",
36 | "disable_javascript": "Disabling JavaScript (not recommended) affects webpage features, disables autocomplete, and slows certain functions. Keeping JavaScript enabled won't compromise your privacy, as requests are processed through server-side endpoints.",
37 | "save_settings": "Save your settings",
38 | "return_to_settings": "Return to settings",
39 | "preferredux_language": "Preferred language for headlines, buttons, and other text from {araa_name}",
40 | "on": "on",
41 | "off": "off",
42 | "any_language": "Any Language"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Suggestions API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/french.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Images",
5 | "video": "Vidéos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Suivant",
9 | "previous": "Précédent"
10 | },
11 | "results": {
12 | "results": "Résultats obtenus en",
13 | "seconds": "secondes"
14 | },
15 | "footer": {
16 | "settings": "Paramètres",
17 | "source_code": "Code source",
18 | "commit": "Validation",
19 | "donate": "Donner"
20 | },
21 | "search_buttons": {
22 | "search_text": "Rechercher avec {araa_name}",
23 | "search_images": "Rechercher des images avec {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Thème",
27 | "user_theme": "Thème de l'utilisateur",
28 | "all_settings": "Tous les paramètres",
29 | "settings_header": "Paramètres",
30 | "discover_themes": "Parcourir les thèmes créés par la communauté",
31 | "discover_themes_button": "Découvrir les thèmes",
32 | "preferred_language": "Langue préférée pour les résultats de recherche",
33 | "google_domain": "Domaine Google",
34 | "safe_search": "Filtrage SafeSearch",
35 | "open_links_new_tab": "Ouvrir les liens dans un nouvel onglet",
36 | "disable_javascript": "La désactivation de JavaScript (non recommandée) affecte les fonctionnalités des pages Web, désactive l'autocomplétion et ralentit certaines fonctions. Le maintien de JavaScript activé ne compromettra pas votre vie privée, car les requêtes sont traitées via des points de terminaison côté serveur.",
37 | "save_settings": "Enregistrer vos paramètres",
38 | "return_to_settings": "Retour aux paramètres",
39 | "preferredux_language": "Langue préférée pour les titres, les boutons et autres textes provenant d'{araa_name}",
40 | "on": "activé",
41 | "off": "désactivé",
42 | "any_language": "Toute langue"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API de suggestions"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/french_canadian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Images",
5 | "video": "Vidéos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Suivant",
9 | "previous": "Précédent"
10 | },
11 | "results": {
12 | "results": "Résultats obtenus en",
13 | "seconds": "secondes"
14 | },
15 | "footer": {
16 | "settings": "Paramètres",
17 | "source_code": "Code source",
18 | "commit": "Validation",
19 | "donate": "Donner"
20 | },
21 | "search_buttons": {
22 | "search_text": "Rechercher avec {araa_name}",
23 | "search_images": "Rechercher des images avec {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Thème",
27 | "user_theme": "Thème de l'utilisateur",
28 | "all_settings": "Tous les paramètres",
29 | "settings_header": "Paramètres",
30 | "discover_themes": "Parcourez les thèmes créés par la communauté",
31 | "discover_themes_button": "Découvrir les thèmes",
32 | "preferred_language": "Langue préférée pour vos résultats de recherche",
33 | "google_domain": "Domaine Google",
34 | "safe_search": "Recherche sécurisée",
35 | "open_links_new_tab": "Ouvrir les liens dans un nouvel onglet",
36 | "disable_javascript": "La désactivation de JavaScript (non recommandée) affecte les fonctionnalités du site, désactive l'autocomplétion et ralentit certaines fonctions. Activer JavaScript ne compromet pas votre vie privée, car les demandes sont traitées via des points de terminaison côté serveur.",
37 | "save_settings": "Enregistrer les paramètres",
38 | "return_to_settings": "Revenir aux paramètres",
39 | "preferredux_language": "Langue préférée pour les titres, les boutons et autres textes provenant d'{araa_name}",
40 | "on": "activé",
41 | "off": "désactivé",
42 | "any_language": "Toute langue"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API de suggestions"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/german.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Bilder",
5 | "video": "Videos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Weiter",
9 | "previous": "Zurück"
10 | },
11 | "results": {
12 | "results": "Ergebnisse abgerufen in",
13 | "seconds": "Sekunden"
14 | },
15 | "footer": {
16 | "settings": "Einstellungen",
17 | "source_code": "Quellcode",
18 | "commit": "Bestätigung",
19 | "donate": "Spenden"
20 | },
21 | "search_buttons": {
22 | "search_text": "Suchen mit {araa_name}",
23 | "search_images": "Suche Bilder mit {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Thema",
27 | "user_theme": "Benutzerthema",
28 | "all_settings": "Alle Einstellungen",
29 | "settings_header": "Einstellungen",
30 | "discover_themes": "Durchsuchen Sie von der Gemeinschaft erstellte Themen",
31 | "discover_themes_button": "Themen entdecken",
32 | "preferred_language": "Bevorzugte Sprache für Ihre Suchergebnisse",
33 | "google_domain": "Google-Domain",
34 | "safe_search": "Sichere Suche",
35 | "open_links_new_tab": "Links in einem neuen Tab öffnen",
36 | "disable_javascript": "Das Deaktivieren von JavaScript (nicht empfohlen) beeinträchtigt Webseitenfunktionen, deaktiviert die Autovervollständigung und verlangsamt bestimmte Funktionen. Das Aktivieren von JavaScript beeinträchtigt nicht Ihre Privatsphäre, da Anfragen über serverseitige Endpunkte verarbeitet werden.",
37 | "save_settings": "Einstellungen speichern",
38 | "return_to_settings": "Zurück zu den Einstellungen",
39 | "preferredux_language": "Bevorzugte Sprache für Überschriften, Schaltflächen und anderen Text von {araa_name}",
40 | "on": "Ein",
41 | "off": "Aus",
42 | "any_language": "Jede Sprache"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Vorschläge API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/greek.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Ιστοσελίδες",
4 | "image": "Εικόνες",
5 | "video": "Βίντεο",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Επόμενο",
9 | "previous": "Προηγούμενο"
10 | },
11 | "results": {
12 | "results": "Τα αποτελέσματα λήφθηκαν σε",
13 | "seconds": "δευτερόλεπτα"
14 | },
15 | "footer": {
16 | "settings": "Ρυθμίσεις",
17 | "source_code": "Κώδικας Πηγής",
18 | "commit": "Καταχώρηση",
19 | "donate": "Δωρίστε"
20 | },
21 | "search_buttons": {
22 | "search_text": "Αναζήτηση με {araa_name}",
23 | "search_images": "Αναζήτηση εικόνων με {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Θέμα",
27 | "user_theme": "Θέμα χρήστη",
28 | "all_settings": "Όλες οι ρυθμίσεις",
29 | "settings_header": "Ρυθμίσεις",
30 | "discover_themes": "Αναζητήστε θέματα που δημιουργήθηκαν από την κοινότητα",
31 | "discover_themes_button": "Ανακαλύψτε Θέματα",
32 | "preferred_language": "Προτιμώμενη γλώσσα για τα αποτελέσματα αναζήτησης",
33 | "google_domain": "Δικτυακός τόπος Google",
34 | "safe_search": "Ασφαλής Αναζήτηση",
35 | "open_links_new_tab": "Άνοιγμα συνδέσεων σε νέα καρτέλα",
36 | "disable_javascript": "Η απενεργοποίηση του JavaScript (δεν συνιστάται) επηρεάζει τη λειτουργικότητα του ιστότοπου, απενεργοποιεί την αυτόματη συμπλήρωση και επιβραδύνει ορισμένες λειτουργίες. Η διατήρηση του JavaScript ενεργοποιημένου δεν θίγει την ιδιωτικότητά σας, διότι οι αιτήσεις επεξεργάζονται μέσω των τερματικών του διακομιστή.",
37 | "save_settings": "Αποθήκευση των ρυθμίσεων",
38 | "return_to_settings": "Επιστροφή στις ρυθμίσεις",
39 | "preferredux_language": "Προτιμώμενη γλώσσα για τίτλους, κουμπιά και άλλο κείμενο από το {araa_name}",
40 | "on": "ενεργό",
41 | "off": "ανενεργό",
42 | "any_language": "Οποιαδήποτε Γλώσσα"
43 | },
44 | "api_links": {
45 | "api_link": "Διεπαφή Προγραμματισμού Εφαρμογών",
46 | "suggestions_api_link": "Διεπαφή Προτάσεων Εφαρμογών"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/italian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Immagini",
5 | "video": "Video",
6 | "reddit": "Reddit",
7 | "torrent": "Torrent",
8 | "next": "Avanti",
9 | "previous": "Precedente"
10 | },
11 | "results": {
12 | "results": "Risultati ottenuti in",
13 | "seconds": "secondi"
14 | },
15 | "footer": {
16 | "settings": "Impostazioni",
17 | "source_code": "Codice sorgente",
18 | "commit": "Conferma",
19 | "donate": "Donare"
20 | },
21 | "search_buttons": {
22 | "search_text": "Cerca con {araa_name}",
23 | "search_images": "Cerca immagini con {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Tema dell'utente",
28 | "all_settings": "Tutte le impostazioni",
29 | "settings_header": "Impostazioni",
30 | "discover_themes": "Sfoglia i temi creati dalla comunità",
31 | "discover_themes_button": "Scopri i temi",
32 | "preferred_language": "Lingua preferita per i tuoi risultati di ricerca",
33 | "google_domain": "Dominio Google",
34 | "safe_search": "Ricerca sicura",
35 | "open_links_new_tab": "Apri i collegamenti in una nuova scheda",
36 | "disable_javascript": "La disabilitazione di JavaScript (non raccomandata) influisce sulle funzionalità del sito, disabilita il completamento automatico e rallenta alcune funzioni. Mantenere JavaScript abilitato non compromette la privacy, poiché le richieste vengono elaborate tramite endpoint lato server.",
37 | "save_settings": "Salva le impostazioni",
38 | "return_to_settings": "Torna alle impostazioni",
39 | "preferredux_language": "Lingua preferita per titoli, pulsanti e altri testi da {araa_name}",
40 | "on": "acceso",
41 | "off": "spento",
42 | "any_language": "Qualsiasi Lingua"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API di suggerimenti"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/japanese.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "ウェブ",
4 | "image": "画像",
5 | "video": "ビデオ",
6 | "reddit": "Reddit",
7 | "torrent": "トレント",
8 | "next": "次へ",
9 | "previous": "前へ"
10 | },
11 | "results": {
12 | "results": "結果を取得しました",
13 | "seconds": "秒"
14 | },
15 | "footer": {
16 | "settings": "設定",
17 | "source_code": "ソースコード",
18 | "commit": "コミット",
19 | "donate": "寄付"
20 | },
21 | "search_buttons": {
22 | "search_text": "{araa_name}で検索",
23 | "search_images": "{araa_name}で画像検索"
24 | },
25 | "settings": {
26 | "theme": "テーマ",
27 | "user_theme": "ユーザーテーマ",
28 | "all_settings": "すべての設定",
29 | "settings_header": "設定",
30 | "discover_themes": "コミュニティが作成したテーマを閲覧",
31 | "discover_themes_button": "テーマを発見",
32 | "preferred_language": "検索結果のための優先言語",
33 | "google_domain": "Googleドメイン",
34 | "safe_search": "安全検索",
35 | "open_links_new_tab": "リンクを新しいタブで開く",
36 | "disable_javascript": "JavaScriptを無効にする(お勧めしません)はウェブサイトの機能に影響を与え、自動入力を無効にし、一部の機能を遅くします。 JavaScriptを有効にしてもプライバシーは侵害されません。リクエストはサーバーサイドのエンドポイントを介して処理されます。",
37 | "save_settings": "設定を保存",
38 | "return_to_settings": "設定に戻る",
39 | "preferredux_language": "{araa_name}からの見出し、ボタン、およびその他のテキストの優先言語",
40 | "on": "オン",
41 | "off": "オフ",
42 | "any_language": "任意の言語"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "提案API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/korean.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "웹",
4 | "image": "이미지",
5 | "video": "비디오",
6 | "reddit": "Reddit",
7 | "torrent": "토렌트",
8 | "next": "다음",
9 | "previous": "이전"
10 | },
11 | "results": {
12 | "results": "결과를 가져온 시간",
13 | "seconds": "초"
14 | },
15 | "footer": {
16 | "settings": "설정",
17 | "source_code": "소스 코드",
18 | "commit": "확인",
19 | "donate": "기부"
20 | },
21 | "search_buttons": {
22 | "search_text": "{araa_name}로 검색",
23 | "search_images": "{araa_name}로 이미지 검색"
24 | },
25 | "settings": {
26 | "theme": "테마",
27 | "user_theme": "사용자 테마",
28 | "all_settings": "모든 설정",
29 | "settings_header": "설정",
30 | "discover_themes": "커뮤니티가 만든 테마 검색",
31 | "discover_themes_button": "테마 찾기",
32 | "preferred_language": "검색 결과의 우선 언어",
33 | "google_domain": "Google 도메인",
34 | "safe_search": "안전 검색",
35 | "open_links_new_tab": "새 탭에서 링크 열기",
36 | "disable_javascript": "JavaScript 비활성화(권장하지 않음)는 웹 사이트 기능에 영향을 미치며 자동 완성을 비활성화하고 일부 기능을 늦춥니다. JavaScript를 사용 중지해도 개인 정보 유출이 발생하지 않으며 요청은 서버 측 엔드포인트를 통해 처리됩니다.",
37 | "save_settings": "설정 저장",
38 | "return_to_settings": "설정으로 돌아가기",
39 | "preferredux_language": "{araa_name}의 제목, 버튼 및 기타 텍스트에 대한 기본 언어",
40 | "on": "켜짐",
41 | "off": "꺼짐",
42 | "any_language": "아무 언어"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "제안 API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/mandarin_chinese.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "网页",
4 | "image": "图片",
5 | "video": "视频",
6 | "reddit": "Reddit",
7 | "torrent": "种子",
8 | "next": "下一步",
9 | "previous": "上一步"
10 | },
11 | "results": {
12 | "results": "结果获取耗时",
13 | "seconds": "秒"
14 | },
15 | "footer": {
16 | "settings": "设置",
17 | "source_code": "源代码",
18 | "commit": "提交",
19 | "donate": "捐赠"
20 | },
21 | "search_buttons": {
22 | "search_text": "使用{araa_name}搜索",
23 | "search_images": "使用{araa_name}搜索图片"
24 | },
25 | "settings": {
26 | "theme": "主题",
27 | "user_theme": "用户主题",
28 | "all_settings": "所有设置",
29 | "settings_header": "设置",
30 | "discover_themes": "浏览社区创建的主题",
31 | "discover_themes_button": "发现主题",
32 | "preferred_language": "搜索结果的首选语言",
33 | "google_domain": "谷歌域名",
34 | "safe_search": "安全搜索",
35 | "open_links_new_tab": "在新标签页中打开链接",
36 | "disable_javascript": "禁用JavaScript(不建议)会影响网站功能,禁用自动完成并减慢某些功能。 启用JavaScript不会泄露隐私,因为请求通过服务器端点处理。",
37 | "save_settings": "保存设置",
38 | "return_to_settings": "返回设置",
39 | "preferredux_language": "{araa_name}的标题、按钮和其他文本的首选语言",
40 | "on": "开",
41 | "off": "关",
42 | "any_language": "任何语言"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "建议API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/norwegian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Nett",
4 | "image": "Bilder",
5 | "video": "Videoer",
6 | "reddit": "Reddit",
7 | "torrent": "Torrenter",
8 | "next": "Neste",
9 | "previous": "Forrige"
10 | },
11 | "results": {
12 | "results": "Resultater hentet på",
13 | "seconds": "sekunder"
14 | },
15 | "footer": {
16 | "settings": "Innstillinger",
17 | "source_code": "Kildekode",
18 | "commit": "Bekreftelse",
19 | "donate": "Doner"
20 | },
21 | "search_buttons": {
22 | "search_text": "Søk med {araa_name}",
23 | "search_images": "Søk bilder med {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Brukertema",
28 | "all_settings": "Alle innstillinger",
29 | "settings_header": "Innstillinger",
30 | "discover_themes": "Bla gjennom temaer laget av fellesskapet",
31 | "discover_themes_button": "Oppdag temaer",
32 | "preferred_language": "Foretrukket språk for søkeresultatene dine",
33 | "google_domain": "Google-domene",
34 | "safe_search": "Sikkerhetsfiltrering",
35 | "open_links_new_tab": "Åpne lenker i en ny fane",
36 | "disable_javascript": "Deaktivering av JavaScript (ikke anbefalt) påvirker websidefunksjoner, deaktiverer automatisk utfylling og bremser visse funksjoner. Å holde JavaScript aktivert vil ikke kompromittere personvernet ditt, da forespørsler behandles via serverbaserte endepunkter.",
37 | "save_settings": "Lagre innstillingene dine",
38 | "return_to_settings": "Tilbake til innstillinger",
39 | "preferredux_language": "Foretrukket språk for overskrifter, knapper og annen tekst fra {araa_name}",
40 | "on": "på",
41 | "off": "av",
42 | "any_language": "Alle språk"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Forslag API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/polish.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Strona internetowa",
4 | "image": "Zdjęcia",
5 | "video": "Filmy",
6 | "reddit": "Reddit",
7 | "torrent": "Torrenty",
8 | "next": "Następny",
9 | "previous": "Poprzedni"
10 | },
11 | "results": {
12 | "results": "Wyniki uzyskano w",
13 | "seconds": "sekundy"
14 | },
15 | "footer": {
16 | "settings": "Ustawienia",
17 | "source_code": "Kod źródłowy",
18 | "commit": "Zatwierdzenie",
19 | "donate": "Przekaż darowiznę"
20 | },
21 | "search_buttons": {
22 | "search_text": "Szukaj z {araa_name}",
23 | "search_images": "Szukaj obrazów z {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Motyw",
27 | "user_theme": "Motyw użytkownika",
28 | "all_settings": "Wszystkie ustawienia",
29 | "settings_header": "Ustawienia",
30 | "discover_themes": "Przeglądaj motywy stworzone przez społeczność",
31 | "discover_themes_button": "Odkryj motywy",
32 | "preferred_language": "Preferowany język dla wyników wyszukiwania",
33 | "google_domain": "Domena Google",
34 | "safe_search": "Bezpieczne wyszukiwanie",
35 | "open_links_new_tab": "Otwieraj linki w nowej karcie",
36 | "disable_javascript": "Wyłączenie JavaScript (niezalecane) wpływa na funkcje strony internetowej, dezaktywuje autouzupełnianie i spowalnia niektóre funkcje. Pozostawienie JavaScript włączonego nie naruszy Twojej prywatności, ponieważ zapytania są przetwarzane za pośrednictwem punktów końcowych po stronie serwera.",
37 | "save_settings": "Zapisz ustawienia",
38 | "return_to_settings": "Powrót do ustawień",
39 | "preferredux_language": "Preferowany język dla nagłówków, przycisków i innych tekstów z {araa_name}",
40 | "on": "włączony",
41 | "off": "wyłączony",
42 | "any_language": "Dowolny język"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API sugestii"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/portuguese.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Imagens",
5 | "video": "Vídeos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Próximo",
9 | "previous": "Anterior"
10 | },
11 | "results": {
12 | "results": "Resultados obtidos em",
13 | "seconds": "segundos"
14 | },
15 | "footer": {
16 | "settings": "Configurações",
17 | "source_code": "Código-fonte",
18 | "commit": "Confirmação",
19 | "donate": "Doar"
20 | },
21 | "search_buttons": {
22 | "search_text": "Pesquisar com {araa_name}",
23 | "search_images": "Pesquisar imagens com {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Tema do usuário",
28 | "all_settings": "Todas as configurações",
29 | "settings_header": "Configurações",
30 | "discover_themes": "Navegue por temas criados pela comunidade",
31 | "discover_themes_button": "Descobrir Temas",
32 | "preferred_language": "Idioma preferido para seus resultados de pesquisa",
33 | "google_domain": "Domínio do Google",
34 | "safe_search": "Busca Segura",
35 | "open_links_new_tab": "Abrir links em uma nova aba",
36 | "disable_javascript": "Desativar JavaScript (não recomendado) afeta as funcionalidades do site, desativa o preenchimento automático e diminui a velocidade de certas funções. Manter o JavaScript habilitado não compromete sua privacidade, pois as solicitações são processadas por meio de pontos de extremidade no servidor.",
37 | "save_settings": "Salvar configurações",
38 | "return_to_settings": "Voltar para as configurações",
39 | "preferredux_language": "Idioma preferido para manchetes, botões e outros textos do {araa_name}",
40 | "on": "ligado",
41 | "off": "desligado",
42 | "any_language": "Qualquer Idioma"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API de Sugestões"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/romanian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Imagini",
5 | "video": "Videoclipuri",
6 | "reddit": "Reddit",
7 | "torrent": "Torrente",
8 | "next": "Următor",
9 | "previous": "Înainte"
10 | },
11 | "results": {
12 | "results": "A obținut rezultatele în",
13 | "seconds": "secunde"
14 | },
15 | "footer": {
16 | "settings": "Setări",
17 | "source_code": "Cod sursă",
18 | "commit": "Commit",
19 | "donate": "Donați"
20 | },
21 | "search_buttons": {
22 | "search_text": "Căutare cu {araa_name}",
23 | "search_images": "Caută imagini cu {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Temă",
27 | "user_theme": "Tema userului",
28 | "all_settings": "Toate setările",
29 | "settings_header": "Setări",
30 | "discover_themes": "Explorați temele create de comunitate",
31 | "discover_themes_button": "Descoperă temele",
32 | "preferred_language": "Limba preferată pentru rezultatele căutării",
33 | "google_domain": "Domeniul Google",
34 | "safe_search": "Căutare sigură",
35 | "open_links_new_tab": "Deschideți linkurile într-o filă nouă",
36 | "disable_javascript": "Dezactivarea JavaScript (nerecomandată) afectează caracteristicile paginilor web, dezactivează completarea automată și încetinește anumite funcții. Menținerea activării JavaScript nu vă va compromite confidențialitatea, deoarece solicitările sunt procesate prin intermediul unor puncte finale de pe server.",
37 | "save_settings": "Salvați setările",
38 | "return_to_settings": "Reveniți la setări",
39 | "preferredux_language": "Limba preferată pentru titlurile, butoanele și alte texte din {araa_name}",
40 | "on": "activat",
41 | "off": "dezactivat",
42 | "any_language": "Orice limbă"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Sugestii API"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/static/lang/russian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Веб",
4 | "image": "Изображения",
5 | "video": "Видео",
6 | "reddit": "Reddit",
7 | "torrent": "Торренты",
8 | "next": "Следующий",
9 | "previous": "Предыдущий"
10 | },
11 | "results": {
12 | "results": "Результаты получены за",
13 | "seconds": "секунд"
14 | },
15 | "footer": {
16 | "settings": "Настройки",
17 | "source_code": "Исходный код",
18 | "commit": "Подтверждение",
19 | "donate": "Пожертвовать"
20 | },
21 | "search_buttons": {
22 | "search_text": "Искать с {araa_name}",
23 | "search_images": "Искать изображения с {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Тема",
27 | "user_theme": "Тема пользователя",
28 | "all_settings": "Все настройки",
29 | "settings_header": "Настройки",
30 | "discover_themes": "Просматривайте темы, созданные сообществом",
31 | "discover_themes_button": "Открывайте темы",
32 | "preferred_language": "Предпочтительный язык для ваших результатов поиска",
33 | "google_domain": "Домен Google",
34 | "safe_search": "Безопасный поиск",
35 | "open_links_new_tab": "Открывать ссылки в новой вкладке",
36 | "disable_javascript": "Отключение JavaScript (не рекомендуется) влияет на функциональность веб-сайта, отключает автозаполнение и замедляет некоторые функции. Включение JavaScript не ущемляет вашу конфиденциальность, поскольку запросы обрабатываются через серверные точки.",
37 | "save_settings": "Сохранить настройки",
38 | "return_to_settings": "Вернуться к настройкам",
39 | "preferredux_language": "Предпочтительный язык для заголовков, кнопок и другого текста из {araa_name}",
40 | "on": "Включено",
41 | "off": "Выключено",
42 | "any_language": "Любой язык"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API предложений"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/spanish.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Imágenes",
5 | "video": "Videos",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Siguiente",
9 | "previous": "Anterior"
10 | },
11 | "results": {
12 | "results": "Resultados obtenidos en",
13 | "seconds": "segundos"
14 | },
15 | "footer": {
16 | "settings": "Configuración",
17 | "source_code": "Código fuente",
18 | "commit": "Confirmación",
19 | "donate": "Donar"
20 | },
21 | "search_buttons": {
22 | "search_text": "Buscar con {araa_name}",
23 | "search_images": "Buscar imágenes con {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Tema de usuario",
28 | "all_settings": "Todas las configuraciones",
29 | "settings_header": "Configuración",
30 | "discover_themes": "Explorar temas creados por la comunidad",
31 | "discover_themes_button": "Descubrir temas",
32 | "preferred_language": "Idioma preferido para tus resultados de búsqueda",
33 | "google_domain": "Dominio de Google",
34 | "safe_search": "Búsqueda segura",
35 | "open_links_new_tab": "Abrir enlaces en una nueva pestaña",
36 | "disable_javascript": "La desactivación de JavaScript (no recomendada) afecta las funciones del sitio web, deshabilita el autocompletado y ralentiza ciertas funciones. Mantener JavaScript habilitado no compromete su privacidad, ya que las solicitudes se procesan a través de puntos finales del lado del servidor.",
37 | "save_settings": "Guardar configuraciones",
38 | "return_to_settings": "Volver a la configuración",
39 | "preferredux_language": "Idioma preferido para encabezados, botones y otros textos de {araa_name}",
40 | "on": "os",
41 | "off": "off",
42 | "any_language": "Cualquier idioma"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API de Sugerencias"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/swedish.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Webb",
4 | "image": "Bilder",
5 | "video": "Videor",
6 | "reddit": "Reddit",
7 | "torrent": "Torrents",
8 | "next": "Nästa",
9 | "previous": "Föregående"
10 | },
11 | "results": {
12 | "results": "Resultat hämtat på",
13 | "seconds": "sekunder"
14 | },
15 | "footer": {
16 | "settings": "Inställningar",
17 | "source_code": "Källkod",
18 | "commit": "Bekräftelse",
19 | "donate": "Donera"
20 | },
21 | "search_buttons": {
22 | "search_text": "Sök med {araa_name}",
23 | "search_images": "Sök bilder med {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Användartema",
28 | "all_settings": "Alla inställningar",
29 | "settings_header": "Inställningar",
30 | "discover_themes": "Bläddra bland teman skapade av gemenskapen",
31 | "discover_themes_button": "Upptäck Teman",
32 | "preferred_language": "Föredraget språk för dina sökresultat",
33 | "google_domain": "Google-domän",
34 | "safe_search": "Säker sökning",
35 | "open_links_new_tab": "Öppna länkar i en ny flik",
36 | "disable_javascript": "Inaktivera JavaScript (inte rekommenderat) påverkar webbplatsfunktioner, stänger av automatisk ifyllning och saktar ned vissa funktioner. Att hålla JavaScript aktiverat kommer inte att äventyra din integritet, eftersom förfrågningar behandlas via serverbaserade ändpunkter.",
37 | "save_settings": "Spara inställningar",
38 | "return_to_settings": "Återgå till inställningar",
39 | "preferredux_language": "Föredraget språk för rubriker, knappar och annan text från {araa_name}",
40 | "on": "på",
41 | "off": "av",
42 | "any_language": "Valfritt språk"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Förslags-API"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/turkish.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Web",
4 | "image": "Resimler",
5 | "video": "Videolar",
6 | "reddit": "Reddit",
7 | "torrent": "Torrentler",
8 | "next": "Sonraki",
9 | "previous": "Önceki"
10 | },
11 | "results": {
12 | "results": "Sonuçlar 1.33 saniyede alındı",
13 | "seconds": "saniye"
14 | },
15 | "footer": {
16 | "settings": "Ayarlar",
17 | "source_code": "Kaynak Kodu",
18 | "commit": "Taahhüt",
19 | "donate": "Bağış Yap"
20 | },
21 | "search_buttons": {
22 | "search_text": "{araa_name} ile ara",
23 | "search_images": "{araa_name} ile resim ara"
24 | },
25 | "settings": {
26 | "theme": "Tema",
27 | "user_theme": "Kullanıcı Teması",
28 | "all_settings": "Tüm Ayarlar",
29 | "settings_header": "Ayarlar",
30 | "discover_themes": "Topluluk tarafından oluşturulan temaları keşfedin",
31 | "discover_themes_button": "Temaları Keşfet",
32 | "preferred_language": "Arama sonuçları için tercih edilen dil",
33 | "google_domain": "Google Alanı",
34 | "safe_search": "Güvenli Arama",
35 | "open_links_new_tab": "Bağlantıları yeni sekmede aç",
36 | "disable_javascript": "JavaScript'i devre dışı bırakma (tavsiye edilmez) web sitesi işlevselliğini etkiler, otomatik tamamlamayı devre dışı bırakır ve bazı işlevleri yavaşlatır. JavaScript'i etkin tutmak gizliliğinizi tehlikeye atmaz, çünkü istekler sunucu tarafındaki uç noktalardan işlenir.",
37 | "save_settings": "Ayarları Kaydet",
38 | "return_to_settings": "Ayarlar'a Dön",
39 | "preferredux_language": "{araa_name}'dan Başlık, Düğmeler ve Diğer Metinler için Tercih Edilen Dil",
40 | "on": "açık",
41 | "off": "kapalı",
42 | "any_language": "Herhangi bir dil"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "Öneriler API'si"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/lang/ukrainian.json:
--------------------------------------------------------------------------------
1 | {
2 | "buttons": {
3 | "text": "Веб",
4 | "image": "Зображення",
5 | "video": "Відео",
6 | "reddit": "Reddit",
7 | "torrent": "Торренти",
8 | "next": "Наступний",
9 | "previous": "Попередній"
10 | },
11 | "results": {
12 | "results": "Результати отримано за",
13 | "seconds": "секунд"
14 | },
15 | "footer": {
16 | "settings": "Налаштування",
17 | "source_code": "Вихідний код",
18 | "commit": "Підтвердження",
19 | "donate": "Пожертвувати"
20 | },
21 | "search_buttons": {
22 | "search_text": "Пошук за допомогою {araa_name}",
23 | "search_images": "Пошук зображень за допомогою {araa_name}"
24 | },
25 | "settings": {
26 | "theme": "Тема",
27 | "user_theme": "Тема користувача",
28 | "all_settings": "Усі налаштування",
29 | "settings_header": "Налаштування",
30 | "discover_themes": "Переглядайте теми, створені спільнотою",
31 | "discover_themes_button": "Відкривайте теми",
32 | "preferred_language": "Улюблена мова для ваших результатів пошуку",
33 | "google_domain": "Домен Google",
34 | "safe_search": "Безпечний пошук",
35 | "open_links_new_tab": "Відкривати посилання в новій вкладці",
36 | "disable_javascript": "Вимкнення JavaScript (не рекомендується) впливає на функціональність веб-сайту, вимикає автозаповнення і сповільнює деякі функції. Включення JavaScript не порушує вашу конфіденційність, оскільки запити обробляються через серверні кінцеві точки.",
37 | "save_settings": "Зберегти налаштування",
38 | "return_to_settings": "Повернутися до налаштувань",
39 | "preferredux_language": "Обрана мова для заголовків, кнопок та іншого тексту від {araa_name}",
40 | "on": "ввімкнено",
41 | "off": "вимкнено",
42 | "any_language": "Будь-яка мова"
43 | },
44 | "api_links": {
45 | "api_link": "API",
46 | "suggestions_api_link": "API пропозицій"
47 | }
48 | }
--------------------------------------------------------------------------------
/static/menu.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./menu.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | const menuVisible = document.querySelector('.search-menu');
30 | const menuDiv = document.querySelector('.settings-search-div-search');
31 |
32 | function getCookie(name) {
33 | const cookies = document.cookie.split("; ");
34 | for (const cookie of cookies) {
35 | const [cookieName, cookieValue] = cookie.split("=");
36 | if (cookieName === name) {
37 | return cookieValue;
38 | }
39 | }
40 | return null;
41 | }
42 |
43 | function setThemeBasedOnCookie() {
44 | const themeCookie = getCookie("theme");
45 | const themeNameSpan = document.getElementById("theme_name");
46 | const themes = {
47 | "dark_blur": "Dark (Default)",
48 | "dark_default": "Dark (no background)",
49 | "light": "Light",
50 | };
51 |
52 | if (themes.hasOwnProperty(themeCookie)) {
53 | themeNameSpan.textContent = themes[themeCookie];
54 | }
55 | }
56 |
57 | setThemeBasedOnCookie();
58 |
59 | document.getElementById("settingsButton").addEventListener("click", function () {
60 | window.location.href = "/settings";
61 | });
62 |
63 | menuDiv.addEventListener('click', function (event) {
64 | event.stopPropagation();
65 |
66 | if (menuVisible.classList.contains('settings-menu-visible')) {
67 | menuVisible.classList.remove('settings-menu-visible');
68 | menuVisible.classList.add('settings-menu-hidden');
69 | } else {
70 | menuVisible.classList.remove('settings-menu-hidden');
71 | menuVisible.classList.add('settings-menu-visible');
72 | }
73 | });
74 |
75 | document.addEventListener('click', function (event) {
76 | if (!menuDiv.contains(event.target) && !menuVisible.contains(event.target)) {
77 | menuVisible.classList.remove('settings-menu-visible');
78 | menuVisible.classList.add('settings-menu-hidden');
79 | }
80 | });
81 |
--------------------------------------------------------------------------------
/static/opensearch.xml.example:
--------------------------------------------------------------------------------
1 |
2 |
3 | Araa
4 |
5 |
6 | UTF-8
7 | UTF-8
8 | en-us
9 |
10 |
--------------------------------------------------------------------------------
/static/preview1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview1.webp
--------------------------------------------------------------------------------
/static/preview2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview2.webp
--------------------------------------------------------------------------------
/static/preview3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/preview3.webp
--------------------------------------------------------------------------------
/static/script.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./script.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | // Removes the 'Apply Settings' button for Javascript users,
30 | // since changing any of the elements causes the settings to apply
31 | // automatically.
32 | const resultsSave = document.querySelector(".results-save");
33 | if (resultsSave != null) {
34 | resultsSave.style.display = "none";
35 | }
36 |
37 | const searchInput = document.getElementById('search-input');
38 | const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0];
39 | const resultsWrapper = document.querySelector('.autocomplete');
40 | const clearSearch = document.querySelector("#clearSearch");
41 |
42 | async function getSuggestions(query) {
43 | try {
44 | params = new URLSearchParams({ "q": query }).toString();
45 | const response = await fetch(`/suggestions?${params}`);
46 | const data = await response.json();
47 | return data[1]; // Return only the array of suggestion strings
48 | } catch (error) {
49 | console.error(error);
50 | }
51 | }
52 |
53 | let currentIndex = -1; // Keep track of the currently selected suggestion
54 |
55 | let results = [];
56 | searchInput.addEventListener('input', async () => {
57 | let input = searchInput.value;
58 | if (input.length) {
59 | results = await getSuggestions(input);
60 | }
61 | renderResults(results);
62 | currentIndex = -1; // Reset index when we return new results
63 | });
64 |
65 | searchInput.addEventListener("focus", async () => {
66 | let input = searchInput.value;
67 | if (results.length === 0 && input.length != 0) {
68 | results = await getSuggestions(input);
69 | }
70 | renderResults(results);
71 | })
72 |
73 | clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users.
74 | clearSearch.addEventListener("click", () => {
75 | searchInput.value = "";
76 | searchInput.focus();
77 | })
78 |
79 | searchInput.addEventListener('keydown', (event) => {
80 | if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
81 | event.preventDefault(); // Prevent the cursor from moving in the search input
82 |
83 | // Find the currently selected suggestion element
84 | const selectedSuggestion = resultsWrapper.querySelector('.selected');
85 | if (selectedSuggestion) {
86 | selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion
87 | }
88 |
89 | // Increment or decrement the current index based on the arrow key pressed
90 | if (event.key === 'ArrowUp') {
91 | currentIndex--;
92 | } else {
93 | currentIndex++;
94 | }
95 |
96 | // Wrap around the index if it goes out of bounds
97 | if (currentIndex < 0) {
98 | currentIndex = resultsWrapper.querySelectorAll('li').length - 1;
99 | } else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) {
100 | currentIndex = 0;
101 | }
102 |
103 | // Select the new suggestion
104 | resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected');
105 | // Update the value of the search input
106 | searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent;
107 | }
108 | });
109 |
110 | function renderResults(results) {
111 | if (!results || !results.length || !searchInput.value) {
112 | return searchWrapper.classList.remove('show');
113 | }
114 |
115 | let content = '';
116 | results.forEach((item) => {
117 | content += `
${item}
`;
118 | });
119 |
120 | // Only show the autocomplete suggestions if the search input has a non-empty value
121 | if (searchInput.value) {
122 | searchWrapper.classList.add('show');
123 | }
124 | resultsWrapper.innerHTML = `
${content}
`;
125 | }
126 |
127 | resultsWrapper.addEventListener('click', (event) => {
128 | if (event.target.tagName === 'LI') {
129 | // Set the value of the search input to the clicked suggestion
130 | searchInput.value = event.target.textContent;
131 | // Reset the current index
132 | currentIndex = -1;
133 | // Submit the form
134 | searchWrapper.querySelector('input[type="submit"]').click();
135 | // Remove the show class from the search wrapper
136 | searchWrapper.classList.remove('show');
137 | }
138 | });
139 |
140 |
141 | document.addEventListener("keypress", (event) => {
142 | if (document.activeElement == searchInput) {
143 | // Allow the '/' character to be pressed when searchInput is active
144 | } else if (document.querySelector(".calc") != null) {
145 | // Do nothing if the calculator is available, so the division keybinding
146 | // will still work
147 | }
148 | else if (event.key == "/") {
149 | event.preventDefault();
150 | searchInput.focus();
151 | searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length;
152 | }
153 | })
154 |
155 | // Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper
156 | document.addEventListener('click', (event) => {
157 | // Check if the target of the event is the search-input or any of its ancestors
158 | if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) {
159 | // Remove the show class from the search wrapper
160 | searchWrapper.classList.remove('show');
161 | }
162 | });
163 |
164 | // Load material icons. If the file cannot be loaded,
165 | // skip them and put a warning in the console.
166 | const font = new FontFace('Material Icons Round', 'url("/fonts/material-icons-round-v108-latin-regular.woff2") format("woff2")');
167 | font.load().then(() => {
168 | const icons = document.getElementsByClassName('material-icons-round');
169 |
170 | // Display all icons.
171 | for (let icon of icons) {
172 | icon.style.visibility = 'visible';
173 | }
174 |
175 | // Ensure icons for the different types of searches are sized correctly.
176 | document.querySelectorAll('#sub-search-wrapper-ico').forEach((el) => {
177 | el.style.fontSize = '17px';
178 | });
179 | }).catch(() => {
180 | console.warn('Failed to load Material Icons Round. Hiding any icons using said pack.');
181 | });
182 |
183 | // load image after server side processing
184 | window.addEventListener('DOMContentLoaded', function () {
185 | var knoTitleElement = document.getElementById('kno_title');
186 | var kno_title = knoTitleElement.dataset.knoTitle;
187 | fetch(kno_title)
188 | .then(response => response.json())
189 | .then(data => {
190 | const pageId = Object.keys(data.query.pages)[0];
191 | const thumbnailSource = data.query.pages[pageId].thumbnail.source;
192 | const url = "/img_proxy?url=" + thumbnailSource;
193 |
194 | // update the img tag with url and add kno_wiki_show
195 | var imgElement = document.querySelector('.kno_wiki');
196 | imgElement.src = url;
197 | imgElement.classList.add('kno_wiki_show');
198 |
199 | console.log(url);
200 | })
201 | .catch(error => {
202 | console.log('Error fetching data:', error);
203 | });
204 | });
205 |
206 | const urlParams = new URLSearchParams(window.location.search);
207 |
208 | if (document.querySelectorAll(".search-active")[1].getAttribute("value") === "image") {
209 |
210 | // image viewer for image search
211 | const closeButton = document.querySelector('.image-close');
212 | const imageView = document.querySelector('.image_view');
213 | const images = document.querySelector('.images');
214 | const viewImageImg = document.querySelector('.view-image-img');
215 | const imageSource = document.querySelector('.image-source');
216 | const imageFull = document.querySelector(".full-size");
217 | const imageProxy = document.querySelector('.proxy-size');
218 | const imageViewerLink = document.querySelector('.image-viewer-link');
219 | const imageSize = document.querySelector('.image-size');
220 | const fullImageSize = document.querySelector(".full-image-size");
221 | const imageAlt = document.querySelector('.image-alt');
222 | const openImageViewer = document.querySelectorAll('.open-image-viewer');
223 | const imageBefore = document.querySelector('.image-before');
224 | const imageNext = document.querySelector('.image-next');
225 | let currentImageIndex = 0;
226 |
227 | closeButton.addEventListener('click', function () {
228 | imageView.classList.remove('image_show');
229 | imageView.classList.add('image_hide');
230 | for (const image of document.querySelectorAll(".image_selected")) {
231 | image.classList = ['image'];
232 | }
233 | images.classList.add('images_viewer_hidden');
234 | });
235 |
236 | openImageViewer.forEach((image, index) => {
237 | image.addEventListener('click', function (event) {
238 | event.preventDefault();
239 | currentImageIndex = index;
240 | showImage();
241 | });
242 | });
243 |
244 | document.addEventListener('keydown', function (event) {
245 | if (searchInput == document.activeElement)
246 | return;
247 | if (event.key === 'ArrowLeft') {
248 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length;
249 | showImage();
250 | }
251 | else if (event.key === 'ArrowRight') {
252 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length;
253 | showImage();
254 | }
255 | });
256 |
257 | imageBefore.addEventListener('click', function () {
258 | currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length;
259 | showImage();
260 | });
261 |
262 | imageNext.addEventListener('click', function () {
263 | currentImageIndex = (currentImageIndex + 1) % openImageViewer.length;
264 | showImage();
265 | });
266 |
267 | function showImage() {
268 | for (const image of document.querySelectorAll(".image_selected")) {
269 | image.classList = ['image'];
270 | }
271 | const current_image = document.querySelectorAll(".image")[currentImageIndex];
272 | current_image.classList.add("image_selected");
273 | var rect = current_image.getBoundingClientRect();
274 | if (!(rect.top >= 0 && rect.left >= 0 &&
275 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
276 | rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
277 | current_image.scrollIntoView(false);
278 | }
279 |
280 | const src = openImageViewer[currentImageIndex].getAttribute('src');
281 | const alt = openImageViewer[currentImageIndex].getAttribute('alt');
282 | const data = openImageViewer[currentImageIndex].getAttribute('data');
283 | const clickableLink = openImageViewer[currentImageIndex].closest('.clickable');
284 | const href = clickableLink.getAttribute('href');
285 | viewImageImg.src = src;
286 | imageProxy.href = src;
287 | imageFull.href = data;
288 | imageSource.href = href;
289 | imageSource.textContent = href;
290 | imageViewerLink.href = href;
291 | images.classList.remove('images_viewer_hidden');
292 | imageView.classList.remove('image_hide');
293 | imageView.classList.add('image_show');
294 | imageAlt.textContent = alt;
295 | fullImageSize.textContent = document.querySelector(".image_selected .resolution").textContent;
296 |
297 | getImageSize(src).then(size => {
298 | imageSize.textContent = size;
299 | });
300 | }
301 |
302 | function getImageSize(url) {
303 | return new Promise((resolve, reject) => {
304 | const img = new Image();
305 | img.onload = function () {
306 | const size = `${this.width} x ${this.height}`;
307 | resolve(size);
308 | };
309 | img.onerror = function () {
310 | reject('Error loading image');
311 | };
312 | img.src = url;
313 | });
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/static/searchicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/searchicon.png
--------------------------------------------------------------------------------
/static/sheng-l-q2dUSl9S4Xg-unsplash.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/sheng-l-q2dUSl9S4Xg-unsplash.webp
--------------------------------------------------------------------------------
/static/themepreview/preview1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview1.webp
--------------------------------------------------------------------------------
/static/themepreview/preview10.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview10.webp
--------------------------------------------------------------------------------
/static/themepreview/preview11.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview11.webp
--------------------------------------------------------------------------------
/static/themepreview/preview12.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview12.webp
--------------------------------------------------------------------------------
/static/themepreview/preview13.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview13.webp
--------------------------------------------------------------------------------
/static/themepreview/preview2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview2.webp
--------------------------------------------------------------------------------
/static/themepreview/preview3.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview3.webp
--------------------------------------------------------------------------------
/static/themepreview/preview4.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview4.webp
--------------------------------------------------------------------------------
/static/themepreview/preview5.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview5.webp
--------------------------------------------------------------------------------
/static/themepreview/preview6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview6.webp
--------------------------------------------------------------------------------
/static/themepreview/preview7.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview7.webp
--------------------------------------------------------------------------------
/static/themepreview/preview8.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview8.webp
--------------------------------------------------------------------------------
/static/themepreview/preview9.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Extravi/araa-search/839240e1175b71ce775a0d8adecd271855bba521/static/themepreview/preview9.webp
--------------------------------------------------------------------------------
/static/torrentSort.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./torrentSort.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2024 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | let options = document.querySelector(".torrent-settings");
30 | options.addEventListener("change", () => {
31 | options.form.submit();
32 | })
33 |
--------------------------------------------------------------------------------
/static/uxlang.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @source: ./uxlang.js
3 | *
4 | * @licstart The following is the entire license notice for the
5 | * JavaScript code in this page.
6 | *
7 | * Copyright (C) 2023 Extravi
8 | *
9 | * The JavaScript code in this page is free software: you can
10 | * redistribute it and/or modify it under the terms of the GNU Affero
11 | * General Public License as published by the Free Software Foundation,
12 | * either version 3 of the License, or (at your option) any later version.
13 | *
14 | * The code is distributed WITHOUT ANY WARRANTY; without even the
15 | * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | * See the GNU Affero General Public License for more details.
17 | *
18 | * As additional permission under GNU Affero General Public License
19 | * section 7, you may distribute non-source (e.g., minimized or compacted)
20 | * forms of that code without the copy of the GNU Affero General Public
21 | * License normally required by section 4, provided you include this
22 | * license notice and a URL through which recipients can access the
23 | * Corresponding Source.
24 | *
25 | * @licend The above is the entire license notice
26 | * for the JavaScript code in this page.
27 | */
28 |
29 | function setCookie(name, value) {
30 | document.cookie = `${name}=${value}; HostOnly=true; SameSite=None; Secure; Max-Age=2147483647`;
31 | }
32 |
33 | function mapToValidLanguage(userLanguage) {
34 | const languageMappings = {
35 | "en": "english",
36 | "da": "danish",
37 | "nl": "dutch",
38 | "fr": "french",
39 | "fr-CA": "french_canadian",
40 | "de": "german",
41 | "el": "greek",
42 | "it": "italian",
43 | "ja": "japanese",
44 | "ko": "korean",
45 | "zh": "mandarin_chinese",
46 | "no": "norwegian",
47 | "pl": "polish",
48 | "pt": "portuguese",
49 | "ru": "russian",
50 | "es": "spanish",
51 | "sv": "swedish",
52 | "tr": "turkish",
53 | "uk": "ukrainian"
54 | };
55 |
56 | const browserLanguage = userLanguage.split("-")[0];
57 | return languageMappings[browserLanguage] || "english";
58 | }
59 |
60 | function setLanguageCookie() {
61 | const userLanguage = navigator.language.toLowerCase();
62 | const mappedLanguage = mapToValidLanguage(userLanguage);
63 | setCookie("ux_lang", mappedLanguage);
64 | if (mappedLanguage != "english") {
65 | // Default language is english, so only refresh if lang != english
66 | window.location.reload();
67 | }
68 | }
69 |
70 | if (!document.cookie.includes("ux_lang")) {
71 | setLanguageCookie();
72 | }
73 |
--------------------------------------------------------------------------------
/templates/discover.html:
--------------------------------------------------------------------------------
1 | {% extends "preresults_layout.html" %}
2 |
3 | {% block body %}
4 |