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