merge: update from dev
This commit is contained in:
commit
35f90b42d9
@ -1,7 +0,0 @@
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
node_modules
|
||||
npm-debug.log
|
||||
README.md
|
||||
.next
|
||||
.git
|
31
.gitignore
vendored
31
.gitignore
vendored
@ -1,3 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
@ -36,5 +61,9 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
.syncignore
|
||||
|
||||
#log
|
||||
# log
|
||||
app.log
|
||||
|
||||
# doc
|
||||
doc/.vitepress/dist
|
||||
doc/.vitepress/cache
|
@ -3,7 +3,6 @@
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "none",
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
||||
}
|
||||
|
67
Dockerfile
67
Dockerfile
@ -1,67 +0,0 @@
|
||||
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
|
@ -1 +1,3 @@
|
||||
# SparkHome
|
||||
# sparkast
|
||||
|
||||
sparkast is your ultimate productivity tool that gives you the answer to all within a single searchbox.
|
||||
|
@ -1,28 +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,39 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
"use client";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function NotFound() {
|
||||
const t = useTranslations("404");
|
||||
return (
|
||||
<div>
|
||||
<h1>{t('title')}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { RecoilRoot } from "recoil";
|
||||
import Homepage from "../../components";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<Homepage />
|
||||
</RecoilRoot>
|
||||
);
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
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();
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
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));
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
<h1>Not Found</h1>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
// app/providers.tsx
|
||||
'use client'
|
||||
|
||||
import {NextUIProvider} from '@nextui-org/react'
|
||||
|
||||
export function Providers({children}: { children: React.ReactNode }) {
|
||||
return (
|
||||
<NextUIProvider>
|
||||
{children}
|
||||
</NextUIProvider>
|
||||
)
|
||||
}
|
22
backend/route.ts
Normal file
22
backend/route.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Express } from "express";
|
||||
import { completeGoogle } from "search-engine-autocomplete";
|
||||
|
||||
export function configureBackendRoutes(app: Express) {
|
||||
app.get('/api/v1/suggestion', async (req, res) => {
|
||||
const query = req.query.q as string;
|
||||
const t = parseInt(req.query.t as string || "0") || null;
|
||||
let language = req.query.l as string || 'en-US';
|
||||
|
||||
try {
|
||||
const data = await completeGoogle(query, language);
|
||||
//logger.info({ type: "onesearch_search_autocomplete", query: query, data: data });
|
||||
res.json({ ...data, time: t });
|
||||
} catch (error) {
|
||||
//logger.error({ type: "onesearch_search_autocomplete_error", error: error.message });
|
||||
res.status(500).json({ error: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
app.get("/api/v1/ping", async (_, res) => {
|
||||
res.status(200).json({message: "pong"});
|
||||
})
|
||||
}
|
@ -1,30 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { bgFocusState } from "./state/background";
|
||||
import { useAtom } from "jotai";
|
||||
import { bgFocusAtom } from "../lib/state/background";
|
||||
import BackgroundContainer from "./backgroundContainer";
|
||||
import useDarkMode from "lib/darkModeHook";
|
||||
|
||||
export default function () {
|
||||
const [isFocus, setFocus] = useRecoilState(bgFocusState);
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
export default function Background() {
|
||||
const [isFocus, setFocus] = useAtom(bgFocusAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
setDarkMode(colorSchemeQueryList.matches ? true : false);
|
||||
const darkMode = useDarkMode();
|
||||
|
||||
const handleChange = () => {
|
||||
setDarkMode(colorSchemeQueryList.matches ? true : false);
|
||||
};
|
||||
|
||||
colorSchemeQueryList.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
colorSchemeQueryList.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div suppressHydrationWarning>
|
||||
<div>
|
||||
{darkMode ? (
|
||||
<BackgroundContainer src="rgb(23,25,29)" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
|
||||
) : (
|
||||
|
@ -1,11 +1,15 @@
|
||||
import Image from "next/image";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { settingsState } from "./state/settings";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
import validUrl from "valid-url";
|
||||
import validateColor from "validate-color";
|
||||
import { useAtomValue } from "jotai";
|
||||
|
||||
export default function (props: { isFocus: boolean; src: string; darkMode: boolean; onClick: () => void }) {
|
||||
const settings = useRecoilValue(settingsState);
|
||||
export default function BackgroundContainer(props: {
|
||||
isFocus: boolean;
|
||||
src: string;
|
||||
darkMode: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
if (validateColor(props.src)) {
|
||||
return (
|
||||
<div
|
||||
@ -16,7 +20,7 @@ export default function (props: { isFocus: boolean; src: string; darkMode: boole
|
||||
);
|
||||
} else if (validUrl.isWebUri(props.src)) {
|
||||
return (
|
||||
<Image
|
||||
<img
|
||||
src={props.src}
|
||||
className={
|
||||
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
|
||||
@ -24,7 +28,6 @@ export default function (props: { isFocus: boolean; src: string; darkMode: boole
|
||||
}
|
||||
alt="background"
|
||||
onClick={props.onClick}
|
||||
fill={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
|
50
components/engineSelector.tsx
Normal file
50
components/engineSelector.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
import { engineTranslation } from "lib/onesearch/translatedEngineList";
|
||||
import { settingsType } from "global";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import Picker, { PickedItem } from "./picker";
|
||||
|
||||
export default function EngineSelector(props: { className: string }) {
|
||||
const { t } = useTranslation();
|
||||
const settings: settingsType = useAtomValue(settingsAtom);
|
||||
const engines = settings.searchEngines;
|
||||
const currentEngine: string = settings.currentSearchEngine;
|
||||
const [selected, setSelected] = useState(currentEngine);
|
||||
const setSettings = useSetAtom(settingsAtom);
|
||||
let engineList: PickedItem = {};
|
||||
for (const engineKey of Object.keys(engines)) {
|
||||
engineList[engineKey] = getName(engineKey);
|
||||
}
|
||||
|
||||
function getName(engineKey: string) {
|
||||
return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function setEngine(engine: string) {
|
||||
setSettings((oldSettings: settingsType) => {
|
||||
return {
|
||||
...oldSettings,
|
||||
currentSearchEngine: engine
|
||||
};
|
||||
});
|
||||
}
|
||||
if (selected !== currentEngine) {
|
||||
setEngine(selected);
|
||||
}
|
||||
}, [currentEngine, selected, setSettings]);
|
||||
|
||||
return (
|
||||
<Picker
|
||||
selectionItems={engineList}
|
||||
selected={selected}
|
||||
selectionOnChange={(selected) => {
|
||||
setSelected(selected);
|
||||
}}
|
||||
displayContent={getName(selected)}
|
||||
className={props.className}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { settingsState } from "./state/settings";
|
||||
import Search from "./search/search";
|
||||
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() {
|
||||
const settings = useRecoilValue(settingsState);
|
||||
const setFocus = useSetRecoilState(bgFocusState);
|
||||
|
||||
return (
|
||||
<div className="h-full fixed overflow-hidden w-full bg-black">
|
||||
<Time showSecond={settings.timeShowSecond} />
|
||||
<EngineSelector
|
||||
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%] translate-y-[-0.2rem]
|
||||
left-1/2 w-11/12 sm:w-[700px] text:black text-right
|
||||
dark:text-white text-3xl text-shadow-lg z-10"
|
||||
/>
|
||||
<Background />
|
||||
<Search onFocus={() => setFocus(true)} />
|
||||
<Onesearch />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
import { settingsType, suggestionItem } from "@/global";
|
||||
import copyToClipboard from "@/lib/copy";
|
||||
import { normalizeURL } from "@/lib/normalizeURL";
|
||||
import search from "@/lib/search";
|
||||
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,
|
||||
_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;
|
||||
//let clipboard: any;
|
||||
if (selected.type === "QUERY" || selected.type === "default") {
|
||||
search(selected.suggestion, engine, newTab);
|
||||
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
|
41
components/onesearch/handleNLUResult.ts
Normal file
41
components/onesearch/handleNLUResult.ts
Normal file
@ -0,0 +1,41 @@
|
||||
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"]
|
||||
);
|
||||
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/onesearch/link.tsx
Normal file
30
components/onesearch/link.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { normalizeURL } from "lib/normalizeURL";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
interface LinkSuggestionProps {
|
||||
children: React.ReactNode;
|
||||
query: string;
|
||||
selected: boolean;
|
||||
inPage?: boolean;
|
||||
}
|
||||
|
||||
export default function LinkSuggestion(props: LinkSuggestionProps) {
|
||||
const className = props.selected
|
||||
? `w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`
|
||||
: `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`;
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={() => {
|
||||
if (props.inPage) {
|
||||
navigate(props.query);
|
||||
} else {
|
||||
window.open(normalizeURL(props.query));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,31 +1,37 @@
|
||||
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 { queryAtom } from "lib/state/query";
|
||||
import { suggestionItem, suggestionsResponse } from "global";
|
||||
import getSearchEngineName from "lib/onesearch/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 { base64NLP } from "@/lib/onesearch/baseCheck";
|
||||
import { suggestionAtom } from "lib/state/suggestion";
|
||||
import validLink from "lib/url/validLink";
|
||||
import LinkSuggestion from "./link";
|
||||
import { selectedSuggestionAtom } from "lib/state/suggestionSelection";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
import PlainText from "./plainText";
|
||||
import { sendError } from "@/lib/telemetering/sendError";
|
||||
import { sendError } from "lib/telemetering/sendError";
|
||||
import { handleNLUResult } from "./handleNLUResult";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import i18next from "i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { keywordSuggestion } from "lib/onesearch/keywordSuggestion";
|
||||
import { NLUType } from "lib/nlp/load";
|
||||
|
||||
export default function () {
|
||||
const [suggestion, setFinalSuggetsion] = useRecoilState(suggestionsState);
|
||||
export default function OneSearch() {
|
||||
const [suggestion, setFinalSuggetsion] = useAtom(suggestionAtom);
|
||||
const [manager, setManager] = useState(null);
|
||||
const [NLUModel, setNLUModel] = useState<NLUType>();
|
||||
const [NLUModelLoaded, setNLUModelLoaded] = useState(false);
|
||||
const lastRequestTimeRef = useRef(0);
|
||||
const selected = useRecoilValue(selectedSuggestionState);
|
||||
const settings = useRecoilValue(settingsState);
|
||||
const devMode = true;
|
||||
const query = useRecoilValue(queryState);
|
||||
const selected = useAtomValue(selectedSuggestionAtom);
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const devMode = false;
|
||||
const query = useAtomValue(queryAtom);
|
||||
const engineName = getSearchEngineName();
|
||||
const engine = settings.currentSearchEngine;
|
||||
const lang = useLocale();
|
||||
const t = useTranslations("Search");
|
||||
const { t } = useTranslation();
|
||||
const lang = i18next.language;
|
||||
|
||||
useEffect(() => {
|
||||
const time = new Date().getTime().toString();
|
||||
@ -33,18 +39,20 @@ export default function () {
|
||||
cleanSuggestion("QUERY", "NAVIGATION");
|
||||
return;
|
||||
}
|
||||
fetch(`/api/suggestion?q=${query}&l=${lang}&t=${time}&engine=${engine}`)
|
||||
fetch(`/api/v1/suggestion?q=${query}&l=${lang}&t=${time}&engine=${engine}`)
|
||||
.then((res) => res.json())
|
||||
.then((data: suggestionsResponse) => {
|
||||
try {
|
||||
let suggestionToUpdate: suggestionItem[] = data.suggestions;
|
||||
const 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: Error | unknown) {
|
||||
if (error instanceof Error) {
|
||||
sendError(error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -56,10 +64,10 @@ export default function () {
|
||||
function updateSuggestion(data: suggestionItem[]) {
|
||||
setFinalSuggetsion((cur: suggestionItem[]) => {
|
||||
const types: string[] = [];
|
||||
for (let sug of data) {
|
||||
for (const sug of data) {
|
||||
if (!types.includes(sug.type)) types.push(sug.type);
|
||||
}
|
||||
for (let type of types) {
|
||||
for (const type of types) {
|
||||
cur = cur.filter((item) => {
|
||||
return item.type !== type;
|
||||
});
|
||||
@ -79,10 +87,33 @@ export default function () {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
cleanSuggestion("default-link", "default", "text");
|
||||
(async function () {
|
||||
const NLU = await import("lib/nlp/load");
|
||||
const mainNLUModel = new NLU.NLU();
|
||||
setNLUModel(mainNLUModel);
|
||||
setNLUModelLoaded(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (NLUModel === null || NLUModel === undefined) {
|
||||
return;
|
||||
}
|
||||
NLUModel.init().then((nlu: typeof NLUModel) => {
|
||||
setManager(nlu.manager);
|
||||
});
|
||||
}, [NLUModelLoaded]);
|
||||
|
||||
useEffect(() => {
|
||||
cleanSuggestion("default-link", "default", "text", "link");
|
||||
if (validLink(query)) {
|
||||
updateSuggestion([
|
||||
{ type: "default-link", suggestion: query, relevance: 3000, prompt: <span>Go to: </span> },
|
||||
{
|
||||
type: "default-link",
|
||||
suggestion: query,
|
||||
relevance: 3000,
|
||||
prompt: <span>Go to: </span>
|
||||
},
|
||||
{ type: "default", suggestion: query, relevance: 1600 }
|
||||
]);
|
||||
} else {
|
||||
@ -94,9 +125,17 @@ export default function () {
|
||||
}
|
||||
]);
|
||||
}
|
||||
const b64 = base64NLP(query);
|
||||
if (b64.suggestion !== null) {
|
||||
updateSuggestion([b64 as suggestionItem]);
|
||||
|
||||
if (keywordSuggestion(query) !== null) {
|
||||
updateSuggestion([keywordSuggestion(query)!]);
|
||||
}
|
||||
|
||||
if (manager != null) {
|
||||
// @ts-ignore
|
||||
manager.process(query).then((result) => {
|
||||
console.log(result);
|
||||
handleNLUResult(result, updateSuggestion);
|
||||
});
|
||||
}
|
||||
}, [query, engineName]);
|
||||
|
||||
@ -109,7 +148,7 @@ export default function () {
|
||||
<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 })}
|
||||
{t("search.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">
|
||||
@ -129,22 +168,30 @@ export default function () {
|
||||
)}
|
||||
</PlainSearch>
|
||||
);
|
||||
} else if (s.type === "NAVIGATION" || s.type === "default-link") {
|
||||
} else if (
|
||||
s.type === "NAVIGATION" ||
|
||||
s.type === "default-link" ||
|
||||
s.type === "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>}
|
||||
<LinkSuggestion 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>
|
||||
</LinkSuggestion>
|
||||
);
|
||||
} 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>}
|
||||
{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">
|
||||
@ -153,6 +200,25 @@ export default function () {
|
||||
)}
|
||||
</PlainText>
|
||||
);
|
||||
} else if (s.type === "inpage-link") {
|
||||
return (
|
||||
<LinkSuggestion
|
||||
key={i}
|
||||
query={s.suggestion}
|
||||
selected={i == selected}
|
||||
inPage={true}
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
</LinkSuggestion>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</SuggestionBox>
|
@ -1,9 +1,9 @@
|
||||
import search from "@/lib/search";
|
||||
import { settingsState } from "@/components/state/settings";
|
||||
import { useRecoilValue } from "recoil";
|
||||
import { useAtomValue } from "jotai";
|
||||
import search from "lib/search";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
|
||||
export default function (props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||
const settings = useRecoilValue(settingsState);
|
||||
export default function PlainSearch(props: { children: React.ReactNode; query: string; selected: boolean }) {
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const engine = settings.searchEngines[settings.currentSearchEngine];
|
||||
const newTab = settings.searchInNewTab;
|
||||
if (props.selected) {
|
@ -1,4 +1,4 @@
|
||||
export default function (props: { children: React.ReactNode; selected: boolean }) {
|
||||
export default function PlainText(props: { children: React.ReactNode; selected: boolean }) {
|
||||
if (props.selected) {
|
||||
return (
|
||||
<div
|
@ -1,4 +1,4 @@
|
||||
export default function(props: { children: React.ReactNode }) {
|
||||
export default function Suggestion(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>
|
11
components/onesearch/suggestionBox.tsx
Normal file
11
components/onesearch/suggestionBox.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export default function SuggestionBox(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 dark:text-white
|
||||
${props.children ? "opacity-100" : "opacity-0"}`}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
178
components/picker.tsx
Normal file
178
components/picker.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import { HTMLAttributes, RefObject, useEffect, useRef, useState } from "react";
|
||||
import { selectedOnChange } from "./selectorItem";
|
||||
import { createPortal } from "react-dom";
|
||||
import { Icon } from "@iconify/react";
|
||||
import React from "react";
|
||||
|
||||
export type selectionType = string;
|
||||
|
||||
interface PickerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
selected: selectionType;
|
||||
selectionOnChange: selectedOnChange;
|
||||
displayContent: string;
|
||||
selectionItems: PickedItem;
|
||||
}
|
||||
|
||||
export interface PickedItem {
|
||||
[key: string]: selectionType;
|
||||
}
|
||||
|
||||
export default function Picker(props: PickerProps) {
|
||||
const itemListRef: RefObject<HTMLDivElement> = useRef(null);
|
||||
const buttonRef: RefObject<HTMLButtonElement> = useRef(null);
|
||||
const [displayList, setDisplayList] = useState(false);
|
||||
|
||||
const updatePosition = () => {
|
||||
if (itemListRef.current == null || buttonRef.current == null) {
|
||||
return;
|
||||
}
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const listRect = itemListRef.current.getBoundingClientRect();
|
||||
// Align to center
|
||||
itemListRef.current.style.left =
|
||||
Math.max(
|
||||
Math.min(
|
||||
buttonRect.x + buttonRect.width / 2 - listRect.width / 2,
|
||||
window.screen.width - listRect.width - 16
|
||||
),
|
||||
0
|
||||
) + "px";
|
||||
if (window.screen.height - buttonRect.top < 192) {
|
||||
itemListRef.current.style.transformOrigin = "bottom center";
|
||||
itemListRef.current.style.top = buttonRect.top - listRect.height - 16 + "px";
|
||||
} else {
|
||||
itemListRef.current.style.top = buttonRect.y + buttonRect.height + 16 + "px";
|
||||
}
|
||||
if (listRect.top + listRect.height > window.screen.height - 16) {
|
||||
itemListRef.current.style.height = window.screen.height - listRect.top - 12 + "px";
|
||||
} else {
|
||||
itemListRef.current.style.height = "fit-content";
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updatePosition();
|
||||
const handleResize = () => {
|
||||
updatePosition();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
// Cleanup event listener on component unmount
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, [itemListRef, buttonRef]);
|
||||
|
||||
function toggleDisplay(targetState?: boolean) {
|
||||
function hideList() {
|
||||
if (itemListRef.current) {
|
||||
itemListRef.current.style.transitionDuration = "200ms";
|
||||
itemListRef.current.style.opacity = "0%";
|
||||
}
|
||||
setTimeout(() => {
|
||||
setDisplayList(false);
|
||||
}, 200);
|
||||
}
|
||||
function showList() {
|
||||
setDisplayList(true);
|
||||
setTimeout(() => {
|
||||
if (!itemListRef.current || !buttonRef.current) {
|
||||
return;
|
||||
}
|
||||
updatePosition();
|
||||
if (window.screen.height - buttonRef.current.getBoundingClientRect().top < 128) {
|
||||
itemListRef.current.style.transformOrigin = "bottom center";
|
||||
}
|
||||
itemListRef.current.style.transitionDuration = "100ms";
|
||||
itemListRef.current.style.opacity = "100%";
|
||||
updatePosition();
|
||||
const listRect = itemListRef.current.getBoundingClientRect();
|
||||
if (listRect.top < 8) {
|
||||
itemListRef.current.style.height = window.screen.height - 8 + "px";
|
||||
itemListRef.current.style.top = "8px";
|
||||
}
|
||||
}, 20);
|
||||
}
|
||||
if (targetState === true) {
|
||||
showList();
|
||||
} else if (targetState === false) {
|
||||
hideList();
|
||||
} else if (displayList === true) {
|
||||
hideList();
|
||||
} else {
|
||||
showList();
|
||||
}
|
||||
}
|
||||
|
||||
const { displayContent, selectionOnChange, selectionItems, selected, ...rest } = props;
|
||||
return (
|
||||
<div {...rest}>
|
||||
<button
|
||||
className="relative border-2 border-gray-500 dark:border-gray-300
|
||||
rounded-xl dark:text-white px-4 py-2"
|
||||
ref={buttonRef}
|
||||
onClick={() => {
|
||||
toggleDisplay();
|
||||
}}
|
||||
>
|
||||
{displayContent}
|
||||
</button>
|
||||
{displayList && (
|
||||
<PickerList
|
||||
ref={itemListRef}
|
||||
selected={selected}
|
||||
selectionOnChange={selectionOnChange}
|
||||
selectionItems={selectionItems}
|
||||
toggleDisplay={toggleDisplay}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PickerListProps {
|
||||
selected: selectionType;
|
||||
selectionOnChange: selectedOnChange;
|
||||
selectionItems: PickedItem;
|
||||
toggleDisplay: Function;
|
||||
}
|
||||
|
||||
const PickerList = React.forwardRef<HTMLDivElement, PickerListProps>((props, ref) => {
|
||||
const { selected, selectionOnChange, selectionItems, toggleDisplay } = props;
|
||||
|
||||
return createPortal(
|
||||
<div className="absolute w-screen h-screen" onClick={()=>{toggleDisplay(false)}}>
|
||||
<div
|
||||
ref={ref}
|
||||
className="overflow-y-auto fixed w-fit text-black dark:text-white opacity-0 duration-200
|
||||
bg-zinc-50 shadow-lg border-1 border-zinc-200 dark:border-zinc-600
|
||||
dark:bg-zinc-800 px-2 py-2 rounded-xl text-align-left"
|
||||
style={{ transformOrigin: "top center" }}
|
||||
>
|
||||
{Object.keys(selectionItems).map((key: string, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="relative py-2 w-full min-w-32 pl-2 cursor-pointer rounded-lg
|
||||
hover:bg-zinc-200 dark:hover:bg-zinc-700 flex justify-between items-center"
|
||||
onClick={() => {
|
||||
selectionOnChange(key);
|
||||
toggleDisplay(false);
|
||||
}}
|
||||
>
|
||||
<span>{selectionItems[key]}</span>
|
||||
<div className="relative w-16"></div>
|
||||
{key === selected && (
|
||||
<Icon className="relative right-2" icon="carbon:checkmark" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
});
|
||||
|
||||
PickerList.displayName = "PickerList";
|
@ -1,24 +1,21 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
import { queryAtom } from "lib/state/query";
|
||||
import { selectedSuggestionAtom } from "lib/state/suggestionSelection";
|
||||
import handleEnter from "lib/onesearch/handleEnter";
|
||||
import { suggestionAtom } from "lib/state/suggestion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
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 { t } = useTranslation();
|
||||
const settings = useAtomValue(settingsAtom);
|
||||
const [query, setQuery] = useAtom(queryAtom);
|
||||
const [selectedSuggestion, setSelected] = useAtom(selectedSuggestionAtom);
|
||||
const suggestions = useAtomValue(suggestionAtom);
|
||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
let style = "default";
|
||||
const style = "default";
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key == "Enter") {
|
||||
@ -42,16 +39,17 @@ export default function Search(props: { onFocus: () => void }) {
|
||||
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
|
||||
<input
|
||||
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-md focus:shadow-none dark:shadow-zinc-800 dark:shadow-md bg-white dark:bg-[rgb(23,25,29)]
|
||||
text-center outline-none border-2 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:placeholder:text-slate-400 text-slate-900 dark:text-white"
|
||||
id="searchBox"
|
||||
type="text"
|
||||
placeholder={t("placeholder")}
|
||||
placeholder={t("search.placeholder")}
|
||||
onFocus={props.onFocus}
|
||||
onKeyDown={handleKeydown}
|
||||
onChange={(e) =>
|
||||
setQuery((_) => {
|
||||
setQuery(() => {
|
||||
return e.target.value;
|
||||
})
|
||||
}
|
||||
@ -81,7 +79,7 @@ export default function Search(props: { onFocus: () => void }) {
|
||||
}
|
||||
id="searchBox"
|
||||
type="text"
|
||||
placeholder={t("placeholder")}
|
||||
placeholder="placeholder"
|
||||
onFocus={props.onFocus}
|
||||
ref={searchBoxRef}
|
||||
/>
|
@ -1,77 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
12
components/selectorItem.tsx
Normal file
12
components/selectorItem.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { ReactNode } from "react";
|
||||
import { selectionType } from "./picker";
|
||||
|
||||
export type selectedOnChange = (target: selectionType) => void;
|
||||
|
||||
export default function SelectionItem(props: {key: selectionType, children: ReactNode, onChange: selectedOnChange}){
|
||||
return (
|
||||
<div onClick={() => props.onChange(props.key)}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { atom, selector } from "recoil";
|
||||
|
||||
const bgFocusState = atom({
|
||||
key: "isBackgroundFocus",
|
||||
default: false
|
||||
});
|
||||
|
||||
export {
|
||||
bgFocusState,
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
const queryState = atom({
|
||||
key: "searchQuery",
|
||||
default: ""
|
||||
});
|
||||
|
||||
export {
|
||||
queryState,
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
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({
|
||||
key: "settings",
|
||||
default: defaultSettings,
|
||||
effects_UNSTABLE: [localStorageEffect("settings")]
|
||||
});
|
||||
|
||||
export { settingsState };
|
@ -1,11 +0,0 @@
|
||||
import { suggestionItem } from "@/global";
|
||||
import { atom } from "recoil";
|
||||
|
||||
const suggestionsState = atom({
|
||||
key: "oneSearchSuggestions",
|
||||
default: [] as suggestionItem[]
|
||||
});
|
||||
|
||||
export {
|
||||
suggestionsState,
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
import { atom } from "recoil";
|
||||
|
||||
const selectedSuggestionState = atom({
|
||||
key: "selectedSuggestion",
|
||||
default: 0
|
||||
});
|
||||
|
||||
export {
|
||||
selectedSuggestionState,
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useFormatter } from "next-intl";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function Time(props: {
|
||||
showSecond: boolean
|
||||
}) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const format = useFormatter();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -35,15 +33,15 @@ export default function Time(props: {
|
||||
<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"
|
||||
dark:text-white text-3xl text-left text-shadow-lg"
|
||||
>
|
||||
{formatTime()}{" "}
|
||||
<span className="text-lg leading-9 relative">
|
||||
{format.dateTime(currentTime, {
|
||||
{new Intl.DateTimeFormat(navigator.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
})}
|
||||
}).format(currentTime)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,32 +0,0 @@
|
||||
"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>
|
||||
);
|
||||
}
|
6
global.d.ts
vendored
6
global.d.ts
vendored
@ -1,6 +1,6 @@
|
||||
import { Suggestion } from "search-engine-autocomplete";
|
||||
|
||||
interface settingsType extends Object{
|
||||
interface settingsType extends object{
|
||||
"version": number,
|
||||
"elementBackdrop": boolean,
|
||||
"bgBlur": boolean,
|
||||
@ -10,9 +10,9 @@ interface settingsType extends Object{
|
||||
"searchEngines": {
|
||||
[key: string]: string
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface suggestionsResponse extends Object{
|
||||
interface suggestionsResponse extends object{
|
||||
suggestions: Suggestion[],
|
||||
query: string,
|
||||
verbatimRelevance: number,
|
||||
|
14
i18n.ts
14
i18n.ts
@ -1,14 +0,0 @@
|
||||
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
|
||||
};
|
||||
});
|
5
i18n/ar.json
Executable file
5
i18n/ar.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "ابحث أو اكتب عنوان URL"
|
||||
}
|
||||
}
|
5
i18n/de.json
Executable file
5
i18n/de.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Suche oder gib eine URL ein"
|
||||
}
|
||||
}
|
33
i18n/en.json
Executable file
33
i18n/en.json
Executable file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"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}"
|
||||
},
|
||||
"notfound": {
|
||||
"title": "page not found",
|
||||
"desc": "Please check if there is a typo in the URL. <br/>If SparkHome brought you to this page,<br/> please <a style=\"text-decoration:underline;\" href=\"mailto:contact@alikia2x.com\">contact us.</a>"
|
||||
},
|
||||
"about": {
|
||||
"title": "SparkHome"
|
||||
},
|
||||
"tools": {
|
||||
"base64": {
|
||||
"title": "Base64 tools - LuminaraUtils",
|
||||
"decode": "Decode",
|
||||
"encode": "Encode",
|
||||
"result": "Result: ",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied"
|
||||
}
|
||||
}
|
||||
}
|
5
i18n/es.json
Executable file
5
i18n/es.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Buscar o escribir una URL"
|
||||
}
|
||||
}
|
5
i18n/fr.json
Executable file
5
i18n/fr.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Rechercher ou saisir une URL"
|
||||
}
|
||||
}
|
5
i18n/it.json
Executable file
5
i18n/it.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Cerca o digita un URL"
|
||||
}
|
||||
}
|
12
i18n/ja.json
Normal file
12
i18n/ja.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"search": {
|
||||
"placeholder": "検索またはURLを入力"
|
||||
},
|
||||
"Search": {
|
||||
"engine-aria": "検索エンジンを切り替える",
|
||||
"engine": {
|
||||
"google": "Google"
|
||||
},
|
||||
"placeholder": "検索またはURLを入力"
|
||||
}
|
||||
}
|
5
i18n/ko.json
Executable file
5
i18n/ko.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "검색 또는 URL 입력"
|
||||
}
|
||||
}
|
5
i18n/pt.json
Executable file
5
i18n/pt.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Pesquisar ou digitar uma URL"
|
||||
}
|
||||
}
|
5
i18n/ru.json
Executable file
5
i18n/ru.json
Executable file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"search" : {
|
||||
"placeholder" : "Искать или ввести URL"
|
||||
}
|
||||
}
|
53
i18n/zh.json
Normal file
53
i18n/zh.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"search": {
|
||||
"placeholder": "搜索或输入网址",
|
||||
"engine-aria": "切换搜索引擎",
|
||||
"engine": {
|
||||
"google": "谷歌",
|
||||
"baidu": "百度",
|
||||
"bing": "必应",
|
||||
"duckduckgo": "DuckDuckGo",
|
||||
"yandex": "Yandex",
|
||||
"yahoo": "雅虎",
|
||||
"ecosia": "Ecosia"
|
||||
},
|
||||
"search-help-text": "用 {engine} 搜索"
|
||||
},
|
||||
"Search": {
|
||||
"placeholder": "搜索或输入网址",
|
||||
"engine-aria": "切换搜索引擎",
|
||||
"engine": {
|
||||
"google": "谷歌",
|
||||
"baidu": "百度",
|
||||
"bing": "必应",
|
||||
"duckduckgo": "DuckDuckGo",
|
||||
"yandex": "Yandex",
|
||||
"yahoo": "雅虎",
|
||||
"ecosia": "Ecosia"
|
||||
},
|
||||
"search-help-text": "用 {engine} 搜索"
|
||||
},
|
||||
"404": {
|
||||
"title": "页面未找到"
|
||||
},
|
||||
"About": {
|
||||
"title": "星火主页"
|
||||
},
|
||||
"tools": {
|
||||
"base64": {
|
||||
"title": "Base64 工具",
|
||||
"decode": "解码",
|
||||
"encode": "编码",
|
||||
"result": "结果: ",
|
||||
"copy": "复制",
|
||||
"copied": "已复制"
|
||||
}
|
||||
},
|
||||
"notfound": {
|
||||
"desc": "请检查网址是否出错。 <br/>如果你从星火主页跳转到这里,<br/> 请 <a style=\"text-decoration:underline;\" href=\"mailto:contact@alikia2x.com\">联系我们</a>",
|
||||
"title": "网页不存在"
|
||||
},
|
||||
"about": {
|
||||
"title": "星火主页"
|
||||
}
|
||||
}
|
13
index.html
Normal file
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>SparkHome</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
@ -1,18 +0,0 @@
|
||||
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)
|
@ -1,15 +0,0 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
@ -5,4 +5,4 @@ export default function copyToClipboard(value: string){
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
};
|
||||
}
|
23
lib/darkModeHook.ts
Normal file
23
lib/darkModeHook.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
// Custom React Hook for dark mode detect
|
||||
export default function useDarkMode() {
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
setDarkMode(colorSchemeQueryList.matches);
|
||||
|
||||
const handleChange = () => {
|
||||
setDarkMode(colorSchemeQueryList.matches);
|
||||
};
|
||||
|
||||
colorSchemeQueryList.addEventListener("change", handleChange);
|
||||
|
||||
return () => {
|
||||
colorSchemeQueryList.removeEventListener("change", handleChange);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return darkMode;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export default function(){
|
||||
var test = 'test';
|
||||
try {
|
||||
localStorage.setItem(test, test);
|
||||
localStorage.removeItem(test);
|
||||
return true;
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
}
|
12839
lib/license.txt
Normal file
12839
lib/license.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,31 +0,0 @@
|
||||
'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
10
lib/log.ts
@ -1,10 +0,0 @@
|
||||
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`));
|
147
lib/nlp/data/en.json
Normal file
147
lib/nlp/data/en.json
Normal file
@ -0,0 +1,147 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
|
||||
"ai.command": [
|
||||
"write a typescript helloworld code",
|
||||
"Check the following content for grammar and clarity",
|
||||
"Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.",
|
||||
"Improve this markdown content in asepcts like grammar and expression, for a GitHub repo README.",
|
||||
"can u think of a short name of my package",
|
||||
"simplify this code"
|
||||
],
|
||||
|
||||
"ai.answer": [
|
||||
"Which framework do you think is the most suitable for performance sensitive projects?"
|
||||
],
|
||||
|
||||
"None": [
|
||||
"free weather api",
|
||||
"js get timezone",
|
||||
"how",
|
||||
"how's",
|
||||
"how's the"
|
||||
]
|
||||
}
|
127
lib/nlp/data/zh.json
Normal file
127
lib/nlp/data/zh.json
Normal file
@ -0,0 +1,127 @@
|
||||
{
|
||||
"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": [
|
||||
"你好",
|
||||
"为什么计算机使用二进制",
|
||||
"什么是",
|
||||
"热",
|
||||
"怎么"
|
||||
]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export default function slotExtract(str: string, keywords: string[]) {
|
||||
let r = str;
|
||||
for (let keyword of keywords) {
|
||||
r = r.replace(keyword, "");
|
||||
}
|
||||
return r.trim();
|
||||
}
|
56
lib/nlp/load.ts
Normal file
56
lib/nlp/load.ts
Normal file
@ -0,0 +1,56 @@
|
||||
// @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';
|
||||
|
||||
export interface NLUType {
|
||||
manager: any;
|
||||
inited: boolean;
|
||||
loadIntentionModel(): Promise<void>;
|
||||
init(): Promise<this>;
|
||||
process(lang: string, text: string): Promise<any>;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
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","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 default function removeStopwords(str: string, extraStopwords: string[] = [], disableDefault: boolean = false){
|
||||
const list = disableDefault ? extraStopwords : stopwords.concat(extraStopwords);
|
||||
for (let word of list){
|
||||
str = str.replace(new RegExp(`\\b${word}\\b`, 'gi'), '');
|
||||
}
|
||||
return str;
|
||||
}
|
71
lib/nlp/train.ts
Normal file
71
lib/nlp/train.ts
Normal file
@ -0,0 +1,71 @@
|
||||
// @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();
|
||||
|
||||
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();
|
@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 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(' ');
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
export function normalizeURL(input: string): string {
|
||||
try {
|
||||
// try to create a URL object
|
||||
const url = new URL(input);
|
||||
const url = new URL(input, window.location.href);
|
||||
// if the URL is valid, return it
|
||||
return url.href;
|
||||
} catch (error) {
|
||||
|
@ -1,17 +0,0 @@
|
||||
export class NLPResult {
|
||||
constructor(
|
||||
public suggestion: string | null,
|
||||
public intention: string | null,
|
||||
public probability: number,
|
||||
public confidence: number,
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
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;
|
||||
}
|
17
lib/onesearch/getSearchEngineName.ts
Normal file
17
lib/onesearch/getSearchEngineName.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { engineTranslation } from "lib/onesearch/translatedEngineList";
|
||||
import { settingsAtom } from "lib/state/settings";
|
||||
import { settingsType } from "global";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function(){
|
||||
const settings: settingsType = useAtomValue(settingsAtom);
|
||||
const currentEngine = settings.currentSearchEngine;
|
||||
const displayEngine = getName(currentEngine);
|
||||
return displayEngine;
|
||||
}
|
||||
|
||||
function getName(engineKey: string) {
|
||||
const { t } = useTranslation();
|
||||
return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey;
|
||||
}
|
28
lib/onesearch/handleEnter.ts
Normal file
28
lib/onesearch/handleEnter.ts
Normal file
@ -0,0 +1,28 @@
|
||||
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;
|
||||
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") {
|
||||
copyToClipboard(selected.suggestion);
|
||||
searchBoxRef.current?.focus();
|
||||
} else if (selected.type === "link") {
|
||||
window.open(normalizeURL(selected.suggestion));
|
||||
} else if (selected.type === "inpage-link") {
|
||||
location.href = normalizeURL(selected.suggestion);
|
||||
}
|
||||
}
|
39
lib/onesearch/keywordSuggestion.ts
Normal file
39
lib/onesearch/keywordSuggestion.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { suggestionItem } from "global";
|
||||
|
||||
interface keywordLinkDict {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const dict_en: keywordLinkDict = {
|
||||
about: "/about"
|
||||
};
|
||||
|
||||
const dict_cn: keywordLinkDict = {
|
||||
关于: "/about"
|
||||
};
|
||||
|
||||
export function keywordSuggestion(query: string) {
|
||||
for (const keyword in dict_cn) {
|
||||
if (query.includes(keyword)) {
|
||||
const result: suggestionItem = {
|
||||
type: "inpage-link",
|
||||
suggestion: dict_cn[keyword],
|
||||
prompt: keyword,
|
||||
relevance: 3000
|
||||
};
|
||||
return result
|
||||
}
|
||||
}
|
||||
for (const keyword in dict_en) {
|
||||
if (query.includes(keyword)) {
|
||||
const result: suggestionItem = {
|
||||
type: "inpage-link",
|
||||
suggestion: dict_en[keyword],
|
||||
prompt: keyword,
|
||||
relevance: 3000
|
||||
};
|
||||
return result
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
5
lib/state/background.ts
Normal file
5
lib/state/background.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
const bgFocusAtom = atom(false);
|
||||
|
||||
export { bgFocusAtom };
|
5
lib/state/query.ts
Normal file
5
lib/state/query.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
const queryAtom = atom("");
|
||||
|
||||
export { queryAtom };
|
24
lib/state/settings.ts
Normal file
24
lib/state/settings.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { settingsType } from "global";
|
||||
import { atomWithStorage } from 'jotai/utils'
|
||||
|
||||
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 settingsAtom = atomWithStorage('settings', defaultSettings);
|
||||
|
||||
export { settingsAtom };
|
6
lib/state/suggestion.ts
Normal file
6
lib/state/suggestion.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { suggestionItem } from "global";
|
||||
import { atom } from "jotai";
|
||||
|
||||
const suggestionAtom = atom([] as suggestionItem[]);
|
||||
|
||||
export { suggestionAtom };
|
5
lib/state/suggestionSelection.ts
Normal file
5
lib/state/suggestionSelection.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
|
||||
const selectedSuggestionAtom = atom(0);
|
||||
|
||||
export { selectedSuggestionAtom };
|
@ -1,7 +1,9 @@
|
||||
import { CLIENT_VERSION } from "../version";
|
||||
import pjson from "package.json"
|
||||
|
||||
const CLIENT_VERSION = pjson.version;
|
||||
|
||||
export function sendError(error: Error) {
|
||||
fetch("/api/error", {
|
||||
fetch("/api/v1/error", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
File diff suppressed because one or more lines are too long
1447
lib/url/tlds.txt
Normal file
1447
lib/url/tlds.txt
Normal file
File diff suppressed because it is too large
Load Diff
3
lib/url/updateTLD.ts
Normal file
3
lib/url/updateTLD.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const response = await fetch("https://data.iana.org/TLD/tlds-alpha-by-domain.txt");
|
||||
const tldText = await response.text();
|
||||
await Bun.write("./lib/url/tlds.txt", tldText);
|
@ -1,22 +1,26 @@
|
||||
import punycode from "punycode/";
|
||||
import { tldList } from "./tldList";
|
||||
import { toASCII } from "tr46";
|
||||
import { getTLD } from "./tldList";
|
||||
|
||||
export default function validLink(link: string) {
|
||||
let finalURL = '';
|
||||
let finalURL;
|
||||
try {
|
||||
const url = new URL(link);
|
||||
finalURL = url.origin;
|
||||
finalURL = url;
|
||||
return true;
|
||||
} catch (error) {
|
||||
// if the URL is invalid, try to add the protocol
|
||||
try {
|
||||
const urlWithHTTP = new URL("http://" + link);
|
||||
finalURL = urlWithHTTP.origin;
|
||||
finalURL = urlWithHTTP;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (validTLD(finalURL)) {
|
||||
if (
|
||||
validTLD(finalURL.host) ||
|
||||
isValidIPv6(finalURL.host.slice(1, finalURL.host.length - 1)) ||
|
||||
isValidIPv4(finalURL.host)
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
@ -24,10 +28,63 @@ export default function validLink(link: string) {
|
||||
}
|
||||
|
||||
export function validTLD(domain: string): boolean {
|
||||
const tld = punycode.toUnicode(domain.split(".").reverse()[0]);
|
||||
if (tldList.includes(tld)) {
|
||||
if (!domain.includes(".")) return false;
|
||||
const tld = toASCII(domain.split(".").reverse()[0]);
|
||||
const tldList = getTLD();
|
||||
if (tldList.includes(tld.toUpperCase())) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidIPv6(ip: string): boolean {
|
||||
const length = ip.length;
|
||||
let groups = 1;
|
||||
let groupDigits = 0;
|
||||
let doubleColonCount = 0;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const char = ip[i];
|
||||
if ("0" <= char && char <= "9") {
|
||||
groupDigits++;
|
||||
} else if ("a" <= char && char <= "f") {
|
||||
groupDigits++;
|
||||
} else if ("A" <= char && char <= "F") {
|
||||
groupDigits++;
|
||||
} else if (char === ":" && i + 1 < length && ip[i + 1] !== ":") {
|
||||
groups++;
|
||||
groupDigits = 0;
|
||||
} else if (char === ":" && i + 1 < length && ip[i + 1] === ":") {
|
||||
doubleColonCount++;
|
||||
i++;
|
||||
groupDigits = 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (groups > 8) {
|
||||
return false;
|
||||
} else if (groupDigits > 4) {
|
||||
return false;
|
||||
} else if (doubleColonCount > 1) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (doubleColonCount === 0 && groups !== 8) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isValidIPv4(ip: string): boolean {
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) {
|
||||
return false;
|
||||
}
|
||||
for (const part of parts) {
|
||||
const num = Number(part);
|
||||
if (isNaN(num) || num < 0 || num > 255 || !part.match(/^\d+$/)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -1,3 +1,8 @@
|
||||
export const SPARKHOME_VERSION="4.14.3";
|
||||
export const CLIENT_VERSION="4.14.2";
|
||||
export const NEXT_API_VERSION="4.14.3";
|
||||
import * as pjson from "package.json";
|
||||
|
||||
export default function getVersion(){
|
||||
return pjson.version;
|
||||
}
|
||||
|
||||
export const clientNLUVersion = 2;
|
||||
export const apiVersion = 1;
|
38
lib/weather/getCurrentWeather.ts
Normal file
38
lib/weather/getCurrentWeather.ts
Normal file
@ -0,0 +1,38 @@
|
||||
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[]): number {
|
||||
const now = new Date();
|
||||
const nowTimestamp = now.getTime();
|
||||
|
||||
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
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
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
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"
|
||||
}
|
||||
}
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"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 工具"
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user