Compare commits

..

No commits in common. "5.3.0" and "main" have entirely different histories.
5.3.0 ... main

78 changed files with 1400 additions and 2076 deletions

32
.gitignore vendored
View File

@ -1,28 +1,3 @@
# 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
@ -60,10 +35,3 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
.syncignore
# log
app.log
# doc
doc/.vitepress/dist
doc/.vitepress/cache

View File

@ -5,4 +5,5 @@
"singleQuote": false,
"printWidth": 120,
"endOfLine": "lf"
}
}

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

33
app/global.css Normal file
View File

@ -0,0 +1,33 @@
@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;
}
}

22
app/layout.tsx Normal file
View File

@ -0,0 +1,22 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./global.css";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app"
};
export default function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}

12
app/page.tsx Normal file
View File

@ -0,0 +1,12 @@
"use client";
import { RecoilRoot } from "recoil";
import Homepage from "../components";
export default function Home() {
return (
<RecoilRoot>
<Homepage />
</RecoilRoot>
);
}

BIN
bun.lockb

Binary file not shown.

View File

@ -1,34 +1,41 @@
import { useEffect, useState } from "react";
import { useAtom } from "jotai";
import { bgFocusAtom } from "../lib/state/background";
import BackgroundContainer from "./backgroundContainer";
import Image from "next/image";
import { useRecoilValue } from "recoil";
import { settingsState } from "./state/settings";
import validUrl from "valid-url";
import validateColor from "validate-color";
export default function Background() {
const [isFocus, setFocus] = useAtom(bgFocusAtom);
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
setDarkMode(colorSchemeQueryList.matches ? true : false);
const handleChange = () => {
setDarkMode(colorSchemeQueryList.matches ? true : false);
};
colorSchemeQueryList.addEventListener("change", handleChange);
return () => {
colorSchemeQueryList.removeEventListener("change", handleChange);
};
}, []);
return (
<div>
{darkMode ? (
<BackgroundContainer src="rgb(23,25,29)" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
) : (
<BackgroundContainer src="white" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/>
)}
</div>
);
function Background(props: {
isFocus: boolean;
src: string;
onClick: () => void;
}) {
const settings = useRecoilValue(settingsState);
if (validateColor(props.src)) {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0"
style={{ backgroundColor: props.src }}
onClick={props.onClick}
></div>
);
} else if (validUrl.isWebUri(props.src)) {
return (
<Image
src={props.src}
className={
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
(props.isFocus
? settings.bgBlur
? "blur-lg scale-110"
: "brightness-50 scale-105"
: "")
}
alt="background"
onClick={props.onClick}
fill={true}
/>
);
}
}
export default Background;

View File

@ -1,50 +0,0 @@
import { settingsAtom } from "lib/state/settings";
import validUrl from "valid-url";
import validateColor from "validate-color";
import { useAtomValue } from "jotai";
export default function BackgroundContainer(props: {
isFocus: boolean;
src: string;
darkMode: boolean;
onClick: () => void;
}) {
const settings = useAtomValue(settingsAtom);
if (validateColor(props.src)) {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0"
style={{ backgroundColor: props.src }}
onClick={props.onClick}
></div>
);
} else if (validUrl.isWebUri(props.src)) {
return (
<img
src={props.src}
className={
"w-full h-full fixed object-cover inset-0 duration-200 z-0 " +
(props.isFocus ? (settings.bgBlur ? "blur-lg scale-110" : "brightness-50 scale-105") : "")
}
alt="background"
onClick={props.onClick}
/>
);
} else {
if (props.darkMode) {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-[rgb(23,25,29)]"
onClick={props.onClick}
></div>
);
} else {
return (
<div
className="w-full h-full fixed object-cover inset-0 duration-200 z-0 bg-white"
onClick={props.onClick}
></div>
);
}
}
}

View File

@ -1,77 +0,0 @@
import React, { useEffect, useState } from "react";
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@nextui-org/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";
export default function EngineSelector(
props: { className: string }
) {
const { t } = useTranslation("Search");
const settings: settingsType = useAtomValue(settingsAtom);
const items = settings.searchEngines;
const currentEngine: string = settings.currentSearchEngine;
const displayEngine = getName(currentEngine);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [selectedKeys, setSelectedKeys] = useState(new Set([currentEngine]) as any);
const selectedValue = React.useMemo(() => Array.from(selectedKeys).join(", "), [selectedKeys]);
const setSettings = useSetAtom(settingsAtom);
function getName(engineKey: string) {
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
}
useEffect(() => {
function setEngine(engine: string) {
setSettings((oldSettings: settingsType) => {
return {
...oldSettings,
currentSearchEngine: engine
};
});
}
if (selectedValue !== currentEngine) {
setEngine(selectedValue);
}
}, [currentEngine, selectedValue, setSettings]);
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>
);
}

23
components/index.tsx Normal file
View File

@ -0,0 +1,23 @@
import { useRecoilState } from "recoil";
import Background from "./background";
import Search from "./search/search";
import { bgFocusState } from "./state/background";
export default function Homepage() {
const [isFocus, setFocus] = useRecoilState(bgFocusState);
return (
<div className="h-full fixed overflow-hidden w-full bg-black">
<Background
src="rgb(23,25,29)"
isFocus={isFocus}
onClick={() => setFocus(false)}
/>
<Search
onFocus={() => {
setFocus(true);
console.log("focus");
}}
/>
</div>
);
}

View File

@ -1,26 +0,0 @@
import { settingsType, suggestionItem } from "global";
import copyToClipboard from "lib/copy";
import { normalizeURL } from "lib/normalizeURL";
import search from "lib/search";
export default function (
index: number,
suggestion: suggestionItem[],
query: string,
settings: settingsType,
searchBoxRef: React.RefObject<HTMLInputElement>
) {
const selected = suggestion[index];
const engine = settings.searchEngines[settings.currentSearchEngine];
const newTab = settings.searchInNewTab;
let clipboard: any;
if (selected.type === "QUERY" || selected.type === "default") {
search(selected.suggestion, engine, newTab);
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
window.open(normalizeURL(selected.suggestion));
} else if (selected.type === "text") {
console.log("????");
copyToClipboard(selected.suggestion);
searchBoxRef.current?.focus();
}
}

View File

@ -1,42 +0,0 @@
import { suggestionItem } from "global";
import { findClosestDateIndex } from "lib/weather/getCurrentWeather";
import { getLocationNative } from "lib/weather/getLocation";
import { getWeather } from "lib/weather/getWeather";
import { WMOCodeTable } from "lib/weather/wmocode";
type UpdateSuggestionFunction = (data: suggestionItem[]) => void;
export function handleNLUResult(result: any, updateSuggestion: UpdateSuggestionFunction){
if (result.intent == "weather.summary") {
getLocationNative((data: GeolocationCoordinates | GeolocationPositionError) => {
console.log(data);
if (data instanceof GeolocationCoordinates) {
getWeather(data.latitude, data.longitude).then((weather) => {
console.log(weather["hourly"]);
let hourIndex = findClosestDateIndex(
weather["hourly"]["time"],
weather["utc_offset_seconds"]
);
let temp = weather["hourly"]["apparent_temperature"][hourIndex];
let weatherCode = weather["hourly"]["weather_code"][hourIndex];
console.log(temp, weatherCode, hourIndex);
updateSuggestion([
{
type: "text",
suggestion: `Weather: ${temp}${weather["hourly_units"]["apparent_temperature"]}, ${WMOCodeTable[weatherCode]["day"].description}`,
relevance: 3000 * result.score
}
]);
});
}
});
} else if (result.intent !== "None") {
updateSuggestion([
{
type: "text",
suggestion: result.intent,
relevance: 2200 * result.score
}
]);
}
}

View File

@ -1,30 +0,0 @@
import { normalizeURL } from "lib/normalizeURL";
export default function Link(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>
);
}
}

View File

@ -1,178 +0,0 @@
import { useEffect, useRef, useState } from "react";
import SuggestionBox from "./suggestionBox";
import { queryAtom } from "lib/state/query";
import { suggestionItem, suggestionsResponse } from "global";
import getSearchEngineName from "lib/onesearch/getSearchEngineName";
import PlainSearch from "./plainSearch";
import { suggestionAtom } from "lib/state/suggestion";
import validLink from "lib/url/validLink";
import Link 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 { NLU } from "lib/nlp/load";
import { handleNLUResult } from "./handleNLUResult";
import { useAtom, useAtomValue } from "jotai";
import i18next from "i18next";
import { useTranslation } from "react-i18next";
export default function OneSearch() {
const [suggestion, setFinalSuggetsion] = useAtom(suggestionAtom);
const [manager, setManager] = useState(null);
const lastRequestTimeRef = useRef(0);
const selected = useAtomValue(selectedSuggestionAtom);
const settings = useAtomValue(settingsAtom);
const devMode = true;
const query = useAtomValue(queryAtom);
const engineName = getSearchEngineName();
const engine = settings.currentSearchEngine;
const { t } = useTranslation("Search");
const lang = i18next.language;
useEffect(() => {
const time = new Date().getTime().toString();
if (query.trim() === "" || query.length > 120) {
cleanSuggestion("QUERY", "NAVIGATION");
return;
}
fetch(`/api/suggestion?q=${query}&l=${lang}&t=${time}&engine=${engine}`)
.then((res) => res.json())
.then((data: suggestionsResponse) => {
try {
const suggestionToUpdate: suggestionItem[] = data.suggestions;
if (data.time > lastRequestTimeRef.current) {
cleanSuggestion("NAVIGATION", "QUERY");
lastRequestTimeRef.current = data.time;
updateSuggestion(suggestionToUpdate);
}
} catch (error: Error | unknown) {
if (error instanceof Error) {
sendError(error);
}
}
})
.catch((error) => {
// Handle fetch error
sendError(error);
});
}, [query]);
function updateSuggestion(data: suggestionItem[]) {
setFinalSuggetsion((cur: suggestionItem[]) => {
const types: string[] = [];
for (const sug of data) {
if (!types.includes(sug.type)) types.push(sug.type);
}
for (const type of types) {
cur = cur.filter((item) => {
return item.type !== type;
});
}
return cur.concat(data).sort((a, b) => {
return b.relevance - a.relevance;
});
});
}
function cleanSuggestion(...types: string[]) {
setFinalSuggetsion((suggestion: suggestionItem[]) => {
return suggestion.filter((item) => {
return !types.includes(item.type);
});
});
}
const NLUModel = new NLU();
useEffect(() => {
NLUModel.init().then((nlu) => {
setManager(nlu.manager);
console.log(nlu.manager);
});
}, []);
useEffect(() => {
cleanSuggestion("default-link", "default", "text");
if (validLink(query)) {
updateSuggestion([
{ type: "default-link", suggestion: query, relevance: 3000, prompt: <span>Go to: </span> },
{ type: "default", suggestion: query, relevance: 1600 }
]);
} else {
updateSuggestion([
{
type: "default",
suggestion: query,
relevance: 2000
}
]);
}
if (manager != null) {
// @ts-ignore
manager.process(query).then((result) => {
console.log(result);
handleNLUResult(result, updateSuggestion);
});
}
}, [query, engineName]);
return (
<SuggestionBox>
{suggestion.map((s, i) => {
if (s.suggestion.trim() === "") return;
if (s.type === "default") {
return (
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
{s.suggestion}&nbsp;
<span className="text-zinc-700 dark:text-zinc-400 text-sm">
{t("search-help-text", { engine: engineName })}
</span>
{devMode && (
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
{s.relevance}
</span>
)}
</PlainSearch>
);
} else if (s.type === "QUERY") {
return (
<PlainSearch key={i} query={s.suggestion} selected={i == selected}>
{s.suggestion}
{devMode && (
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
{s.relevance}
</span>
)}
</PlainSearch>
);
} else if (s.type === "NAVIGATION" || s.type === "default-link") {
return (
<Link key={i} query={s.suggestion} selected={i == selected}>
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
{s.suggestion}
{devMode && (
<span className="absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
{s.relevance}
</span>
)}
</Link>
);
} else if (s.type === "text") {
return (
<PlainText key={i} selected={i == selected}>
{s.prompt && <span className="text-zinc-700 dark:text-zinc-400">{s.prompt}</span>}
<p>{s.suggestion}</p>
{devMode && (
<span className="bottom-0 absolute text-zinc-700 dark:text-zinc-400 text-sm leading-10 h-10 right-2">
{s.relevance}
</span>
)}
</PlainText>
);
}
})}
</SuggestionBox>
);
}

View File

@ -1,35 +0,0 @@
import { useAtomValue } from "jotai";
import search from "lib/search";
import { settingsAtom } from "lib/state/settings";
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) {
return (
<div
className={`relative w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700
px-5 z-10 cursor-pointer duration-100 truncate`}
onClick={() => {
search(props.query, engine, newTab);
}}
>
{props.children}
</div>
);
}
else {
return (
<div
className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100 truncate`}
onClick={() => {
search(props.query, engine, newTab);
}}
>
{props.children}
</div>
);
}
}

View File

@ -1,21 +0,0 @@
export default function PlainText(props: { children: React.ReactNode; selected: boolean }) {
if (props.selected) {
return (
<div
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-300 dark:bg-zinc-700
px-5 z-10 cursor-pointer duration-100`}
>
{props.children}
</div>
);
} else {
return (
<div
className={`relative w-full h-auto leading-6 break-all py-[0.6rem] bg-zinc-100 hover:bg-zinc-300
dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100`}
>
{props.children}
</div>
);
}
}

View File

@ -1,6 +0,0 @@
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>
);
}

View File

@ -1,9 +0,0 @@
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
${props.children ? "opacity-100" : "opacity-0"}`}>
{props.children}
</div>
);
}

View File

@ -1,89 +0,0 @@
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 { 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);
const style = "default";
function handleKeydown(e: KeyboardEvent) {
if (e.key == "Enter") {
e.preventDefault();
handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef);
return;
} else if (e.key == "ArrowUp") {
e.preventDefault();
const len = suggestions.length;
setSelected((selectedSuggestion - 1 + len) % len);
} else if (e.key == "ArrowDown") {
e.preventDefault();
const len = suggestions.length;
setSelected((selectedSuggestion + 1) % len);
}
}
if (style === "default") {
return (
// 祖传样式,勿动
<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)]
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('search.placeholder')}
onFocus={props.onFocus}
onKeyDown={handleKeydown}
onChange={(e) =>
setQuery(() => {
return e.target.value;
})
}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
ref={searchBoxRef}
value={query}
/>
</div>
);
} else if (style == "image") {
return (
// 祖传样式,勿动
<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-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
border-solid border-0 duration-200 pr-2 shadow-md focus:shadow-none` +
(settings.bgBlur
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
: `bg-[rgba(235,235,235,0.9)] dark:bg-[rgba(20,20,20,0.9)] placeholder:text-slate-500
text-slate-800 dark:text-white`)
}
id="searchBox"
type="text"
placeholder="placeholder"
onFocus={props.onFocus}
ref={searchBoxRef}
/>
</div>
);
}
}

View File

@ -0,0 +1,48 @@
import { atom, useRecoilValue } from "recoil";
import { settingsState } from "../state/settings";
export default function Search(props: {
onFocus: () => void;
}) {
const settings = useRecoilValue(settingsState);
let style = "default";
if (style === "default") {
return (
// 祖传样式,勿动
<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-lg 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="Search"
onFocus={props.onFocus}
/>
</div>
);
} else if (style == "image") {
return (
// 祖传样式,勿动
<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-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
border-solid border-0 duration-200 pr-2 shadow-lg` +
(settings.bgBlur
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
: `bg-[rgba(235,235,235,0.9)] dark:bg-[rgba(20,20,20,0.9)] placeholder:text-slate-500
text-slate-800 dark:text-white`)
}
id="searchBox"
type="text"
placeholder="Search"
onFocus={props.onFocus}
/>
</div>
);
}
}

View File

@ -0,0 +1,10 @@
import { atom, selector } from "recoil";
const bgFocusState = atom({
key: "isBackgroundFocus",
default: false
});
export {
bgFocusState,
}

View File

@ -0,0 +1,14 @@
import { atom, selector } from "recoil";
const settingsState = atom({
key: "settings",
default: {
version: 1,
elementBackdrop: true,
bgBlur: true
}
});
export {
settingsState,
}

View File

@ -1,48 +0,0 @@
"use client";
import { useState, useEffect } from "react";
export default function Time(props: {
showSecond: boolean
}) {
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 150);
return () => {
clearInterval(timer);
};
}, []);
const formatTime = () => {
const hours = currentTime.getHours().toString().padStart(2, "0");
const minutes = currentTime.getMinutes().toString().padStart(2, "0");
const seconds = currentTime.getSeconds().toString().padStart(2, "0");
if (props.showSecond) {
return `${hours}:${minutes}:${seconds}`;
} else {
return `${hours}:${minutes}`;
}
};
return (
<div
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%]
left-1/2 w-11/12 sm:w-[700px] text:black
dark:text-white text-3xl text-left text-shadow-lg"
>
{formatTime()}{" "}
<span className="text-lg leading-9 relative">
{new Intl.DateTimeFormat(navigator.language, {
year: "numeric",
month: "short",
day: "numeric"
}).format(currentTime)}
</span>
</div>
);
}

34
global.d.ts vendored
View File

@ -1,31 +1,3 @@
import { Suggestion } from "search-engine-autocomplete";
interface settingsType extends object{
"version": number,
"elementBackdrop": boolean,
"bgBlur": boolean,
"timeShowSecond": boolean,
"currentSearchEngine": string,
"searchInNewTab": boolean,
"searchEngines": {
[key: string]: string
},
}
interface suggestionsResponse extends object{
suggestions: Suggestion[],
query: string,
verbatimRelevance: number,
time: number
}
type suggestionItem = {
suggestion: string,
type: string,
relativeRelevance?: number,
relevance: number,
prompt?: string | React.ReactElement,
intention?: string | null,
probability?: number,
confidence?: number,
}
type settings = {
bgBlur: boolean
};

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "ابحث أو اكتب عنوان URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Suche oder gib eine URL ein"
}
}

View File

@ -1,32 +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",
"decode": "Decode",
"encode": "Encode",
"result": "Result: ",
"copy": "Copy",
"copied": "Copied"
}
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Buscar o escribir una URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Rechercher ou saisir une URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Cerca o digita un URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "検索またはURLを入力"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "검색 또는 URL 입력"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Pesquisar ou digitar uma URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "Искать или ввести URL"
}
}

View File

@ -1,5 +0,0 @@
{
"search" : {
"placeholder" : "搜索或输入网址"
}
}

View File

@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@ -1,8 +0,0 @@
export default function copyToClipboard(value: string){
const textarea = document.createElement("textarea");
textarea.value = value;
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);
}

View File

@ -1,134 +0,0 @@
{
"weather.summary": [
"how's the weather",
"What's going on with the weather?",
"Can you give me an update on the weather?",
"How's the forecast looking today?",
"Give me a summary of the current weather.",
"Can you tell me the current weather?",
"What is the weather situation at the moment?",
"Could you provide a quick weather update?",
"Is it raining or sunny outside?",
"What's the weather like right now?",
"Tell me the current weather conditions.",
"How about the weather today?",
"Is it a good day to be outside?",
"What should I expect in terms of weather today?",
"Is there any severe weather to be aware of?",
"Can you summarize today's weather forecast?",
"What's the weather looking like for the next few hours?",
"Is it going to stay this way all day?",
"Could you give me a brief overview of the weather?",
"What's the general weather situation in our area?",
"Is it cloudy or clear outside?",
"Any weather alerts I should know about?",
"How's the weather looking for outdoor activities?",
"What's the forecast saying for today's weather?",
"Is it going to be a warm day?",
"Are we expecting any storms today?",
"What's the weather condition outside my window?",
"Is it a typical day for this season in terms of weather?",
"how's the weather now?"
],
"weather.temp": [
"What's the temperature like right now?",
"Can you tell me the current temperature?",
"How hot is it outside?",
"What's the temperature supposed to be today?",
"What is the current temp outside?",
"Could you tell me the outdoor temperature?",
"Is it cold or warm outside?",
"What's the high temperature for today?",
"What's the low temperature expected tonight?",
"How does the temperature feel outside?",
"Is it going to get warmer or cooler today?",
"What's the temperature in the shade?",
"Can you provide the current temp in Celsius?",
"What's the temperature in Fahrenheit right now?",
"Is it too hot to be outside?",
"What's the temperature like in the morning?",
"How about the temperature in the evening?",
"Is it warm enough to go swimming?",
"What's the temperature in the city center?",
"Can you tell me the temp in the nearby area?",
"Is it below freezing outside?",
"What's the average temperature for today?",
"Is the temperature dropping or rising?",
"What should I wear considering the temperature?"
],
"base64.encode": [
"Please encode this data with base64: %s",
"I need to encode the following data in base64: %s",
"Could you encode this string using base64? %s",
"Convert this data to b64 encoding: %s",
"I want to encode this information with base64: %s",
"Help me encode this in base64: %s",
"Can you encode this data to base64 format? %s",
"b64 encode",
"base64 encode",
"encode base64 %s"
],
"base64.decode": [
"Please decode this base64 data: %s",
"I have a base64 encoded string that needs decoding: %s",
"Could you decode this base64 string for me? %s",
"Convert this base64 encoded data back to its original form: %s",
"I need to decode this base64 information: %s",
"Help me decode this base64 data: %s",
"Can you translate this base64 back to normal text? %s",
"b64 decode",
"base64 decode",
"decode base64 %s"
],
"url.encode": [
"Please encode this URL: %s",
"I need to encode this URL component: %s",
"Could you encode this part of the URL? %s",
"Convert this URL to its encoded form: %s",
"I want to encode this URL for safe transmission: %s",
"Help me encode this URL segment: %s",
"Can you encode this URL data? %s"
],
"url.decode": [
"Please decode this URL: %s",
"I have an encoded URL that needs decoding: %s",
"Could you decode this URL for me? %s",
"Convert this encoded URL back to its original form: %s",
"I need to decode this URL component: %s",
"Help me decode this URL segment: %s",
"Can you translate this encoded URL back to normal? %s"
],
"html.encode": [
"Please encode this HTML entity: %s",
"I need to encode this text to HTML entity: %s",
"Could you encode this as an HTML entity? %s",
"Convert this text to HTML entity encoding: %s",
"I want to encode this to prevent HTML interpretation: %s",
"Help me encode this into HTML entity: %s",
"Can you encode this for HTML usage? %s"
],
"html.decode": [
"Please decode this HTML entity: %s",
"I have an HTML entity that needs decoding: %s",
"Could you decode this HTML entity for me? %s",
"Convert this HTML entity back to its original text: %s",
"I need to decode this HTML entity to plain text: %s",
"Help me decode this HTML entity: %s",
"Can you translate this HTML entity back to normal text? %s"
],
"None": [
"free weather api",
"js get timezone",
"how",
"how's",
"how's the"
]
}

View File

@ -1,124 +0,0 @@
{
"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": [
"你好",
"为什么计算机使用二进制"
]
}

View File

@ -1,55 +0,0 @@
// @ts-ignore
import { containerBootstrap } from "@nlpjs/core";
// @ts-ignore
import { Nlp } from "@nlpjs/nlp";
// @ts-ignore
import { NluManager, NluNeural } from "@nlpjs/nlu";
// @ts-ignore
import { LangEn } from "@nlpjs/lang-en-min";
// @ts-ignore
import { LangZh } from "@nlpjs/lang-zh";
import * as fflate from 'fflate';
let zh: TrainData = {};
let en: TrainData = {};
type TrainData = {
[key: string]: string[];
};
export class NLU {
manager: any;
inited: boolean = false;
async loadIntentionModel() {
const container = await containerBootstrap();
container.use(Nlp);
container.use(LangEn);
container.use(LangZh);
container.use(NluNeural);
const manager = new NluManager({
container,
locales: ["en", "zh"],
nlu: {
useNoneFeature: true
}
});
const response = await fetch("/model");
const responseBuf = await response.arrayBuffer();
const compressed = new Uint8Array(responseBuf);
const decompressed = fflate.decompressSync(compressed);
const modelText = fflate.strFromU8(decompressed);
manager.fromJSON(JSON.parse(modelText));
this.manager = manager;
// console.log(this.manager);
}
async init() {
await this.loadIntentionModel();
this.inited = true;
return this;
}
async process(lang: string, text: string): Promise<any> {
const actual = await this.manager.process(lang, text);
return actual;
}
}

View File

@ -1,76 +0,0 @@
// @ts-ignore
import { containerBootstrap } from "@nlpjs/core";
// @ts-ignore
import { Nlp } from "@nlpjs/nlp";
// @ts-ignore
import { NluManager, NluNeural } from "@nlpjs/nlu";
// @ts-ignore
import { LangEn } from "@nlpjs/lang-en-min";
// @ts-ignore
import { LangZh } from "@nlpjs/lang-zh";
import fs from "node:fs";
import * as fflate from 'fflate';
let zh: TrainData = {};
let en: TrainData = {};
type TrainData = {
[key: string]: string[];
};
export async function trainIntentionModel() {
try {
const dataZH = fs.readFileSync("./lib/nlp/data/zh.json", "utf8");
const dataEN = fs.readFileSync("./lib/nlp/data/en.json", "utf8");
zh = JSON.parse(dataZH);
en = JSON.parse(dataEN);
} catch (err) {
console.error(err);
}
const container = await containerBootstrap();
container.use(Nlp);
container.use(LangEn);
container.use(LangZh);
container.use(NluNeural);
const manager = new NluManager({
container,
locales: ["en", "zh"],
nlu: {
useNoneFeature: true
}
});
// Adds the utterances and intents for the NLP
for (const key in zh) {
for (const value of zh[key]) {
manager.add("zh", value, key);
}
}
for (const key in en) {
for (const value of en[key]) {
manager.add("en", value, key);
}
}
await manager.train();
// let actual = await manager.process("en", "base64 decode bilibili");
// console.log(actual);
// let actualZH = await manager.process("zh", "去除百分号");
// console.log(actualZH);
const resultModel = manager.toJSON();
const buf = fflate.strToU8(JSON.stringify(resultModel));
const gzipped = fflate.gzipSync(buf, {
filename: 'model.json',
mtime: new Date().getTime()
});
fs.writeFileSync("./public/model", Buffer.from(gzipped));
}
trainIntentionModel();

View File

@ -1,18 +0,0 @@
export function normalizeURL(input: string): string {
try {
// try to create a URL object
const url = new URL(input);
// if the URL is valid, return it
return url.href;
} catch (error) {
// if the URL is invalid, try to add the protocol
const withHTTP = "http://" + input;
try {
const urlWithHTTP = new URL(withHTTP);
return urlWithHTTP.href;
} catch (error) {
// if the URL is still invalid, return the original input
return input;
}
}
}

View File

@ -1,17 +0,0 @@
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("Search");
return engineTranslation.includes(engineKey) ? t(`engine.${engineKey}`) : engineKey;
}

View File

@ -1,25 +0,0 @@
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") {
console.log("????");
copyToClipboard(selected.suggestion);
searchBoxRef.current?.focus();
}
}

View File

@ -1 +0,0 @@
export const engineTranslation = ["google", "bing", "baidu", "duckduckgo", "yandex", "ecosia", "yahoo"];

View File

@ -1,4 +0,0 @@
export default function(query: string, engine: string, newTab: boolean = true) {
if(newTab) window.open(engine.replace("%s", query));
else window.location.href = engine.replace("%s", query);
}

View File

@ -1,5 +0,0 @@
import { atom } from "jotai";
const bgFocusAtom = atom(false);
export { bgFocusAtom };

View File

@ -1,5 +0,0 @@
import { atom } from "jotai";
const queryAtom = atom("");
export { queryAtom };

View File

@ -1,24 +0,0 @@
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 };

View File

@ -1,6 +0,0 @@
import { suggestionItem } from "global";
import { atom } from "jotai";
const suggestionAtom = atom([] as suggestionItem[]);
export { suggestionAtom };

View File

@ -1,5 +0,0 @@
import { atom } from "jotai";
const selectedSuggestionAtom = atom(0);
export { selectedSuggestionAtom };

View File

@ -1,21 +0,0 @@
import pjson from "package.json"
const CLIENT_VERSION = pjson.version;
export function sendError(error: Error) {
fetch("/api/error", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: error.message,
name: error.name,
time: new Date().getTime()/1000,
version: CLIENT_VERSION,
ua: navigator.userAgent,
cause: error.cause,
stack: error.stack
})
})
}

File diff suppressed because one or more lines are too long

View File

@ -1,33 +0,0 @@
import punycode from "punycode/";
import { tldList } from "./tldList";
export default function validLink(link: string) {
let finalURL = '';
try {
const url = new URL(link);
finalURL = url.origin;
return true;
} catch (error) {
// if the URL is invalid, try to add the protocol
try {
const urlWithHTTP = new URL("http://" + link);
finalURL = urlWithHTTP.origin;
} catch (error) {
return false;
}
}
if (validTLD(finalURL)) {
return true;
} else {
return false;
}
}
export function validTLD(domain: string): boolean {
const tld = punycode.toUnicode(domain.split(".").reverse()[0]);
if (tldList.includes(tld)) {
return true;
} else {
return false;
}
}

View File

@ -1,39 +0,0 @@
export function getClosestHourTimestamp(): string {
const now = new Date();
now.setMinutes(0, 0, 0); // 设置分钟、秒和毫秒为0
// 获取本地时间的年份、月份、日期、小时
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // 月份从0开始
const day = String(now.getDate()).padStart(2, '0');
const hour = String(now.getHours()).padStart(2, '0');
// 拼接成所需的格式
const localHourTimestamp = `${year}-${month}-${day}T${hour}:00`;
return localHourTimestamp;
}
export function findClosestDateIndex(dates: string[], utc_offset_seconds: number): number {
const now = new Date();
const nowTimestamp = now.getTime();
const offsetMilliseconds = utc_offset_seconds * 1000;
let closestIndex = -1;
let closestDiff = Infinity;
for (let i = 0; i < dates.length; i++) {
const date = new Date(dates[i]);
const adjustedTimestamp = date.getTime();
if (adjustedTimestamp <= nowTimestamp) {
const diff = nowTimestamp - adjustedTimestamp;
if (diff < closestDiff) {
closestDiff = diff;
closestIndex = i;
}
}
}
return closestIndex;
}

View File

@ -1,17 +0,0 @@
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
);
}

View File

@ -1,23 +0,0 @@
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;
}

View File

@ -1,294 +0,0 @@
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"
}
}
};

15
next.config.mjs Normal file
View File

@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "a2x.pub",
port: "",
pathname: "/*"
}
]
}
};
export default nextConfig;

View File

@ -1,53 +1,29 @@
{
"name": "sparkhome",
"version": "4.1.0",
"private": false,
"version": "5.2.0",
"type": "module",
"scripts": {
"dev": "bun server.ts",
"build": "tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "NODE_ENV=production bun server.ts"
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@nextui-org/react": "^2.4.2",
"@types/express": "^4.17.21",
"cac": "^6.7.14",
"chalk": "^5.3.0",
"express": "^4.19.2",
"fflate": "^0.8.2",
"framer-motion": "^11.2.12",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-icu": "^2.3.0",
"jotai": "^2.8.3",
"node-nlp": "^4.27.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.2",
"react-router": "^6.23.1",
"react-router-dom": "^6.23.1",
"search-engine-autocomplete": "^0.4.3",
"next": "14.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recoil": "^0.7.7",
"valid-url": "^1.0.9",
"validate-color": "^2.2.4",
"vite-express": "^0.17.0"
"validate-color": "^2.2.4"
},
"devDependencies": {
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/valid-url": "^1.0.7",
"@typescript-eslint/eslint-plugin": "^7.13.1",
"@typescript-eslint/parser": "^7.13.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-refresh": "^0.4.7",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.4",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"vite-plugin-pages": "^0.32.2",
"vite-tsconfig-paths": "^4.3.2"
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5"
}
}

View File

@ -1,25 +0,0 @@
import Background from "components/background";
import Time from "components/time";
import { useAtomValue, useSetAtom } from "jotai";
import Search from "components/search";
import { settingsAtom } from "lib/state/settings";
import { bgFocusAtom } from "lib/state/background";
import EngineSelector from "components/engineSelector";
import OneSearch from "components/onesearch/onesearch";
export default function Homepage() {
const settings = useAtomValue(settingsAtom);
const setBgFocus = useSetAtom(bgFocusAtom);
return (
<div className="h-full fixed overflow-hidden w-full bg-black">
<Background />
<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"/>
<Search onFocus={() => setBgFocus(true)} />
<Time showSecond={settings.timeShowSecond} />
<OneSearch />
</div>
);
}

1118
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/background/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

BIN
public/background/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 MiB

BIN
public/background/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

View File

@ -1,87 +0,0 @@
import chalk from "chalk";
import express from "express";
import ViteExpress from "vite-express";
import pjson from "./package.json";
import { networkInterfaces } from "os";
import cac from "cac";
const start = new Date();
const cli = cac();
const nets = networkInterfaces();
const ips: string[] = [];
for (const name of Object.keys(nets)) {
if (nets[name] === undefined) {
continue;
}
for (const net of nets[name]) {
// Skip over non-IPv4 and internal (i.e. 127.0.0.1) addresses
// 'IPv4' is in Node <= 17, from 18 it's a number 4 or 6
const familyV4Value = typeof net.family === "string" ? "IPv4" : 4;
if (net.family === familyV4Value && !net.internal) {
ips.push(net.address);
}
}
}
const app = express();
const port = 3000;
let host = "localhost";
cli.option("--host [host]", "Sepcify host name")
cli.help()
cli.version(pjson.version);
const parsed = cli.parse();
if (parsed.options.host!==undefined && typeof parsed.options.host == "boolean" && parsed.options.host) {
host = "0.0.0.0";
}
app.get("/message", (_, res) => res.send("Hello from express!"));
async function helloMessage() {
const { base } = await ViteExpress.getViteConfig();
//console.clear();
const timeCost = new Date().getTime() - start.getTime();
console.log("");
console.log(
" ",
chalk.redBright("SparkHome"),
chalk.redBright("v" + pjson.version),
chalk.whiteBright(" ready in"),
`${Math.round(timeCost)} ms`
);
console.log("");
console.log(" ", chalk.redBright("➜ "), "Local:\t", chalk.cyan(`http://${host}:${port}${base}`));
if (host !== "localhost") {
for (const ip of ips) {
console.log(" ", chalk.redBright("➜ "), "Network:\t", chalk.cyan(`http://${ip}:${port}${base}`));
}
}
console.log(" ", chalk.red("➜ "), chalk.whiteBright("press"), "h + enter", chalk.whiteBright("to show help"))
}
const server = app.listen(port, host);
ViteExpress.bind(app, server, helloMessage);
async function a() {
for await (const line of console) {
switch (line) {
case "h":
console.log(" Shortcuts");
console.log(" ", chalk.whiteBright("press"), "c + enter ", chalk.whiteBright("to clear console"));
console.log(" ", chalk.whiteBright("press"), "q + enter ", chalk.whiteBright("to quit"));
break;
case "c":
console.clear();
break;
case "q":
server.on("vite:close", ()=>{});
server.close();
return;
default:
break;
}
}
}
a();

View File

@ -1,78 +0,0 @@
import { Suspense } from "react";
import { useRoutes } from "react-router-dom";
import routes from "~react-pages";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import ICU from "i18next-icu";
import * as en from "i18n/en.json";
import * as zh from "i18n/zh.json";
import * as ja from "i18n/ja.json";
import * as ar from "i18n/ar.json";
import * as de from "i18n/de.json";
import * as es from "i18n/es.json";
import * as fr from "i18n/fr.json";
import * as it from "i18n/it.json";
import * as ko from "i18n/ko.json";
import * as pt from "i18n/pt.json";
import * as ru from "i18n/ru.json";
import { NextUIProvider } from "@nextui-org/react";
i18n.use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector)
.use(ICU)
.init({
resources: {
en: {
translation: en
},
zh: {
translation: zh
},
ja: {
translation: ja
},
ar: {
translation: ar
},
de: {
translation: de
},
es: {
translation: es
},
fr: {
translation: fr
},
it: {
translation: it
},
ko: {
translation: ko
},
pt: {
translation: pt
},
ru: {
translation: ru
}
},
fallbackLng: "en",
interpolation: {
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
},
detection: {
order: ["navigator"],
caches: []
}
});
export function App() {
return (
<NextUIProvider>
<Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>
</NextUIProvider>
);
}

View File

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -1,15 +0,0 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { App } from "./app";
import "./index.css";
const app = createRoot(document.getElementById("root")!);
app.render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);

2
src/vite-env.d.ts vendored
View File

@ -1,2 +0,0 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-pages/client-react" />

View File

@ -1,23 +1,20 @@
import type { Config } from "tailwindcss";
import { nextui } from "@nextui-org/react";
const config: Config = {
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic": "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))"
}
}
content: [
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
backgroundImage: {
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
"gradient-conic":
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
},
},
darkMode: "class",
plugins: [nextui()],
},
plugins: [],
};
export default config;

View File

@ -1,28 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "**/*.ts", "**/*.tsx", "global.d.ts"]
}

View File

@ -1,11 +1,26 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "global.d.ts", "app/store/store.js"],
"exclude": ["node_modules"]
}

View File

@ -1,13 +0,0 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true,
"noEmit": true
},
"include": ["vite.config.ts"]
}

View File

@ -1,15 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
import Pages from "vite-plugin-pages";
import tsconfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
Pages({
dirs: "./pages/"
}),
tsconfigPaths()
]
});