Compare commits

...

10 Commits

Author SHA1 Message Date
c6f1156bc6 changed approach, got rid of settings.yml binding entirely
Some checks failed
Documentation / Release (push) Has been cancelled
Integration / Python 3.10 (push) Has been cancelled
Integration / Python 3.11 (push) Has been cancelled
Integration / Python 3.12 (push) Has been cancelled
Integration / Python 3.13 (push) Has been cancelled
Integration / Python 3.9 (push) Has been cancelled
Integration / Theme (push) Has been cancelled
Integration / Docker (push) Has been cancelled
2025-05-10 22:50:37 -05:00
cf5dbbbc77 fixed...? 2025-05-10 22:45:56 -05:00
8477fb4846 oh, portainer didn't like that one 2025-05-10 22:43:33 -05:00
8204f4ee72 sets the config volume to read-only 2025-05-10 22:37:36 -05:00
9d9a0fe95e reverted 2025-05-10 22:31:02 -05:00
427a84d823 attempt #2 2025-05-10 22:29:01 -05:00
e971a3d7ab experimental mobile touchscreen support for oneko 2025-05-10 22:20:36 -05:00
89cb563793 fixed issue where oneko.gif was not referenced correctly, causing invisible neko 2025-05-10 21:51:46 -05:00
ac5c83b87d fixed typo in docker compose 2025-05-10 21:45:51 -05:00
34437e8491 implemented oneko 2025-05-10 21:41:29 -05:00
11 changed files with 913 additions and 228 deletions

View File

@ -4,6 +4,4 @@ services:
pull_policy: build
restart: unless-stopped
ports:
- "${NEKOSEARCH_PORT]:8080"
volumes:
- ./nekosearch_config:/etc/searxng
- "${NEKOSEARCH_PORT}:8080"

View File

@ -2,7 +2,7 @@ general:
# Debug mode, only for development. Is overwritten by ${SEARXNG_DEBUG}
debug: false
# displayed name
instance_name: "SearXNG"
instance_name: "nekosearch"
# For example: https://example.com/privacy
privacypolicy_url: false
# use true to use your own donation page written in searx/info/en/donate.md
@ -32,16 +32,16 @@ brand:
search:
# Filter results. 0: None, 1: Moderate, 2: Strict
safe_search: 0
safe_search: 2
# Existing autocomplete backends: "360search", "baidu", "brave", "dbpedia", "duckduckgo", "google", "yandex",
# "mwmbl", "seznam", "sogou", "stract", "swisscows", "quark", "qwant", "wikipedia" -
# "mwmbl", "seznam", "sogou", "stract", "swisscows", "qwant", "wikipedia" -
# leave blank to turn it off by default.
autocomplete: ""
autocomplete: "duckduckgo"
# minimun characters to type before autocompleter starts
autocomplete_min: 4
# backend for the favicon near URL in search results.
# Available resolvers: "allesedv", "duckduckgo", "google", "yandex" - leave blank to turn it off by default.
favicon_resolver: ""
favicon_resolver: "duckduckgo"
# Default search language - leave blank to detect from browser information or
# use codes from 'languages.py'
default_lang: "auto"
@ -83,8 +83,8 @@ server:
port: 8888
bind_address: "127.0.0.1"
# public URL of the instance, to ensure correct inbound links. Is overwritten
# by ${SEARXNG_BASE_URL}.
base_url: false # "http://example.com/location"
# by ${SEARXNG_URL}.
base_url: https://nekosear.ch # "http://example.com/location"
# rate limit the number of request on the instance, block some bots.
# Is overwritten by ${SEARXNG_LIMITER}
limiter: false
@ -95,14 +95,13 @@ server:
# If your instance owns a /etc/searxng/settings.yml file, then set the following
# values there.
secret_key: "ultrasecretkey" # Is overwritten by ${SEARXNG_SECRET}
secret_key: "a9b0aafe71f3dbe131d66762f03e27ab02dfa409541184f3d34036926d7dd79e" # Is overwritten by ${SEARXNG_SECRET}
# Proxy image results through SearXNG. Is overwritten by ${SEARXNG_IMAGE_PROXY}
image_proxy: false
# 1.0 and 1.1 are supported
http_protocol_version: "1.0"
# POST queries are more secure as they don't show up in history but may cause
# problems when using Firefox containers.
# Is overwritten by ${SEARXNG_METHOD}
# problems when using Firefox containers
method: "POST"
default_http_headers:
X-Content-Type-Options: nosniff
@ -124,13 +123,13 @@ ui:
templates_path: ""
# query_in_title: When true, the result page's titles contains the query
# it decreases the privacy, since the browser can records the page titles.
query_in_title: false
query_in_title: true
# infinite_scroll: When true, automatically loads the next page when scrolling to bottom of the current page.
infinite_scroll: false
infinite_scroll: true
# ui theme
default_theme: simple
# center the results ?
center_alignment: false
center_alignment: true
# URL prefix of the internet archive, don't forget trailing slash (if needed).
# cache_url: "https://webcache.googleusercontent.com/search?q=cache:"
# Default interface locale - leave blank to detect from browser information or
@ -140,7 +139,7 @@ ui:
# results_on_new_tab: false
theme_args:
# style of simple theme: auto, light, dark
simple_style: auto
simple_style: dark
# Perform search immediately if a category selected.
# Disable to select multiple categories at once and start the search manually.
search_on_category_select: true
@ -152,7 +151,7 @@ ui:
# Lock arbitrary settings on the preferences page.
#
# preferences:
# lock:
# lock:
# - categories
# - language
# - autocomplete
@ -208,8 +207,16 @@ outgoing:
# are also supported: see
# https://2.python-requests.org/en/latest/user/advanced/#socks
#
# proxies:
# all://:
proxies:
all://:
- http://38.33.146.13:2000
- http://38.33.148.3:2000
- http://38.38.254.150:2000
- http://38.33.146.96:2000
#https: http://192.168.1.78:8888
#proxies:
#all://:
#- http://192.168.1.78:8888
# - http://proxy1:8080
# - http://proxy2:8080
#
@ -217,7 +224,7 @@ outgoing:
#
# Extra seconds to add in order to account for the time taken by the proxy
#
# extra_proxy_timeout: 10
# extra_proxy_timeout: 20
#
# uncomment below section only if you have more than one network interface
# which can be the source of outgoing search requests
@ -227,38 +234,29 @@ outgoing:
# - 1.1.1.2
# - fe80::/126
# Plugin configuration, for more details see
# External plugin configuration, for more details see
# https://docs.searxng.org/admin/settings/settings_plugins.html
#
plugins:
searx.plugins.calculator.SXNGPlugin:
active: true
searx.plugins.hash_plugin.SXNGPlugin:
active: true
searx.plugins.self_info.SXNGPlugin:
active: true
searx.plugins.unit_converter.SXNGPlugin:
active: true
searx.plugins.ahmia_filter.SXNGPlugin:
active: true
searx.plugins.hostnames.SXNGPlugin:
active: true
searx.plugins.oa_doi_rewrite.SXNGPlugin:
active: false
searx.plugins.tor_check.SXNGPlugin:
active: false
searx.plugins.tracker_url_remover.SXNGPlugin:
active: false
# plugins:
# - mypackage.mymodule.MyPlugin
# - mypackage.mymodule.MyOtherPlugin
# - ...
# Comment or un-comment plugin to activate / deactivate by default.
# https://docs.searxng.org/admin/settings/settings_plugins.html
#
# enabled_plugins:
# # these plugins are enabled if nothing is configured ..
# - 'Basic Calculator'
# - 'Hash plugin'
# - 'Self Information'
# - 'Tracker URL remover'
# - 'Unit converter plugin'
# - 'Ahmia blacklist' # activation depends on outgoing.using_tor_proxy
# # these plugins are disabled if nothing is configured ..
# - 'Hostnames plugin' # see 'hostnames' configuration below
# - 'Open Access DOI rewrite'
# - 'Tor check plugin'
# Configuration of the "Hostnames plugin":
#
@ -362,11 +360,6 @@ engines:
shortcut: 9g
disabled: true
- name: acfun
engine: acfun
shortcut: acf
disabled: true
- name: adobe stock
engine: adobe_stock
shortcut: asi
@ -434,11 +427,6 @@ engines:
disabled: true
shortcut: aa
- name: ansa
engine: ansa
shortcut: ans
disabled: true
# - name: annas articles
# engine: annas_archive
# shortcut: aaa
@ -495,6 +483,7 @@ engines:
engine: artic
shortcut: arc
timeout: 4.0
disabled: true
- name: arxiv
engine: arxiv
@ -516,27 +505,6 @@ engines:
shortcut: bc
categories: music
- name: baidu
baidu_category: general
categories: [general]
engine: baidu
shortcut: bd
disabled: true
- name: baidu images
baidu_category: images
categories: [images]
engine: baidu
shortcut: bdi
disabled: true
- name: baidu kaifa
baidu_category: it
categories: [it]
engine: baidu
shortcut: bdk
disabled: true
- name: wikipedia
engine: wikipedia
shortcut: wp
@ -552,7 +520,6 @@ engines:
- name: bing
engine: bing
shortcut: bi
disabled: true
- name: bing images
engine: bing_images
@ -566,11 +533,6 @@ engines:
engine: bing_videos
shortcut: biv
- name: bitchute
engine: bitchute
shortcut: bit
disabled: true
- name: bitbucket
engine: xpath
paging: true
@ -619,38 +581,6 @@ engines:
# to show premium or plus results too:
# skip_premium: false
# WARNING: links from chinaso.com voilate users privacy
# Before activate these engines its mandatory to read
# - https://github.com/searxng/searxng/issues/4694
# - https://docs.searxng.org/dev/engines/online/chinaso.html
- name: chinaso news
engine: chinaso
shortcut: chinaso
categories: [news]
chinaso_category: news
chinaso_news_source: all
disabled: true
inactive: true
- name: chinaso images
engine: chinaso
network: chinaso news
shortcut: chinasoi
categories: [images]
chinaso_category: images
disabled: true
inactive: true
- name: chinaso videos
engine: chinaso
network: chinaso news
shortcut: chinasov
categories: [videos]
chinaso_category: videos
disabled: true
inactive: true
- name: cloudflareai
engine: cloudflareai
shortcut: cfai
@ -725,6 +655,26 @@ engines:
search_type: news
disabled: true
- name: curlie
engine: xpath
shortcut: cl
categories: general
disabled: true
paging: true
lang_all: ''
search_url: https://curlie.org/search?q={query}&lang={lang}&start={pageno}&stime=92452189
page_size: 20
results_xpath: //div[@id="site-list-content"]/div[@class="site-item"]
url_xpath: ./div[@class="title-and-desc"]/a/@href
title_xpath: ./div[@class="title-and-desc"]/a/div
content_xpath: ./div[@class="title-and-desc"]/div[@class="site-descr"]
about:
website: https://curlie.org/
wikidata_id: Q60715723
use_official_api: false
require_api_key: false
results: HTML
- name: currency
engine: currency_convert
categories: general
@ -744,6 +694,7 @@ engines:
engine: deviantart
shortcut: da
timeout: 3.0
disabled: true
- name: ddg definitions
engine: duckduckgo_definitions
@ -803,18 +754,17 @@ engines:
results: HTML
# - name: elasticsearch
# shortcut: els
# shortcut: es
# engine: elasticsearch
# base_url: http://localhost:9200
# username: elastic
# password: changeme
# index: my-index
# enable_http: true
# # available options: match, simple_query_string, term, terms, custom
# query_type: match
# # if query_type is set to custom, provide your query here
# # custom_query_json: {"query":{"match_all": {}}}
# # show_metadata: false
# #custom_query_json: {"query":{"match_all": {}}}
# #show_metadata: false
# disabled: true
- name: wikidata
@ -925,6 +875,7 @@ engines:
# api_key: 'apikey' # required!
# Or you can use the html non-stable engine, activated by default
engine: flickr_noapi
disabled: true
- name: free software directory
engine: mediawiki
@ -1013,11 +964,11 @@ engines:
engine: goodreads
shortcut: good
timeout: 4.0
disabled: true
- name: google
engine: google
shortcut: go
disabled: true
# additional_tests:
# android: *test_android
@ -1126,27 +1077,6 @@ engines:
require_api_key: false
results: JSON
- name: il post
engine: il_post
shortcut: pst
- name: huggingface
engine: huggingface
shortcut: hf
disabled: true
- name: huggingface datasets
huggingface_endpoint: datasets
engine: huggingface
shortcut: hfd
disabled: true
- name: huggingface spaces
huggingface_endpoint: spaces
engine: huggingface
shortcut: hfs
disabled: true
- name: imdb
engine: imdb
shortcut: imdb
@ -1180,11 +1110,6 @@ engines:
shortcut: ip
disabled: true
- name: iqiyi
engine: iqiyi
shortcut: iq
disabled: true
- name: jisho
engine: jisho
shortcut: js
@ -1333,19 +1258,12 @@ engines:
disabled: true
number_of_results: 20
# https://docs.searxng.org/dev/engines/offline/search-indexer-engines.html#module-searx.engines.meilisearch
# - name: meilisearch
# engine: meilisearch
# shortcut: mes
# enable_http: true
# base_url: http://localhost:7700
# index: my-index
# auth_key: Bearer XXXX
- name: microsoft learn
engine: microsoft_learn
shortcut: msl
disabled: true
- name: mixcloud
engine: mixcloud
@ -1382,11 +1300,6 @@ engines:
shortcut: mwm
disabled: true
- name: niconico
engine: niconico
shortcut: nico
disabled: true
- name: npm
engine: npm
shortcut: npm
@ -1414,32 +1327,20 @@ engines:
require_api_key: false
results: JSON
# https://docs.searxng.org/dev/engines/online/mullvad_leta.html
- name: mullvadleta
engine: mullvad_leta
disabled: true
leta_engine: google
categories: [general, web]
shortcut: ml
- name: mullvadleta brave
engine: mullvad_leta
network: mullvadleta
disabled: true
leta_engine: brave
categories: [general, web]
shortcut: mlb
# read https://docs.searxng.org/dev/engines/online/mullvad_leta.html
# - name: mullvadleta
# engine: mullvad_leta
# leta_engine: google # choose one of the following: google, brave
# use_cache: true # Only 100 non-cache searches per day, suggested only for private instances
# search_url: https://leta.mullvad.net
# categories: [general, web]
# shortcut: ml
- name: odysee
engine: odysee
shortcut: od
disabled: true
- name: ollama
engine: ollama
shortcut: ollama
disabled: true
- name: openairedatasets
engine: json_engine
paging: true
@ -1701,24 +1602,11 @@ engines:
shortcut: pypi
engine: pypi
- name: quark
quark_category: general
categories: [general]
engine: quark
shortcut: qk
disabled: true
- name: quark images
quark_category: images
categories: [images]
engine: quark
shortcut: qki
disabled: true
- name: qwant
qwant_categ: web
engine: qwant
shortcut: qw
disabled: true
categories: [general, web]
additional_tests:
rosebud: *test_rosebud
@ -1776,12 +1664,6 @@ engines:
page_size: 25
disabled: true
- name: reuters
engine: reuters
shortcut: reu
# https://docs.searxng.org/dev/engines/online/reuters.html
# sort_order = "relevance"
- name: right dao
engine: xpath
paging: true
@ -1836,12 +1718,6 @@ engines:
about:
website: https://searchmysite.net
- name: selfhst icons
engine: selfhst
shortcut: si
inactive: true
disabled: true
- name: sepiasearch
engine: sepiasearch
shortcut: sep
@ -1972,10 +1848,6 @@ engines:
startpage_categ: images
categories: [images, web]
shortcut: spi
- name: steam
engine: steam
shortcut: stm
disabled: true
- name: tokyotoshokan
@ -1995,15 +1867,15 @@ engines:
# For this demo of the sqlite engine download:
# https://liste.mediathekview.de/filmliste-v2.db.bz2
# and unpack into searx/data/filmliste-v2.db
# Query to test: "!mediathekview concert"
# Query to test: "!demo concert"
#
# - name: mediathekview
# - name: demo
# engine: sqlite
# shortcut: mediathekview
# categories: [general, videos]
# result_type: MainResult
# shortcut: demo
# categories: general
# result_template: default.html
# database: searx/data/filmliste-v2.db
# query_str: >-
# query_str: >-
# SELECT title || ' (' || time(duration, 'unixepoch') || ')' AS title,
# COALESCE( NULLIF(url_video_hd,''), NULLIF(url_video_sd,''), url_video) AS url,
# description AS content
@ -2137,7 +2009,6 @@ engines:
content_query: Snippet
categories: [general, web]
shortcut: wib
disabled: true
about:
website: https://wiby.me/
@ -2346,8 +2217,8 @@ engines:
- name: mojeek
shortcut: mjk
engine: mojeek
categories: [general, web]
disabled: true
categories: [general, web]
- name: mojeek images
shortcut: mjkimg
@ -2381,7 +2252,6 @@ engines:
content_xpath: //div[@class="total_dsc_wrap"]/a
first_page_num: 1
page_size: 10
disabled: true
about:
website: https://www.naver.com/
wikidata_id: Q485639
@ -2670,12 +2540,6 @@ engines:
shortcut: pgo
disabled: true
- name: senscritique
engine: senscritique
shortcut: scr
timeout: 4.0
disabled: true
# Doku engine lets you access to any Doku wiki instance:
# A public one or a privete/corporate one.
# - name: ubuntuwiki

View File

@ -0,0 +1,242 @@
function neko() {
var nekoEl = document.createElement("div");
var nekoPosX = 32;
var nekoPosY = 32;
var mousePosX = 0;
var mousePosY = 0;
var frameCount = 0;
var idleTime = 0;
var idleAnimation = null;
var idleAnimationFrame = 0;
var direction;
var IE = document.all ? true : false;
var nekoSpeed = 10;
var spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "absolute";
nekoEl.style.pointerEvents = "none";
nekoEl.style.backgroundImage = "url('oneko.gif')";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = nekoPosX - 16 + "px";
nekoEl.style.top = nekoPosY - 16 + "px";
nekoEl.style.zIndex = Number.MAX_VALUE;
document.body.appendChild(nekoEl);
function mousePos(event) {
if (IE) {
event = window.event;
}
mousePosX = event.clientX;
mousePosY = event.clientY - 16;
}
document.onmousemove = mousePos;
window.onekoInterval = setInterval(frame, 100);
}
function setSprite(name, frame) {
var length = spriteSets[name].length;
if (IE) {
length = 0;
// Internet explorer is really fucking dumb
while (length < spriteSets[name].length) {
if (spriteSets[name][length] != null) {
length = length + 1;
continue;
}
break;
}
}
var sprite = spriteSets[name][frame % length];
nekoEl.style.backgroundPosition =
sprite["0"] * 32 + "px " + sprite["1"] * 32 + "px";
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime = idleTime + 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
var avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame = idleAnimationFrame + 1;
}
function frame() {
frameCount = frameCount + 1;
var diffX = nekoPosX - mousePosX;
var diffY = nekoPosY - mousePosY;
var distance = Math.sqrt(Math.pow(diffX, 2) + Math.pow(diffY, 2));
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime = idleTime - 1;
return;
}
direction = "";
if (diffY / distance > 0.5) {
direction = "N";
} else if (diffY / distance < -0.5) {
direction = "S";
}
if (diffX / distance > 0.5) {
direction = direction + "W";
} else if (diffX / distance < -0.5) {
direction = direction + "E";
}
setSprite(direction, frameCount);
if (distance > nekoSpeed) {
nekoPosX = nekoPosX - (diffX / distance) * nekoSpeed;
nekoPosY = nekoPosY - (diffY / distance) * nekoSpeed;
} else {
nekoPosX = mousePosX;
nekoPosY = mousePosY;
}
nekoPosX = Math.min(
Math.max(16, nekoPosX),
document.getElementsByTagName("body")[0].clientWidth - 16
);
nekoPosY = Math.min(
Math.max(16, nekoPosY),
document.getElementsByTagName("body")[0].clientHeight - 16
);
nekoEl.style.left = nekoPosX - 16 + "px";
nekoEl.style.top = nekoPosY - 16 + "px";
}
init();
}
neko();

View File

@ -0,0 +1,291 @@
// oneko.js: https://github.com/adryd325/oneko.js (webring variant)
(function oneko() {
const isReducedMotion =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
// please use data-neko="true" on your A elements that link to another site with oneko-webring.js instead of this
// this is deprecated and will eventually be removed
const nekoSites = [
"localhost",
];
try {
const searchParams = location.search
.replace("?", "")
.split("&")
.map((keyvaluepair) => keyvaluepair.split("="));
// This is so much repeated code, I don't like it
tmp = searchParams.find((a) => a[0] == "catx");
if (tmp && tmp[1]) nekoPosX = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "caty");
if (tmp && tmp[1]) nekoPosY = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "catdx");
if (tmp && tmp[1]) mousePosX = parseInt(tmp[1]);
tmp = searchParams.find((a) => a[0] == "catdy");
if (tmp && tmp[1]) mousePosY = parseInt(tmp[1]);
} catch (e) {
console.error("oneko.js: failed to parse query params.");
console.error(e);
}
function onClick(event) {
const target = event.target.closest("A");
if (target === null || !target.getAttribute("href")) {
return;
}
let newLocation;
try {
newLocation = new URL(target.href);
} catch (e) {
return;
}
if (
(nekoSites.includes(newLocation.host) && newLocation.pathname == "/") ||
target.dataset.neko
) {
newLocation.searchParams.append("catx", Math.floor(nekoPosX));
newLocation.searchParams.append("caty", Math.floor(nekoPosY));
newLocation.searchParams.append("catdx", Math.floor(mousePosX));
newLocation.searchParams.append("catdy", Math.floor(mousePosY));
event.preventDefault();
window.location.href = newLocation.toString();
}
}
document.addEventListener("click", onClick);
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = Number.MAX_VALUE;
let nekoFile = "./oneko.gif"
const curScript = document.currentScript
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
})();

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,240 @@
// oneko.js: https://github.com/adryd325/oneko.js
(function oneko() {
const isReducedMotion =
window.matchMedia(`(prefers-reduced-motion: reduce)`) === true ||
window.matchMedia(`(prefers-reduced-motion: reduce)`).matches === true;
if (isReducedMotion) return;
const nekoEl = document.createElement("div");
let nekoPosX = 32;
let nekoPosY = 32;
let mousePosX = 0;
let mousePosY = 0;
let frameCount = 0;
let idleTime = 0;
let idleAnimation = null;
let idleAnimationFrame = 0;
const nekoSpeed = 10;
const spriteSets = {
idle: [[-3, -3]],
alert: [[-7, -3]],
scratchSelf: [
[-5, 0],
[-6, 0],
[-7, 0],
],
scratchWallN: [
[0, 0],
[0, -1],
],
scratchWallS: [
[-7, -1],
[-6, -2],
],
scratchWallE: [
[-2, -2],
[-2, -3],
],
scratchWallW: [
[-4, 0],
[-4, -1],
],
tired: [[-3, -2]],
sleeping: [
[-2, 0],
[-2, -1],
],
N: [
[-1, -2],
[-1, -3],
],
NE: [
[0, -2],
[0, -3],
],
E: [
[-3, 0],
[-3, -1],
],
SE: [
[-5, -1],
[-5, -2],
],
S: [
[-6, -3],
[-7, -2],
],
SW: [
[-5, -3],
[-6, -1],
],
W: [
[-4, -2],
[-4, -3],
],
NW: [
[-1, 0],
[-1, -1],
],
};
function init() {
nekoEl.id = "oneko";
nekoEl.ariaHidden = true;
nekoEl.style.width = "32px";
nekoEl.style.height = "32px";
nekoEl.style.position = "fixed";
nekoEl.style.pointerEvents = "none";
nekoEl.style.imageRendering = "pixelated";
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
nekoEl.style.zIndex = 2147483647;
let nekoFile = "/static/themes/simple/js/oneko.gif"
const curScript = document.currentScript
if (curScript && curScript.dataset.cat) {
nekoFile = curScript.dataset.cat
}
nekoEl.style.backgroundImage = `url(${nekoFile})`;
document.body.appendChild(nekoEl);
document.addEventListener("mousemove", function (event) {
mousePosX = event.clientX;
mousePosY = event.clientY;
});
window.requestAnimationFrame(onAnimationFrame);
}
let lastFrameTimestamp;
function onAnimationFrame(timestamp) {
// Stops execution if the neko element is removed from DOM
if (!nekoEl.isConnected) {
return;
}
if (!lastFrameTimestamp) {
lastFrameTimestamp = timestamp;
}
if (timestamp - lastFrameTimestamp > 100) {
lastFrameTimestamp = timestamp
frame()
}
window.requestAnimationFrame(onAnimationFrame);
}
function setSprite(name, frame) {
const sprite = spriteSets[name][frame % spriteSets[name].length];
nekoEl.style.backgroundPosition = `${sprite[0] * 32}px ${sprite[1] * 32}px`;
}
function resetIdleAnimation() {
idleAnimation = null;
idleAnimationFrame = 0;
}
function idle() {
idleTime += 1;
// every ~ 20 seconds
if (
idleTime > 10 &&
Math.floor(Math.random() * 200) == 0 &&
idleAnimation == null
) {
let avalibleIdleAnimations = ["sleeping", "scratchSelf"];
if (nekoPosX < 32) {
avalibleIdleAnimations.push("scratchWallW");
}
if (nekoPosY < 32) {
avalibleIdleAnimations.push("scratchWallN");
}
if (nekoPosX > window.innerWidth - 32) {
avalibleIdleAnimations.push("scratchWallE");
}
if (nekoPosY > window.innerHeight - 32) {
avalibleIdleAnimations.push("scratchWallS");
}
idleAnimation =
avalibleIdleAnimations[
Math.floor(Math.random() * avalibleIdleAnimations.length)
];
}
switch (idleAnimation) {
case "sleeping":
if (idleAnimationFrame < 8) {
setSprite("tired", 0);
break;
}
setSprite("sleeping", Math.floor(idleAnimationFrame / 4));
if (idleAnimationFrame > 192) {
resetIdleAnimation();
}
break;
case "scratchWallN":
case "scratchWallS":
case "scratchWallE":
case "scratchWallW":
case "scratchSelf":
setSprite(idleAnimation, idleAnimationFrame);
if (idleAnimationFrame > 9) {
resetIdleAnimation();
}
break;
default:
setSprite("idle", 0);
return;
}
idleAnimationFrame += 1;
}
function frame() {
frameCount += 1;
const diffX = nekoPosX - mousePosX;
const diffY = nekoPosY - mousePosY;
const distance = Math.sqrt(diffX ** 2 + diffY ** 2);
if (distance < nekoSpeed || distance < 48) {
idle();
return;
}
idleAnimation = null;
idleAnimationFrame = 0;
if (idleTime > 1) {
setSprite("alert", 0);
// count down after being alerted before moving
idleTime = Math.min(idleTime, 7);
idleTime -= 1;
return;
}
let direction;
direction = diffY / distance > 0.5 ? "N" : "";
direction += diffY / distance < -0.5 ? "S" : "";
direction += diffX / distance > 0.5 ? "W" : "";
direction += diffX / distance < -0.5 ? "E" : "";
setSprite(direction, frameCount);
nekoPosX -= (diffX / distance) * nekoSpeed;
nekoPosY -= (diffY / distance) * nekoSpeed;
nekoPosX = Math.min(Math.max(16, nekoPosX), window.innerWidth - 16);
nekoPosY = Math.min(Math.max(16, nekoPosY), window.innerHeight - 16);
nekoEl.style.left = `${nekoPosX - 16}px`;
nekoEl.style.top = `${nekoPosY - 16}px`;
}
init();
})();

View File

@ -0,0 +1,7 @@
Copyright © 2022 adryd
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,19 @@
# oneko.js
A hacky script I wrote to put a cat on my site.
The default image is `oneko.gif` in the same directory as the script. This can be changed by adding `data-cat="yourimage.png"` to your `<script>` tag.
demo: https://adryd.com
This script is meant to be simple so that it can easily be extended upon. Pull requests adding features not seen in the [original neko program
](https://en.wikipedia.org/wiki/Neko_(software)) will probably not be merged.
implemented in a few different places
- Userscript: https://openuserjs.org/scripts/sjehuda/Oneko_WebMate
- Vencord: https://vencord.dev/plugins/oneko
- Spicetify: https://github.com/kyrie25/spicetify-oneko
feature forks
- Pet the cat: https://github.com/tylxr59/oneko.js/tree/main
- Move the cat using scroll wheel: https://github.com/rozbrajaczpoziomow/fork-oneko.js/tree/main

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>oneko.js</title>
</head>
<body>
<!--[if IE]>
<script src="./oneko-ie6.js"></script>
<![endif]-->
<!--[if !IE]><!-->
<script src="./oneko.js"></script>
<!--<![endif]-->
</body>
</html>

View File

@ -5,6 +5,12 @@
"src": "js/searxng.head.js",
"isEntry": true
},
"js/oneko.js": {
"file": "js/oneko.js",
"name": "js/oneko.js",
"src": "js/oneko.js",
"isEntry": true
},
"js/searxng.js": {
"file": "js/searxng.min.js",
"name": "js/searxng.min",
@ -26,4 +32,4 @@
"src": "less/style-rtl.less",
"isEntry": true
}
}
}

View File

@ -84,6 +84,7 @@
</footer>
<!--[if gte IE 9]>-->
<script src="{{ url_for('static', filename='js/searxng.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/oneko.js') }}"></script>
<!--<![endif]-->
</body>
</html>