Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
421e4fcdb8 | ||
![]() |
669ad510ed | ||
![]() |
5519d313ee | ||
![]() |
e7f6f69dfe | ||
![]() |
f85162622d | ||
![]() |
24f23b3573 | ||
![]() |
ff0d05542d | ||
![]() |
969ed948b5 | ||
![]() |
e3ec2bb897 | ||
![]() |
6fe4fc28c1 | ||
![]() |
1faee002a9 | ||
![]() |
38b144a6f4 | ||
![]() |
86d4015ae5 | ||
![]() |
b46f1adddb | ||
![]() |
1d7aaf6a8a | ||
![]() |
136450f93d | ||
![]() |
a9cf5630fe | ||
![]() |
a136c0ab9b | ||
![]() |
50c526c095 | ||
![]() |
7dd7b70db8 | ||
![]() |
f1d22f9d91 | ||
![]() |
04d72d3ca8 | ||
![]() |
8e65767ac1 | ||
![]() |
ced6bffc28 | ||
![]() |
67b5f47440 | ||
![]() |
7d412ad439 | ||
![]() |
5ca8d32778 | ||
![]() |
1ab4f1ce63 | ||
![]() |
6bcf445848 | ||
![]() |
8223e696f9 | ||
![]() |
8a9ef8fcb4 | ||
![]() |
b3255de375 | ||
![]() |
09a7941eca | ||
![]() |
090154115a | ||
![]() |
0aef1e1641 | ||
![]() |
c1930d6bd7 | ||
![]() |
24de7bcaa3 | ||
![]() |
62a0e85554 | ||
![]() |
1f5a36832c | ||
![]() |
09d099a625 | ||
![]() |
a5f5112fc1 | ||
![]() |
8c005b3769 | ||
![]() |
9a623164e4 | ||
![]() |
304ea14003 |
7
.dockerignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
README.md
|
||||||
|
.next
|
||||||
|
.git
|
9
.gitignore
vendored
@ -34,4 +34,11 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.syncignore
|
.syncignore
|
||||||
|
|
||||||
|
# log
|
||||||
|
app.log
|
||||||
|
|
||||||
|
# doc
|
||||||
|
doc/.vitepress/dist
|
||||||
|
doc/.vitepress/cache
|
67
Dockerfile
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies based on the preferred package manager
|
||||||
|
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc ./
|
||||||
|
RUN \
|
||||||
|
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||||
|
elif [ -f package-lock.json ]; then npm ci; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||||
|
else echo "Lockfile not found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN \
|
||||||
|
if [ -f yarn.lock ]; then yarn run build; \
|
||||||
|
elif [ -f package-lock.json ]; then npm run build; \
|
||||||
|
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||||
|
else echo "Lockfile not found." && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during runtime.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
EXPOSE 4594
|
||||||
|
|
||||||
|
ENV PORT 4594
|
||||||
|
|
||||||
|
# server.js is created by next build from the standalone output
|
||||||
|
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
|
||||||
|
CMD HOSTNAME="0.0.0.0" node server.js
|
28
app/[locale]/global.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 214, 219, 220;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 255, 255, 255;
|
||||||
|
--background-start-rgb: 0, 0, 0;
|
||||||
|
--background-end-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: rgb(var(--foreground-rgb));
|
||||||
|
background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
39
app/[locale]/layout.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import "./global.css";
|
||||||
|
import { NextIntlClientProvider, useMessages } from "next-intl";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import { Providers } from "../providers";
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "SparkHome",
|
||||||
|
description: "Your best browser homepage, with a magic searchbox.",
|
||||||
|
icons: {
|
||||||
|
icon: "/favicon.ico"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function LocaleLayout({
|
||||||
|
children,
|
||||||
|
params: { locale }
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: { locale: string };
|
||||||
|
}) {
|
||||||
|
const messages = useMessages();
|
||||||
|
return (
|
||||||
|
<html lang={locale} suppressHydrationWarning>
|
||||||
|
<body className={inter.className}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<Providers>
|
||||||
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||||
|
{children}
|
||||||
|
</NextIntlClientProvider>
|
||||||
|
</Providers>
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
11
app/[locale]/not-found.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use client";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function NotFound() {
|
||||||
|
const t = useTranslations("404");
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{t('title')}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RecoilRoot } from "recoil";
|
import { RecoilRoot } from "recoil";
|
||||||
import Homepage from "../components";
|
import Homepage from "../../components";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
82
app/[locale]/tools/base64/page.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Switcher from "@/components/switcher";
|
||||||
|
import Notice from "@/components/tools/notice";
|
||||||
|
import base64ToHex from "@/lib/base64ToHex";
|
||||||
|
import copyToClipboard from "@/lib/copy";
|
||||||
|
import normalizeHex from "@/lib/normalizeHex";
|
||||||
|
import { validBase64 } from "@/lib/onesearch/baseCheck";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { utoa, atou } from "unicode-encode";
|
||||||
|
|
||||||
|
export default function Base64() {
|
||||||
|
const t = useTranslations("tools");
|
||||||
|
const [mode, setMode] = useState("Encode");
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [messageResult, setMessageResult] = useState("");
|
||||||
|
const [isHex, setHex] = useState(false);
|
||||||
|
const [info, setInfo] = useState("");
|
||||||
|
const [type, setType] = useState("");
|
||||||
|
useEffect(() => {
|
||||||
|
setType("");
|
||||||
|
setInfo("");
|
||||||
|
setHex(false);
|
||||||
|
if (mode == "Encode") {
|
||||||
|
setMessageResult(utoa(message));
|
||||||
|
} else {
|
||||||
|
if (validBase64(message)) {
|
||||||
|
try {
|
||||||
|
setMessageResult(atou(message));
|
||||||
|
} catch (e) {
|
||||||
|
setMessageResult(normalizeHex(base64ToHex(message)));
|
||||||
|
setHex(true);
|
||||||
|
setType("info");
|
||||||
|
setInfo("Showing HEX result.");
|
||||||
|
}
|
||||||
|
} else if (message.trim() !== "") {
|
||||||
|
setMessageResult("");
|
||||||
|
setType("warning");
|
||||||
|
setInfo("Invalid Base64.");
|
||||||
|
} else {
|
||||||
|
setMessageResult("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mode, message]);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-semibold">{t("base64.title")}</h1>
|
||||||
|
<Switcher items={["Encode", "Decode"]} selected={mode} setSelected={setMode} class="mt-4" />
|
||||||
|
<textarea
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
className="w-full h-80 mt-4 p-4 rounded-lg bg-zinc-100 dark:bg-zinc-800 resize-none outline-none duration-200 transition-colors-opacity border-2 border-transparent focus:border-zinc-600 dark:focus:border-zinc-300"
|
||||||
|
/>
|
||||||
|
<div className="w-full h-12 mt-4">
|
||||||
|
<span className="w-fit text-2xl font-bold leading-10">Result:</span>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
copyToClipboard(messageResult);
|
||||||
|
setType("info");
|
||||||
|
setInfo("Copied");
|
||||||
|
setTimeout(() => {
|
||||||
|
setInfo("");
|
||||||
|
setType("");
|
||||||
|
}, 3000);
|
||||||
|
}}
|
||||||
|
className="absolute right-0 w-fit h-10 rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-300 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-300"
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`empty:py-0 mt-6 w-full h-fit rounded-md leading-10 bg-zinc-100 dark:bg-zinc-800 py-2 px-5 z-10 cursor-pointer duration-100 break-all ${
|
||||||
|
isHex ? "font-mono" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{messageResult}
|
||||||
|
</div>
|
||||||
|
<Notice type={type} info={info} class="mt-4" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
10
app/[locale]/tools/layout.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default function ToolsLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]">
|
||||||
|
<main className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4
|
||||||
|
pt-12">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
app/api/autocomplete/route.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { completeGoogle } from "search-engine-autocomplete";
|
||||||
|
import { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const query = request.nextUrl.searchParams.get('q')!;
|
||||||
|
let language = request.nextUrl.searchParams.get('l');
|
||||||
|
if (language === null) language = 'en-US';
|
||||||
|
const data = await completeGoogle(query, language);
|
||||||
|
const completeWord: string | undefined = data.suggestions.filter((s) => {
|
||||||
|
return s.relativeRelevance > 0.96
|
||||||
|
})[0]?.suggestion;
|
||||||
|
return new Response(completeWord ? completeWord : query);
|
||||||
|
}
|
8
app/api/error/route.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { logger } from "@/lib/log";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: NextRequest) {
|
||||||
|
const data = await req.json();
|
||||||
|
logger.warn({type:"client_telemetering",error:data});
|
||||||
|
return new Response();
|
||||||
|
}
|
13
app/api/ping/route.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { NEXT_API_VERSION, SPARKHOME_VERSION, CLIENT_VERSION } from "@/lib/version";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic"; // defaults to auto
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const time = new Date().getTime() / 1000;
|
||||||
|
const responseString =
|
||||||
|
`SparkHome ${SPARKHOME_VERSION}
|
||||||
|
Client ${CLIENT_VERSION}
|
||||||
|
API ${NEXT_API_VERSION}
|
||||||
|
ServerTime ${time}
|
||||||
|
Powered by alikia2x (∠・ω< )⌒★`;
|
||||||
|
return new Response(responseString);
|
||||||
|
}
|
14
app/api/suggestion/route.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { completeGoogle } from "search-engine-autocomplete";
|
||||||
|
import { NextRequest } from "next/server"
|
||||||
|
import { suggestionsResponse } from "@/global";
|
||||||
|
import { logger } from "@/lib/log";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const query = request.nextUrl.searchParams.get('q')!;
|
||||||
|
const t = parseInt(request.nextUrl.searchParams.get('t') || "0") || null;
|
||||||
|
let language = request.nextUrl.searchParams.get('l');
|
||||||
|
if (language === null) language = 'en-US';
|
||||||
|
const data = await completeGoogle(query, language);
|
||||||
|
logger.info({ type: "onesearch_search_autocomplete", query: query, data: data });
|
||||||
|
return new Response(JSON.stringify({...data, time: t} as suggestionsResponse));
|
||||||
|
}
|
BIN
app/favicon.ico
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 64 KiB |
@ -1,33 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 0, 0, 0;
|
|
||||||
--background-start-rgb: 214, 219, 220;
|
|
||||||
--background-end-rgb: 255, 255, 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--foreground-rgb: 255, 255, 255;
|
|
||||||
--background-start-rgb: 0, 0, 0;
|
|
||||||
--background-end-rgb: 0, 0, 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
color: rgb(var(--foreground-rgb));
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
transparent,
|
|
||||||
rgb(var(--background-end-rgb))
|
|
||||||
)
|
|
||||||
rgb(var(--background-start-rgb));
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer utilities {
|
|
||||||
.text-balance {
|
|
||||||
text-wrap: balance;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import "./global.css";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Create Next App",
|
|
||||||
description: "Generated by create next app"
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children
|
children,
|
||||||
}: Readonly<{
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}) {
|
||||||
return (
|
return children;
|
||||||
<html lang="en">
|
|
||||||
<body className={inter.className}>{children}</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
9
app/not-found.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Not Found</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
12
app/providers.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
// app/providers.tsx
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import {NextUIProvider} from '@nextui-org/react'
|
||||||
|
|
||||||
|
export function Providers({children}: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<NextUIProvider>
|
||||||
|
{children}
|
||||||
|
</NextUIProvider>
|
||||||
|
)
|
||||||
|
}
|
@ -1,41 +1,35 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
import { useRecoilValue } from "recoil";
|
|
||||||
import { settingsState } from "./state/settings";
|
|
||||||
import validUrl from "valid-url";
|
|
||||||
import validateColor from "validate-color";
|
|
||||||
|
|
||||||
function Background(props: {
|
import { useEffect, useState } from "react";
|
||||||
isFocus: boolean;
|
import { useRecoilState } from "recoil";
|
||||||
src: string;
|
import { bgFocusState } from "./state/background";
|
||||||
onClick: () => void;
|
import BackgroundContainer from "./backgroundContainer";
|
||||||
}) {
|
|
||||||
const settings = useRecoilValue(settingsState);
|
export default function () {
|
||||||
if (validateColor(props.src)) {
|
const [isFocus, setFocus] = useRecoilState(bgFocusState);
|
||||||
return (
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
<div
|
|
||||||
className="w-full h-full fixed object-cover inset-0 duration-200 z-0"
|
useEffect(() => {
|
||||||
style={{ backgroundColor: props.src }}
|
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
onClick={props.onClick}
|
setDarkMode(colorSchemeQueryList.matches ? true : false);
|
||||||
></div>
|
|
||||||
);
|
const handleChange = () => {
|
||||||
} else if (validUrl.isWebUri(props.src)) {
|
setDarkMode(colorSchemeQueryList.matches ? true : false);
|
||||||
return (
|
};
|
||||||
<Image
|
|
||||||
src={props.src}
|
colorSchemeQueryList.addEventListener("change", handleChange);
|
||||||
className={
|
|
||||||
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
|
return () => {
|
||||||
(props.isFocus
|
colorSchemeQueryList.removeEventListener("change", handleChange);
|
||||||
? settings.bgBlur
|
};
|
||||||
? "blur-lg scale-110"
|
}, []);
|
||||||
: "brightness-50 scale-105"
|
return (
|
||||||
: "")
|
<div suppressHydrationWarning>
|
||||||
}
|
{darkMode ? (
|
||||||
alt="background"
|
<BackgroundContainer src="rgb(23,25,29)" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
|
||||||
onClick={props.onClick}
|
) : (
|
||||||
fill={true}
|
<BackgroundContainer src="white" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
|
||||||
/>
|
)}
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Background;
|
|
||||||
|
47
components/backgroundContainer.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
import { settingsState } from "./state/settings";
|
||||||
|
import validUrl from "valid-url";
|
||||||
|
import validateColor from "validate-color";
|
||||||
|
|
||||||
|
export default function (props: { isFocus: boolean; src: string; darkMode: boolean; onClick: () => void }) {
|
||||||
|
const settings = useRecoilValue(settingsState);
|
||||||
|
if (validateColor(props.src)) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-full fixed object-cover inset-0 duration-200 z-0"
|
||||||
|
style={{ backgroundColor: props.src }}
|
||||||
|
onClick={props.onClick}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
} else if (validUrl.isWebUri(props.src)) {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={props.src}
|
||||||
|
className={
|
||||||
|
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
|
||||||
|
(props.isFocus ? (settings.bgBlur ? "blur-lg scale-110" : "brightness-50 scale-105") : "")
|
||||||
|
}
|
||||||
|
alt="background"
|
||||||
|
onClick={props.onClick}
|
||||||
|
fill={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (props.darkMode) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-[rgb(23,25,29)]"
|
||||||
|
onClick={props.onClick}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-white"
|
||||||
|
onClick={props.onClick}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +1,35 @@
|
|||||||
import { useRecoilState } from "recoil";
|
"use client";
|
||||||
import Background from "./background";
|
|
||||||
|
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||||
|
import { settingsState } from "./state/settings";
|
||||||
import Search from "./search/search";
|
import Search from "./search/search";
|
||||||
import { bgFocusState } from "./state/background";
|
import { bgFocusState } from "./state/background";
|
||||||
|
import EngineSelector from "./search/engineSelector";
|
||||||
|
import Onesearch from "./search/onesearch/onesearch";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const Background = dynamic(() => import("./background"), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
const Time = dynamic(() => import("./time"), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
export default function Homepage() {
|
export default function Homepage() {
|
||||||
const [isFocus, setFocus] = useRecoilState(bgFocusState);
|
const settings = useRecoilValue(settingsState);
|
||||||
|
const setFocus = useSetRecoilState(bgFocusState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full fixed overflow-hidden w-full bg-black">
|
<div className="h-full fixed overflow-hidden w-full bg-black">
|
||||||
<Background
|
<Time showSecond={settings.timeShowSecond} />
|
||||||
src="rgb(23,25,29)"
|
<EngineSelector
|
||||||
isFocus={isFocus}
|
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%] translate-y-[-0.2rem]
|
||||||
onClick={() => setFocus(false)}
|
left-1/2 w-11/12 sm:w-[700px] text:black text-right
|
||||||
/>
|
dark:text-white text-3xl text-shadow-lg z-10"
|
||||||
<Search
|
|
||||||
onFocus={() => {
|
|
||||||
setFocus(true);
|
|
||||||
console.log("focus");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
<Background />
|
||||||
|
<Search onFocus={() => setFocus(true)} />
|
||||||
|
<Onesearch />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
77
components/search/engineSelector.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { SetStateAction, useEffect, useState } from "react";
|
||||||
|
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@nextui-org/react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||||
|
import { settingsState } from "../state/settings";
|
||||||
|
import { engineTranslation } from "./translatedEngineList";
|
||||||
|
import { settingsType } from "@/global";
|
||||||
|
|
||||||
|
export default function(
|
||||||
|
props: { className: string }
|
||||||
|
) {
|
||||||
|
const t = useTranslations("Search");
|
||||||
|
const settings: settingsType = useRecoilValue(settingsState);
|
||||||
|
const items = settings.searchEngines;
|
||||||
|
const currentEngine: string = settings.currentSearchEngine;
|
||||||
|
const displayEngine = getName(currentEngine);
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState(new Set([currentEngine]) as any);
|
||||||
|
const selectedValue = React.useMemo(() => Array.from(selectedKeys).join(", "), [selectedKeys]);
|
||||||
|
const setSettings = useSetRecoilState(settingsState);
|
||||||
|
|
||||||
|
function setEngine(engine: string) {
|
||||||
|
setSettings((oldSettings: settingsType) => {
|
||||||
|
return {
|
||||||
|
...oldSettings,
|
||||||
|
currentSearchEngine: engine
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getName(engineKey: string) {
|
||||||
|
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedValue !== currentEngine) {
|
||||||
|
setEngine(selectedValue);
|
||||||
|
}
|
||||||
|
}, [selectedValue]);
|
||||||
|
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={props.className}>
|
||||||
|
{
|
||||||
|
isClient &&
|
||||||
|
(
|
||||||
|
<Dropdown>
|
||||||
|
<DropdownTrigger>
|
||||||
|
<Button variant="bordered" className="capitalize">
|
||||||
|
{displayEngine}
|
||||||
|
</Button>
|
||||||
|
</DropdownTrigger>
|
||||||
|
<DropdownMenu
|
||||||
|
aria-label={t("engine-aria")}
|
||||||
|
variant="light"
|
||||||
|
disallowEmptySelection
|
||||||
|
selectionMode="single"
|
||||||
|
selectedKeys={selectedKeys}
|
||||||
|
onSelectionChange={setSelectedKeys}
|
||||||
|
>
|
||||||
|
{Object.keys(items).map((item) => (
|
||||||
|
<DropdownItem key={item} suppressHydrationWarning>
|
||||||
|
{getName(item)}
|
||||||
|
</DropdownItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenu>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
26
components/search/onesearch/handleEnter.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { settingsType, suggestionItem } from "@/global";
|
||||||
|
import copyToClipboard from "@/lib/copy";
|
||||||
|
import { normalizeURL } from "@/lib/normalizeURL";
|
||||||
|
import search from "@/lib/search";
|
||||||
|
|
||||||
|
export default function (
|
||||||
|
index: number,
|
||||||
|
suggestion: suggestionItem[],
|
||||||
|
query: string,
|
||||||
|
settings: settingsType,
|
||||||
|
searchBoxRef: React.RefObject<HTMLInputElement>
|
||||||
|
) {
|
||||||
|
const selected = suggestion[index];
|
||||||
|
const engine = settings.searchEngines[settings.currentSearchEngine];
|
||||||
|
const newTab = settings.searchInNewTab;
|
||||||
|
let clipboard: any;
|
||||||
|
if (selected.type === "QUERY" || selected.type === "default") {
|
||||||
|
search(selected.suggestion, engine, newTab);
|
||||||
|
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
|
||||||
|
window.open(normalizeURL(selected.suggestion));
|
||||||
|
} else if (selected.type === "text") {
|
||||||
|
console.log("????");
|
||||||
|
copyToClipboard(selected.suggestion);
|
||||||
|
searchBoxRef.current?.focus();
|
||||||
|
}
|
||||||
|
}
|
42
components/search/onesearch/handleNLUResult.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { suggestionItem } from "@/global";
|
||||||
|
import { findClosestDateIndex } from "@/lib/weather/getCurrentWeather";
|
||||||
|
import { getLocationNative } from "@/lib/weather/getLocation";
|
||||||
|
import { getWeather } from "@/lib/weather/getWeather";
|
||||||
|
import { WMOCodeTable } from "@/lib/weather/wmocode";
|
||||||
|
|
||||||
|
type UpdateSuggestionFunction = (data: suggestionItem[]) => void;
|
||||||
|
|
||||||
|
export function handleNLUResult(result: any, updateSuggestion: UpdateSuggestionFunction){
|
||||||
|
if (result.intent == "weather.summary") {
|
||||||
|
getLocationNative((data: GeolocationCoordinates | GeolocationPositionError) => {
|
||||||
|
console.log(data);
|
||||||
|
if (data instanceof GeolocationCoordinates) {
|
||||||
|
getWeather(data.latitude, data.longitude).then((weather) => {
|
||||||
|
console.log(weather["hourly"]);
|
||||||
|
let hourIndex = findClosestDateIndex(
|
||||||
|
weather["hourly"]["time"],
|
||||||
|
weather["utc_offset_seconds"]
|
||||||
|
);
|
||||||
|
let temp = weather["hourly"]["apparent_temperature"][hourIndex];
|
||||||
|
let weatherCode = weather["hourly"]["weather_code"][hourIndex];
|
||||||
|
console.log(temp, weatherCode, hourIndex);
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
suggestion: `Weather: ${temp}${weather["hourly_units"]["apparent_temperature"]}, ${WMOCodeTable[weatherCode]["day"].description}`,
|
||||||
|
relevance: 3000 * result.score
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (result.intent !== "None") {
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
suggestion: result.intent,
|
||||||
|
relevance: 2200 * result.score
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
30
components/search/onesearch/link.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { normalizeURL } from "@/lib/normalizeURL";
|
||||||
|
|
||||||
|
export default function (props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(normalizeURL(props.query));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
onClick={() => {
|
||||||
|
window.open(normalizeURL(props.query));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
180
components/search/onesearch/onesearch.tsx
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import SuggestionBox from "./suggestionBox";
|
||||||
|
import { useRecoilState, useRecoilValue } from "recoil";
|
||||||
|
import { queryState } from "@/components/state/query";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import { suggestionItem, suggestionsResponse } from "@/global";
|
||||||
|
import getSearchEngineName from "@/lib/getSearchEngineName";
|
||||||
|
import PlainSearch from "./plainSearch";
|
||||||
|
import { suggestionsState } from "@/components/state/suggestion";
|
||||||
|
import validLink from "@/lib/url/validLink";
|
||||||
|
import Link from "./link";
|
||||||
|
import { selectedSuggestionState } from "@/components/state/suggestionSelection";
|
||||||
|
import { settingsState } from "@/components/state/settings";
|
||||||
|
import PlainText from "./plainText";
|
||||||
|
import { sendError } from "@/lib/telemetering/sendError";
|
||||||
|
import { NLU } from "@/lib/nlp/load";
|
||||||
|
import { getLocationNative } from "@/lib/weather/getLocation";
|
||||||
|
import { getWeather } from "@/lib/weather/getWeather";
|
||||||
|
import { findClosestDateIndex, getClosestHourTimestamp } from "@/lib/weather/getCurrentWeather";
|
||||||
|
import { WMOCodeTable } from "@/lib/weather/wmocode";
|
||||||
|
import { handleNLUResult } from "./handleNLUResult";
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const [suggestion, setFinalSuggetsion] = useRecoilState(suggestionsState);
|
||||||
|
const [location, setLocation] = useState(null);
|
||||||
|
const [manager, setManager] = useState(null);
|
||||||
|
const lastRequestTimeRef = useRef(0);
|
||||||
|
const selected = useRecoilValue(selectedSuggestionState);
|
||||||
|
const settings = useRecoilValue(settingsState);
|
||||||
|
const devMode = true;
|
||||||
|
const query = useRecoilValue(queryState);
|
||||||
|
const engineName = getSearchEngineName();
|
||||||
|
const engine = settings.currentSearchEngine;
|
||||||
|
const lang = useLocale();
|
||||||
|
const t = useTranslations("Search");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const time = new Date().getTime().toString();
|
||||||
|
if (query.trim() === "" || query.length > 120) {
|
||||||
|
cleanSuggestion("QUERY", "NAVIGATION");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fetch(`/api/suggestion?q=${query}&l=${lang}&t=${time}&engine=${engine}`)
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: suggestionsResponse) => {
|
||||||
|
try {
|
||||||
|
let suggestionToUpdate: suggestionItem[] = data.suggestions;
|
||||||
|
if (data.time > lastRequestTimeRef.current) {
|
||||||
|
cleanSuggestion("NAVIGATION", "QUERY");
|
||||||
|
lastRequestTimeRef.current = data.time;
|
||||||
|
updateSuggestion(suggestionToUpdate);
|
||||||
|
}
|
||||||
|
} catch (error: Error | any) {
|
||||||
|
sendError(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// Handle fetch error
|
||||||
|
sendError(error);
|
||||||
|
});
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
function updateSuggestion(data: suggestionItem[]) {
|
||||||
|
setFinalSuggetsion((cur: suggestionItem[]) => {
|
||||||
|
const types: string[] = [];
|
||||||
|
for (let sug of data) {
|
||||||
|
if (!types.includes(sug.type)) types.push(sug.type);
|
||||||
|
}
|
||||||
|
for (let type of types) {
|
||||||
|
cur = cur.filter((item) => {
|
||||||
|
return item.type !== type;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cur.concat(data).sort((a, b) => {
|
||||||
|
return b.relevance - a.relevance;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanSuggestion(...types: string[]) {
|
||||||
|
setFinalSuggetsion((suggestion: suggestionItem[]) => {
|
||||||
|
return suggestion.filter((item) => {
|
||||||
|
return !types.includes(item.type);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const NLUModel = new NLU();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
NLUModel.init().then((nlu) => {
|
||||||
|
setManager(nlu.manager);
|
||||||
|
console.log(nlu.manager);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
cleanSuggestion("default-link", "default", "text");
|
||||||
|
if (validLink(query)) {
|
||||||
|
updateSuggestion([
|
||||||
|
{ type: "default-link", suggestion: query, relevance: 3000, prompt: <span>Go to: </span> },
|
||||||
|
{ type: "default", suggestion: query, relevance: 1600 }
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
updateSuggestion([
|
||||||
|
{
|
||||||
|
type: "default",
|
||||||
|
suggestion: query,
|
||||||
|
relevance: 2000
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manager != null) {
|
||||||
|
// @ts-ignore
|
||||||
|
manager.process(query).then((result) => {
|
||||||
|
console.log(result);
|
||||||
|
handleNLUResult(result, updateSuggestion);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [query, engineName]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SuggestionBox>
|
||||||
|
{suggestion.map((s, i) => {
|
||||||
|
if (s.suggestion.trim() === "") return;
|
||||||
|
if (s.type === "default") {
|
||||||
|
return (
|
||||||
|
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.suggestion}
|
||||||
|
<span className="text-zinc-700 dark:text-zinc-400 text-sm">
|
||||||
|
{t("search-help-text", { engine: engineName })}
|
||||||
|
</span>
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainSearch>
|
||||||
|
);
|
||||||
|
} else if (s.type === "QUERY") {
|
||||||
|
return (
|
||||||
|
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.suggestion}
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainSearch>
|
||||||
|
);
|
||||||
|
} else if (s.type === "NAVIGATION" || s.type === "default-link") {
|
||||||
|
return (
|
||||||
|
<Link key={i} query={s.suggestion} selected={i == selected}>
|
||||||
|
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
|
||||||
|
{s.suggestion}
|
||||||
|
{devMode && (
|
||||||
|
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else if (s.type === "text") {
|
||||||
|
return (
|
||||||
|
<PlainText key={i} selected={i == selected}>
|
||||||
|
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
|
||||||
|
<p>{s.suggestion}</p>
|
||||||
|
{devMode && (
|
||||||
|
<span className="bottom-0 absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
|
||||||
|
{s.relevance}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</PlainText>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</SuggestionBox>
|
||||||
|
);
|
||||||
|
}
|
35
components/search/onesearch/plainSearch.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import search from "@/lib/search";
|
||||||
|
import { settingsState } from "@/components/state/settings";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
|
||||||
|
export default function (props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||||
|
const settings = useRecoilValue(settingsState);
|
||||||
|
const engine = settings.searchEngines[settings.currentSearchEngine];
|
||||||
|
const newTab = settings.searchInNewTab;
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100 truncate`}
|
||||||
|
onClick={() => {
|
||||||
|
search(props.query, engine, newTab);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100 truncate`}
|
||||||
|
onClick={() => {
|
||||||
|
search(props.query, engine, newTab);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
components/search/onesearch/plainText.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export default function (props: { children: React.ReactNode; selected: boolean }) {
|
||||||
|
if (props.selected) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-300 dark:bg-zinc-700
|
||||||
|
px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-100 hover:bg-zinc-300
|
||||||
|
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
6
components/search/onesearch/suggestion.tsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: `<p>${props.children}</p>` as string }} className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300 dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
9
components/search/onesearch/suggestionBox.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export default function(props: { children?: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className={`relative bg-zinc-100 dark:bg-zinc-800 w-11/12 sm:w-[700px] h-auto max-h-[calc(100vh-20rem)]
|
||||||
|
overflow-y-auto left-1/2 translate-x-[-50%] top-72 z-20 rounded-md overflow-hidden duration-250
|
||||||
|
${props.children ? "opacity-100" : "opacity-0"}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,24 +1,66 @@
|
|||||||
import { atom, useRecoilValue } from "recoil";
|
"use client";
|
||||||
import { settingsState } from "../state/settings";
|
|
||||||
|
import { useRecoilState, useRecoilValue } from "recoil";
|
||||||
|
import { settingsState } from "../state/settings";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { queryState } from "../state/query";
|
||||||
|
import { settingsType } from "@/global";
|
||||||
|
import handleEnter from "./onesearch/handleEnter";
|
||||||
|
import { selectedSuggestionState } from "../state/suggestionSelection";
|
||||||
|
import { suggestionsState } from "../state/suggestion";
|
||||||
|
import { KeyboardEvent, useRef } from "react";
|
||||||
|
|
||||||
|
export default function Search(props: { onFocus: () => void }) {
|
||||||
|
const settings: settingsType = useRecoilValue(settingsState);
|
||||||
|
const t = useTranslations("Search");
|
||||||
|
const [query, setQuery] = useRecoilState(queryState);
|
||||||
|
const [selectedSuggestion, setSelected] = useRecoilState(selectedSuggestionState);
|
||||||
|
const suggestions = useRecoilValue(suggestionsState);
|
||||||
|
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
export default function Search(props: {
|
|
||||||
onFocus: () => void;
|
|
||||||
}) {
|
|
||||||
const settings = useRecoilValue(settingsState);
|
|
||||||
let style = "default";
|
let style = "default";
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef);
|
||||||
|
return;
|
||||||
|
} else if (e.key == "ArrowUp") {
|
||||||
|
e.preventDefault();
|
||||||
|
const len = suggestions.length;
|
||||||
|
setSelected((selectedSuggestion - 1 + len) % len);
|
||||||
|
} else if (e.key == "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
const len = suggestions.length;
|
||||||
|
setSelected((selectedSuggestion + 1) % len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (style === "default") {
|
if (style === "default") {
|
||||||
return (
|
return (
|
||||||
// 祖传样式,勿动
|
// 祖传样式,勿动
|
||||||
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
|
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
|
||||||
<input
|
<input
|
||||||
className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%]
|
className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%]
|
||||||
text-center outline-none border-[1px] focus:border-2 duration-200 pr-2 shadow-lg bg-white dark:bg-[rgb(23,25,29)]
|
text-center outline-none border-[1px] focus:border-2 duration-200 pr-2 shadow-md focus:shadow-none dark:shadow-zinc-800 dark:shadow-md bg-white dark:bg-[rgb(23,25,29)]
|
||||||
dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500
|
dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500
|
||||||
dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
|
dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
|
||||||
id="searchBox"
|
id="searchBox"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder={t("placeholder")}
|
||||||
onFocus={props.onFocus}
|
onFocus={props.onFocus}
|
||||||
|
onKeyDown={handleKeydown}
|
||||||
|
onChange={(e) =>
|
||||||
|
setQuery((_) => {
|
||||||
|
return e.target.value;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
autoCapitalize="off"
|
||||||
|
spellCheck="false"
|
||||||
|
ref={searchBoxRef}
|
||||||
|
value={query}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -30,7 +72,7 @@ export default function Search(props: {
|
|||||||
className={
|
className={
|
||||||
`absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
|
`absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
|
||||||
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
|
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
|
||||||
border-solid border-0 duration-200 pr-2 shadow-lg` +
|
border-solid border-0 duration-200 pr-2 shadow-md focus:shadow-none` +
|
||||||
(settings.bgBlur
|
(settings.bgBlur
|
||||||
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
|
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
|
||||||
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
|
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
|
||||||
@ -39,8 +81,9 @@ export default function Search(props: {
|
|||||||
}
|
}
|
||||||
id="searchBox"
|
id="searchBox"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search"
|
placeholder={t("placeholder")}
|
||||||
onFocus={props.onFocus}
|
onFocus={props.onFocus}
|
||||||
|
ref={searchBoxRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
1
components/search/translatedEngineList.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const engineTranslation = ["google", "bing", "baidu", "duckduckgo", "yandex", "ecosia", "yahoo"];
|
10
components/state/query.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const queryState = atom({
|
||||||
|
key: "searchQuery",
|
||||||
|
default: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
queryState,
|
||||||
|
}
|
@ -1,14 +1,58 @@
|
|||||||
import { atom, selector } from "recoil";
|
import { settingsType } from "@/global";
|
||||||
|
import isLocalStorageAvailable from "@/lib/isLocalStorageAvailable";
|
||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const defaultSettings: settingsType = {
|
||||||
|
"version": 2,
|
||||||
|
"elementBackdrop": true,
|
||||||
|
"bgBlur": true,
|
||||||
|
"timeShowSecond": false,
|
||||||
|
"currentSearchEngine": "google",
|
||||||
|
"searchInNewTab": true,
|
||||||
|
"searchEngines": {
|
||||||
|
"google": "https://www.google.com/search?q=%s",
|
||||||
|
"bing": "https://www.bing.com/search?q=%s",
|
||||||
|
"baidu": "https://www.baidu.com/s?wd=%s",
|
||||||
|
"duckduckgo": "https://duckduckgo.com/?q=%s",
|
||||||
|
"yandex": "https://yandex.com/search/?text=%s",
|
||||||
|
"yahoo": "https://search.yahoo.com/search?p=%s",
|
||||||
|
"ecosia": "https://www.ecosia.org/search?q=%s"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const localStorageEffect =
|
||||||
|
(key: string) =>
|
||||||
|
({ setSelf, onSet }: any) => {
|
||||||
|
if (isLocalStorageAvailable()===false){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(key) === null) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(defaultSettings));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let settings =JSON.parse(JSON.stringify(defaultSettings));
|
||||||
|
const savedSettings = localStorage.getItem(key)!;
|
||||||
|
const parsedSettings = JSON.parse(savedSettings);
|
||||||
|
|
||||||
|
Object.keys(settings).map((key) => {
|
||||||
|
if (parsedSettings[key] !== undefined && key !== "version"){
|
||||||
|
settings[key] = parsedSettings[key];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setSelf(settings);
|
||||||
|
localStorage.setItem(key, JSON.stringify(settings));
|
||||||
|
|
||||||
|
onSet((newValue: settingsType) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(newValue));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const settingsState = atom({
|
const settingsState = atom({
|
||||||
key: "settings",
|
key: "settings",
|
||||||
default: {
|
default: defaultSettings,
|
||||||
version: 1,
|
effects_UNSTABLE: [localStorageEffect("settings")]
|
||||||
elementBackdrop: true,
|
|
||||||
bgBlur: true
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export { settingsState };
|
||||||
settingsState,
|
|
||||||
}
|
|
||||||
|
11
components/state/suggestion.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { suggestionItem } from "@/global";
|
||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const suggestionsState = atom({
|
||||||
|
key: "oneSearchSuggestions",
|
||||||
|
default: [] as suggestionItem[]
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
suggestionsState,
|
||||||
|
}
|
10
components/state/suggestionSelection.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { atom } from "recoil";
|
||||||
|
|
||||||
|
const selectedSuggestionState = atom({
|
||||||
|
key: "selectedSuggestion",
|
||||||
|
default: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
selectedSuggestionState,
|
||||||
|
}
|
31
components/switcher.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export default function Switcher(props: { items: string[]; selected: string, setSelected: Function, class?: string }) {
|
||||||
|
const selectedRef = useRef(null);
|
||||||
|
const [selectedCoordinate, setSelectedCoordinate] = useState(0);
|
||||||
|
const [selectedWidth, setSelectedWidth] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRef.current){
|
||||||
|
setSelectedCoordinate((selectedRef.current as HTMLElement)?.offsetLeft);
|
||||||
|
setSelectedWidth((selectedRef.current as HTMLElement)?.getBoundingClientRect().width);
|
||||||
|
}
|
||||||
|
}, [props.selected]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative w-fit h-12 px-1 flex rounded-lg bg-zinc-100 dark:bg-zinc-800 z-0 ${props.class}`}>
|
||||||
|
{props.items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative mt-[0.375rem] rounded-md w-fit h-9 leading-9 px-4 mx-1 z-20 cursor-pointer duration-100"
|
||||||
|
ref={item == props.selected ? selectedRef : null}
|
||||||
|
onClick={() => props.setSelected(item)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="absolute mt-[0.375rem] rounded-md h-9 bg-zinc-300 dark:bg-zinc-600 z-10 duration-250 ease-[cubic-bezier(.15,.16,.2,1.2)]" style={{ left: selectedCoordinate, width: selectedWidth }}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
50
components/time.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useFormatter } from "next-intl";
|
||||||
|
|
||||||
|
export default function Time(props: {
|
||||||
|
showSecond: boolean
|
||||||
|
}) {
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
const format = useFormatter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentTime(new Date());
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatTime = () => {
|
||||||
|
const hours = currentTime.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = currentTime.getMinutes().toString().padStart(2, "0");
|
||||||
|
const seconds = currentTime.getSeconds().toString().padStart(2, "0");
|
||||||
|
|
||||||
|
if (props.showSecond) {
|
||||||
|
return `${hours}:${minutes}:${seconds}`;
|
||||||
|
} else {
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%]
|
||||||
|
left-1/2 w-11/12 sm:w-[700px] text:black
|
||||||
|
dark:text-white text-3xl text-left text-shadow-lg z-10"
|
||||||
|
>
|
||||||
|
{formatTime()}{" "}
|
||||||
|
<span className="text-lg leading-9 relative">
|
||||||
|
{format.dateTime(currentTime, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric"
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
32
components/tools/notice.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { Icon } from "@iconify-icon/react";
|
||||||
|
|
||||||
|
const typeToColor: Record<string, string> = {
|
||||||
|
success: "bg-green-500",
|
||||||
|
info: "bg-blue-500",
|
||||||
|
warning: "bg-orange-500",
|
||||||
|
error: "bg-red-500"
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeToIcon: Record<string, string> = {
|
||||||
|
success: "material-symbols:check-circle",
|
||||||
|
info: "material-symbols:info",
|
||||||
|
warning: "material-symbols:warning",
|
||||||
|
error: "material-symbols:error"
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Notice(props: { type: string; info: string; class?: string }) {
|
||||||
|
if (props.type && props.info)
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`relative ${props.class} ${
|
||||||
|
typeToColor[props.type]
|
||||||
|
} rounded-md w-full min-h-12 h-fit empty:px-0 px-4 z-20 cursor-pointer duration-100 `}
|
||||||
|
>
|
||||||
|
<Icon className="text-2xl mt-3" icon={typeToIcon[props.type]} />
|
||||||
|
<span className="absolute text-base mt-3 ml-1">{props.info}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
40
doc/.vitepress/config.mts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { defineConfig } from "vitepress";
|
||||||
|
|
||||||
|
// https://vitepress.dev/reference/site-config
|
||||||
|
export default defineConfig({
|
||||||
|
title: "SparkHome",
|
||||||
|
description: "The official documentation of SparkHome",
|
||||||
|
themeConfig: {
|
||||||
|
// https://vitepress.dev/reference/default-theme-config
|
||||||
|
nav: [
|
||||||
|
{ text: "Home", link: "/" },
|
||||||
|
{ text: "Examples", link: "/markdown-examples" }
|
||||||
|
],
|
||||||
|
|
||||||
|
sidebar: [
|
||||||
|
{
|
||||||
|
text: "Examples",
|
||||||
|
items: [
|
||||||
|
{ text: "Markdown Examples", link: "/markdown-examples" },
|
||||||
|
{ text: "Runtime API Examples", link: "/api-examples" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
socialLinks: [{ icon: "github", link: "https://github.com/alikia2x/sparkhome" }],
|
||||||
|
|
||||||
|
logo: "/favicon.ico"
|
||||||
|
},
|
||||||
|
locales: {
|
||||||
|
root: {
|
||||||
|
label: "English",
|
||||||
|
lang: "en"
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
label: "中文",
|
||||||
|
lang: "zh-CN",
|
||||||
|
link: "/zh/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
head: [['link', { rel: 'icon', href: '/favicon.ico' }]]
|
||||||
|
});
|
118
doc/.vitepress/theme/NotFoundLayout.vue
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<script setup>
|
||||||
|
import DefaultTheme from 'vitepress/theme'
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
const currentLanguage = ref("");
|
||||||
|
const rootPage = ref("")
|
||||||
|
|
||||||
|
const { lang, localeIndex, site } = useData()
|
||||||
|
|
||||||
|
if (localeIndex.value === "root") {
|
||||||
|
rootPage.value = "/"
|
||||||
|
} else {
|
||||||
|
if (site.value.locales[localeIndex.value].link != undefined) {
|
||||||
|
rootPage.value = site.value.locales[localeIndex.value].link
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rootPage.value = "/" + localeIndex.value + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentLanguage.value = lang.value;
|
||||||
|
|
||||||
|
console.log(currentLanguage);
|
||||||
|
|
||||||
|
const i18nStrings = {
|
||||||
|
"zh-CN": {
|
||||||
|
notFoundTitle: "页面未找到",
|
||||||
|
notFoundQuote: "抱歉,但我们无法找到对应的页面。",
|
||||||
|
notFoundActionLink: "回到主页",
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
notFoundTitle: "PAGE NOT FOUND",
|
||||||
|
notFoundQuote: "Sorry, the page you are looking for does not exist.",
|
||||||
|
notFoundActionLink: "Go Home",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Layout } = DefaultTheme
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
<template #not-found>
|
||||||
|
<div class="notFound">
|
||||||
|
<p class="NotFoundCode">404</p>
|
||||||
|
<h1 class="NotFoundTitle">{{ i18nStrings[currentLanguage].notFoundTitle }}</h1>
|
||||||
|
<div class="NotFoundDivider"></div>
|
||||||
|
<blockquote class="NotFoundQuote">
|
||||||
|
<p>
|
||||||
|
{{ i18nStrings[currentLanguage].notFoundQuote }}
|
||||||
|
</p>
|
||||||
|
</blockquote>
|
||||||
|
<div class="NotFoundAction">
|
||||||
|
<a class="NotFoundLink" :href="rootPage" :aria-label="i18nStrings[currentLanguage].notFoundActionLink">
|
||||||
|
{{ i18nStrings[currentLanguage].notFoundActionLink }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.NotFoundCode {
|
||||||
|
font-size: 4rem;
|
||||||
|
line-height: 4rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotFoundTitle {
|
||||||
|
padding-top: 12px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
line-height: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotFoundDivider {
|
||||||
|
margin: 24px auto 18px;
|
||||||
|
width: 64px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--vp-c-divider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notFound {
|
||||||
|
text-align: center;
|
||||||
|
padding: 64px 24px 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotFoundQuote {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 256px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(60, 60, 67, .78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotFoundLink {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--vp-c-brand-1);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 3px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--vp-c-brand-1);
|
||||||
|
transition: border-color .25s, color .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.NotFoundAction{
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.notFound {
|
||||||
|
padding: 96px 32px 168px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
18
doc/.vitepress/theme/index.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// https://vitepress.dev/guide/custom-theme
|
||||||
|
import { h } from "vue";
|
||||||
|
import type { Theme } from "vitepress";
|
||||||
|
import DefaultTheme from "vitepress/theme";
|
||||||
|
import "./style.css";
|
||||||
|
import NotFoundLayout from "./NotFoundLayout.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: DefaultTheme,
|
||||||
|
Layout() {
|
||||||
|
return h(DefaultTheme.Layout, null, {
|
||||||
|
"not-found": () => h(NotFoundLayout)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
enhanceApp({ app, router, siteData }) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
} satisfies Theme;
|
139
doc/.vitepress/theme/style.css
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* Customize default theme styling by overriding CSS variables:
|
||||||
|
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colors
|
||||||
|
*
|
||||||
|
* Each colors have exact same color scale system with 3 levels of solid
|
||||||
|
* colors with different brightness, and 1 soft color.
|
||||||
|
*
|
||||||
|
* - `XXX-1`: The most solid color used mainly for colored text. It must
|
||||||
|
* satisfy the contrast ratio against when used on top of `XXX-soft`.
|
||||||
|
*
|
||||||
|
* - `XXX-2`: The color used mainly for hover state of the button.
|
||||||
|
*
|
||||||
|
* - `XXX-3`: The color for solid background, such as bg color of the button.
|
||||||
|
* It must satisfy the contrast ratio with pure white (#ffffff) text on
|
||||||
|
* top of it.
|
||||||
|
*
|
||||||
|
* - `XXX-soft`: The color used for subtle background such as custom container
|
||||||
|
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
|
||||||
|
* on top of it.
|
||||||
|
*
|
||||||
|
* The soft color must be semi transparent alpha channel. This is crucial
|
||||||
|
* because it allows adding multiple "soft" colors on top of each other
|
||||||
|
* to create a accent, such as when having inline code block inside
|
||||||
|
* custom containers.
|
||||||
|
*
|
||||||
|
* - `default`: The color used purely for subtle indication without any
|
||||||
|
* special meanings attched to it such as bg color for menu hover state.
|
||||||
|
*
|
||||||
|
* - `brand`: Used for primary brand colors, such as link text, button with
|
||||||
|
* brand theme, etc.
|
||||||
|
*
|
||||||
|
* - `tip`: Used to indicate useful information. The default theme uses the
|
||||||
|
* brand color for this by default.
|
||||||
|
*
|
||||||
|
* - `warning`: Used to indicate warning to the users. Used in custom
|
||||||
|
* container, badges, etc.
|
||||||
|
*
|
||||||
|
* - `danger`: Used to show error, or dangerous message to the users. Used
|
||||||
|
* in custom container, badges, etc.
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-c-default-1: var(--vp-c-gray-1);
|
||||||
|
--vp-c-default-2: var(--vp-c-gray-2);
|
||||||
|
--vp-c-default-3: var(--vp-c-gray-3);
|
||||||
|
--vp-c-default-soft: var(--vp-c-gray-soft);
|
||||||
|
|
||||||
|
--vp-c-brand-1: var(--vp-c-indigo-1);
|
||||||
|
--vp-c-brand-2: var(--vp-c-indigo-2);
|
||||||
|
--vp-c-brand-3: var(--vp-c-indigo-3);
|
||||||
|
--vp-c-brand-soft: var(--vp-c-indigo-soft);
|
||||||
|
|
||||||
|
--vp-c-tip-1: var(--vp-c-brand-1);
|
||||||
|
--vp-c-tip-2: var(--vp-c-brand-2);
|
||||||
|
--vp-c-tip-3: var(--vp-c-brand-3);
|
||||||
|
--vp-c-tip-soft: var(--vp-c-brand-soft);
|
||||||
|
|
||||||
|
--vp-c-warning-1: var(--vp-c-yellow-1);
|
||||||
|
--vp-c-warning-2: var(--vp-c-yellow-2);
|
||||||
|
--vp-c-warning-3: var(--vp-c-yellow-3);
|
||||||
|
--vp-c-warning-soft: var(--vp-c-yellow-soft);
|
||||||
|
|
||||||
|
--vp-c-danger-1: var(--vp-c-red-1);
|
||||||
|
--vp-c-danger-2: var(--vp-c-red-2);
|
||||||
|
--vp-c-danger-3: var(--vp-c-red-3);
|
||||||
|
--vp-c-danger-soft: var(--vp-c-red-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Button
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-button-brand-border: transparent;
|
||||||
|
--vp-button-brand-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-bg: var(--vp-c-brand-3);
|
||||||
|
--vp-button-brand-hover-border: transparent;
|
||||||
|
--vp-button-brand-hover-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
|
||||||
|
--vp-button-brand-active-border: transparent;
|
||||||
|
--vp-button-brand-active-text: var(--vp-c-white);
|
||||||
|
--vp-button-brand-active-bg: var(--vp-c-brand-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Home
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-name-color: transparent;
|
||||||
|
--vp-home-hero-name-background: -webkit-linear-gradient(
|
||||||
|
120deg,
|
||||||
|
#bd34fe 30%,
|
||||||
|
#41d1ff
|
||||||
|
);
|
||||||
|
|
||||||
|
--vp-home-hero-image-background-image: linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#bd34fe 50%,
|
||||||
|
#47caff 50%
|
||||||
|
);
|
||||||
|
--vp-home-hero-image-filter: blur(44px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(56px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
|
:root {
|
||||||
|
--vp-home-hero-image-filter: blur(68px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Custom Block
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--vp-custom-block-tip-border: transparent;
|
||||||
|
--vp-custom-block-tip-text: var(--vp-c-text-1);
|
||||||
|
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
|
||||||
|
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component: Algolia
|
||||||
|
* -------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.DocSearch {
|
||||||
|
--docsearch-primary-color: var(--vp-c-brand-1) !important;
|
||||||
|
}
|
||||||
|
|
BIN
doc/favicon.ico
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
doc/img/homepage.dark.png
Normal file
After Width: | Height: | Size: 821 KiB |
BIN
doc/img/homepage.png
Normal file
After Width: | Height: | Size: 798 KiB |
32
doc/index.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "SparkHome"
|
||||||
|
text: "The final browser homepage."
|
||||||
|
tagline: Everything you want, in a magic searchbox.
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: Get started
|
||||||
|
link: /intro
|
||||||
|
- theme: alt
|
||||||
|
text: Doc
|
||||||
|
link: /ref
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
const { isDark } = useData();
|
||||||
|
const dark = ref(isDark);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# Why SparkHome?
|
||||||
|
|
||||||
|
## Simple UI
|
||||||
|
|
||||||
|
SparkHome's intuitive design creates a seamless user experience, allowing you to effortlessly access all the information and resources you need from one place. The sleek and modern interface features a simple search box that becomes your one-stop place for navigating the web, getting answers to questions, and leveling up your productivity with our powerful toolbox.
|
||||||
|
|
||||||
|
<img src="./img/homepage.dark.png" v-if="isDark">
|
||||||
|
<img src="./img/homepage.png" v-else>
|
BIN
doc/zh/img/homepage.dark.png
Normal file
After Width: | Height: | Size: 828 KiB |
BIN
doc/zh/img/homepage.png
Normal file
After Width: | Height: | Size: 799 KiB |
32
doc/zh/index.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
# https://vitepress.dev/reference/default-theme-home-page
|
||||||
|
layout: home
|
||||||
|
|
||||||
|
hero:
|
||||||
|
name: "星火主页"
|
||||||
|
text: "你的终浏览器主页."
|
||||||
|
tagline: 想要的一切,尽在一个搜索框中
|
||||||
|
actions:
|
||||||
|
- theme: brand
|
||||||
|
text: 即刻开始
|
||||||
|
link: ./intro
|
||||||
|
- theme: alt
|
||||||
|
text: 文档
|
||||||
|
link: ./ref
|
||||||
|
---
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useData } from 'vitepress'
|
||||||
|
const { isDark } = useData();
|
||||||
|
const dark = ref(isDark);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
# 我们的优势
|
||||||
|
|
||||||
|
## 极简的界面
|
||||||
|
|
||||||
|
SparkHome 的直观设计打造出无缝的用户体验,让你可以毫不费力地从一个地方访问到所有需要的信息和资源。其精致现代的界面设计了一个简易的搜索框,成为您一站式解决网络导航、获取问题的答案和利用我们强大的工具箱提高生产力的地方。
|
||||||
|
|
||||||
|
<img src="./img/homepage.dark.png" v-if="isDark">
|
||||||
|
<img src="./img/homepage.png" v-else>
|
34
global.d.ts
vendored
@ -1,3 +1,31 @@
|
|||||||
type settings = {
|
import { Suggestion } from "search-engine-autocomplete";
|
||||||
bgBlur: boolean
|
|
||||||
};
|
interface settingsType extends Object{
|
||||||
|
"version": number,
|
||||||
|
"elementBackdrop": boolean,
|
||||||
|
"bgBlur": boolean,
|
||||||
|
"timeShowSecond": boolean,
|
||||||
|
"currentSearchEngine": string,
|
||||||
|
"searchInNewTab": boolean,
|
||||||
|
"searchEngines": {
|
||||||
|
[key: string]: string
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface suggestionsResponse extends Object{
|
||||||
|
suggestions: Suggestion[],
|
||||||
|
query: string,
|
||||||
|
verbatimRelevance: number,
|
||||||
|
time: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type suggestionItem = {
|
||||||
|
suggestion: string,
|
||||||
|
type: string,
|
||||||
|
relativeRelevance?: number,
|
||||||
|
relevance: number,
|
||||||
|
prompt?: string | React.ReactElement,
|
||||||
|
intention?: string | null,
|
||||||
|
probability?: number,
|
||||||
|
confidence?: number,
|
||||||
|
}
|
14
i18n.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
|
||||||
|
// Can be imported from a shared config
|
||||||
|
const locales = ["en-US", "zh-CN"];
|
||||||
|
|
||||||
|
export default getRequestConfig(async ({ locale }) => {
|
||||||
|
// Validate that the incoming `locale` parameter is valid
|
||||||
|
if (!locales.includes(locale as any)) notFound();
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: (await import(`/messages/${locale}.json`)).default
|
||||||
|
};
|
||||||
|
});
|
18
jest.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Config } from 'jest'
|
||||||
|
import nextJest from 'next/jest.js'
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||||
|
dir: './',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const config: Config = {
|
||||||
|
coverageProvider: 'v8',
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
// Add more setup options before each test is run
|
||||||
|
// setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
|
||||||
|
}
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
export default createJestConfig(config)
|
15
lib/base64ToHex.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Converts a base64 string to a hexadecimal string.
|
||||||
|
*
|
||||||
|
* @param {string} base64String - The base64 string to convert.
|
||||||
|
* @return {string} The hexadecimal representation of the base64 string.
|
||||||
|
*/
|
||||||
|
export default function base64ToHex(base64String: string): string {
|
||||||
|
const raw = atob(base64String);
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < raw.length; i++) {
|
||||||
|
const hex = raw.charCodeAt(i).toString(16);
|
||||||
|
result += hex.length === 2 ? hex : "0" + hex;
|
||||||
|
}
|
||||||
|
return result.toUpperCase();
|
||||||
|
}
|
81
lib/browser_history_search/chrome/chrome.sql
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
SELECT processed_url, title,
|
||||||
|
timeK * 4.4 + (visitK + typeK) * 6.5 + typeK * 0.22 + relativeK AS searchRank
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN processed_url LIKE '[query]%' THEN
|
||||||
|
CASE
|
||||||
|
WHEN (title like '% [query]%' or title like '[query]%') THEN 12.5
|
||||||
|
ELSE 12.3
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN processed_url LIKE '%[query]%' THEN 2.5
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN (title like '% [query]%' or title like '[query]%') THEN 1.6
|
||||||
|
ELSE -1
|
||||||
|
END
|
||||||
|
END
|
||||||
|
END AS relativeK
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
(1 / (5.2 * LOG(38, 0.000015 * (strftime('%s', 'now') - last_visit_time / 1000000 - (strftime('%s', '1601-01-01'))) + 1) + 1)) AS timeK,
|
||||||
|
(1 / (-1 * ln(5 * visit_count + 2.71828)) + 1.07) AS visitK,
|
||||||
|
(1 / (-1 * ln(7 * typed_count + 2.71828)) + 1.12) AS typeK,
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(url, '://') > 0 THEN
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), 'www.') = 1 THEN SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), INSTR(SUBSTR(url, INSTR(url, '://') + 3), '.') + 1)
|
||||||
|
ELSE SUBSTR(url, INSTR(url, '://') + 3)
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(url, 'www.') = 1 THEN SUBSTR(url, INSTR(url, '.') + 1)
|
||||||
|
ELSE url
|
||||||
|
END
|
||||||
|
END AS processed_url
|
||||||
|
FROM urls
|
||||||
|
) AS subquery where relativeK > 0 and hidden <> 1
|
||||||
|
) AS subquery
|
||||||
|
group by processed_url
|
||||||
|
ORDER BY searchRank DESC
|
||||||
|
LIMIT 9
|
||||||
|
;
|
||||||
|
|
||||||
|
SELECT processed_url, title,
|
||||||
|
timeK * 4.4 + (visitK + typeK) * 6.5 + typeK * 0.22 + relativeK AS searchRank
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
CASE
|
||||||
|
WHEN processed_url LIKE '[query]%' THEN -1
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN (title like '% [query]%' or title like '[query]%') THEN 1.6
|
||||||
|
ELSE -1
|
||||||
|
END
|
||||||
|
END AS relativeK
|
||||||
|
FROM (
|
||||||
|
SELECT *,
|
||||||
|
(1 / (5.2 * LOG(38, 0.000015 * (strftime('%s', 'now') - last_visit_time / 1000000 - (strftime('%s', '1601-01-01'))) + 1) + 1)) AS timeK,
|
||||||
|
(1 / (-1 * ln(5 * visit_count + 2.71828)) + 1.07) AS visitK,
|
||||||
|
(1 / (-1 * ln(7 * typed_count + 2.71828)) + 1.12) AS typeK,
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(url, '://') > 0 THEN
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), 'www.') = 1 THEN SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), INSTR(SUBSTR(url, INSTR(url, '://') + 3), '.') + 1)
|
||||||
|
ELSE SUBSTR(url, INSTR(url, '://') + 3)
|
||||||
|
END
|
||||||
|
ELSE
|
||||||
|
CASE
|
||||||
|
WHEN INSTR(url, 'www.') = 1 THEN SUBSTR(url, INSTR(url, '.') + 1)
|
||||||
|
ELSE url
|
||||||
|
END
|
||||||
|
END AS processed_url
|
||||||
|
FROM urls
|
||||||
|
) AS subquery where hidden <> 1 and relativeK > 0
|
||||||
|
) AS subquery
|
||||||
|
group by processed_url
|
||||||
|
ORDER BY searchRank DESC
|
||||||
|
LIMIT 6
|
||||||
|
;
|
40
lib/browser_history_search/firefox/firefox.sql
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
moz_places
|
||||||
|
where
|
||||||
|
rev_host like '%amgif.www.'
|
||||||
|
or rev_host like '%amgif.'
|
||||||
|
group by
|
||||||
|
rev_host
|
||||||
|
order by
|
||||||
|
sum(frecency) desc
|
||||||
|
limit
|
||||||
|
8;
|
||||||
|
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
moz_places
|
||||||
|
where
|
||||||
|
rev_host like '%amgif%'
|
||||||
|
or url like '%figma%'
|
||||||
|
group by
|
||||||
|
rev_host
|
||||||
|
order by
|
||||||
|
sum(frecency) desc
|
||||||
|
limit
|
||||||
|
4;
|
||||||
|
|
||||||
|
select
|
||||||
|
*
|
||||||
|
from
|
||||||
|
moz_places
|
||||||
|
where
|
||||||
|
title like '%figma%'
|
||||||
|
group by
|
||||||
|
rev_host
|
||||||
|
order by
|
||||||
|
sum(frecency) desc
|
||||||
|
limit
|
||||||
|
3;
|
8
lib/copy.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function copyToClipboard(value: string){
|
||||||
|
const textarea = document.createElement("textarea");
|
||||||
|
textarea.value = value;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
};
|
17
lib/getSearchEngineName.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { engineTranslation } from "@/components/search/translatedEngineList";
|
||||||
|
import { settingsState } from "@/components/state/settings";
|
||||||
|
import { settingsType } from "@/global";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useRecoilValue } from "recoil";
|
||||||
|
|
||||||
|
export default function(){
|
||||||
|
const settings: settingsType = useRecoilValue(settingsState);
|
||||||
|
const currentEngine = settings.currentSearchEngine;
|
||||||
|
const displayEngine = getName(currentEngine);
|
||||||
|
return displayEngine;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getName(engineKey: string) {
|
||||||
|
const t = useTranslations("Search");
|
||||||
|
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
|
||||||
|
}
|
10
lib/isLocalStorageAvailable.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export default function(){
|
||||||
|
var test = 'test';
|
||||||
|
try {
|
||||||
|
localStorage.setItem(test, test);
|
||||||
|
localStorage.removeItem(test);
|
||||||
|
return true;
|
||||||
|
} catch(e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
31
lib/loadSettings.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import isLocalStorageAvailable from "./isLocalStorageAvailable";
|
||||||
|
|
||||||
|
const defaultSettings = {
|
||||||
|
version: 1,
|
||||||
|
elementBackdrop: true,
|
||||||
|
bgBlur: true,
|
||||||
|
timeShowSecond: false,
|
||||||
|
currentSearchEngine: "google",
|
||||||
|
searchEngines: {
|
||||||
|
"google": "https://www.google.com/search?q=%s",
|
||||||
|
"bing": "https://www.bing.com/search?q=%s",
|
||||||
|
"baidu": "https://www.baidu.com/s?wd=%s",
|
||||||
|
"duckduckgo": "https://duckduckgo.com/?q=%s",
|
||||||
|
"yandex": "https://yandex.com/search/?text=%s",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function (setSettings: any) {
|
||||||
|
if (isLocalStorageAvailable()===false){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (localStorage.getItem("settings") === null) {
|
||||||
|
localStorage.setItem("settings", JSON.stringify(defaultSettings));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localSettings = JSON.parse(localStorage.getItem("settings") as string);
|
||||||
|
setSettings(localSettings);
|
||||||
|
}
|
10
lib/log.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import pino from "pino";
|
||||||
|
|
||||||
|
export const logger = pino({
|
||||||
|
level: process.env.PINO_LOG_LEVEL || "info",
|
||||||
|
formatters: {
|
||||||
|
level: (label) => {
|
||||||
|
return { level: label.toUpperCase() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},pino.destination(`./app.log`));
|
32
lib/nlp/base.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { NLPResult } from "../onesearch/NLPResult";
|
||||||
|
import { stopwords } from "./stopwords";
|
||||||
|
|
||||||
|
export class NLP {
|
||||||
|
result: NLPResult;
|
||||||
|
constructor(
|
||||||
|
public query: String,
|
||||||
|
public task: String,
|
||||||
|
public intentionKeywords?: String[],
|
||||||
|
) {
|
||||||
|
this.result = new NLPResult();
|
||||||
|
}
|
||||||
|
public removeStopwords(extraStopwords: string[] = [], disableDefault: boolean = false){
|
||||||
|
const list = disableDefault ? extraStopwords : stopwords.concat(extraStopwords);
|
||||||
|
if (list.includes(this.query.trim())) {
|
||||||
|
this.query = "";
|
||||||
|
}
|
||||||
|
for (let word of list){
|
||||||
|
this.query = this.query.replace(new RegExp(`\\b${word}\\b`, 'gi'), '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public extractSlots(str: string, useNER = false): string[]{
|
||||||
|
const slots: string[] = [];
|
||||||
|
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
public trim() {
|
||||||
|
this.query = this.query.trim();
|
||||||
|
const wordList = this.query.split(" ").filter(word => word !== "");
|
||||||
|
this.query = wordList.join(" ");
|
||||||
|
}
|
||||||
|
}
|
134
lib/nlp/data/en.json
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"weather.summary": [
|
||||||
|
"how's the weather",
|
||||||
|
"What's going on with the weather?",
|
||||||
|
"Can you give me an update on the weather?",
|
||||||
|
"How's the forecast looking today?",
|
||||||
|
"Give me a summary of the current weather.",
|
||||||
|
"Can you tell me the current weather?",
|
||||||
|
"What is the weather situation at the moment?",
|
||||||
|
"Could you provide a quick weather update?",
|
||||||
|
"Is it raining or sunny outside?",
|
||||||
|
"What's the weather like right now?",
|
||||||
|
"Tell me the current weather conditions.",
|
||||||
|
"How about the weather today?",
|
||||||
|
"Is it a good day to be outside?",
|
||||||
|
"What should I expect in terms of weather today?",
|
||||||
|
"Is there any severe weather to be aware of?",
|
||||||
|
"Can you summarize today's weather forecast?",
|
||||||
|
"What's the weather looking like for the next few hours?",
|
||||||
|
"Is it going to stay this way all day?",
|
||||||
|
"Could you give me a brief overview of the weather?",
|
||||||
|
"What's the general weather situation in our area?",
|
||||||
|
"Is it cloudy or clear outside?",
|
||||||
|
"Any weather alerts I should know about?",
|
||||||
|
"How's the weather looking for outdoor activities?",
|
||||||
|
"What's the forecast saying for today's weather?",
|
||||||
|
"Is it going to be a warm day?",
|
||||||
|
"Are we expecting any storms today?",
|
||||||
|
"What's the weather condition outside my window?",
|
||||||
|
"Is it a typical day for this season in terms of weather?",
|
||||||
|
"how's the weather now?"
|
||||||
|
],
|
||||||
|
|
||||||
|
"weather.temp": [
|
||||||
|
"What's the temperature like right now?",
|
||||||
|
"Can you tell me the current temperature?",
|
||||||
|
"How hot is it outside?",
|
||||||
|
"What's the temperature supposed to be today?",
|
||||||
|
"What is the current temp outside?",
|
||||||
|
"Could you tell me the outdoor temperature?",
|
||||||
|
"Is it cold or warm outside?",
|
||||||
|
"What's the high temperature for today?",
|
||||||
|
"What's the low temperature expected tonight?",
|
||||||
|
"How does the temperature feel outside?",
|
||||||
|
"Is it going to get warmer or cooler today?",
|
||||||
|
"What's the temperature in the shade?",
|
||||||
|
"Can you provide the current temp in Celsius?",
|
||||||
|
"What's the temperature in Fahrenheit right now?",
|
||||||
|
"Is it too hot to be outside?",
|
||||||
|
"What's the temperature like in the morning?",
|
||||||
|
"How about the temperature in the evening?",
|
||||||
|
"Is it warm enough to go swimming?",
|
||||||
|
"What's the temperature in the city center?",
|
||||||
|
"Can you tell me the temp in the nearby area?",
|
||||||
|
"Is it below freezing outside?",
|
||||||
|
"What's the average temperature for today?",
|
||||||
|
"Is the temperature dropping or rising?",
|
||||||
|
"What should I wear considering the temperature?"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.encode": [
|
||||||
|
"Please encode this data with base64: %s",
|
||||||
|
"I need to encode the following data in base64: %s",
|
||||||
|
"Could you encode this string using base64? %s",
|
||||||
|
"Convert this data to b64 encoding: %s",
|
||||||
|
"I want to encode this information with base64: %s",
|
||||||
|
"Help me encode this in base64: %s",
|
||||||
|
"Can you encode this data to base64 format? %s",
|
||||||
|
"b64 encode",
|
||||||
|
"base64 encode",
|
||||||
|
"encode base64 %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.decode": [
|
||||||
|
"Please decode this base64 data: %s",
|
||||||
|
"I have a base64 encoded string that needs decoding: %s",
|
||||||
|
"Could you decode this base64 string for me? %s",
|
||||||
|
"Convert this base64 encoded data back to its original form: %s",
|
||||||
|
"I need to decode this base64 information: %s",
|
||||||
|
"Help me decode this base64 data: %s",
|
||||||
|
"Can you translate this base64 back to normal text? %s",
|
||||||
|
"b64 decode",
|
||||||
|
"base64 decode",
|
||||||
|
"decode base64 %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.encode": [
|
||||||
|
"Please encode this URL: %s",
|
||||||
|
"I need to encode this URL component: %s",
|
||||||
|
"Could you encode this part of the URL? %s",
|
||||||
|
"Convert this URL to its encoded form: %s",
|
||||||
|
"I want to encode this URL for safe transmission: %s",
|
||||||
|
"Help me encode this URL segment: %s",
|
||||||
|
"Can you encode this URL data? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.decode": [
|
||||||
|
"Please decode this URL: %s",
|
||||||
|
"I have an encoded URL that needs decoding: %s",
|
||||||
|
"Could you decode this URL for me? %s",
|
||||||
|
"Convert this encoded URL back to its original form: %s",
|
||||||
|
"I need to decode this URL component: %s",
|
||||||
|
"Help me decode this URL segment: %s",
|
||||||
|
"Can you translate this encoded URL back to normal? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.encode": [
|
||||||
|
"Please encode this HTML entity: %s",
|
||||||
|
"I need to encode this text to HTML entity: %s",
|
||||||
|
"Could you encode this as an HTML entity? %s",
|
||||||
|
"Convert this text to HTML entity encoding: %s",
|
||||||
|
"I want to encode this to prevent HTML interpretation: %s",
|
||||||
|
"Help me encode this into HTML entity: %s",
|
||||||
|
"Can you encode this for HTML usage? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.decode": [
|
||||||
|
"Please decode this HTML entity: %s",
|
||||||
|
"I have an HTML entity that needs decoding: %s",
|
||||||
|
"Could you decode this HTML entity for me? %s",
|
||||||
|
"Convert this HTML entity back to its original text: %s",
|
||||||
|
"I need to decode this HTML entity to plain text: %s",
|
||||||
|
"Help me decode this HTML entity: %s",
|
||||||
|
"Can you translate this HTML entity back to normal text? %s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"None": [
|
||||||
|
"free weather api",
|
||||||
|
"js get timezone",
|
||||||
|
"how",
|
||||||
|
"how's",
|
||||||
|
"how's the"
|
||||||
|
]
|
||||||
|
}
|
124
lib/nlp/data/zh.json
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"weather.summary": [
|
||||||
|
"天气如何",
|
||||||
|
"现在的天气",
|
||||||
|
"今天的天气预报",
|
||||||
|
"现在的天气状况",
|
||||||
|
"今天天气怎么样",
|
||||||
|
"目前是什么天气",
|
||||||
|
"今天的天气概述",
|
||||||
|
"当前天气状况如何",
|
||||||
|
"今天会下雨吗",
|
||||||
|
"今天会下雪吗",
|
||||||
|
"今天晴天吗",
|
||||||
|
"今天的天气状况如何",
|
||||||
|
"现在外面是什么天气",
|
||||||
|
"今天天气好么",
|
||||||
|
"今天适合外出吗",
|
||||||
|
"今天的天气适宜做什么",
|
||||||
|
"今天有没有雾霾",
|
||||||
|
"今天的空气质量如何",
|
||||||
|
"今天的紫外线指数是多少",
|
||||||
|
"今天有没有大风",
|
||||||
|
"今天会不会很冷",
|
||||||
|
"今天的天气会变化吗",
|
||||||
|
"今天晚上的天气如何",
|
||||||
|
"今天夜里会下雨吗",
|
||||||
|
"今天的天气对出行有影响吗",
|
||||||
|
"今天的天气对运动有影响吗",
|
||||||
|
"今天的天气对工作有影响吗",
|
||||||
|
"今天的天气对旅游有影响吗",
|
||||||
|
"今天的天气对健康有影响吗"
|
||||||
|
],
|
||||||
|
"weather.temp": [
|
||||||
|
"现在的温度",
|
||||||
|
"现在多少度",
|
||||||
|
"外面有多热",
|
||||||
|
"明天热不热?",
|
||||||
|
"现在的气温是多少",
|
||||||
|
"今天最高温度是多少",
|
||||||
|
"今天最低温度是多少",
|
||||||
|
"现在外面感觉冷吗",
|
||||||
|
"现在需要穿外套吗",
|
||||||
|
"现在适合穿短袖吗",
|
||||||
|
"现在的温度适合外出吗",
|
||||||
|
"现在的温度适合运动吗",
|
||||||
|
"现在的温度适合睡觉吗",
|
||||||
|
"明天会比今天热吗",
|
||||||
|
"明天会比今天冷吗",
|
||||||
|
"今天的温度变化大吗",
|
||||||
|
"现在的温度适合开空调吗",
|
||||||
|
"现在的温度适合开暖气吗",
|
||||||
|
"室外的温度是多少",
|
||||||
|
"室内的温度是多少",
|
||||||
|
"现在的温度适合种植吗",
|
||||||
|
"现在的温度适合养宠物吗",
|
||||||
|
"现在的温度对健康有影响吗",
|
||||||
|
"现在的温度是否舒适",
|
||||||
|
"现在的温度是否适合工作"
|
||||||
|
],
|
||||||
|
"base64.encode": [
|
||||||
|
"请将数据使用base64编码:%s",
|
||||||
|
"需要将以下数据base64编码:%s",
|
||||||
|
"请将此字符串转为base64:%s",
|
||||||
|
"将数据转为base64编码:%s",
|
||||||
|
"信息base64编码:%s",
|
||||||
|
"请帮忙编码base64:%s",
|
||||||
|
"将数据编码为base64:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"base64.decode": [
|
||||||
|
"请解码这个base64数据:%s",
|
||||||
|
"有base64编码字符串需要解码:%s",
|
||||||
|
"帮忙解码base64:%s",
|
||||||
|
"将base64编码转回原数据:%s",
|
||||||
|
"解码base64信息:%s",
|
||||||
|
"解码这个base64:%s",
|
||||||
|
"将base64转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.encode": [
|
||||||
|
"请编码这个URL:%s",
|
||||||
|
"URL部分需要编码:%s",
|
||||||
|
"请将URL部分编码:%s",
|
||||||
|
"URL编码转换:%s",
|
||||||
|
"安全传输需编码URL:%s",
|
||||||
|
"编码URL段:%s",
|
||||||
|
"URL数据编码:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"url.decode": [
|
||||||
|
"请解码这个URL:%s",
|
||||||
|
"有URL编码需要解码:%s",
|
||||||
|
"解码这个URL:%s",
|
||||||
|
"URL编码转回原URL:%s",
|
||||||
|
"解码URL部分:%s",
|
||||||
|
"解码URL段:%s",
|
||||||
|
"URL编码转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.encode": [
|
||||||
|
"请编码HTML实体:%s",
|
||||||
|
"文本转为HTML实体:%s",
|
||||||
|
"编码为HTML实体:%s",
|
||||||
|
"文本HTML实体编码:%s",
|
||||||
|
"预防HTML解析编码:%s",
|
||||||
|
"HTML实体编码:%s",
|
||||||
|
"文本HTML使用编码:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"html.decode": [
|
||||||
|
"请解码HTML实体:%s",
|
||||||
|
"HTML实体需要解码:%s",
|
||||||
|
"解码HTML实体:%s",
|
||||||
|
"HTML实体转回文本:%s",
|
||||||
|
"HTML实体解码:%s",
|
||||||
|
"解码HTML实体:%s",
|
||||||
|
"HTML实体转文本:%s"
|
||||||
|
],
|
||||||
|
|
||||||
|
"None": [
|
||||||
|
"你好",
|
||||||
|
"为什么计算机使用二进制"
|
||||||
|
]
|
||||||
|
}
|
7
lib/nlp/extract.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function slotExtract(str: string, keywords: string[]) {
|
||||||
|
let r = str;
|
||||||
|
for (let keyword of keywords) {
|
||||||
|
r = r.replace(keyword, "");
|
||||||
|
}
|
||||||
|
return r.trim();
|
||||||
|
}
|
55
lib/nlp/load.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { containerBootstrap } from "@nlpjs/core";
|
||||||
|
// @ts-ignore
|
||||||
|
import { Nlp } from "@nlpjs/nlp";
|
||||||
|
// @ts-ignore
|
||||||
|
import { NluManager, NluNeural } from "@nlpjs/nlu";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangEn } from "@nlpjs/lang-en-min";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangZh } from "@nlpjs/lang-zh";
|
||||||
|
import * as fflate from 'fflate';
|
||||||
|
|
||||||
|
let zh: TrainData = {};
|
||||||
|
let en: TrainData = {};
|
||||||
|
|
||||||
|
type TrainData = {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class NLU {
|
||||||
|
manager: any;
|
||||||
|
inited: boolean = false;
|
||||||
|
async loadIntentionModel() {
|
||||||
|
const container = await containerBootstrap();
|
||||||
|
container.use(Nlp);
|
||||||
|
container.use(LangEn);
|
||||||
|
container.use(LangZh);
|
||||||
|
container.use(NluNeural);
|
||||||
|
const manager = new NluManager({
|
||||||
|
container,
|
||||||
|
locales: ["en", "zh"],
|
||||||
|
nlu: {
|
||||||
|
useNoneFeature: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const response = await fetch("/model");
|
||||||
|
|
||||||
|
const responseBuf = await response.arrayBuffer();
|
||||||
|
const compressed = new Uint8Array(responseBuf);
|
||||||
|
const decompressed = fflate.decompressSync(compressed);
|
||||||
|
const modelText = fflate.strFromU8(decompressed);
|
||||||
|
manager.fromJSON(JSON.parse(modelText));
|
||||||
|
this.manager = manager;
|
||||||
|
// console.log(this.manager);
|
||||||
|
}
|
||||||
|
async init() {
|
||||||
|
await this.loadIntentionModel();
|
||||||
|
this.inited = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
async process(lang: string, text: string): Promise<any> {
|
||||||
|
const actual = await this.manager.process(lang, text);
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
}
|
3
lib/nlp/stopwords.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const stopwords = ["a","about","above","after","again","against","all","am","an","and","any","are","aren't","as","at","be","because","been","before","being","below","between","both","but","by","can't","cannot","could","couldn't","did","didn't","do","does","doesn't","doing","don't","down","during","each","few","for","from","further","had","hadn't","has","hasn't","have","haven't","having","he","he'd","he'll","he's","her","here","here's","hers","herself","him","himself","his","how","how's","i","i'd","i'll","i'm","i've","if","in","into","is","isn't","it","it's","its","itself","let's","me","more","most","mustn't","my","myself","no","nor","not","of","off","on","once","only","or","other","ought","our","ours","ourselves","out","over","own","please","same","shan't","she","she'd","she'll","she's","should","shouldn't","so","some","such","than","that","that's","the","their","theirs","them","themselves","then","there","there's","these","they","they'd","they'll","they're","they've","this","those","through","to","too","under","until","up","very","was","wasn't","we","we'd","we'll","we're","we've","were","weren't","what","what's","when","when's","where","where's","which","while","who","who's","whom","why","why's","with","won't","would","wouldn't","you","you'd","you'll","you're","you've","your","yours","yourself","yourselves"];
|
||||||
|
|
||||||
|
export const convertStopwords = ["transform", "change", "translate", "convert"];
|
76
lib/nlp/train.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// @ts-ignore
|
||||||
|
import { containerBootstrap } from "@nlpjs/core";
|
||||||
|
// @ts-ignore
|
||||||
|
import { Nlp } from "@nlpjs/nlp";
|
||||||
|
// @ts-ignore
|
||||||
|
import { NluManager, NluNeural } from "@nlpjs/nlu";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangEn } from "@nlpjs/lang-en-min";
|
||||||
|
// @ts-ignore
|
||||||
|
import { LangZh } from "@nlpjs/lang-zh";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import * as fflate from 'fflate';
|
||||||
|
|
||||||
|
let zh: TrainData = {};
|
||||||
|
let en: TrainData = {};
|
||||||
|
|
||||||
|
type TrainData = {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function trainIntentionModel() {
|
||||||
|
try {
|
||||||
|
const dataZH = fs.readFileSync("./lib/nlp/data/zh.json", "utf8");
|
||||||
|
const dataEN = fs.readFileSync("./lib/nlp/data/en.json", "utf8");
|
||||||
|
zh = JSON.parse(dataZH);
|
||||||
|
en = JSON.parse(dataEN);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = await containerBootstrap();
|
||||||
|
container.use(Nlp);
|
||||||
|
container.use(LangEn);
|
||||||
|
container.use(LangZh);
|
||||||
|
container.use(NluNeural);
|
||||||
|
const manager = new NluManager({
|
||||||
|
container,
|
||||||
|
locales: ["en", "zh"],
|
||||||
|
nlu: {
|
||||||
|
useNoneFeature: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Adds the utterances and intents for the NLP
|
||||||
|
|
||||||
|
for (const key in zh) {
|
||||||
|
for (const value of zh[key]) {
|
||||||
|
manager.add("zh", value, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in en) {
|
||||||
|
for (const value of en[key]) {
|
||||||
|
manager.add("en", value, key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.train();
|
||||||
|
|
||||||
|
// let actual = await manager.process("en", "base64 decode bilibili");
|
||||||
|
// console.log(actual);
|
||||||
|
// let actualZH = await manager.process("zh", "去除百分号");
|
||||||
|
// console.log(actualZH);
|
||||||
|
|
||||||
|
const resultModel = manager.toJSON();
|
||||||
|
|
||||||
|
const buf = fflate.strToU8(JSON.stringify(resultModel));
|
||||||
|
|
||||||
|
const gzipped = fflate.gzipSync(buf, {
|
||||||
|
filename: 'model.json',
|
||||||
|
mtime: new Date().getTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync("./public/model", Buffer.from(gzipped));
|
||||||
|
}
|
||||||
|
|
||||||
|
trainIntentionModel();
|
16
lib/normalizeHex.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* A description of the entire function.
|
||||||
|
*
|
||||||
|
* @param {string} hexString - The input hexadecimal string to normalize.
|
||||||
|
* @return {string} The normalized hexadecimal string.
|
||||||
|
*/
|
||||||
|
export default function normalizeHex(hexString: string): string {
|
||||||
|
const chunkSize = 4;
|
||||||
|
const chunks: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < hexString.length; i += chunkSize) {
|
||||||
|
chunks.push(hexString.substr(i, chunkSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks.join(' ');
|
||||||
|
}
|
18
lib/normalizeURL.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export function normalizeURL(input: string): string {
|
||||||
|
try {
|
||||||
|
// try to create a URL object
|
||||||
|
const url = new URL(input);
|
||||||
|
// if the URL is valid, return it
|
||||||
|
return url.href;
|
||||||
|
} catch (error) {
|
||||||
|
// if the URL is invalid, try to add the protocol
|
||||||
|
const withHTTP = "http://" + input;
|
||||||
|
try {
|
||||||
|
const urlWithHTTP = new URL(withHTTP);
|
||||||
|
return urlWithHTTP.href;
|
||||||
|
} catch (error) {
|
||||||
|
// if the URL is still invalid, return the original input
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
lib/onesearch/NLPResult.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export class NLPResult {
|
||||||
|
constructor(
|
||||||
|
public suggestion: string | null = null,
|
||||||
|
public intention: string | null = null,
|
||||||
|
public probability: number = 0,
|
||||||
|
public confidence: number = 0,
|
||||||
|
public relevanceBase: number = 2000,
|
||||||
|
public confidenceWeight: number = 0.2,
|
||||||
|
public type: string = "text",
|
||||||
|
public prompt?: string | React.ReactElement
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
get relevance(): number {
|
||||||
|
return this.relevanceBase * this.probability + this.confidence * this.relevanceBase * this.confidenceWeight;
|
||||||
|
}
|
||||||
|
}
|
90
lib/onesearch/baseCheck.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import slotExtract from "../nlp/extract";
|
||||||
|
import removeStopwords from "../nlp/stopwords";
|
||||||
|
import { NLPResult } from "./NLPResult";
|
||||||
|
import { Kbd } from "@nextui-org/react";
|
||||||
|
|
||||||
|
interface KeywordsDict {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IntentionsDict {
|
||||||
|
[key: string]: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validBase64(str: string) {
|
||||||
|
return str.length % 4 == 0 && /^[A-Za-z0-9+/]+[=]{0,2}$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function base64NLP(str: string) {
|
||||||
|
const keywords: KeywordsDict = {
|
||||||
|
base64: 1,
|
||||||
|
b64: 0.95,
|
||||||
|
base: 0.5
|
||||||
|
};
|
||||||
|
let result = new NLPResult(null, null, 0.0, 0.0);
|
||||||
|
for (let keyword of Object.keys(keywords)) {
|
||||||
|
const pos = str.trim().indexOf(keyword);
|
||||||
|
const l = str.length;
|
||||||
|
const w = str.split(" ").length;
|
||||||
|
if (w > 1 && (pos === 0 || pos == l)) {
|
||||||
|
result.probability += keywords[keyword];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const intentions: IntentionsDict = {
|
||||||
|
decode: 0.1,
|
||||||
|
encode: 1
|
||||||
|
};
|
||||||
|
for (let intention of Object.keys(intentions)) {
|
||||||
|
const pos = str.trim().indexOf(intention);
|
||||||
|
const w = str.split(" ").length;
|
||||||
|
if (w > 1 && pos !== -1) {
|
||||||
|
result.confidence += intentions[intention];
|
||||||
|
result.intention = `base64.${intention}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedQuery = str;
|
||||||
|
if (result.intention === "base64.encode") {
|
||||||
|
const blacklist = Object.keys(keywords).concat(Object.keys(intentions)).concat(["convert", "turn"]);
|
||||||
|
processedQuery = slotExtract(str, blacklist);
|
||||||
|
} else if (result.intention === "base64.decode") {
|
||||||
|
processedQuery = removeStopwords(str, Object.keys(keywords).concat(Object.keys(intentions))).trim();
|
||||||
|
}
|
||||||
|
if (result.intention === "base64.decode") {
|
||||||
|
if (validBase64(processedQuery)) {
|
||||||
|
result.confidence = 1;
|
||||||
|
} else {
|
||||||
|
result.confidence = 0;
|
||||||
|
}
|
||||||
|
} else if (validBase64(processedQuery) && result.intention !== "base64.encode") {
|
||||||
|
result.intention = "base64.decode";
|
||||||
|
result.confidence += Math.max(1 / Math.log2(1 / processedQuery.length) + 1, 0);
|
||||||
|
result.probability += Math.max(1 / Math.log2(1 / processedQuery.length) + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.intention) {
|
||||||
|
case "base64.encode":
|
||||||
|
result.suggestion = btoa(processedQuery);
|
||||||
|
result.prompt = (
|
||||||
|
<span>
|
||||||
|
Base64 Encode (Hit <Kbd keys={["enter"]}></Kbd> to copy):
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "base64.decode":
|
||||||
|
if (result.confidence > 0.1) result.suggestion = atob(processedQuery);
|
||||||
|
result.prompt = (
|
||||||
|
<span>
|
||||||
|
Base64 Decode (Hit <Kbd keys={["enter"]}></Kbd> to copy):
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
4
lib/search.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default function(query: string, engine: string, newTab: boolean = true) {
|
||||||
|
if(newTab) window.open(engine.replace("%s", query));
|
||||||
|
else window.location.href = engine.replace("%s", query);
|
||||||
|
}
|
19
lib/telemetering/sendError.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { CLIENT_VERSION } from "../version";
|
||||||
|
|
||||||
|
export function sendError(error: Error) {
|
||||||
|
fetch("/api/error", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
message: error.message,
|
||||||
|
name: error.name,
|
||||||
|
time: new Date().getTime()/1000,
|
||||||
|
version: CLIENT_VERSION,
|
||||||
|
ua: navigator.userAgent,
|
||||||
|
cause: error.cause,
|
||||||
|
stack: error.stack
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
1
lib/url/tldList.ts
Normal file
33
lib/url/validLink.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import punycode from "punycode/";
|
||||||
|
import { tldList } from "./tldList";
|
||||||
|
|
||||||
|
export default function validLink(link: string) {
|
||||||
|
let finalURL = '';
|
||||||
|
try {
|
||||||
|
const url = new URL(link);
|
||||||
|
finalURL = url.origin;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
// if the URL is invalid, try to add the protocol
|
||||||
|
try {
|
||||||
|
const urlWithHTTP = new URL("http://" + link);
|
||||||
|
finalURL = urlWithHTTP.origin;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validTLD(finalURL)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validTLD(domain: string): boolean {
|
||||||
|
const tld = punycode.toUnicode(domain.split(".").reverse()[0]);
|
||||||
|
if (tldList.includes(tld)) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
3
lib/version.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const SPARKHOME_VERSION="4.17.0";
|
||||||
|
export const CLIENT_VERSION="4.17.0";
|
||||||
|
export const NEXT_API_VERSION="4.14.3";
|
39
lib/weather/getCurrentWeather.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export function getClosestHourTimestamp(): string {
|
||||||
|
const now = new Date();
|
||||||
|
now.setMinutes(0, 0, 0); // 设置分钟、秒和毫秒为0
|
||||||
|
|
||||||
|
// 获取本地时间的年份、月份、日期、小时
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(now.getHours()).padStart(2, '0');
|
||||||
|
|
||||||
|
// 拼接成所需的格式
|
||||||
|
const localHourTimestamp = `${year}-${month}-${day}T${hour}:00`;
|
||||||
|
|
||||||
|
return localHourTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findClosestDateIndex(dates: string[], utc_offset_seconds: number): number {
|
||||||
|
const now = new Date();
|
||||||
|
const nowTimestamp = now.getTime();
|
||||||
|
const offsetMilliseconds = utc_offset_seconds * 1000;
|
||||||
|
|
||||||
|
let closestIndex = -1;
|
||||||
|
let closestDiff = Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const date = new Date(dates[i]);
|
||||||
|
const adjustedTimestamp = date.getTime();
|
||||||
|
|
||||||
|
if (adjustedTimestamp <= nowTimestamp) {
|
||||||
|
const diff = nowTimestamp - adjustedTimestamp;
|
||||||
|
if (diff < closestDiff) {
|
||||||
|
closestDiff = diff;
|
||||||
|
closestIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestIndex;
|
||||||
|
}
|
17
lib/weather/getLocation.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
const options = {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 10000,
|
||||||
|
maximumAge: 3600
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocationNative(callback: Function) {
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos: GeolocationPosition) => {
|
||||||
|
callback(pos.coords);
|
||||||
|
},
|
||||||
|
(err: GeolocationPositionError) => {
|
||||||
|
callback(err);
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
}
|
23
lib/weather/getWeather.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export async function getWeather(lat: number, lon: number) {
|
||||||
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
const cacheKey = `weather-cache-${lat.toFixed(2)}-${lon.toFixed(2)}-${timezone}`;
|
||||||
|
const localData = localStorage.getItem(cacheKey);
|
||||||
|
if (localData != null) {
|
||||||
|
console.log('Using cache');
|
||||||
|
const parsedLocalData = JSON.parse(localData);
|
||||||
|
if (parsedLocalData["hourly"]["time"][0] != undefined &&
|
||||||
|
new Date().getTime() - new Date(parsedLocalData["hourly"]["time"][0]).getTime() < 86400 * 1000
|
||||||
|
) {
|
||||||
|
return parsedLocalData;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Cache expired');
|
||||||
|
localStorage.removeItem(cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = `https://api.open-meteo.com/v1/cma?latitude=${lat.toString()}&longitude=${lon.toString()}&hourly=apparent_temperature,precipitation,weather_code&timezone=${encodeURIComponent(timezone)}&forecast_days=1`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const responseJson = await response.json();
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(responseJson));
|
||||||
|
return responseJson;
|
||||||
|
}
|
294
lib/weather/wmocode.ts
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
type WeatherInfo = {
|
||||||
|
description: string;
|
||||||
|
image: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WMOCodeTable = {
|
||||||
|
[key: string]: {
|
||||||
|
day: WeatherInfo;
|
||||||
|
night: WeatherInfo;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export let WMOCodeTable: WMOCodeTable = {
|
||||||
|
"0": {
|
||||||
|
day: {
|
||||||
|
description: "Sunny",
|
||||||
|
image: "http://openweathermap.org/img/wn/01d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Clear",
|
||||||
|
image: "http://openweathermap.org/img/wn/01n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"1": {
|
||||||
|
day: {
|
||||||
|
description: "Mainly Sunny",
|
||||||
|
image: "http://openweathermap.org/img/wn/01d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Mainly Clear",
|
||||||
|
image: "http://openweathermap.org/img/wn/01n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2": {
|
||||||
|
day: {
|
||||||
|
description: "Partly Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/02d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Partly Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/02n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3": {
|
||||||
|
day: {
|
||||||
|
description: "Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/03d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Cloudy",
|
||||||
|
image: "http://openweathermap.org/img/wn/03n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"45": {
|
||||||
|
day: {
|
||||||
|
description: "Foggy",
|
||||||
|
image: "http://openweathermap.org/img/wn/50d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Foggy",
|
||||||
|
image: "http://openweathermap.org/img/wn/50n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"48": {
|
||||||
|
day: {
|
||||||
|
description: "Rime Fog",
|
||||||
|
image: "http://openweathermap.org/img/wn/50d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Rime Fog",
|
||||||
|
image: "http://openweathermap.org/img/wn/50n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"51": {
|
||||||
|
day: {
|
||||||
|
description: "Light Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"53": {
|
||||||
|
day: {
|
||||||
|
description: "Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"55": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"56": {
|
||||||
|
day: {
|
||||||
|
description: "Light Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"57": {
|
||||||
|
day: {
|
||||||
|
description: "Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Freezing Drizzle",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"61": {
|
||||||
|
day: {
|
||||||
|
description: "Light Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"63": {
|
||||||
|
day: {
|
||||||
|
description: "Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"65": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"66": {
|
||||||
|
day: {
|
||||||
|
description: "Light Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"67": {
|
||||||
|
day: {
|
||||||
|
description: "Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Freezing Rain",
|
||||||
|
image: "http://openweathermap.org/img/wn/10n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"71": {
|
||||||
|
day: {
|
||||||
|
description: "Light Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"73": {
|
||||||
|
day: {
|
||||||
|
description: "Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"75": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Snow",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"77": {
|
||||||
|
day: {
|
||||||
|
description: "Snow Grains",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow Grains",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"80": {
|
||||||
|
day: {
|
||||||
|
description: "Light Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"81": {
|
||||||
|
day: {
|
||||||
|
description: "Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"82": {
|
||||||
|
day: {
|
||||||
|
description: "Heavy Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Heavy Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/09n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"85": {
|
||||||
|
day: {
|
||||||
|
description: "Light Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"86": {
|
||||||
|
day: {
|
||||||
|
description: "Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Snow Showers",
|
||||||
|
image: "http://openweathermap.org/img/wn/13n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"95": {
|
||||||
|
day: {
|
||||||
|
description: "Thunderstorm",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Thunderstorm",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"96": {
|
||||||
|
day: {
|
||||||
|
description: "Light Thunderstorms With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Light Thunderstorms With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"99": {
|
||||||
|
day: {
|
||||||
|
description: "Thunderstorm With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11d@2x.png"
|
||||||
|
},
|
||||||
|
night: {
|
||||||
|
description: "Thunderstorm With Hail",
|
||||||
|
image: "http://openweathermap.org/img/wn/11n@2x.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
27
messages/en-US.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"Search": {
|
||||||
|
"placeholder": "Search or type a URL",
|
||||||
|
"engine-aria": "Switch search engine",
|
||||||
|
"engine": {
|
||||||
|
"google": "Google",
|
||||||
|
"baidu": "Baidu",
|
||||||
|
"bing": "Bing",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"yandex": "Yandex",
|
||||||
|
"yahoo": "Yahoo",
|
||||||
|
"ecosia": "Ecosia"
|
||||||
|
},
|
||||||
|
"search-help-text": "Search {engine}"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"title": "Page Not Found"
|
||||||
|
},
|
||||||
|
"About": {
|
||||||
|
"title": "SparkHome"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"base64": {
|
||||||
|
"title": "Base64 tools - LuminaraUtils"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
messages/zh-CN.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"Search": {
|
||||||
|
"placeholder": "搜索或输入网址",
|
||||||
|
"engine-aria": "搜索引擎切换",
|
||||||
|
"engine": {
|
||||||
|
"google": "谷歌",
|
||||||
|
"baidu": "百度",
|
||||||
|
"bing": "必应",
|
||||||
|
"duckduckgo": "DuckDuckGo",
|
||||||
|
"yandex": "Yandex",
|
||||||
|
"yahoo": "雅虎",
|
||||||
|
"ecosia": "Ecosia"
|
||||||
|
},
|
||||||
|
"search-help-text": "{engine} 搜索"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"title": "未找到"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"base64": {
|
||||||
|
"title": "Base64 工具"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
middleware.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import createMiddleware from 'next-intl/middleware';
|
||||||
|
|
||||||
|
export default createMiddleware({
|
||||||
|
// A list of all locales that are supported
|
||||||
|
locales: ['en-US', 'zh-CN'],
|
||||||
|
|
||||||
|
// Used when no locale matches
|
||||||
|
defaultLocale: 'en-US'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
// Match only internationalized pathnames
|
||||||
|
matcher: ['/', '/(zh-CN|en-US)/:path*']
|
||||||
|
};
|
@ -1,4 +1,9 @@
|
|||||||
|
import createNextIntlPlugin from 'next-intl/plugin';
|
||||||
|
|
||||||
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
@ -9,7 +14,8 @@ const nextConfig = {
|
|||||||
pathname: "/*"
|
pathname: "/*"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
output: 'standalone'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default withNextIntl(nextConfig);
|
61
package.json
@ -1,29 +1,64 @@
|
|||||||
{
|
{
|
||||||
"name": "sparkhome",
|
"name": "sparkhome",
|
||||||
"version": "4.1.0",
|
"version": "4.17.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
|
"build:NLP": "tsx ./lib/nlp/train.ts",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"test": "jest",
|
||||||
|
"docs:dev": "vitepress dev doc",
|
||||||
|
"docs:build": "vitepress build doc",
|
||||||
|
"docs:preview": "vitepress preview doc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"next": "14.1.1",
|
"@nextui-org/react": "^2.3.6",
|
||||||
"react": "^18.2.0",
|
"@nlpjs/basic": "^4.27.0",
|
||||||
"react-dom": "^18.2.0",
|
"@nlpjs/builtin-compromise": "^4.26.1",
|
||||||
|
"@nlpjs/core": "^4.26.1",
|
||||||
|
"@nlpjs/lang-en-min": "^4.26.1",
|
||||||
|
"@nlpjs/lang-zh": "^4.26.1",
|
||||||
|
"@nlpjs/nlp": "^4.27.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"fflate": "^0.8.2",
|
||||||
|
"framer-motion": "^11.1.7",
|
||||||
|
"next": "14.1.4",
|
||||||
|
"next-intl": "^3.12.0",
|
||||||
|
"next-themes": "^0.3.0",
|
||||||
|
"openmeteo": "^1.1.4",
|
||||||
|
"pino": "^9.0.0",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
|
"react": "^18.3.0",
|
||||||
|
"react-dom": "^18.3.0",
|
||||||
"recoil": "^0.7.7",
|
"recoil": "^0.7.7",
|
||||||
|
"search-engine-autocomplete": "^0.4.3",
|
||||||
|
"tailwind-merge": "^2.3.0",
|
||||||
|
"unicode-encode": "^1.4.2",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"validate-color": "^2.2.4"
|
"validate-color": "^2.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@iconify-icon/react": "^2.1.0",
|
||||||
"@types/react": "^18",
|
"@jest/globals": "^29.7.0",
|
||||||
"@types/react-dom": "^18",
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
|
"@testing-library/react": "^14.3.1",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/punycode": "^2.1.4",
|
||||||
|
"@types/react": "^18.3.0",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/valid-url": "^1.0.7",
|
"@types/valid-url": "^1.0.7",
|
||||||
"autoprefixer": "^10.0.1",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8",
|
"jest": "^29.7.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"typescript": "^5"
|
"postcss": "^8.4.38",
|
||||||
|
"tailwindcss": "^3.4.3",
|
||||||
|
"ts-jest": "^29.1.2",
|
||||||
|
"tsx": "^4.15.6",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"vitepress": "^1.2.3",
|
||||||
|
"vue": "^3.4.29"
|
||||||
}
|
}
|
||||||
}
|
}
|
9828
pnpm-lock.yaml
BIN
public/model
Normal file
@ -1,20 +1,21 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
import { nextui } from "@nextui-org/react";
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
|
||||||
theme: {
|
],
|
||||||
extend: {
|
theme: {
|
||||||
backgroundImage: {
|
extend: {
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
backgroundImage: {
|
||||||
"gradient-conic":
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))"
|
||||||
},
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
plugins: [nextui()]
|
||||||
plugins: [],
|
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
18
test/NLP/removeStopwords.test.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
import { NLP } from "@/lib/nlp/base";
|
||||||
|
import { convertStopwords } from "@/lib/nlp/stopwords";
|
||||||
|
import { describe, expect, test } from "@jest/globals";
|
||||||
|
|
||||||
|
describe("Test 1", () => {
|
||||||
|
test("basic", () => {
|
||||||
|
const nlp = new NLP("please", "remove-stopword");
|
||||||
|
nlp.removeStopwords();
|
||||||
|
expect(nlp.query).toBe("");
|
||||||
|
});
|
||||||
|
test("convert something", () => {
|
||||||
|
const nlp = new NLP("please convert 1cm to m", "remove-stopword");
|
||||||
|
nlp.removeStopwords(convertStopwords);
|
||||||
|
nlp.trim();
|
||||||
|
expect(nlp.query).toBe("1cm m");
|
||||||
|
});
|
||||||
|
});
|
11
test/base64.test.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { base64NLP } from "@/lib/onesearch/baseCheck";
|
||||||
|
import { describe, expect, test } from "@jest/globals";
|
||||||
|
|
||||||
|
describe("To auto-detect the intention of decoding an base64 string", () => {
|
||||||
|
test("Implicit declaration", () => {
|
||||||
|
expect(base64NLP("base64 encode encode MjM6MjQgQXByIDI1LCAyMDI0").intention).toBe("base64.encode");
|
||||||
|
expect(base64NLP("base64 encode encode MjM6MjQgQXByIDI1LCAyMDI0").suggestion).toBe(
|
||||||
|
"ZW5jb2RlIE1qTTZNalFnUVhCeUlESTFMQ0F5TURJMA=="
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
64
test/validLink.test.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, test } from "@jest/globals";
|
||||||
|
import validLink, { validTLD } from "../lib/url/validLink";
|
||||||
|
|
||||||
|
describe("Check if a string is an accessible domain/URL", () => {
|
||||||
|
test("Plain, full URL", () => {
|
||||||
|
// Plain form
|
||||||
|
expect(validLink("http://example.com")).toBe(true);
|
||||||
|
// With https and path
|
||||||
|
expect(validLink("https://jestjs.io/docs/getting-started/")).toBe(true);
|
||||||
|
// With anchor
|
||||||
|
expect(validLink("https://difftastic.wilfred.me.uk/zh-CN/git.html#git-difftool")).toBe(true);
|
||||||
|
// With params
|
||||||
|
expect(validLink("https://www.bilibili.com/list/ml2252204359?oid=990610203&bvid=BV1sx4y1g7Hh")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Punycode URL", () => {
|
||||||
|
expect(validLink("https://原神大学.com/")).toBe(true);
|
||||||
|
expect(validLink("中国原神大学.com")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Invalid TLD with protocol", () => {
|
||||||
|
expect(validLink("https://www.example.notexist")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Invalid TLD with no protocol", () => {
|
||||||
|
expect(validLink("www.example.notexist")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reference: https://www.iana.org/domains/root/db
|
||||||
|
describe("Check if the given TLD exist and assigned.", () => {
|
||||||
|
test("Valid normal TLD", () => {
|
||||||
|
expect(validTLD("com")).toBe(true);
|
||||||
|
expect(validTLD("top")).toBe(true);
|
||||||
|
expect(validTLD("net")).toBe(true);
|
||||||
|
expect(validTLD("org")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Valid new TLDs", () => {
|
||||||
|
// they really exist!
|
||||||
|
expect(validTLD("foo")).toBe(true);
|
||||||
|
expect(validTLD("bar")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Exist but not assigned TLD", () => {
|
||||||
|
expect(validTLD("active")).toBe(false);
|
||||||
|
expect(validTLD("off")).toBe(false);
|
||||||
|
});
|
||||||
|
test("with dot", () => {
|
||||||
|
expect(validTLD(".com")).toBe(true);
|
||||||
|
expect(validTLD(".us")).toBe(true);
|
||||||
|
expect(validTLD(".cn")).toBe(true);
|
||||||
|
expect(validTLD(".io")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Punycode TLDs", () => {
|
||||||
|
expect(validTLD(".中国")).toBe(true);
|
||||||
|
expect(validTLD(".РФ")).toBe(true);
|
||||||
|
expect(validTLD(".कॉम")).toBe(true);
|
||||||
|
expect(validTLD("ایران")).toBe(true);
|
||||||
|
expect(validTLD("இலங்கை")).toBe(true);
|
||||||
|
expect(validTLD("გე")).toBe(true);
|
||||||
|
expect(validTLD("ポイント")).toBe(true);
|
||||||
|
});
|
||||||
|
test("Punycode TLDs but not assigned", () => {
|
||||||
|
expect(validTLD("テスト")).toBe(false);
|
||||||
|
expect(validTLD("परीक्षा")).toBe(false);
|
||||||
|
expect(validTLD("测试")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|