Compare commits

...

36 Commits

Author SHA1 Message Date
5032672341 Add opensearch.xml to gitignore 2025-05-09 10:03:39 -05:00
eec2d36526 Fixes before testing & deploying 2025-05-09 10:01:56 -05:00
Gib
3f3c0d72f8 Add opensearch.xml to site 2025-05-09 09:54:18 -05:00
f6b6639905 Add stuff so we can figure out how to be able to add this in native firefox 2025-05-09 09:06:15 -05:00
3781ae935d Revert nextcloud stuff because it was actually right the first time 2025-02-28 15:33:25 -06:00
ede223d9d5 Update nextcloud to work 2025-02-28 15:29:03 -06:00
dc1fa6d08a fix plex. overseerr broken but idk what I can do 2025-02-28 15:09:59 -06:00
7df4b38d09 Update README.md 2025-02-28 15:02:19 -06:00
7644c2c810 Add host-bang script as well to make things super easy 2025-02-28 14:56:22 -06:00
b5e740a5c2 Fixed network env issues in docker compose 2025-02-28 14:49:19 -06:00
48a526f404 Update README.md instructions. Clean up docker compose & add .env for docker compose file. 2025-02-28 14:38:45 -06:00
94a7880db3 Update README.md instructions. Clean up docker composee & add .env for docker compose file. 2025-02-28 14:34:26 -06:00
6f336b18d3 Add a bunch of self hosted services 2025-02-28 12:59:09 -06:00
1451ab0ef1 Update README.md 2025-02-28 12:05:47 -06:00
b35f479def Update README.md 2025-02-28 12:04:52 -06:00
740ef0f748 Update README.md 2025-02-28 12:01:28 -06:00
5d6e12c308 update readme 2025-02-28 11:57:34 -06:00
c75f426065 Add environment variables to make it easy for anyone to host 2025-02-28 11:50:53 -06:00
69239287b2 Make prettier 2025-02-28 10:06:04 -06:00
8f77173b8b Remove search.svg 2025-02-28 09:45:48 -06:00
cdcd418239 Make it prettier 2025-02-28 09:44:56 -06:00
537bf9d613 fix webpage 2025-02-27 16:49:08 -06:00
eba6c8273c Add links 2025-02-27 15:15:23 -06:00
97fe722790 add my own bangs for open web ui 2025-02-27 13:18:59 -06:00
8ea7faf483 Docker works now 2025-02-27 12:28:50 -06:00
c010ea4ea7 Add docker stuff 2025-02-27 11:44:25 -06:00
991fd13184 Update README.md 2025-02-27 11:21:38 -06:00
3032a81915 Make it my own to self host 2025-02-27 11:19:34 -06:00
cea48a6584 * the name 2025-02-19 03:58:47 -08:00
f96ce5706d Merge pull request #5 from samyakbardiya/fix/handle-non-alphanumeric-chars
fix: Handle non-alphanumeric characters in bangs
2025-02-15 18:16:14 -08:00
5cf34e3467 Merge pull request #7 from irsyadpage/main
Add padding to .content-container
2025-02-15 18:08:35 -08:00
34f64869c0 pwa because why not 2025-02-15 18:03:21 -08:00
b9763e2124 Add padding to .content-container 2025-02-15 22:22:03 +08:00
001e48129b fix: update regex to handle non-alphanumeric chars 2025-02-15 18:33:21 +05:30
3165c3dccb default bang in localstorage 2025-02-14 23:33:45 -08:00
0b569d2b86 hack to handle slashes 2025-02-14 23:27:00 -08:00
23 changed files with 4585 additions and 362 deletions

26
.env.example Normal file
View File

@ -0,0 +1,26 @@
# All environment variables must be prefixed with VITE_ to be seen by client.
# Self-hosted Services URLs. Do not include the trailing slash.
VITE_BANG_URL="https://bang."
VITE_GITEA_URL="https://git."
VITE_SEARXNG_URL="https://search."
VITE_OPENWEBUI_URL="https://chat."
VITE_NEXTCLOUD_URL="https://apps.cloud."
VITE_PLEX_URL="https://plex."
VITE_OVERSEERR_URL="https://overseerr."
VITE_SONARR_URL="https://sonarr."
VITE_RADARR_URL="https://radarr."
VITE_LIDARR_URL="https://lidarr."
# Set default bang here. For example "g" would be Google.
# Google is the default if you don't set this.
# 's' is for your searxng instance, replacing startpage
VITE_DEFAULT_BANG="s"
# SPECIFIC MODELS FOR OPENWEBUI
# Local
VITE_LLAMA_MODEL="llama3.1" # !llama
VITE_DEEPSEEK_MODEL="deepseek-r1:8b" # !r1
# Remote
VITE_OPENAI_MODEL="gpt-4o-mini" # !openai
VITE_CLAUDE_MODEL="anthropic/claude-3.5-sonnet" # !claude

8
.gitignore vendored
View File

@ -6,6 +6,7 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
public/opensearch.xml
node_modules
dist
@ -22,3 +23,10 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# ENV
docker/.env
!docker/.env.example
.env
.env*
!.env.example

View File

@ -1,13 +1,44 @@
# Unduck
# 💣Bang
*This is a fork of [unduck](https://unduck.link) by [Theo](https://github.com/t3dotgg/unduck).*
## Why fork?
This fork allows anyone to easily self host this service & add bangs for their own self-hosted websites. Bang comes pre-configured to work with
- Searxng *!s*
- Gitea *!tea*
- OpenWebUI *!ai*
- Plex *!plex*
- Sonarr *!tv*
- Radarr *!mv*
- Lidarr *!mp3*
- ~Overseerr *!ov*~ *Overseerr doesn't seem to work correctly*
*Note: We have replaced some of the default bangs from duckduckgo with our own bangs in order to have more simple bangs for the self-hosted websites.*
All you have to do is fill out the environment variables in the .env.example files located in the root directory & the docker directory & rename them to .env.
You can also easily add your own bangs by adding a new entry in the bangs.ts file.
## How to self host
1. Clone the repo & fill out the .env.example files in the root directory & the docker directory & rename them to .env.
- *Note: Our docker compose assumes you plan to select an external network.*
2. Run the bash script `host-bang` in the docker directory with the root directory of the project as an argument.
- *Note: You can also simply run it from the root or docker directory without an argument.*
### How to update the website
1. Run the bash script `update-bang` in the docker directory with the root directory of the project as an argument.
- *Note: You can also simply run it from the root or docker directory without an argument.*
## Theo's words
DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables all of DuckDuckGo's bangs to work, but much faster.
```
https://unduck.link?q=%s
```
## How is it that much faster?
### How is it that much faster?
DuckDuckGo does their redirects server side. Their DNS is...not always great. Result is that it often takes ages.
I solved this by doing all of the work client side. Once you've went to https://unduck.link once, the JS is all cache'd and will never need to be downloaded again. Your device does the redirects, not me.
Theo solved this by doing all of the work client side. Once you've went to https://unduck.link once, the JS is all cache'd and will never need to be downloaded again. Your device does the redirects, not me.

3
docker/.env.example Normal file
View File

@ -0,0 +1,3 @@
PORT=5000
DOMAIN=bang.mydomain.com
NETWORK=bridge

33
docker/Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# Stage 1: Build the project
FROM node:18 AS builder
WORKDIR /app
# Copy package.json and pnpm-lock.yaml to the working directory
COPY package.json pnpm-lock.yaml ./
# Install dependencies
RUN npm install -g pnpm
RUN pnpm install
# Copy project files into the docker image
COPY . .
# Build the project
RUN pnpm run prebuild
RUN pnpm run build
# Stage 2: Serve the app using the same version of Node
FROM node:18-alpine
WORKDIR /app
# Install a simple http server
RUN npm install -g serve
# Copy built assets from the builder stage
COPY --from=builder /app/dist ./
# Expose port 5000 for the server
EXPOSE 5000
# Start the server using the `serve` package
CMD ["serve", "-s", ".", "-l", "5000"]

21
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,21 @@
services:
bang-web-server:
build:
context: ../
dockerfile: docker/Dockerfile
container_name: bang
hostname: bang.gib
domainname: bang.gbrown.org
networks:
- nginx-bridge
#ports:
#- 5000:5000
tty: true
restart: unless-stopped
volumes:
- ../:/app
command: serve -s /app/dist -l 5000
networks:
nginx-bridge:
external: true

51
docker/host-bang Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash
# Function to check if we're in the correct directory
check_directory() {
if [ -d "docker" ] && [ -f "package.json" ]; then
return 0 # We're in the root directory
fi
return 1
}
# Initialize root_dir
root_dir=""
# Check if argument is provided
if [ $# -eq 1 ]; then
# Use provided path
if [ -d "$1" ]; then
root_dir="$1"
cd "$root_dir" || exit 1
else
echo "Error: Provided directory does not exist"
exit 1
fi
else
# No argument provided, try to determine location
current_dir=$(basename "$(pwd)")
if [ "$current_dir" = "docker" ]; then
cd .. || exit 1
elif [ "$current_dir" != "Bang" ]; then
echo "Error: Not in the correct directory and no valid path provided"
echo "Please either:"
echo "1. Run this script from the Bang root directory"
echo "2. Run this script from the docker directory"
echo "3. Provide the path to the Bang root directory as an argument"
exit 1
fi
fi
# Verify we're in the correct directory
if ! check_directory; then
echo "Error: Not in the correct directory structure"
echo "Make sure you're in a directory with 'docker' folder and package.json"
exit 1
fi
pnpm install
pnpm build
cd docker || exit 1
sudo docker compose up -d
cd ..

52
docker/update-bang Executable file
View File

@ -0,0 +1,52 @@
#!/bin/bash
# Function to check if we're in the correct directory
check_directory() {
if [ -d "docker" ] && [ -f "package.json" ]; then
return 0 # We're in the root directory
fi
return 1
}
# Initialize root_dir
root_dir=""
# Check if argument is provided
if [ $# -eq 1 ]; then
# Use provided path
if [ -d "$1" ]; then
root_dir="$1"
cd "$root_dir" || exit 1
else
echo "Error: Provided directory does not exist"
exit 1
fi
else
# No argument provided, try to determine location
current_dir=$(basename "$(pwd)")
if [ "$current_dir" = "docker" ]; then
cd .. || exit 1
elif [ "$current_dir" != "Bang" ]; then
echo "Error: Not in the correct directory and no valid path provided"
echo "Please either:"
echo "1. Run this script from the Bang root directory"
echo "2. Run this script from the docker directory"
echo "3. Provide the path to the Bang root directory as an argument"
exit 1
fi
fi
# Verify we're in the correct directory
if ! check_directory; then
echo "Error: Not in the correct directory structure"
echo "Make sure you're in a directory with 'docker' folder and package.json"
exit 1
fi
git pull
pnpm build
cd docker || exit 1
sudo docker compose down
sudo docker compose up -d
cd ..

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/search.svg" />
<link rel="icon" type="image/svg+xml" href="/bang.svg" />
<link
rel="preconnect"
href="https://fonts.googleapis.com"
@ -24,13 +24,14 @@
media="print"
onload="this.media='all'"
/>
<script
defer
data-domain="unduck.link"
src="https://plausible.io/js/script.js"
></script>
<link
rel="search"
type="application/opensearchdescription+xml"
title="Bang!"
href="/opensearch.xml"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Unduck</title>
<title>Bang!</title>
<meta
name="description"
content="A better default search engine (with bangs!)"

View File

@ -1,15 +1,28 @@
{
"name": "unduck",
"name": "bang",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"generate-opensearch": "ts-node scripts/generateOpenSearch.ts",
"prebuild": "pnpm generate-opensearch",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "~5.7.2",
"vite": "^6.1.0"
"@types/node": "^22.15.17",
"ts-node": "^10.9.2",
"typescript": "~5.7.3",
"vite": "^6.3.5"
},
"dependencies": {
"vite-plugin-pwa": "^0.21.2"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
}
}

3825
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

53
public/bang.svg Normal file
View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#FFE36C;" points="81.13,29.99 62.79,30.17 78.47,21.62 74.76,8.38 85.17,16.28 95.64,8.27 92.89,19.05
110.15,21.03 92.19,28.03 85.03,40.53 "/>
<path style="fill:#FF8F00;" d="M92.27,13.37l-0.76,2.96l-1.13,4.45l4.56,0.52l6.94,0.79l-9.76,3.81l-1.32,0.51l-0.7,1.23
l-4.68,8.17l-1.92-5.2l-0.98-2.65L79.7,28l-8.91,0.09l7.24-3.95l2.8-1.53l-0.86-3.07l-1.67-5.96l4.44,3.37l2.43,1.84l2.42-1.85
L92.27,13.37 M85.16,13.76L72.49,4.15c-0.41-0.31-0.98,0.08-0.84,0.57l4.46,15.91l-19.44,10.6c-0.49,0.27-0.3,1.01,0.26,1
l22.81-0.23l4.5,12.17c0.16,0.43,0.74,0.47,0.97,0.08l8.37-14.62l22.82-8.9c0.52-0.2,0.42-0.97-0.13-1.03L95.4,17.32l3.22-12.65
c0.13-0.49-0.44-0.87-0.84-0.56L85.16,13.76z"/>
</g>
<path style="fill:#4D4D4D;" d="M94.59,44.29c-1.27-0.18-7.6,6.13-12.17,2.17c0,0-0.01,0.01-0.01,0.01
c-2.63-2.13-5.53-3.99-8.72-5.51c-21.75-10.37-47.8-1.15-58.18,20.61c-10.37,21.75-1.15,47.8,20.61,58.18
c21.75,10.37,47.8,1.15,58.18-20.61c3.37-7.06,4.67-14.57,4.14-21.85c-0.04-0.51-0.13-1.64,0.12-2.48c1.71-5.74,7.93-8.6,7.8-8.87
C106.23,65.66,95.87,44.47,94.59,44.29z"/>
<ellipse transform="matrix(0.8667 -0.4988 0.4988 0.8667 -14.1049 57.4504)" style="fill:#757575;" cx="100.46" cy="55.12" rx="5.83" ry="12.33"/>
<ellipse transform="matrix(0.866 -0.5 0.5 0.866 -13.8369 58.5564)" style="fill:#212121;" cx="102.35" cy="55.1" rx="2.29" ry="3.52"/>
<path style="opacity:0.9;fill:#757575;" d="M53.9,52.57c2.74,7.78-1.27,12.42-6.46,15.39c-2.48,1.42-5.36,2.04-7.87,3.41
c-4.88,2.66-7.84,7.79-12.09,11.36c-1.06,0.89-2.27,1.71-3.65,1.86c-1.84,0.2-3.64-0.88-4.74-2.37C18,80.72,13.53,66.5,29.85,51.95
C37.14,45.45,50.9,44.04,53.9,52.57z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="42.079" y1="34.5524" x2="83.8122" y2="48.0625">
<stop offset="0" style="stop-color:#FFF9C4;stop-opacity:0.1"/>
<stop offset="1.516624e-03" style="stop-color:#FFF9C4;stop-opacity:0.1007"/>
<stop offset="0.3781" style="stop-color:#FFFCA6;stop-opacity:0.2626"/>
<stop offset="0.7007" style="stop-color:#FFFE94;stop-opacity:0.4014"/>
<stop offset="0.93" style="stop-color:#FFFF8D;stop-opacity:0.5"/>
</linearGradient>
<path style="fill:url(#SVGID_1_);" d="M41.86,38.72c3.69,0.12,11.23,0.17,20.77,3.92c3.21,1.26,5.92,3.02,8.83,4.82
c2.35,1.45,5.02,2.66,7.77,2.4c0.97-0.09,2.04-0.47,2.42-1.37c0.23-0.56,0.21-1.13,0.01-1.69c-0.24-0.66-1.06-1.58-2.32-2.61
c-1.77-1.19-3.65-2.28-5.64-3.23C63.34,36.02,52.01,35.53,41.86,38.72z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="102.0288" y1="57.3021" x2="91.1394" y2="42.9353">
<stop offset="0.1096" style="stop-color:#FFF9C4;stop-opacity:0"/>
<stop offset="1" style="stop-color:#FFFF8D"/>
</linearGradient>
<path style="fill:url(#SVGID_2_);" d="M102.13,47.64c-0.05-0.05-0.1-0.11-0.15-0.16c-0.01-0.01-0.01-0.02-0.02-0.02
c-0.23-0.23-0.46-0.45-0.69-0.67c-0.03-0.03-0.07-0.06-0.1-0.1c-2.53-2.29-5.12-3.26-6.86-2.27c-2.79,1.6-2.3,7.69,1.1,13.59
c0.73,1.26,1.53,2.41,2.37,3.43c-0.1-0.29-3.84-11.52-1.51-14.53C98.26,44.37,101.78,47.34,102.13,47.64z"/>
<path style="fill:#212121;" d="M88.16,74.53c-2.42-1.59-4.79-3.75-6.26-9.18c-0.19-0.69-0.83-4.12,1.02-4.67
c1.6-0.48,2.79,2.15,4.17,4.46c1.5,2.53,3.93,4.72,6.54,6.15c0.76,0.41,4.29,2,1.97,4.22C94.07,76.96,89.58,75.47,88.16,74.53z"/>
<g>
<path style="fill:#C69461;" d="M100.85,53.44c0.1-0.04,10.25-4.65,9.37-11.34c-0.58-4.42-2.3-6.62-11.4-8.78
c-8.9-2.11-13.25-6.24-13.43-6.42c-0.89-0.86-0.92-2.29-0.05-3.19c0.86-0.89,2.28-0.92,3.18-0.06c0.07,0.06,3.77,3.48,11.35,5.28
c8.82,2.09,13.79,4.72,14.83,12.57c1.16,8.78-8.52,14.25-11.29,15.7C101.69,58.11,99.71,53.94,100.85,53.44z"/>
</g>
<path style="fill:#F6BC41;" d="M88.5,23.66c-0.89-0.86-2.32-0.83-3.18,0.06c-0.86,0.9-0.84,2.32,0.05,3.19
c0.13,0.13,2.5,2.37,7.25,4.39l0.95-1.66l3.66-1.43C91.42,26.35,88.56,23.71,88.5,23.66z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

1
public/chrome.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48" height="48" width="48"><defs><linearGradient id="a" x1="3.2173" y1="15" x2="44.7812" y2="15" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#d93025"/><stop offset="1" stop-color="#ea4335"/></linearGradient><linearGradient id="b" x1="20.7219" y1="47.6791" x2="41.5039" y2="11.6837" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fcc934"/><stop offset="1" stop-color="#fbbc04"/></linearGradient><linearGradient id="c" x1="26.5981" y1="46.5015" x2="5.8161" y2="10.506" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1e8e3e"/><stop offset="1" stop-color="#34a853"/></linearGradient></defs><circle cx="24" cy="23.9947" r="12" style="fill:#fff"/><path d="M3.2154,36A24,24,0,1,0,12,3.2154,24,24,0,0,0,3.2154,36ZM34.3923,18A12,12,0,1,1,18,13.6077,12,12,0,0,1,34.3923,18Z" style="fill:none"/><path d="M24,12H44.7812a23.9939,23.9939,0,0,0-41.5639.0029L13.6079,30l.0093-.0024A11.9852,11.9852,0,0,1,24,12Z" style="fill:url(#a)"/><circle cx="24" cy="24" r="9.5" style="fill:#1a73e8"/><path d="M34.3913,30.0029,24.0007,48A23.994,23.994,0,0,0,44.78,12.0031H23.9989l-.0025.0093A11.985,11.985,0,0,1,34.3913,30.0029Z" style="fill:url(#b)"/><path d="M13.6086,30.0031,3.218,12.006A23.994,23.994,0,0,0,24.0025,48L34.3931,30.0029l-.0067-.0068a11.9852,11.9852,0,0,1-20.7778.007Z" style="fill:url(#c)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#bbb" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clipboard"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/></svg>

Before

Width:  |  Height:  |  Size: 357 B

After

Width:  |  Height:  |  Size: 350 B

120
public/firefox.svg Normal file
View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg width="77.42" height="79.97" version="1.1" viewBox="0 0 77.42 79.97" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<title>Firefox Browser logo</title>
<defs>
<linearGradient id="a" x1="70.79" x2="6.447" y1="12.39" y2="74.47" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".048"/>
<stop stop-color="#ffe847" offset=".111"/>
<stop stop-color="#ffc830" offset=".225"/>
<stop stop-color="#ff980e" offset=".368"/>
<stop stop-color="#ff8b16" offset=".401"/>
<stop stop-color="#ff672a" offset=".462"/>
<stop stop-color="#ff3647" offset=".534"/>
<stop stop-color="#e31587" offset=".705"/>
</linearGradient>
<radialGradient id="b" cx="-7907" cy="-8515" r="80.8" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffbd4f" offset=".129"/>
<stop stop-color="#ffac31" offset=".186"/>
<stop stop-color="#ff9d17" offset=".247"/>
<stop stop-color="#ff980e" offset=".283"/>
<stop stop-color="#ff563b" offset=".403"/>
<stop stop-color="#ff3750" offset=".467"/>
<stop stop-color="#f5156c" offset=".71"/>
<stop stop-color="#eb0878" offset=".782"/>
<stop stop-color="#e50080" offset=".86"/>
</radialGradient>
<radialGradient id="c" cx="-7937" cy="-8482" r="80.8" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#960e18" offset=".3"/>
<stop stop-color="#b11927" stop-opacity=".74" offset=".351"/>
<stop stop-color="#db293d" stop-opacity=".343" offset=".435"/>
<stop stop-color="#f5334b" stop-opacity=".094" offset=".497"/>
<stop stop-color="#ff3750" stop-opacity="0" offset=".53"/>
</radialGradient>
<radialGradient id="d" cx="-7927" cy="-8533" r="58.53" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".132"/>
<stop stop-color="#ffdc3e" offset=".252"/>
<stop stop-color="#ff9d12" offset=".506"/>
<stop stop-color="#ff980e" offset=".526"/>
</radialGradient>
<radialGradient id="e" cx="-7946" cy="-8461" r="38.47" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#3a8ee6" offset=".353"/>
<stop stop-color="#5c79f0" offset=".472"/>
<stop stop-color="#9059ff" offset=".669"/>
<stop stop-color="#c139e6" offset="1"/>
</radialGradient>
<radialGradient id="f" cx="-7936" cy="-8492" r="20.4" gradientTransform="matrix(.972 -.235 .275 1.138 10090 7834)" gradientUnits="userSpaceOnUse">
<stop stop-color="#9059ff" stop-opacity="0" offset=".206"/>
<stop stop-color="#8c4ff3" stop-opacity=".064" offset=".278"/>
<stop stop-color="#7716a8" stop-opacity=".45" offset=".747"/>
<stop stop-color="#6e008b" stop-opacity=".6" offset=".975"/>
</radialGradient>
<radialGradient id="g" cx="-7938" cy="-8518" r="27.68" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffe226" offset="0"/>
<stop stop-color="#ffdb27" offset=".121"/>
<stop stop-color="#ffc82a" offset=".295"/>
<stop stop-color="#ffa930" offset=".502"/>
<stop stop-color="#ff7e37" offset=".732"/>
<stop stop-color="#ff7139" offset=".792"/>
</radialGradient>
<radialGradient id="h" cx="-7916" cy="-8536" r="118.1" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".113"/>
<stop stop-color="#ff980e" offset=".456"/>
<stop stop-color="#ff5634" offset=".622"/>
<stop stop-color="#ff3647" offset=".716"/>
<stop stop-color="#e31587" offset=".904"/>
</radialGradient>
<radialGradient id="i" cx="-7927" cy="-8523" r="86.5" gradientTransform="matrix(.105 .995 -.653 .069 -4685 8470)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset="0"/>
<stop stop-color="#ffe847" offset=".06"/>
<stop stop-color="#ffc830" offset=".168"/>
<stop stop-color="#ff980e" offset=".304"/>
<stop stop-color="#ff8b16" offset=".356"/>
<stop stop-color="#ff672a" offset=".455"/>
<stop stop-color="#ff3647" offset=".57"/>
<stop stop-color="#e31587" offset=".737"/>
</radialGradient>
<radialGradient id="j" cx="-7938" cy="-8508" r="73.72" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".137"/>
<stop stop-color="#ff980e" offset=".48"/>
<stop stop-color="#ff5634" offset=".592"/>
<stop stop-color="#ff3647" offset=".655"/>
<stop stop-color="#e31587" offset=".904"/>
</radialGradient>
<radialGradient id="k" cx="-7919" cy="-8504" r="80.69" gradientTransform="translate(7974,8524)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" offset=".094"/>
<stop stop-color="#ffe141" offset=".231"/>
<stop stop-color="#ffaf1e" offset=".509"/>
<stop stop-color="#ff980e" offset=".626"/>
</radialGradient>
<linearGradient id="l" x1="70.01" x2="15.27" y1="12.06" y2="66.81" gradientTransform="translate(-1.3 -.004086)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fff44f" stop-opacity=".8" offset=".167"/>
<stop stop-color="#fff44f" stop-opacity=".634" offset=".266"/>
<stop stop-color="#fff44f" stop-opacity=".217" offset=".489"/>
<stop stop-color="#fff44f" stop-opacity="0" offset=".6"/>
</linearGradient>
</defs>
<g transform="matrix(.9819843 0 0 .9819843 .6974849 .7199239)">
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53a19.39 19.39 0 0 0-10.67 4.111 11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.336 2.049-0.381 2.42 0 0.029-6e-3 0.056-9e-3 0.085a36.94 36.94 0 0 0-0.629 5.343v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-44.67 30.34c0.181 0.087 0.351 0.181 0.537 0.264l0.027 0.017q-0.282-0.135-0.564-0.281zm8.878-23.38m31.95-4.934v-0.037l7e-3 0.041z" fill="url(#a)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" fill="url(#b)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76v0.037l7e-3 0.041a35.1 35.1 0 0 1-1.206 26.16c-4.442 9.531-15.19 19.3-32.02 18.82-18.18-0.515-34.2-14.01-37.19-31.68-0.545-2.787 0-4.2 0.274-6.465a28.88 28.88 0 0 0-0.623 5.348v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47z" fill="url(#c)"/>
<path d="m55.78 31.38c0.084 0.059 0.162 0.118 0.241 0.177a21.1 21.1 0 0 0-3.6-4.695c-12.05-12.05-3.157-26.12-1.658-26.84l0.015-0.022c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917z" fill="url(#d)"/>
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" fill="url(#e)"/>
<path d="m38.82 33.79c-0.064 0.964-3.47 4.289-4.661 4.289-11.02 0-12.81 6.667-12.81 6.667 0.488 5.614 4.4 10.24 9.129 12.68 0.216 0.112 0.435 0.213 0.654 0.312q0.569 0.252 1.138 0.466a17.24 17.24 0 0 0 5.043 0.973c19.32 0.906 23.06-23.1 9.119-30.07a13.38 13.38 0 0 1 9.345 2.269 19.56 19.56 0 0 0-16.98-9.917c-0.46 0-0.91 0.038-1.362 0.069a19.39 19.39 0 0 0-10.67 4.111c0.591 0.5 1.258 1.168 2.663 2.553 2.63 2.591 9.375 5.275 9.39 5.59z" fill="url(#f)"/>
<path d="m24.96 24.36c0.314 0.2 0.573 0.374 0.8 0.531a17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21c0.189-5e-3 5.811-0.106 8.638 1.732z" fill="url(#g)"/>
<path d="m0.354 42.16c2.991 17.67 19.01 31.17 37.19 31.68 16.83 0.476 27.58-9.294 32.02-18.82a35.1 35.1 0 0 0 1.206-26.16v-0.037c0-0.029-6e-3 -0.046 0-0.037l7e-3 0.065c1.375 8.977-3.191 17.67-10.33 23.56l-0.022 0.05c-13.91 11.33-27.22 6.834-29.91 5q-0.282-0.135-0.564-0.281c-8.109-3.876-11.46-11.26-10.74-17.6a9.953 9.953 0 0 1-9.181-5.775 14.62 14.62 0 0 1 14.25-0.572 19.3 19.3 0 0 0 14.55 0.572c-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758c-0.23-0.157-0.489-0.327-0.8-0.531-2.827-1.838-8.449-1.737-8.635-1.732h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.638 4.337v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74c-0.019 0.079-1.396 6.099-0.717 9.221z" fill="url(#h)"/>
<path d="m52.42 26.86a21.1 21.1 0 0 1 3.6 4.7c0.213 0.161 0.412 0.321 0.581 0.476 8.787 8.1 4.183 19.55 3.84 20.36 7.138-5.881 11.7-14.58 10.33-23.56-4.384-10.93-11.82-15.34-17.88-24.93-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015c-1.499 0.711-10.39 14.79 1.66 26.83z" fill="url(#i)"/>
<path d="m56.6 32.04c-0.169-0.155-0.368-0.315-0.581-0.476-0.079-0.059-0.157-0.118-0.241-0.177a13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017c2.694 1.839 16 6.332 29.91-5l0.022-0.05c0.347-0.81 4.951-12.26-3.84-20.36z" fill="url(#j)"/>
<path d="m21.35 44.74s1.789-6.667 12.81-6.667c1.191 0 4.6-3.325 4.661-4.289a19.3 19.3 0 0 1-14.55-0.572 14.62 14.62 0 0 0-14.25 0.572 9.953 9.953 0 0 0 9.181 5.775c-0.718 6.337 2.632 13.72 10.74 17.6 0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.445-8.641-7.069-9.129-12.68z" fill="url(#k)"/>
<path d="m74.62 26.83c-1.684-4.052-5.1-8.427-7.775-9.81a40.27 40.27 0 0 1 3.925 11.76l7e-3 0.065c-4.382-10.92-11.81-15.33-17.88-24.92-0.307-0.485-0.614-0.971-0.913-1.484-0.171-0.293-0.308-0.557-0.427-0.8a7.053 7.053 0 0 1-0.578-1.535 0.1 0.1 0 0 0-0.088-0.1 0.138 0.138 0 0 0-0.073 0c-5e-3 0-0.013 9e-3 -0.019 0.011s-0.019 0.011-0.028 0.015l0.015-0.026c-9.735 5.7-13.04 16.25-13.34 21.53 0.452-0.031 0.9-0.069 1.362-0.069a19.56 19.56 0 0 1 16.98 9.917 13.38 13.38 0 0 0-9.345-2.269c13.94 6.97 10.2 30.97-9.119 30.07a17.24 17.24 0 0 1-5.043-0.973q-0.569-0.213-1.138-0.466c-0.219-0.1-0.438-0.2-0.654-0.312l0.027 0.017q-0.282-0.135-0.564-0.281c0.181 0.087 0.351 0.181 0.537 0.264-4.733-2.446-8.641-7.07-9.129-12.68 0 0 1.789-6.667 12.81-6.667 1.191 0 4.6-3.325 4.661-4.289-0.015-0.315-6.76-3-9.39-5.59-1.405-1.385-2.072-2.052-2.663-2.553a11.59 11.59 0 0 0-1-0.758 17.97 17.97 0 0 1-0.109-9.473 28.7 28.7 0 0 0-9.329 7.21h-0.018c-1.536-1.947-1.428-8.367-1.34-9.708a6.928 6.928 0 0 0-1.294 0.687 28.22 28.22 0 0 0-3.788 3.245 33.84 33.84 0 0 0-3.623 4.347v6e-3 -7e-3a32.73 32.73 0 0 0-5.2 11.74l-0.052 0.256c-0.073 0.341-0.4 2.073-0.447 2.445a45.09 45.09 0 0 0-0.572 5.403v0.2a38.76 38.76 0 0 0 76.95 6.554c0.065-0.5 0.118-0.995 0.176-1.5a39.86 39.86 0 0 0-2.514-19.47zm-3.845 1.991 7e-3 0.041z" fill="url(#l)"/>
</g>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:title>Firefox Browser logo</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

10
public/opensearch.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Bang!</ShortName>
<Description>A better default search engine with bangs</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/svg+xml">https://bang.gbrown.org/bang.svg</Image>
<Url type="text/html" method="get" template="https://bang.gbrown.org?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="https://bang.gbrown.org/opensearch.xml"/>
<moz:SearchForm xmlns:moz="http://www.mozilla.org/2006/browser/search/">https://bang.gbrown.org</moz:SearchForm>
</OpenSearchDescription>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200pt" height="1200pt" version="1.1" viewBox="0 0 1200 1200" xmlns="http://www.w3.org/2000/svg">
<rect width="1200" height="1200" fill="black"/>
<path fill="white" d="m980.48 905.76-198.84-199.08c89.203-125.34 77.953-300.14-34.406-412.64-125.26-125.44-328.03-125.44-453.32 0-125.26 125.44-125.26 328.45 0 453.84 112.36 112.5 286.97 123.74 412.18 34.406l198.84 199.08c53.25 53.297 132.05-19.078 75.562-75.609zm-610.87-233.58c-83.391-83.531-83.391-218.86 0-302.39s218.58-83.531 302.02 0c83.391 83.531 83.391 218.86 0 302.39-83.438 83.484-218.63 83.484-302.02 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 624 B

98
scripts/files_to_clipboard Executable file
View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
import os
import sys
import argparse
from pathlib import Path
import pyperclip
import questionary
# List of directories to exclude
EXCLUDED_DIRS = {'node_modules', '.next', '.venv', '.git', '__pycache__', '.idea', '.vscode', 'ui'}
def collect_files(project_path):
"""
Collects files from the project directory, excluding specified directories and filtering by extensions.
Returns a list of file paths relative to the project directory.
"""
collected_files = []
for root, dirs, files in os.walk(project_path):
# Exclude specified directories
dirs[:] = [d for d in dirs if d not in EXCLUDED_DIRS]
for file in files:
file_path = Path(root) / file
relative_path = file_path.relative_to(project_path)
collected_files.append(relative_path)
return collected_files
def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(description='Generate Markdown from selected files.')
parser.add_argument('path', nargs='?', default='.', help='Path to the project directory')
args = parser.parse_args()
project_path = Path(args.path).resolve()
if not project_path.is_dir():
print(f"Error: '{project_path}' is not a directory.")
sys.exit(1)
# Collect files from the project directory
file_list = collect_files(project_path)
if not file_list:
print("No files found in the project directory with the specified extensions.")
sys.exit(1)
# Sort file_list for better organization
file_list.sort()
# Interactive file selection using questionary
print("\nSelect the files you want to include:")
selected_files = questionary.checkbox(
"Press space to select files, and Enter when you're done:",
choices=[str(f) for f in file_list]
).ask()
if not selected_files:
print("No files selected.")
sys.exit(1)
# Generate markdown
markdown_lines = []
markdown_lines.append('')
for selected_file in selected_files:
file_path = project_path / selected_file
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Determine the language for code block from file extension
language = file_path.suffix.lstrip('.')
markdown_lines.append(f'{selected_file}')
markdown_lines.append(f'```{language}')
markdown_lines.append(content)
markdown_lines.append('```')
markdown_lines.append('')
except Exception as e:
print(f"Error reading file {selected_file}: {e}")
# Copy markdown content to clipboard
markdown_text = '\n'.join(markdown_lines)
pyperclip.copy(markdown_text)
print("Markdown content has been copied to the clipboard.")
if __name__ == "__main__":
# Check if required libraries are installed
try:
import questionary
import pyperclip
except ImportError as e:
missing_module = e.name
print(f"Error: Missing required module '{missing_module}'.")
print(f"Please install it by running: pip install {missing_module}")
sys.exit(1)
main()

View File

@ -0,0 +1,33 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
// Get the current file's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get the Bang URL from environment or use a default for local development
const bangUrl = process.env.VITE_BANG_URL || 'https://bang.gbrown.org';
// Create the OpenSearch XML content
const openSearchXml = `<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>Bang!</ShortName>
<Description>A better default search engine with bangs</Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/svg+xml">${bangUrl}/bang.svg</Image>
<Url type="text/html" method="get" template="${bangUrl}?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="${bangUrl}/opensearch.xml"/>
<moz:SearchForm xmlns:moz="http://www.mozilla.org/2006/browser/search/">${bangUrl}</moz:SearchForm>
</OpenSearchDescription>`;
// Ensure the public directory exists
const publicDir = path.resolve(__dirname, '../public');
if (!fs.existsSync(publicDir)) {
fs.mkdirSync(publicDir, { recursive: true });
}
// Write the OpenSearch XML file
fs.writeFileSync(path.join(publicDir, 'opensearch.xml'), openSearchXml);
console.log('OpenSearch XML file generated successfully!');

View File

@ -1,6 +1,191 @@
// This file was (mostly) ripped from https://duckduckgo.com/bang.js
const SEARXNG_URL = import.meta.env.VITE_SEARXNG_URL as string;
const GITEA_URL = import.meta.env.VITE_GITEA_URL as string;
const OPENWEBUI_URL = import.meta.env.VITE_OPENWEBUI_URL as string;
const PLEX_URL = import.meta.env.VITE_PLEX_URL as string;
const OVERSEERR_URL = import.meta.env.VITE_OVERSEERR_URL as string;
const SONARR_URL = import.meta.env.VITE_SONARR_URL as string;
const RADARR_URL = import.meta.env.VITE_RADARR_URL as string;
const LIDARR_URL = import.meta.env.VITE_LIDARR_URL as string;
const LLAMA_MODEL = import.meta.env.VITE_LLAMA_MODEL.replace(":", "%3A") as string;
const DEEPSEEK_MODEL = import.meta.env.VITE_DEEPSEEK_MODEL.replace(":", "%3A") as string;
const OPENAI_MODEL = import.meta.env.VITE_OPENAI_MODEL as string;
const CLAUDE_MODEL = import.meta.env.VITE_CLAUDE_MODEL.replace("/", "%2F") as string;
export const bangs = [
{
c: "Online Services",
d: SEARXNG_URL,
r: 0,
s: "Searxng",
sc: "Search",
t: "s",
u: `${SEARXNG_URL}/?q={{{s}}}`,
},
{
c: "Tech",
d: GITEA_URL,
r: 0,
s: "Gitea",
sc: "Cryptocurrency",
t: "tea",
u: `${GITEA_URL}/?repo-search-query={{{s}}}`,
},
{
c: "Entertainment",
d: PLEX_URL,
r: 0,
s: "Plex",
sc: "Movies",
t: "plex",
u: `${PLEX_URL}/web/index.html#!/search?pivot=top&query={{{s}}}`,
},
{
c: "Entertainment",
d: OVERSEERR_URL,
r: 0,
s: "Overseerr",
sc: "Movies",
t: "ov",
u: `${OVERSEERR_URL}/search?query={{{s}}}`,
},
{
c: "Entertainment",
d: LIDARR_URL,
r: 0,
s: "Lidarr",
sc: "Music",
t: "mp3",
u: `${LIDARR_URL}/add/new?term={{{s}}}`,
},
{
c: "Entertainment",
d: SONARR_URL,
r: 0,
s: "Sonarr",
sc: "TV",
t: "tv",
u: `${SONARR_URL}/add/new?term={{{s}}}`,
},
{
c: "Entertainment",
d: RADARR_URL,
r: 0,
s: "Radarr",
sc: "Movies",
t: "mv",
u: `${RADARR_URL}/add/new?term={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "ai",
u: `${OPENWEBUI_URL}/?q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "c",
u: `${OPENWEBUI_URL}/?q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "llama",
u: `${OPENWEBUI_URL}/?models=${LLAMA_MODEL}&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "r1",
u: `${OPENWEBUI_URL}/?models=${DEEPSEEK_MODEL}&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "r1-7b",
u: `${OPENWEBUI_URL}/?models=deepseek-r1%3A7b&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "r1-32b",
u: `${OPENWEBUI_URL}/?models=deepseek-r1%3A32b&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "claude",
u: `${OPENWEBUI_URL}/?models=${CLAUDE_MODEL}&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "openai",
u: `${OPENWEBUI_URL}/?models=${OPENAI_MODEL}&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "o3-mini",
u: `${OPENWEBUI_URL}/?models=o3-mini&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "o1",
u: `${OPENWEBUI_URL}/?models=o1&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "4o-mini",
u: `${OPENWEBUI_URL}/?models=gpt-4o-mini&q={{{s}}}`,
},
{
c: "AI",
d: OPENWEBUI_URL,
r: 0,
s: "OpenWebUI",
sc: "AI",
t: "4o",
u: `${OPENWEBUI_URL}/?models=gpt-4o&q={{{s}}}`,
},
{
c: "AI",
d: "www.t3.chat",
@ -10,24 +195,6 @@ export const bangs = [
t: "t3",
u: "https://www.t3.chat/new?q={{{s}}}",
},
{
c: "Tech",
d: "www.01net.com",
r: 4,
s: "01net",
sc: "Downloads (apps)",
t: "01net",
u: "http://www.01net.com/recherche/recherche.php?searchstring={{{s}}}&chaine=home",
},
{
c: "Entertainment",
d: "www.bmw2002faq.com",
r: 0,
s: "BMW 2002 FAQ",
sc: "Forum",
t: "02faq",
u: "https://www.bmw2002faq.com/search/?q={{{s}}}",
},
{
c: "Entertainment",
d: "boxofficemojo.com",
@ -3184,7 +3351,7 @@ export const bangs = [
r: 8351,
s: "Duck.ai",
sc: "Reference",
t: "ai",
t: "ddgai",
u: "https://www.duckduckgo.com/?q={{{s}}}&ia=chat&bang=true ",
},
{
@ -15988,7 +16155,7 @@ export const bangs = [
r: 165,
s: "CNET",
sc: "Downloads",
t: "c",
t: "cnet",
u: "http://www.cnet.com/1770-5_1-0.html?query={{{s}}}",
},
{
@ -71306,7 +71473,7 @@ export const bangs = [
r: 10,
s: "Myvideo",
sc: "Video",
t: "mv",
t: "myvideo",
u: "http://www.myvideo.de/Videos_A-Z?searchWord={{{s}}}",
},
{
@ -73419,8 +73586,8 @@ export const bangs = [
r: 0,
s: "Nextcloud App Store",
sc: "Sysadmin",
t: "nextcloudapp",
u: "https://apps.nextcloud.com/?search={{{s}}}",
t: "cloudapp",
u: "https://apps.nextcloud.com/search/?q={{{s}}}",
},
{
c: "Entertainment",
@ -78745,7 +78912,7 @@ export const bangs = [
r: 0,
s: "StackOverflow",
sc: "Programming",
t: "ov",
t: "ovfl",
u: "http://stackoverflow.com/search?q={{{s}}}",
},
{
@ -93872,7 +94039,7 @@ export const bangs = [
r: 46,
s: "Sourcegraph",
sc: "Programming",
t: "sg",
t: "srcg",
u: "https://sourcegraph.com/search?q={{{s}}}",
},
{
@ -98325,7 +98492,7 @@ export const bangs = [
r: 12469,
s: "startpage.com",
sc: "Search",
t: "s",
t: "startpage",
u: "http://startpage.com/do/metasearch.pl?query={{{s}}}",
},
{
@ -106698,15 +106865,6 @@ export const bangs = [
t: "tva",
u: "https://tweakers.net/aanbod/zoeken/?keyword={{{s}}}",
},
{
c: "Entertainment",
d: "www.tv.com",
r: 0,
s: "TV.com",
sc: "TV",
t: "tvcom",
u: "http://www.tv.com/search?q={{{s}}}",
},
{
c: "Entertainment",
d: "www.thetvdb.com",
@ -106866,8 +107024,8 @@ export const bangs = [
r: 31,
s: "tv.com",
sc: "TV",
t: "tv",
u: "http://www.tv.com/search?q= {{{s}}}",
t: "tvcom",
u: "http://www.tv.com/search?q={{{s}}}",
},
{
c: "Shopping",

View File

@ -1,13 +1,10 @@
/* @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); */
/* Font fallback that closely matches Inter metrics */
/* --- Import and fonts unchanged --- */
@font-face {
font-family: "Inter Fallback";
size-adjust: 107%;
ascent-override: 90%;
src: local("Arial");
}
:root {
font-family:
Inter,
@ -27,27 +24,41 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* --- Dark mode color variables --- */
--bg-color: #121212; /* Dark background */
--text-color: #e2e2e2; /* Light text */
--input-bg-color: #1e1e1e; /* Dark input background */
--input-border-color: #333; /* Darker border for inputs */
--accent-color: #bbb; /* Accent color for links/buttons */
--hover-bg-color: #2c2c2c; /* Hover state background color */
--active-bg-color: #3a3a3a; /* Active state background color */
--success-color: #28a745; /* Green for copy-success feedback */
}
/* --- Normalized layout reset stays the same --- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
width: 100%;
background-color: var(--bg-color);
}
/* --- Modified to Dark mode --- */
body {
line-height: 1.5;
font-weight: 400;
font-size: 16px;
color: #1a1a1a;
background-color: var(--bg-color); /* Changed: Added dark background */
color: var(--text-color); /* Changed: Light text color */
}
/* Header and fonts remain unchanged (already good contrast with dark mode) */
h1,
h2,
h3,
@ -58,26 +69,43 @@ h6 {
line-height: 1.2;
}
/* --- Link colors modified for contrast --- */
a {
color: #444444;
color: var(--accent-color); /* Changed: Lighter accent/link color */
}
a:hover {
color: #888888;
color: white; /* Changed: Bright when hovered */
}
/* --- Basic element reset unchanged --- */
button {
font: inherit;
border: none;
background: none;
cursor: pointer;
}
input,
textarea {
font: inherit;
}
.bang-title {
margin-bottom: 16px;
}
/* Add these new styles */
/* --- Container layout unchanged --- */
.main-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px 0px;
}
.content-container {
max-width: 36rem;
text-align: center;
padding: 0 8px;
}
.url-container {
display: flex;
align-items: center;
@ -85,65 +113,112 @@ textarea {
margin-top: 16px;
}
/* Add this new style */
.content-container {
max-width: 36rem;
/* --- URL input element styling adjustments --- */
.url-input {
padding: 8px 12px;
border: 1px solid var(--input-border-color); /* Changed: Darker border */
border-radius: 4px;
width: 100%;
background: var(--input-bg-color); /* Changed: Dark input bg */
color: var(--text-color); /* Added: Input text color */
text-align: center;
}
/* Update url-input width to be 100% since container will control max width */
.url-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
width: 100%;
background: #f5f5f5;
}
.copy-button {
padding: 8px;
color: #666;
border-radius: 4px;
/* --- Copy-button style adjustments --- */
.copy-button, .copy-firefox, .copy-chrome {
padding: 6px;
color: var(--accent-color); /* Changed: Accent color instead of gray */
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.copy-button:hover {
background: #f0f0f0;
.copy-button:hover, .copy-firefox:hover, .copy-chrome:hover {
background: var(--hover-bg-color); /* Changed: Hover bg darker */
border-radius: 8px;
}
.copy-button:active {
background: #e5e5e5;
.copy-button:active, .copy-firefox:active, .copy-chrome:active {
background: var(--active-bg-color); /* Changed: Active state even darker */
}
.copy-button img {
.copy-button img, .copy-firefox img, .copy-chrome img {
width: 20px;
height: 20px;
}
.copy-button.copied {
background: #28a745;
/* Copy feedback unchanged (Green stands out in dark mode as well) */
.copy-button.copied, .copy-firefox.copied, .copy-chrome.copied {
background: var(--success-color);
color: white; /* Ensures contrast */
}
/* Add footer styles */
.footer {
position: fixed;
bottom: 16px;
left: 0;
right: 0;
text-align: center;
.copy-textbox, .firefox-textbox, .chrome-textbox {
padding: 6px 10px;
border: 1px solid var(--input-border-color);
border-radius: 4px;
background: var(--input-bg-color);
color: var(--text-color);
font-size: 14px;
color: #666;
width: 220px;
cursor: pointer;
}
.footer a {
color: #666;
text-decoration: none;
font-weight: 500;
.copy-textbox:focus, .firefox-textbox:focus, .chrome-textbox:focus {
outline: none;
border-color: var(--accent-color);
}
.footer a:hover {
color: #333;
.settings-title {
margin-top: 36px;
margin-bottom: 16px;
font-size: 20px;
}
.settings-links-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 24px;
}
.browser-link-container {
display: flex;
align-items: center;
gap: 8px;
}
.browser-instruction {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: normal;
}
.form-table {
font-size: 14px;
margin: 16px auto;
text-align: left;
}
.form-table td {
padding: 8px 6px;
border: 1px solid var(--input-border-color);
}
.form-table td:first-child {
background-color: var(--input-bg-color);
width: 100px;
}
/* --- Mobile --- */
@media (max-width: 500px) {
.settings-links-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 32px;
}
.main-container {
justify-content: flex-start;
padding: 40px;
}
}

View File

@ -1,51 +1,122 @@
import { bangs } from "./bang";
import "./global.css";
const defaultBang = bangs.find((b) => b.t === "g");
function noSearchDefaultPageRender() {
const app = document.querySelector<HTMLDivElement>("#app")!;
app.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh;">
<main class="main-container">
<div class="content-container">
<h1>Unduck</h1>
<p>DuckDuckGo's bang redirects are too slow. Add the following URL as a custom search engine to your browser. Enables <a href="https://duckduckgo.com/bang.html" target="_blank">all of DuckDuckGo's bangs.</a></p>
<h1 class="bang-title">💣 Bang!</h1>
<p>
Add the following URL as a custom search engine to your browser in order to enable
<a href="https://duckduckgo.com/bang.html" target="_blank">
all of DuckDuckGo's bangs
</a>
right from your browser's search bar!
</p>
<div class="url-container">
<input
type="text"
class="url-input"
value="https://unduck.link?q=%s"
value="${import.meta.env.VITE_BANG_URL}?q=%s"
readonly
/>
<button class="copy-button">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<h3 class="settings-title">How to add Bang! Search Engine</h3>
<p style="margin-bottom: 8px; font-size: 14px;">
Below are links to the search engine settings in your browser.
<br />
Copy the URL for the browser engine you are using & paste it into a new tab.
<br />
From there you should see an option to add a search engine. Copy the link above
& fill out the form as follows.
</p>
<table class="form-table">
<tr>
<td><b>Name:</b></td>
<td>Bang!</td>
</tr>
<tr>
<td><b>Engine URL:</b></td>
<td>${import.meta.env.VITE_BANG_URL}?q=%s</td>
</tr>
<tr>
<td><b>Alias:</b></td>
<td>@bang</td>
</tr>
</table>
<div class="settings-links-container">
<div class="browser-link-container">
<img src="/firefox.svg" alt="Firefox" width="24" />
<input
type="text"
readonly value="about:preferences#search"
class="firefox-textbox"
title="Click to copy Firefox settings URL"
/>
<button class="copy-firefox">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
<div class="browser-link-container">
<img src="/chrome.svg" alt="Chrome" width="24" />
<input
type="text"
readonly value="chrome://settings/searchEngines"
class="chrome-textbox"
title="Click to copy Chrome settings URL"
/>
<button class="copy-chrome">
<img src="/clipboard.svg" alt="Copy" />
</button>
</div>
</div>
</div>
<footer class="footer">
<a href="https://t3.chat" target="_blank">t3.chat</a>
<a href="https://x.com/theo" target="_blank">theo</a>
<a href="https://github.com/t3dotgg/unduck" target="_blank">github</a>
</footer>
</div>
</main>
`;
const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
const copyIcon = copyButton.querySelector("img")!;
const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
const copyFirefox = app.querySelector<HTMLInputElement>(".copy-firefox")!;
const copyFirefoxIcon = copyFirefox.querySelector("img")!;
const firefoxInput = app.querySelector<HTMLInputElement>(".firefox-textbox")!;
const copyChrome = app.querySelector<HTMLInputElement>(".copy-chrome")!;
const copyChromeIcon = copyChrome.querySelector("img")!;
const chromeInput = app.querySelector<HTMLInputElement>(".chrome-textbox")!;
copyButton.addEventListener("click", async () => {
await navigator.clipboard.writeText(urlInput.value);
copyIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyIcon.src = "/clipboard.svg";
}, 2000);
});
copyFirefox.addEventListener("click", async () => {
await navigator.clipboard.writeText(firefoxInput.value);
copyFirefoxIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyFirefoxIcon.src = "/clipboard.svg";
}, 2000);
});
copyChrome.addEventListener("click", async () => {
await navigator.clipboard.writeText(chromeInput.value);
copyChromeIcon.src = "/clipboard-check.svg";
setTimeout(() => {
copyChromeIcon.src = "/clipboard.svg";
}, 2000);
});
}
const envDefaultBang = import.meta.env.VITE_DEFAULT_BANG ?? "g";
const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? envDefaultBang;
const defaultBang = bangs.find((b) => b.t === LS_DEFAULT_BANG);
function getBangredirectUrl() {
const url = new URL(window.location.href);
const query = url.searchParams.get("q")?.trim() ?? "";
@ -54,19 +125,20 @@ function getBangredirectUrl() {
return null;
}
const match = query.match(/!([a-z0-9]+)/i);
const match = query.match(/!(\S+)/i);
const bangCandidate = match?.[1]?.toLowerCase();
const selectedBang = bangs.find((b) => b.t === bangCandidate) ?? defaultBang;
// Remove the first bang from the query
const cleanQuery = query.replace(/![a-z0-9]+\s*/i, "").trim();
const cleanQuery = query.replace(/!\S+\s*/i, "").trim();
// Format of the url is:
// https://www.google.com/search?q={{{s}}}
const searchUrl = selectedBang?.u.replace(
"{{{s}}}",
encodeURIComponent(cleanQuery)
// Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
encodeURIComponent(cleanQuery).replace(/%2F/g, "/")
);
if (!searchUrl) return null;

10
vite.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
export default defineConfig({
plugins: [
VitePWA({
registerType: "autoUpdate",
}),
],
});