This commit is contained in:
commit
9fb7400ef8
14 changed files with 127323 additions and 0 deletions
37
.forgejo/workflows/docker.yml
Normal file
37
.forgejo/workflows/docker.yml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: code.threepop.com
|
||||||
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: nixos-x86_64-linux
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.pat }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
|
||||||
|
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM node:latest AS node
|
||||||
|
USER node
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN ["npm", "ci"]
|
||||||
|
RUN ["npm", "run", "build"]
|
||||||
|
|
||||||
|
FROM ghcr.io/static-web-server/static-web-server:latest
|
||||||
|
WORKDIR /
|
||||||
|
COPY --from=node /usr/src/app/dist /public
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Theo Browne
|
||||||
|
|
||||||
|
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.
|
16
index.html
Normal file
16
index.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Unduck</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="A better default search engine (with bangs!)"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body style="background-color: transparent">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
4815
package-lock.json
generated
Normal file
4815
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
21
package.json
Normal file
21
package.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"name": "unduck",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"format:write": "prettier --write .",
|
||||||
|
"update": "npx npm-check-updates -u && npm install"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"npm-check-updates": "^18.0.1",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"vite": "^6.3.4",
|
||||||
|
"vite-plugin-pwa": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
6
shell.nix
Normal file
6
shell.nix
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{pkgs ? import <nixpkgs> {}}:
|
||||||
|
pkgs.mkShell {
|
||||||
|
nativeBuildInputs = with pkgs.buildPackages; [
|
||||||
|
nodejs
|
||||||
|
];
|
||||||
|
}
|
122056
src/bang.ts
Normal file
122056
src/bang.ts
Normal file
File diff suppressed because it is too large
Load diff
175
src/global.css
Normal file
175
src/global.css
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
:root {
|
||||||
|
font-family:
|
||||||
|
system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
|
||||||
|
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #888888;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add these new styles */
|
||||||
|
.url-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add this new style */
|
||||||
|
.content-container {
|
||||||
|
max-width: 36rem;
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:active {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button img {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button.copied {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add footer styles */
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
background-color: #131313;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #a9a9a9;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer,
|
||||||
|
.footer a {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer a:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-input {
|
||||||
|
border-color: #3d3d3d;
|
||||||
|
background-color: #191919;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button img {
|
||||||
|
filter: invert(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:active {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
}
|
105
src/main.ts
Normal file
105
src/main.ts
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import { bangs } from "./bang";
|
||||||
|
import "./global.css";
|
||||||
|
|
||||||
|
function noSearchDefaultPageRender() {
|
||||||
|
const currentUrl = window.location.href.replace(/\/+$/, "");
|
||||||
|
const app = document.querySelector<HTMLDivElement>("#app")!;
|
||||||
|
app.innerHTML = `
|
||||||
|
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh;">
|
||||||
|
<div class="content-container">
|
||||||
|
<h1>Und*ck</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/bangs" target="_blank">all of DuckDuckGo's bangs.</a></p>
|
||||||
|
<div class="url-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="url-input"
|
||||||
|
value="${currentUrl}?q=%s"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
<button class="copy-button">
|
||||||
|
<p>Copy</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<details style="margin-top: 16px;">
|
||||||
|
<summary>Demo search</summary>
|
||||||
|
<p>Added so that some browsers treat this page as a search engine</p>
|
||||||
|
<form class="url-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="q"
|
||||||
|
class="url-input"
|
||||||
|
placeholder="doom on typescript types !yt"
|
||||||
|
role="searchbox"
|
||||||
|
/>
|
||||||
|
<button type="submit" class="copy-button">
|
||||||
|
<p>Search</p>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<a href="https://code.troylusty.com/troy/unduck" target="_blank">code</a>
|
||||||
|
•
|
||||||
|
forked from
|
||||||
|
<a href="https://github.com/t3dotgg/unduck" target="_blank">t3dotgg/unduck</a>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const copyButton = app.querySelector<HTMLButtonElement>(".copy-button")!;
|
||||||
|
const copyIcon = copyButton.querySelector("p")!;
|
||||||
|
const urlInput = app.querySelector<HTMLInputElement>(".url-input")!;
|
||||||
|
|
||||||
|
copyButton.addEventListener("click", async () => {
|
||||||
|
await navigator.clipboard.writeText(urlInput.value);
|
||||||
|
copyIcon.textContent = "Copied";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
copyIcon.textContent = "Copy";
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const LS_DEFAULT_BANG = localStorage.getItem("default-bang") ?? "ddg";
|
||||||
|
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() ?? "";
|
||||||
|
if (!query) {
|
||||||
|
noSearchDefaultPageRender();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(/!\S+\s*/i, "").trim();
|
||||||
|
|
||||||
|
// If the query is just `!gh`, use `github.com` instead of `github.com/search?q=`
|
||||||
|
if (cleanQuery === "")
|
||||||
|
return selectedBang ? `https://${selectedBang.d}` : null;
|
||||||
|
|
||||||
|
// Format of the url is:
|
||||||
|
// https://www.google.com/search?q={{{s}}}
|
||||||
|
const searchUrl = selectedBang?.u.replace(
|
||||||
|
"{{{s}}}",
|
||||||
|
// Replace %2F with / to fix formats like "!ghr+t3dotgg/unduck"
|
||||||
|
encodeURIComponent(cleanQuery).replace(/%2F/g, "/"),
|
||||||
|
);
|
||||||
|
if (!searchUrl) return null;
|
||||||
|
|
||||||
|
return searchUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doRedirect() {
|
||||||
|
const searchUrl = getBangredirectUrl();
|
||||||
|
if (!searchUrl) return;
|
||||||
|
window.location.replace(searchUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
doRedirect();
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
24
tsconfig.json
Normal file
24
tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
10
vite.config.ts
Normal file
10
vite.config.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue