ref: format

This commit is contained in:
alikia2x (寒寒) 2024-09-21 21:31:08 +08:00
parent 35f90b42d9
commit d97e678206
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
72 changed files with 2240 additions and 2190 deletions

View File

@ -1,8 +1,8 @@
{ {
"useTabs": false, "useTabs": true,
"tabWidth": 4, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"singleQuote": false, "singleQuote": false,
"printWidth": 100, "printWidth": 100,
"endOfLine": "lf" "endOfLine": "lf"
} }

View File

@ -2,21 +2,21 @@ import { Express } from "express";
import { completeGoogle } from "search-engine-autocomplete"; import { completeGoogle } from "search-engine-autocomplete";
export function configureBackendRoutes(app: Express) { export function configureBackendRoutes(app: Express) {
app.get('/api/v1/suggestion', async (req, res) => { app.get("/api/v1/suggestion", async (req, res) => {
const query = req.query.q as string; const query = req.query.q as string;
const t = parseInt(req.query.t as string || "0") || null; const t = parseInt((req.query.t as string) || "0") || null;
let language = req.query.l as string || 'en-US'; let language = (req.query.l as string) || "en-US";
try { try {
const data = await completeGoogle(query, language); const data = await completeGoogle(query, language);
//logger.info({ type: "onesearch_search_autocomplete", query: query, data: data }); //logger.info({ type: "onesearch_search_autocomplete", query: query, data: data });
res.json({ ...data, time: t }); res.json({ ...data, time: t });
} catch (error) { } catch (error) {
//logger.error({ type: "onesearch_search_autocomplete_error", error: error.message }); //logger.error({ type: "onesearch_search_autocomplete_error", error: error.message });
res.status(500).json({ error: 'Internal Server Error' }); res.status(500).json({ error: "Internal Server Error" });
} }
}); });
app.get("/api/v1/ping", async (_, res) => { app.get("/api/v1/ping", async (_, res) => {
res.status(200).json({message: "pong"}); res.status(200).json({ message: "pong" });
}) });
} }

BIN
bun.lockb

Binary file not shown.

View File

@ -4,17 +4,27 @@ import BackgroundContainer from "./backgroundContainer";
import useDarkMode from "lib/darkModeHook"; import useDarkMode from "lib/darkModeHook";
export default function Background() { export default function Background() {
const [isFocus, setFocus] = useAtom(bgFocusAtom); const [isFocus, setFocus] = useAtom(bgFocusAtom);
const darkMode = useDarkMode(); const darkMode = useDarkMode();
return ( return (
<div> <div>
{darkMode ? ( {darkMode ? (
<BackgroundContainer src="rgb(23,25,29)" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/> <BackgroundContainer
) : ( src="rgb(23,25,29)"
<BackgroundContainer src="white" isFocus={isFocus} onClick={() => setFocus(false)} darkMode={darkMode}/> isFocus={isFocus}
)} onClick={() => setFocus(false)}
</div> darkMode={darkMode}
); />
) : (
<BackgroundContainer
src="white"
isFocus={isFocus}
onClick={() => setFocus(false)}
darkMode={darkMode}
/>
)}
</div>
);
} }

View File

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

View File

@ -7,44 +7,44 @@ import { useAtomValue, useSetAtom } from "jotai";
import Picker, { PickedItem } from "./picker"; import Picker, { PickedItem } from "./picker";
export default function EngineSelector(props: { className: string }) { export default function EngineSelector(props: { className: string }) {
const { t } = useTranslation(); const { t } = useTranslation();
const settings: settingsType = useAtomValue(settingsAtom); const settings: settingsType = useAtomValue(settingsAtom);
const engines = settings.searchEngines; const engines = settings.searchEngines;
const currentEngine: string = settings.currentSearchEngine; const currentEngine: string = settings.currentSearchEngine;
const [selected, setSelected] = useState(currentEngine); const [selected, setSelected] = useState(currentEngine);
const setSettings = useSetAtom(settingsAtom); const setSettings = useSetAtom(settingsAtom);
let engineList: PickedItem = {}; let engineList: PickedItem = {};
for (const engineKey of Object.keys(engines)) { for (const engineKey of Object.keys(engines)) {
engineList[engineKey] = getName(engineKey); engineList[engineKey] = getName(engineKey);
} }
function getName(engineKey: string) { function getName(engineKey: string) {
return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey; return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey;
} }
useEffect(() => { useEffect(() => {
function setEngine(engine: string) { function setEngine(engine: string) {
setSettings((oldSettings: settingsType) => { setSettings((oldSettings: settingsType) => {
return { return {
...oldSettings, ...oldSettings,
currentSearchEngine: engine currentSearchEngine: engine
}; };
}); });
} }
if (selected !== currentEngine) { if (selected !== currentEngine) {
setEngine(selected); setEngine(selected);
} }
}, [currentEngine, selected, setSettings]); }, [currentEngine, selected, setSettings]);
return ( return (
<Picker <Picker
selectionItems={engineList} selectionItems={engineList}
selected={selected} selected={selected}
selectionOnChange={(selected) => { selectionOnChange={(selected) => {
setSelected(selected); setSelected(selected);
}} }}
displayContent={getName(selected)} displayContent={getName(selected)}
className={props.className} className={props.className}
/> />
); );
} }

View File

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

View File

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

View File

@ -2,29 +2,29 @@ import { normalizeURL } from "lib/normalizeURL";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
interface LinkSuggestionProps { interface LinkSuggestionProps {
children: React.ReactNode; children: React.ReactNode;
query: string; query: string;
selected: boolean; selected: boolean;
inPage?: boolean; inPage?: boolean;
} }
export default function LinkSuggestion(props: LinkSuggestionProps) { export default function LinkSuggestion(props: LinkSuggestionProps) {
const className = props.selected 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-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`; : `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(); const navigate = useNavigate();
return ( return (
<div <div
className={className} className={className}
onClick={() => { onClick={() => {
if (props.inPage) { if (props.inPage) {
navigate(props.query); navigate(props.query);
} else { } else {
window.open(normalizeURL(props.query)); window.open(normalizeURL(props.query));
} }
}} }}
> >
{props.children} {props.children}
</div> </div>
); );
} }

View File

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

View File

@ -2,34 +2,37 @@ import { useAtomValue } from "jotai";
import search from "lib/search"; import search from "lib/search";
import { settingsAtom } from "lib/state/settings"; import { settingsAtom } from "lib/state/settings";
export default function PlainSearch(props: { children: React.ReactNode; query: string; selected: boolean }) { export default function PlainSearch(props: {
const settings = useAtomValue(settingsAtom); children: React.ReactNode;
const engine = settings.searchEngines[settings.currentSearchEngine]; query: string;
const newTab = settings.searchInNewTab; selected: boolean;
if (props.selected) { }) {
return ( const settings = useAtomValue(settingsAtom);
<div const engine = settings.searchEngines[settings.currentSearchEngine];
className={`relative w-full h-10 leading-10 bg-zinc-300 dark:bg-zinc-700 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`} px-5 z-10 cursor-pointer duration-100 truncate`}
onClick={() => { onClick={() => {
search(props.query, engine, newTab); search(props.query, engine, newTab);
}} }}
> >
{props.children} {props.children}
</div> </div>
); );
} } else {
else { return (
return ( <div
<div className={`relative w-full h-10 leading-10 bg-zinc-100 hover:bg-zinc-300
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`} dark:bg-zinc-800 hover:dark:bg-zinc-700 px-5 z-10 cursor-pointer duration-100 truncate`}
onClick={() => { onClick={() => {
search(props.query, engine, newTab); search(props.query, engine, newTab);
}} }}
> >
{props.children} {props.children}
</div> </div>
); );
} }
} }

View File

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

View File

@ -1,6 +1,8 @@
export default function Suggestion(props: { children: React.ReactNode }) { export default function Suggestion(props: { children: React.ReactNode }) {
return ( 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
</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,11 +1,11 @@
export default function SuggestionBox(props: { children?: React.ReactNode }) { export default function SuggestionBox(props: { children?: React.ReactNode }) {
return ( return (
<div <div
className={`relative bg-zinc-100 dark:bg-zinc-800 w-11/12 sm:w-[700px] h-auto max-h-[calc(100vh-20rem)] 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 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 ? "opacity-100" : "opacity-0"}`}
> >
{props.children} {props.children}
</div> </div>
); );
} }

View File

@ -7,172 +7,177 @@ import React from "react";
export type selectionType = string; export type selectionType = string;
interface PickerProps extends HTMLAttributes<HTMLDivElement> { interface PickerProps extends HTMLAttributes<HTMLDivElement> {
selected: selectionType; selected: selectionType;
selectionOnChange: selectedOnChange; selectionOnChange: selectedOnChange;
displayContent: string; displayContent: string;
selectionItems: PickedItem; selectionItems: PickedItem;
} }
export interface PickedItem { export interface PickedItem {
[key: string]: selectionType; [key: string]: selectionType;
} }
export default function Picker(props: PickerProps) { export default function Picker(props: PickerProps) {
const itemListRef: RefObject<HTMLDivElement> = useRef(null); const itemListRef: RefObject<HTMLDivElement> = useRef(null);
const buttonRef: RefObject<HTMLButtonElement> = useRef(null); const buttonRef: RefObject<HTMLButtonElement> = useRef(null);
const [displayList, setDisplayList] = useState(false); const [displayList, setDisplayList] = useState(false);
const updatePosition = () => { const updatePosition = () => {
if (itemListRef.current == null || buttonRef.current == null) { if (itemListRef.current == null || buttonRef.current == null) {
return; return;
} }
const buttonRect = buttonRef.current.getBoundingClientRect(); const buttonRect = buttonRef.current.getBoundingClientRect();
const listRect = itemListRef.current.getBoundingClientRect(); const listRect = itemListRef.current.getBoundingClientRect();
// Align to center // Align to center
itemListRef.current.style.left = itemListRef.current.style.left =
Math.max( Math.max(
Math.min( Math.min(
buttonRect.x + buttonRect.width / 2 - listRect.width / 2, buttonRect.x + buttonRect.width / 2 - listRect.width / 2,
window.screen.width - listRect.width - 16 window.screen.width - listRect.width - 16
), ),
0 0
) + "px"; ) + "px";
if (window.screen.height - buttonRect.top < 192) { if (window.screen.height - buttonRect.top < 192) {
itemListRef.current.style.transformOrigin = "bottom center"; itemListRef.current.style.transformOrigin = "bottom center";
itemListRef.current.style.top = buttonRect.top - listRect.height - 16 + "px"; itemListRef.current.style.top = buttonRect.top - listRect.height - 16 + "px";
} else { } else {
itemListRef.current.style.top = buttonRect.y + buttonRect.height + 16 + "px"; itemListRef.current.style.top = buttonRect.y + buttonRect.height + 16 + "px";
} }
if (listRect.top + listRect.height > window.screen.height - 16) { if (listRect.top + listRect.height > window.screen.height - 16) {
itemListRef.current.style.height = window.screen.height - listRect.top - 12 + "px"; itemListRef.current.style.height = window.screen.height - listRect.top - 12 + "px";
} else { } else {
itemListRef.current.style.height = "fit-content"; itemListRef.current.style.height = "fit-content";
} }
}; };
useEffect(() => { useEffect(() => {
updatePosition(); updatePosition();
const handleResize = () => { const handleResize = () => {
updatePosition(); updatePosition();
}; };
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
// Cleanup event listener on component unmount // Cleanup event listener on component unmount
return () => { return () => {
window.removeEventListener("resize", handleResize); window.removeEventListener("resize", handleResize);
}; };
}, [itemListRef, buttonRef]); }, [itemListRef, buttonRef]);
function toggleDisplay(targetState?: boolean) { function toggleDisplay(targetState?: boolean) {
function hideList() { function hideList() {
if (itemListRef.current) { if (itemListRef.current) {
itemListRef.current.style.transitionDuration = "200ms"; itemListRef.current.style.transitionDuration = "200ms";
itemListRef.current.style.opacity = "0%"; itemListRef.current.style.opacity = "0%";
} }
setTimeout(() => { setTimeout(() => {
setDisplayList(false); setDisplayList(false);
}, 200); }, 200);
} }
function showList() { function showList() {
setDisplayList(true); setDisplayList(true);
setTimeout(() => { setTimeout(() => {
if (!itemListRef.current || !buttonRef.current) { if (!itemListRef.current || !buttonRef.current) {
return; return;
} }
updatePosition(); updatePosition();
if (window.screen.height - buttonRef.current.getBoundingClientRect().top < 128) { if (window.screen.height - buttonRef.current.getBoundingClientRect().top < 128) {
itemListRef.current.style.transformOrigin = "bottom center"; itemListRef.current.style.transformOrigin = "bottom center";
} }
itemListRef.current.style.transitionDuration = "100ms"; itemListRef.current.style.transitionDuration = "100ms";
itemListRef.current.style.opacity = "100%"; itemListRef.current.style.opacity = "100%";
updatePosition(); updatePosition();
const listRect = itemListRef.current.getBoundingClientRect(); const listRect = itemListRef.current.getBoundingClientRect();
if (listRect.top < 8) { if (listRect.top < 8) {
itemListRef.current.style.height = window.screen.height - 8 + "px"; itemListRef.current.style.height = window.screen.height - 8 + "px";
itemListRef.current.style.top = "8px"; itemListRef.current.style.top = "8px";
} }
}, 20); }, 20);
} }
if (targetState === true) { if (targetState === true) {
showList(); showList();
} else if (targetState === false) { } else if (targetState === false) {
hideList(); hideList();
} else if (displayList === true) { } else if (displayList === true) {
hideList(); hideList();
} else { } else {
showList(); showList();
} }
} }
const { displayContent, selectionOnChange, selectionItems, selected, ...rest } = props; const { displayContent, selectionOnChange, selectionItems, selected, ...rest } = props;
return ( return (
<div {...rest}> <div {...rest}>
<button <button
className="relative border-2 border-gray-500 dark:border-gray-300 className="relative border-2 border-gray-500 dark:border-gray-300
rounded-xl dark:text-white px-4 py-2" rounded-xl dark:text-white px-4 py-2"
ref={buttonRef} ref={buttonRef}
onClick={() => { onClick={() => {
toggleDisplay(); toggleDisplay();
}} }}
> >
{displayContent} {displayContent}
</button> </button>
{displayList && ( {displayList && (
<PickerList <PickerList
ref={itemListRef} ref={itemListRef}
selected={selected} selected={selected}
selectionOnChange={selectionOnChange} selectionOnChange={selectionOnChange}
selectionItems={selectionItems} selectionItems={selectionItems}
toggleDisplay={toggleDisplay} toggleDisplay={toggleDisplay}
/> />
)} )}
</div> </div>
); );
} }
interface PickerListProps { interface PickerListProps {
selected: selectionType; selected: selectionType;
selectionOnChange: selectedOnChange; selectionOnChange: selectedOnChange;
selectionItems: PickedItem; selectionItems: PickedItem;
toggleDisplay: Function; toggleDisplay: Function;
} }
const PickerList = React.forwardRef<HTMLDivElement, PickerListProps>((props, ref) => { const PickerList = React.forwardRef<HTMLDivElement, PickerListProps>((props, ref) => {
const { selected, selectionOnChange, selectionItems, toggleDisplay } = props; const { selected, selectionOnChange, selectionItems, toggleDisplay } = props;
return createPortal( return createPortal(
<div className="absolute w-screen h-screen" onClick={()=>{toggleDisplay(false)}}> <div
<div className="absolute w-screen h-screen"
ref={ref} onClick={() => {
className="overflow-y-auto fixed w-fit text-black dark:text-white opacity-0 duration-200 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 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" dark:bg-zinc-800 px-2 py-2 rounded-xl text-align-left"
style={{ transformOrigin: "top center" }} style={{ transformOrigin: "top center" }}
> >
{Object.keys(selectionItems).map((key: string, index) => { {Object.keys(selectionItems).map((key: string, index) => {
return ( return (
<div <div
key={index} key={index}
className="relative py-2 w-full min-w-32 pl-2 cursor-pointer rounded-lg 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" hover:bg-zinc-200 dark:hover:bg-zinc-700 flex justify-between items-center"
onClick={() => { onClick={() => {
selectionOnChange(key); selectionOnChange(key);
toggleDisplay(false); toggleDisplay(false);
}} }}
> >
<span>{selectionItems[key]}</span> <span>{selectionItems[key]}</span>
<div className="relative w-16"></div> <div className="relative w-16"></div>
{key === selected && ( {key === selected && (
<Icon className="relative right-2" icon="carbon:checkmark" /> <Icon className="relative right-2" icon="carbon:checkmark" />
)} )}
</div> </div>
); );
})} })}
</div> </div>
</div>, </div>,
document.body document.body
); );
}); });
PickerList.displayName = "PickerList"; PickerList.displayName = "PickerList";

View File

@ -8,82 +8,82 @@ import { suggestionAtom } from "lib/state/suggestion";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function Search(props: { onFocus: () => void }) { export default function Search(props: { onFocus: () => void }) {
const { t } = useTranslation(); const { t } = useTranslation();
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const [query, setQuery] = useAtom(queryAtom); const [query, setQuery] = useAtom(queryAtom);
const [selectedSuggestion, setSelected] = useAtom(selectedSuggestionAtom); const [selectedSuggestion, setSelected] = useAtom(selectedSuggestionAtom);
const suggestions = useAtomValue(suggestionAtom); const suggestions = useAtomValue(suggestionAtom);
const searchBoxRef = useRef<HTMLInputElement>(null); const searchBoxRef = useRef<HTMLInputElement>(null);
const style = "default"; const style = "default";
function handleKeydown(e: KeyboardEvent) { function handleKeydown(e: KeyboardEvent) {
if (e.key == "Enter") { if (e.key == "Enter") {
e.preventDefault(); e.preventDefault();
handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef); handleEnter(selectedSuggestion, suggestions, query, settings, searchBoxRef);
return; return;
} else if (e.key == "ArrowUp") { } else if (e.key == "ArrowUp") {
e.preventDefault(); e.preventDefault();
const len = suggestions.length; const len = suggestions.length;
setSelected((selectedSuggestion - 1 + len) % len); setSelected((selectedSuggestion - 1 + len) % len);
} else if (e.key == "ArrowDown") { } else if (e.key == "ArrowDown") {
e.preventDefault(); e.preventDefault();
const len = suggestions.length; const len = suggestions.length;
setSelected((selectedSuggestion + 1) % len); setSelected((selectedSuggestion + 1) % len);
} }
} }
if (style === "default") { if (style === "default") {
return ( return (
// 祖传样式,勿动 // 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] "> <div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input <input
className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%] className="absolute z-1 w-11/12 sm:w-[700px] h-10 rounded-lg left-1/2 translate-x-[-50%]
text-center outline-none border-2 focus:border-2 duration-200 pr-2 shadow-md focus:shadow-none 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:shadow-zinc-800 dark:shadow-md bg-white dark:bg-[rgb(23,25,29)]
dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500 dark:border-neutral-500 dark:focus:border-neutral-300 placeholder:text-slate-500
dark:placeholder:text-slate-400 text-slate-900 dark:text-white" dark:placeholder:text-slate-400 text-slate-900 dark:text-white"
id="searchBox" id="searchBox"
type="text" type="text"
placeholder={t("search.placeholder")} placeholder={t("search.placeholder")}
onFocus={props.onFocus} onFocus={props.onFocus}
onKeyDown={handleKeydown} onKeyDown={handleKeydown}
onChange={(e) => onChange={(e) =>
setQuery(() => { setQuery(() => {
return e.target.value; return e.target.value;
}) })
} }
autoComplete="off" autoComplete="off"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
spellCheck="false" spellCheck="false"
ref={searchBoxRef} ref={searchBoxRef}
value={query} value={query}
/> />
</div> </div>
); );
} else if (style == "image") { } else if (style == "image") {
return ( return (
// 祖传样式,勿动 // 祖传样式,勿动
<div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] "> <div className="absolute w-full top-[8.5rem] lg:top-56 short:top-24 z-1 left-1/2 translate-x-[-50%] ">
<input <input
className={ className={
`absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12 `absolute z-1 w-2/3 sm:w-80 md:w-[400px] focus:w-11/12 focus:sm:w-[700px] hover:w-11/12
hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none hover:sm:w-[700px] h-10 rounded-3xl left-1/2 translate-x-[-50%] text-center outline-none
border-solid border-0 duration-200 pr-2 shadow-md focus:shadow-none` + border-solid border-0 duration-200 pr-2 shadow-md focus:shadow-none` +
(settings.bgBlur (settings.bgBlur
? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl ? `bg-[rgba(255,255,255,0.5)] dark:bg-[rgba(24,24,24,0.75)] backdrop-blur-xl
placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white` placeholder:text-slate-500 dark:placeholder:text-slate-400 text-slate-900 dark:text-white`
: `bg-[rgba(235,235,235,0.9)] dark:bg-[rgba(20,20,20,0.9)] placeholder:text-slate-500 : `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`) text-slate-800 dark:text-white`)
} }
id="searchBox" id="searchBox"
type="text" type="text"
placeholder="placeholder" placeholder="placeholder"
onFocus={props.onFocus} onFocus={props.onFocus}
ref={searchBoxRef} ref={searchBoxRef}
/> />
</div> </div>
); );
} }
} }

View File

@ -3,10 +3,10 @@ import { selectionType } from "./picker";
export type selectedOnChange = (target: selectionType) => void; export type selectedOnChange = (target: selectionType) => void;
export default function SelectionItem(props: {key: selectionType, children: ReactNode, onChange: selectedOnChange}){ export default function SelectionItem(props: {
return ( key: selectionType;
<div onClick={() => props.onChange(props.key)}> children: ReactNode;
{props.children} onChange: selectedOnChange;
</div> }) {
) return <div onClick={() => props.onChange(props.key)}>{props.children}</div>;
} }

View File

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

48
global.d.ts vendored
View File

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

View File

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

View File

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

View File

@ -1,33 +1,33 @@
{ {
"search": { "search": {
"placeholder": "Search or type a URL", "placeholder": "Search or type a URL",
"engine-aria": "Switch search engine", "engine-aria": "Switch search engine",
"engine": { "engine": {
"google": "Google", "google": "Google",
"baidu": "Baidu", "baidu": "Baidu",
"bing": "Bing", "bing": "Bing",
"duckduckgo": "DuckDuckGo", "duckduckgo": "DuckDuckGo",
"yandex": "Yandex", "yandex": "Yandex",
"yahoo": "Yahoo", "yahoo": "Yahoo",
"ecosia": "Ecosia" "ecosia": "Ecosia"
}, },
"search-help-text": "Search {engine}" "search-help-text": "Search {engine}"
}, },
"notfound": { "notfound": {
"title": "page not found", "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>" "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": { "about": {
"title": "SparkHome" "title": "SparkHome"
}, },
"tools": { "tools": {
"base64": { "base64": {
"title": "Base64 tools - LuminaraUtils", "title": "Base64 tools - LuminaraUtils",
"decode": "Decode", "decode": "Decode",
"encode": "Encode", "encode": "Encode",
"result": "Result: ", "result": "Result: ",
"copy": "Copy", "copy": "Copy",
"copied": "Copied" "copied": "Copied"
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
{ {
"search": { "search": {
"placeholder": "検索またはURLを入力" "placeholder": "検索またはURLを入力"
}, },
"Search": { "Search": {
"engine-aria": "検索エンジンを切り替える", "engine-aria": "検索エンジンを切り替える",
"engine": { "engine": {
"google": "Google" "google": "Google"
}, },
"placeholder": "検索またはURLを入力" "placeholder": "検索またはURLを入力"
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,53 +1,53 @@
{ {
"search": { "search": {
"placeholder": "搜索或输入网址", "placeholder": "搜索或输入网址",
"engine-aria": "切换搜索引擎", "engine-aria": "切换搜索引擎",
"engine": { "engine": {
"google": "谷歌", "google": "谷歌",
"baidu": "百度", "baidu": "百度",
"bing": "必应", "bing": "必应",
"duckduckgo": "DuckDuckGo", "duckduckgo": "DuckDuckGo",
"yandex": "Yandex", "yandex": "Yandex",
"yahoo": "雅虎", "yahoo": "雅虎",
"ecosia": "Ecosia" "ecosia": "Ecosia"
}, },
"search-help-text": "用 {engine} 搜索" "search-help-text": "用 {engine} 搜索"
}, },
"Search": { "Search": {
"placeholder": "搜索或输入网址", "placeholder": "搜索或输入网址",
"engine-aria": "切换搜索引擎", "engine-aria": "切换搜索引擎",
"engine": { "engine": {
"google": "谷歌", "google": "谷歌",
"baidu": "百度", "baidu": "百度",
"bing": "必应", "bing": "必应",
"duckduckgo": "DuckDuckGo", "duckduckgo": "DuckDuckGo",
"yandex": "Yandex", "yandex": "Yandex",
"yahoo": "雅虎", "yahoo": "雅虎",
"ecosia": "Ecosia" "ecosia": "Ecosia"
}, },
"search-help-text": "用 {engine} 搜索" "search-help-text": "用 {engine} 搜索"
}, },
"404": { "404": {
"title": "页面未找到" "title": "页面未找到"
}, },
"About": { "About": {
"title": "星火主页" "title": "星火主页"
}, },
"tools": { "tools": {
"base64": { "base64": {
"title": "Base64 工具", "title": "Base64 工具",
"decode": "解码", "decode": "解码",
"encode": "编码", "encode": "编码",
"result": "结果: ", "result": "结果: ",
"copy": "复制", "copy": "复制",
"copied": "已复制" "copied": "已复制"
} }
}, },
"notfound": { "notfound": {
"desc": "请检查网址是否出错。 <br/>如果你从星火主页跳转到这里,<br/> 请 <a style=\"text-decoration:underline;\" href=\"mailto:contact@alikia2x.com\">联系我们</a>", "desc": "请检查网址是否出错。 <br/>如果你从星火主页跳转到这里,<br/> 请 <a style=\"text-decoration:underline;\" href=\"mailto:contact@alikia2x.com\">联系我们</a>",
"title": "网页不存在" "title": "网页不存在"
}, },
"about": { "about": {
"title": "星火主页" "title": "星火主页"
} }
} }

View File

@ -1,13 +1,13 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SparkHome</title> <title>SparkHome</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

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

View File

@ -2,22 +2,22 @@ import { useState, useEffect } from "react";
// Custom React Hook for dark mode detect // Custom React Hook for dark mode detect
export default function useDarkMode() { export default function useDarkMode() {
const [darkMode, setDarkMode] = useState(false); const [darkMode, setDarkMode] = useState(false);
useEffect(() => { useEffect(() => {
const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)"); const colorSchemeQueryList = window.matchMedia("(prefers-color-scheme: dark)");
setDarkMode(colorSchemeQueryList.matches); setDarkMode(colorSchemeQueryList.matches);
const handleChange = () => { const handleChange = () => {
setDarkMode(colorSchemeQueryList.matches); setDarkMode(colorSchemeQueryList.matches);
}; };
colorSchemeQueryList.addEventListener("change", handleChange); colorSchemeQueryList.addEventListener("change", handleChange);
return () => { return () => {
colorSchemeQueryList.removeEventListener("change", handleChange); colorSchemeQueryList.removeEventListener("change", handleChange);
}; };
}, []); }, []);
return darkMode; return darkMode;
} }

View File

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

View File

@ -1,127 +1,121 @@
{ {
"weather.summary": [ "weather.summary": [
"天气如何", "天气如何",
"现在的天气", "现在的天气",
"今天的天气预报", "今天的天气预报",
"现在的天气状况", "现在的天气状况",
"今天天气怎么样", "今天天气怎么样",
"目前是什么天气", "目前是什么天气",
"今天的天气概述", "今天的天气概述",
"当前天气状况如何", "当前天气状况如何",
"今天会下雨吗", "今天会下雨吗",
"今天会下雪吗", "今天会下雪吗",
"今天晴天吗", "今天晴天吗",
"今天的天气状况如何", "今天的天气状况如何",
"现在外面是什么天气", "现在外面是什么天气",
"今天天气好么", "今天天气好么",
"今天适合外出吗", "今天适合外出吗",
"今天的天气适宜做什么", "今天的天气适宜做什么",
"今天有没有雾霾", "今天有没有雾霾",
"今天的空气质量如何", "今天的空气质量如何",
"今天的紫外线指数是多少", "今天的紫外线指数是多少",
"今天有没有大风", "今天有没有大风",
"今天会不会很冷", "今天会不会很冷",
"今天的天气会变化吗", "今天的天气会变化吗",
"今天晚上的天气如何", "今天晚上的天气如何",
"今天夜里会下雨吗", "今天夜里会下雨吗",
"今天的天气对出行有影响吗", "今天的天气对出行有影响吗",
"今天的天气对运动有影响吗", "今天的天气对运动有影响吗",
"今天的天气对工作有影响吗", "今天的天气对工作有影响吗",
"今天的天气对旅游有影响吗", "今天的天气对旅游有影响吗",
"今天的天气对健康有影响吗" "今天的天气对健康有影响吗"
], ],
"weather.temp": [ "weather.temp": [
"现在的温度", "现在的温度",
"现在多少度", "现在多少度",
"外面有多热", "外面有多热",
"明天热不热?", "明天热不热?",
"现在的气温是多少", "现在的气温是多少",
"今天最高温度是多少", "今天最高温度是多少",
"今天最低温度是多少", "今天最低温度是多少",
"现在外面感觉冷吗", "现在外面感觉冷吗",
"现在需要穿外套吗", "现在需要穿外套吗",
"现在适合穿短袖吗", "现在适合穿短袖吗",
"现在的温度适合外出吗", "现在的温度适合外出吗",
"现在的温度适合运动吗", "现在的温度适合运动吗",
"现在的温度适合睡觉吗", "现在的温度适合睡觉吗",
"明天会比今天热吗", "明天会比今天热吗",
"明天会比今天冷吗", "明天会比今天冷吗",
"今天的温度变化大吗", "今天的温度变化大吗",
"现在的温度适合开空调吗", "现在的温度适合开空调吗",
"现在的温度适合开暖气吗", "现在的温度适合开暖气吗",
"室外的温度是多少", "室外的温度是多少",
"室内的温度是多少", "室内的温度是多少",
"现在的温度适合种植吗", "现在的温度适合种植吗",
"现在的温度适合养宠物吗", "现在的温度适合养宠物吗",
"现在的温度对健康有影响吗", "现在的温度对健康有影响吗",
"现在的温度是否舒适", "现在的温度是否舒适",
"现在的温度是否适合工作" "现在的温度是否适合工作"
], ],
"base64.encode": [ "base64.encode": [
"请将数据使用base64编码%s", "请将数据使用base64编码%s",
"需要将以下数据base64编码%s", "需要将以下数据base64编码%s",
"请将此字符串转为base64%s", "请将此字符串转为base64%s",
"将数据转为base64编码%s", "将数据转为base64编码%s",
"信息base64编码%s", "信息base64编码%s",
"请帮忙编码base64%s", "请帮忙编码base64%s",
"将数据编码为base64%s" "将数据编码为base64%s"
], ],
"base64.decode": [ "base64.decode": [
"请解码这个base64数据%s", "请解码这个base64数据%s",
"有base64编码字符串需要解码%s", "有base64编码字符串需要解码%s",
"帮忙解码base64%s", "帮忙解码base64%s",
"将base64编码转回原数据%s", "将base64编码转回原数据%s",
"解码base64信息%s", "解码base64信息%s",
"解码这个base64%s", "解码这个base64%s",
"将base64转文本%s" "将base64转文本%s"
], ],
"url.encode": [ "url.encode": [
"请编码这个URL%s", "请编码这个URL%s",
"URL部分需要编码%s", "URL部分需要编码%s",
"请将URL部分编码%s", "请将URL部分编码%s",
"URL编码转换%s", "URL编码转换%s",
"安全传输需编码URL%s", "安全传输需编码URL%s",
"编码URL段%s", "编码URL段%s",
"URL数据编码%s" "URL数据编码%s"
], ],
"url.decode": [ "url.decode": [
"请解码这个URL%s", "请解码这个URL%s",
"有URL编码需要解码%s", "有URL编码需要解码%s",
"解码这个URL%s", "解码这个URL%s",
"URL编码转回原URL%s", "URL编码转回原URL%s",
"解码URL部分%s", "解码URL部分%s",
"解码URL段%s", "解码URL段%s",
"URL编码转文本%s" "URL编码转文本%s"
], ],
"html.encode": [ "html.encode": [
"请编码HTML实体%s", "请编码HTML实体%s",
"文本转为HTML实体%s", "文本转为HTML实体%s",
"编码为HTML实体%s", "编码为HTML实体%s",
"文本HTML实体编码%s", "文本HTML实体编码%s",
"预防HTML解析编码%s", "预防HTML解析编码%s",
"HTML实体编码%s", "HTML实体编码%s",
"文本HTML使用编码%s" "文本HTML使用编码%s"
], ],
"html.decode": [ "html.decode": [
"请解码HTML实体%s", "请解码HTML实体%s",
"HTML实体需要解码%s", "HTML实体需要解码%s",
"解码HTML实体%s", "解码HTML实体%s",
"HTML实体转回文本%s", "HTML实体转回文本%s",
"HTML实体解码%s", "HTML实体解码%s",
"解码HTML实体%s", "解码HTML实体%s",
"HTML实体转文本%s" "HTML实体转文本%s"
], ],
"None": [ "None": ["你好", "为什么计算机使用二进制", "什么是", "热", "怎么"]
"你好",
"为什么计算机使用二进制",
"什么是",
"热",
"怎么"
]
} }

View File

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

View File

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

View File

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

View File

@ -4,14 +4,14 @@ import { settingsType } from "global";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function(){ export default function () {
const settings: settingsType = useAtomValue(settingsAtom); const settings: settingsType = useAtomValue(settingsAtom);
const currentEngine = settings.currentSearchEngine; const currentEngine = settings.currentSearchEngine;
const displayEngine = getName(currentEngine); const displayEngine = getName(currentEngine);
return displayEngine; return displayEngine;
} }
function getName(engineKey: string) { function getName(engineKey: string) {
const { t } = useTranslation(); const { t } = useTranslation();
return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey; return engineTranslation.includes(engineKey) ? t(`search.engine.${engineKey}`) : engineKey;
} }

View File

@ -4,25 +4,25 @@ import { normalizeURL } from "lib/normalizeURL";
import search from "lib/search"; import search from "lib/search";
export default function ( export default function (
index: number, index: number,
suggestion: suggestionItem[], suggestion: suggestionItem[],
_query: string, _query: string,
settings: settingsType, settings: settingsType,
searchBoxRef: React.RefObject<HTMLInputElement> searchBoxRef: React.RefObject<HTMLInputElement>
) { ) {
const selected = suggestion[index]; const selected = suggestion[index];
const engine = settings.searchEngines[settings.currentSearchEngine]; const engine = settings.searchEngines[settings.currentSearchEngine];
const newTab = settings.searchInNewTab; const newTab = settings.searchInNewTab;
if (selected.type === "QUERY" || selected.type === "default") { if (selected.type === "QUERY" || selected.type === "default") {
search(selected.suggestion, engine, newTab); search(selected.suggestion, engine, newTab);
} else if (selected.type === "NAVIGATION" || selected.type === "default-link") { } else if (selected.type === "NAVIGATION" || selected.type === "default-link") {
window.open(normalizeURL(selected.suggestion)); window.open(normalizeURL(selected.suggestion));
} else if (selected.type === "text") { } else if (selected.type === "text") {
copyToClipboard(selected.suggestion); copyToClipboard(selected.suggestion);
searchBoxRef.current?.focus(); searchBoxRef.current?.focus();
} else if (selected.type === "link") { } else if (selected.type === "link") {
window.open(normalizeURL(selected.suggestion)); window.open(normalizeURL(selected.suggestion));
} else if (selected.type === "inpage-link") { } else if (selected.type === "inpage-link") {
location.href = normalizeURL(selected.suggestion); location.href = normalizeURL(selected.suggestion);
} }
} }

View File

@ -1,39 +1,39 @@
import { suggestionItem } from "global"; import { suggestionItem } from "global";
interface keywordLinkDict { interface keywordLinkDict {
[key: string]: string; [key: string]: string;
} }
const dict_en: keywordLinkDict = { const dict_en: keywordLinkDict = {
about: "/about" about: "/about"
}; };
const dict_cn: keywordLinkDict = { const dict_cn: keywordLinkDict = {
: "/about" : "/about"
}; };
export function keywordSuggestion(query: string) { export function keywordSuggestion(query: string) {
for (const keyword in dict_cn) { for (const keyword in dict_cn) {
if (query.includes(keyword)) { if (query.includes(keyword)) {
const result: suggestionItem = { const result: suggestionItem = {
type: "inpage-link", type: "inpage-link",
suggestion: dict_cn[keyword], suggestion: dict_cn[keyword],
prompt: keyword, prompt: keyword,
relevance: 3000 relevance: 3000
}; };
return result return result;
} }
} }
for (const keyword in dict_en) { for (const keyword in dict_en) {
if (query.includes(keyword)) { if (query.includes(keyword)) {
const result: suggestionItem = { const result: suggestionItem = {
type: "inpage-link", type: "inpage-link",
suggestion: dict_en[keyword], suggestion: dict_en[keyword],
prompt: keyword, prompt: keyword,
relevance: 3000 relevance: 3000
}; };
return result return result;
} }
} }
return null; return null;
} }

View File

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

View File

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

View File

@ -1,24 +1,24 @@
import { settingsType } from "global"; import { settingsType } from "global";
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from "jotai/utils";
const defaultSettings: settingsType = { const defaultSettings: settingsType = {
"version": 2, version: 2,
"elementBackdrop": true, elementBackdrop: true,
"bgBlur": true, bgBlur: true,
"timeShowSecond": false, timeShowSecond: false,
"currentSearchEngine": "google", currentSearchEngine: "google",
"searchInNewTab": true, searchInNewTab: true,
"searchEngines": { searchEngines: {
"google": "https://www.google.com/search?q=%s", google: "https://www.google.com/search?q=%s",
"bing": "https://www.bing.com/search?q=%s", bing: "https://www.bing.com/search?q=%s",
"baidu": "https://www.baidu.com/s?wd=%s", baidu: "https://www.baidu.com/s?wd=%s",
"duckduckgo": "https://duckduckgo.com/?q=%s", duckduckgo: "https://duckduckgo.com/?q=%s",
"yandex": "https://yandex.com/search/?text=%s", yandex: "https://yandex.com/search/?text=%s",
"yahoo": "https://search.yahoo.com/search?p=%s", yahoo: "https://search.yahoo.com/search?p=%s",
"ecosia": "https://www.ecosia.org/search?q=%s" ecosia: "https://www.ecosia.org/search?q=%s"
} }
}; };
const settingsAtom = atomWithStorage('settings', defaultSettings); const settingsAtom = atomWithStorage("settings", defaultSettings);
export { settingsAtom }; export { settingsAtom };

View File

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

View File

@ -1,5 +1,5 @@
import TLDtxt from "./tlds.txt?raw" import TLDtxt from "./tlds.txt?raw";
export function getTLD(){ export function getTLD() {
return TLDtxt.split("\n").filter((line) => line[0] !== "#") return TLDtxt.split("\n").filter((line) => line[0] !== "#");
} }

View File

@ -2,89 +2,89 @@ import { toASCII } from "tr46";
import { getTLD } from "./tldList"; import { getTLD } from "./tldList";
export default function validLink(link: string) { export default function validLink(link: string) {
let finalURL; let finalURL;
try { try {
const url = new URL(link); const url = new URL(link);
finalURL = url; finalURL = url;
return true; return true;
} catch (error) { } catch (error) {
// if the URL is invalid, try to add the protocol // if the URL is invalid, try to add the protocol
try { try {
const urlWithHTTP = new URL("http://" + link); const urlWithHTTP = new URL("http://" + link);
finalURL = urlWithHTTP; finalURL = urlWithHTTP;
} catch (error) { } catch (error) {
return false; return false;
} }
} }
if ( if (
validTLD(finalURL.host) || validTLD(finalURL.host) ||
isValidIPv6(finalURL.host.slice(1, finalURL.host.length - 1)) || isValidIPv6(finalURL.host.slice(1, finalURL.host.length - 1)) ||
isValidIPv4(finalURL.host) isValidIPv4(finalURL.host)
) { ) {
return true; return true;
} else { } else {
return false; return false;
} }
} }
export function validTLD(domain: string): boolean { export function validTLD(domain: string): boolean {
if (!domain.includes(".")) return false; if (!domain.includes(".")) return false;
const tld = toASCII(domain.split(".").reverse()[0]); const tld = toASCII(domain.split(".").reverse()[0]);
const tldList = getTLD(); const tldList = getTLD();
if (tldList.includes(tld.toUpperCase())) { if (tldList.includes(tld.toUpperCase())) {
return true; return true;
} else { } else {
return false; return false;
} }
} }
export function isValidIPv6(ip: string): boolean { export function isValidIPv6(ip: string): boolean {
const length = ip.length; const length = ip.length;
let groups = 1; let groups = 1;
let groupDigits = 0; let groupDigits = 0;
let doubleColonCount = 0; let doubleColonCount = 0;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const char = ip[i]; const char = ip[i];
if ("0" <= char && char <= "9") { if ("0" <= char && char <= "9") {
groupDigits++; groupDigits++;
} else if ("a" <= char && char <= "f") { } else if ("a" <= char && char <= "f") {
groupDigits++; groupDigits++;
} else if ("A" <= char && char <= "F") { } else if ("A" <= char && char <= "F") {
groupDigits++; groupDigits++;
} else if (char === ":" && i + 1 < length && ip[i + 1] !== ":") { } else if (char === ":" && i + 1 < length && ip[i + 1] !== ":") {
groups++; groups++;
groupDigits = 0; groupDigits = 0;
} else if (char === ":" && i + 1 < length && ip[i + 1] === ":") { } else if (char === ":" && i + 1 < length && ip[i + 1] === ":") {
doubleColonCount++; doubleColonCount++;
i++; i++;
groupDigits = 0; groupDigits = 0;
} else { } else {
return false; return false;
} }
if (groups > 8) { if (groups > 8) {
return false; return false;
} else if (groupDigits > 4) { } else if (groupDigits > 4) {
return false; return false;
} else if (doubleColonCount > 1) { } else if (doubleColonCount > 1) {
return false; return false;
} }
} }
if (doubleColonCount === 0 && groups !== 8) { if (doubleColonCount === 0 && groups !== 8) {
return false; return false;
} }
return true; return true;
} }
export function isValidIPv4(ip: string): boolean { export function isValidIPv4(ip: string): boolean {
const parts = ip.split("."); const parts = ip.split(".");
if (parts.length !== 4) { if (parts.length !== 4) {
return false; return false;
} }
for (const part of parts) { for (const part of parts) {
const num = Number(part); const num = Number(part);
if (isNaN(num) || num < 0 || num > 255 || !part.match(/^\d+$/)) { if (isNaN(num) || num < 0 || num > 255 || !part.match(/^\d+$/)) {
return false; return false;
} }
} }
return true; return true;
} }

View File

@ -1,7 +1,7 @@
import * as pjson from "package.json"; import * as pjson from "package.json";
export default function getVersion(){ export default function getVersion() {
return pjson.version; return pjson.version;
} }
export const clientNLUVersion = 2; export const clientNLUVersion = 2;

View File

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

View File

@ -1,17 +1,17 @@
const options = { const options = {
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 10000, timeout: 10000,
maximumAge: 3600 maximumAge: 3600
}; };
export function getLocationNative(callback: Function) { export function getLocationNative(callback: Function) {
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos: GeolocationPosition) => { (pos: GeolocationPosition) => {
callback(pos.coords); callback(pos.coords);
}, },
(err: GeolocationPositionError) => { (err: GeolocationPositionError) => {
callback(err); callback(err);
}, },
options options
); );
} }

View File

@ -1,23 +1,24 @@
export async function getWeather(lat: number, lon: number) { export async function getWeather(lat: number, lon: number) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const cacheKey = `weather-cache-${lat.toFixed(2)}-${lon.toFixed(2)}-${timezone}`; const cacheKey = `weather-cache-${lat.toFixed(2)}-${lon.toFixed(2)}-${timezone}`;
const localData = localStorage.getItem(cacheKey); const localData = localStorage.getItem(cacheKey);
if (localData != null) { if (localData != null) {
console.log('Using cache'); console.log("Using cache");
const parsedLocalData = JSON.parse(localData); const parsedLocalData = JSON.parse(localData);
if (parsedLocalData["hourly"]["time"][0] != undefined && if (
new Date().getTime() - new Date(parsedLocalData["hourly"]["time"][0]).getTime() < 86400 * 1000 parsedLocalData["hourly"]["time"][0] != undefined &&
) { new Date().getTime() - new Date(parsedLocalData["hourly"]["time"][0]).getTime() <
return parsedLocalData; 86400 * 1000
} ) {
else { return parsedLocalData;
console.log('Cache expired'); } else {
localStorage.removeItem(cacheKey); 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 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 responseJson = await response.json(); const response = await fetch(url);
localStorage.setItem(cacheKey, JSON.stringify(responseJson)); const responseJson = await response.json();
return responseJson; localStorage.setItem(cacheKey, JSON.stringify(responseJson));
return responseJson;
} }

View File

@ -1,294 +1,294 @@
type WeatherInfo = { type WeatherInfo = {
description: string; description: string;
image: string; image: string;
}; };
type WMOCodeTable = { type WMOCodeTable = {
[key: string]: { [key: string]: {
day: WeatherInfo; day: WeatherInfo;
night: WeatherInfo; night: WeatherInfo;
}; };
}; };
export let WMOCodeTable: WMOCodeTable = { export let WMOCodeTable: WMOCodeTable = {
"0": { "0": {
day: { day: {
description: "Sunny", description: "Sunny",
image: "http://openweathermap.org/img/wn/01d@2x.png" image: "http://openweathermap.org/img/wn/01d@2x.png"
}, },
night: { night: {
description: "Clear", description: "Clear",
image: "http://openweathermap.org/img/wn/01n@2x.png" image: "http://openweathermap.org/img/wn/01n@2x.png"
} }
}, },
"1": { "1": {
day: { day: {
description: "Mainly Sunny", description: "Mainly Sunny",
image: "http://openweathermap.org/img/wn/01d@2x.png" image: "http://openweathermap.org/img/wn/01d@2x.png"
}, },
night: { night: {
description: "Mainly Clear", description: "Mainly Clear",
image: "http://openweathermap.org/img/wn/01n@2x.png" image: "http://openweathermap.org/img/wn/01n@2x.png"
} }
}, },
"2": { "2": {
day: { day: {
description: "Partly Cloudy", description: "Partly Cloudy",
image: "http://openweathermap.org/img/wn/02d@2x.png" image: "http://openweathermap.org/img/wn/02d@2x.png"
}, },
night: { night: {
description: "Partly Cloudy", description: "Partly Cloudy",
image: "http://openweathermap.org/img/wn/02n@2x.png" image: "http://openweathermap.org/img/wn/02n@2x.png"
} }
}, },
"3": { "3": {
day: { day: {
description: "Cloudy", description: "Cloudy",
image: "http://openweathermap.org/img/wn/03d@2x.png" image: "http://openweathermap.org/img/wn/03d@2x.png"
}, },
night: { night: {
description: "Cloudy", description: "Cloudy",
image: "http://openweathermap.org/img/wn/03n@2x.png" image: "http://openweathermap.org/img/wn/03n@2x.png"
} }
}, },
"45": { "45": {
day: { day: {
description: "Foggy", description: "Foggy",
image: "http://openweathermap.org/img/wn/50d@2x.png" image: "http://openweathermap.org/img/wn/50d@2x.png"
}, },
night: { night: {
description: "Foggy", description: "Foggy",
image: "http://openweathermap.org/img/wn/50n@2x.png" image: "http://openweathermap.org/img/wn/50n@2x.png"
} }
}, },
"48": { "48": {
day: { day: {
description: "Rime Fog", description: "Rime Fog",
image: "http://openweathermap.org/img/wn/50d@2x.png" image: "http://openweathermap.org/img/wn/50d@2x.png"
}, },
night: { night: {
description: "Rime Fog", description: "Rime Fog",
image: "http://openweathermap.org/img/wn/50n@2x.png" image: "http://openweathermap.org/img/wn/50n@2x.png"
} }
}, },
"51": { "51": {
day: { day: {
description: "Light Drizzle", description: "Light Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Light Drizzle", description: "Light Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"53": { "53": {
day: { day: {
description: "Drizzle", description: "Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Drizzle", description: "Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"55": { "55": {
day: { day: {
description: "Heavy Drizzle", description: "Heavy Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Heavy Drizzle", description: "Heavy Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"56": { "56": {
day: { day: {
description: "Light Freezing Drizzle", description: "Light Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Light Freezing Drizzle", description: "Light Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"57": { "57": {
day: { day: {
description: "Freezing Drizzle", description: "Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Freezing Drizzle", description: "Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"61": { "61": {
day: { day: {
description: "Light Rain", description: "Light Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png" image: "http://openweathermap.org/img/wn/10d@2x.png"
}, },
night: { night: {
description: "Light Rain", description: "Light Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png" image: "http://openweathermap.org/img/wn/10n@2x.png"
} }
}, },
"63": { "63": {
day: { day: {
description: "Rain", description: "Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png" image: "http://openweathermap.org/img/wn/10d@2x.png"
}, },
night: { night: {
description: "Rain", description: "Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png" image: "http://openweathermap.org/img/wn/10n@2x.png"
} }
}, },
"65": { "65": {
day: { day: {
description: "Heavy Rain", description: "Heavy Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png" image: "http://openweathermap.org/img/wn/10d@2x.png"
}, },
night: { night: {
description: "Heavy Rain", description: "Heavy Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png" image: "http://openweathermap.org/img/wn/10n@2x.png"
} }
}, },
"66": { "66": {
day: { day: {
description: "Light Freezing Rain", description: "Light Freezing Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png" image: "http://openweathermap.org/img/wn/10d@2x.png"
}, },
night: { night: {
description: "Light Freezing Rain", description: "Light Freezing Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png" image: "http://openweathermap.org/img/wn/10n@2x.png"
} }
}, },
"67": { "67": {
day: { day: {
description: "Freezing Rain", description: "Freezing Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png" image: "http://openweathermap.org/img/wn/10d@2x.png"
}, },
night: { night: {
description: "Freezing Rain", description: "Freezing Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png" image: "http://openweathermap.org/img/wn/10n@2x.png"
} }
}, },
"71": { "71": {
day: { day: {
description: "Light Snow", description: "Light Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Light Snow", description: "Light Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"73": { "73": {
day: { day: {
description: "Snow", description: "Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Snow", description: "Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"75": { "75": {
day: { day: {
description: "Heavy Snow", description: "Heavy Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Heavy Snow", description: "Heavy Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"77": { "77": {
day: { day: {
description: "Snow Grains", description: "Snow Grains",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Snow Grains", description: "Snow Grains",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"80": { "80": {
day: { day: {
description: "Light Showers", description: "Light Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Light Showers", description: "Light Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"81": { "81": {
day: { day: {
description: "Showers", description: "Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Showers", description: "Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"82": { "82": {
day: { day: {
description: "Heavy Showers", description: "Heavy Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png" image: "http://openweathermap.org/img/wn/09d@2x.png"
}, },
night: { night: {
description: "Heavy Showers", description: "Heavy Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png" image: "http://openweathermap.org/img/wn/09n@2x.png"
} }
}, },
"85": { "85": {
day: { day: {
description: "Light Snow Showers", description: "Light Snow Showers",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Light Snow Showers", description: "Light Snow Showers",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"86": { "86": {
day: { day: {
description: "Snow Showers", description: "Snow Showers",
image: "http://openweathermap.org/img/wn/13d@2x.png" image: "http://openweathermap.org/img/wn/13d@2x.png"
}, },
night: { night: {
description: "Snow Showers", description: "Snow Showers",
image: "http://openweathermap.org/img/wn/13n@2x.png" image: "http://openweathermap.org/img/wn/13n@2x.png"
} }
}, },
"95": { "95": {
day: { day: {
description: "Thunderstorm", description: "Thunderstorm",
image: "http://openweathermap.org/img/wn/11d@2x.png" image: "http://openweathermap.org/img/wn/11d@2x.png"
}, },
night: { night: {
description: "Thunderstorm", description: "Thunderstorm",
image: "http://openweathermap.org/img/wn/11n@2x.png" image: "http://openweathermap.org/img/wn/11n@2x.png"
} }
}, },
"96": { "96": {
day: { day: {
description: "Light Thunderstorms With Hail", description: "Light Thunderstorms With Hail",
image: "http://openweathermap.org/img/wn/11d@2x.png" image: "http://openweathermap.org/img/wn/11d@2x.png"
}, },
night: { night: {
description: "Light Thunderstorms With Hail", description: "Light Thunderstorms With Hail",
image: "http://openweathermap.org/img/wn/11n@2x.png" image: "http://openweathermap.org/img/wn/11n@2x.png"
} }
}, },
"99": { "99": {
day: { day: {
description: "Thunderstorm With Hail", description: "Thunderstorm With Hail",
image: "http://openweathermap.org/img/wn/11d@2x.png" image: "http://openweathermap.org/img/wn/11d@2x.png"
}, },
night: { night: {
description: "Thunderstorm With Hail", description: "Thunderstorm With Hail",
image: "http://openweathermap.org/img/wn/11n@2x.png" image: "http://openweathermap.org/img/wn/11n@2x.png"
} }
} }
}; };

View File

@ -1,61 +1,63 @@
{ {
"name": "sparkhome", "name": "sparkhome",
"private": false, "private": false,
"version": "5.8.0", "version": "5.8.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bun server.ts", "dev": "bun server.ts",
"build": "bun license-gen && tsc -b && vite build", "build": "bun license-gen && tsc -b && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "NODE_ENV=production bun server.ts", "preview": "NODE_ENV=production bun server.ts",
"license-gen": "bunx generate-license-file --input package.json --output lib/license.txt --overwrite" "license-gen": "bunx generate-license-file --input package.json --output lib/license.txt --overwrite",
}, "format": "prettier --write ."
"dependencies": { },
"@iconify/react": "^5.0.1", "dependencies": {
"@nextui-org/react": "^2.4.2", "@iconify/react": "^5.0.1",
"@types/bun": "^1.1.6", "@nextui-org/react": "^2.4.2",
"@types/express": "^4.17.21", "@types/bun": "^1.1.6",
"@types/tr46": "^5.0.0", "@types/express": "^4.17.21",
"cac": "^6.7.14", "@types/tr46": "^5.0.0",
"chalk": "^5.3.0", "cac": "^6.7.14",
"express": "^4.19.2", "chalk": "^5.3.0",
"fflate": "^0.8.2", "express": "^4.19.2",
"framer-motion": "^11.2.12", "fflate": "^0.8.2",
"generate-license-file": "^3.5.1", "framer-motion": "^11.2.12",
"i18next": "^23.11.5", "generate-license-file": "^3.5.1",
"i18next-browser-languagedetector": "^8.0.0", "i18next": "^23.11.5",
"i18next-icu": "^2.3.0", "i18next-browser-languagedetector": "^8.0.0",
"jest": "^29.7.0", "i18next-icu": "^2.3.0",
"jotai": "^2.8.3", "jest": "^29.7.0",
"node-nlp": "^4.27.0", "jotai": "^2.8.3",
"react": "^18.3.1", "node-nlp": "^4.27.0",
"react-dom": "^18.3.1", "react": "^18.3.1",
"react-i18next": "^14.1.2", "react-dom": "^18.3.1",
"react-router": "^6.23.1", "react-i18next": "^14.1.2",
"react-router-dom": "^6.23.1", "react-router": "^6.23.1",
"search-engine-autocomplete": "^0.4.3", "react-router-dom": "^6.23.1",
"tr46": "^5.0.0", "search-engine-autocomplete": "^0.4.3",
"valid-url": "^1.0.9", "tr46": "^5.0.0",
"validate-color": "^2.2.4", "valid-url": "^1.0.9",
"vite-express": "^0.17.0" "validate-color": "^2.2.4",
}, "vite-express": "^0.17.0"
"devDependencies": { },
"@types/react": "^18.3.3", "devDependencies": {
"@types/react-dom": "^18.3.0", "@types/react": "^18.3.3",
"@types/valid-url": "^1.0.7", "@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.13.1", "@types/valid-url": "^1.0.7",
"@typescript-eslint/parser": "^7.13.1", "@typescript-eslint/eslint-plugin": "^7.13.1",
"@vitejs/plugin-react-swc": "^3.5.0", "@typescript-eslint/parser": "^7.13.1",
"autoprefixer": "^10.4.19", "@vitejs/plugin-react-swc": "^3.5.0",
"eslint": "^8.57.0", "autoprefixer": "^10.4.19",
"eslint-plugin-react-hooks": "^4.6.2", "eslint": "^8.57.0",
"eslint-plugin-react-refresh": "^0.4.7", "eslint-plugin-react-hooks": "^4.6.2",
"postcss": "^8.4.38", "eslint-plugin-react-refresh": "^0.4.7",
"tailwindcss": "^3.4.4", "postcss": "^8.4.38",
"typescript": "^5.2.2", "prettier": "^3.3.3",
"vite": "^5.3.1", "tailwindcss": "^3.4.4",
"vite-plugin-chunk-split": "^0.5.0", "typescript": "^5.2.2",
"vite-plugin-pages": "^0.32.2", "vite": "^5.3.1",
"vite-tsconfig-paths": "^4.3.2" "vite-plugin-chunk-split": "^0.5.0",
} "vite-plugin-pages": "^0.32.2",
"vite-tsconfig-paths": "^4.3.2"
}
} }

View File

@ -1,17 +1,20 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export default function NotFound() { export default function NotFound() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="relative w-screen h-screen flex justify-center items-center"> <div className="relative w-screen h-screen flex justify-center items-center">
<div className="flex items-center"> <div className="flex items-center">
<h1 className="text-7xl font-thin">404</h1> <h1 className="text-7xl font-thin">404</h1>
<div className="relative h-20 mx-4 w-[0.15rem] bg-black dark:bg-white"></div> <div className="relative h-20 mx-4 w-[0.15rem] bg-black dark:bg-white"></div>
<div className="flex flex-col"> <div className="flex flex-col">
<div className="uppercase text-3xl font-light">{t("notfound.title")}</div> <div className="uppercase text-3xl font-light">{t("notfound.title")}</div>
<div className="text-sm" dangerouslySetInnerHTML={{__html:t("notfound.desc")}}></div> <div
</div> className="text-sm"
</div> dangerouslySetInnerHTML={{ __html: t("notfound.desc") }}
</div> ></div>
); </div>
</div>
</div>
);
} }

View File

@ -3,66 +3,66 @@ import getVersion, { apiVersion, clientNLUVersion } from "lib/version";
import AboutLayout from "./layout"; import AboutLayout from "./layout";
export default function AboutPage() { export default function AboutPage() {
const darkMode = useDarkMode(); const darkMode = useDarkMode();
return ( return (
<AboutLayout> <AboutLayout>
<h1 className="text-4xl font-bold mb-6">About SparkHome</h1> <h1 className="text-4xl font-bold mb-6">About SparkHome</h1>
<div className="flex mb-8"> <div className="flex mb-8">
<img src="/favicon.ico" className="relative w-20 h-20" /> <img src="/favicon.ico" className="relative w-20 h-20" />
<div className="flex flex-col ml-4"> <div className="flex flex-col ml-4">
<span className="leading-7 md:leading-9 text-3xl font-bold">SparkHome</span> <span className="leading-7 md:leading-9 text-3xl font-bold">SparkHome</span>
<p className="mt-2 leading-5 text-base md:text-xl"> <p className="mt-2 leading-5 text-base md:text-xl">
Made with <span className="text-red-500"></span> by Made with <span className="text-red-500"></span> by
<a className="underline text-red-500 mx-1" href="https://alikia2x.com"> <a className="underline text-red-500 mx-1" href="https://alikia2x.com">
alikia2x alikia2x
</a> </a>
from Luminara Studio from Luminara Studio
</p> </p>
</div> </div>
</div> </div>
<Version title="Overall Version" version={getVersion()} versionClass="bg-red-500" /> <Version title="Overall Version" version={getVersion()} versionClass="bg-red-500" />
<Version <Version
title="Browser NLU Model Version" title="Browser NLU Model Version"
version={"Build " + clientNLUVersion} version={"Build " + clientNLUVersion}
versionClass="bg-purple-500" versionClass="bg-purple-500"
/> />
<Version <Version
title="Backend API Version" title="Backend API Version"
version={"/api/v" + apiVersion} version={"/api/v" + apiVersion}
versionClass="bg-orange-500" versionClass="bg-orange-500"
/> />
<p className="flex items-center my-3"> <p className="flex items-center my-3">
<span className="font-bold text-xl md:text-2xl mr-4 w-[36rem]">License</span> <span className="font-bold text-xl md:text-2xl mr-4 w-[36rem]">License</span>
<span <span
className="relative px-2 py-1 text-sm font-bold rounded-md text-nowrap underline className="relative px-2 py-1 text-sm font-bold rounded-md text-nowrap underline
bg-green-600 text-white" bg-green-600 text-white"
> >
<a href="/about/license"> view</a> <a href="/about/license"> view</a>
</span> </span>
</p> </p>
<p className="relative font-bold text-2xl mt-12">Presented By</p> <p className="relative font-bold text-2xl mt-12">Presented By</p>
{!darkMode && <img src="/LuminaraStudio.png" className="relative md:h-64 mt-6" />} {!darkMode && <img src="/LuminaraStudio.png" className="relative md:h-64 mt-6" />}
{darkMode && <img src="/LuminaraStudioDark.png" className="relative md:h-56 mt-6" />} {darkMode && <img src="/LuminaraStudioDark.png" className="relative md:h-56 mt-6" />}
</AboutLayout> </AboutLayout>
); );
} }
function Version(props: { title: string; version: string; versionClass?: string }) { function Version(props: { title: string; version: string; versionClass?: string }) {
document.title = "About SparkHome"; document.title = "About SparkHome";
return ( return (
<p className="flex items-center my-3"> <p className="flex items-center my-3">
<span className="font-bold text-xl md:text-2xl mr-4 w-[36rem]">{props.title}</span> <span className="font-bold text-xl md:text-2xl mr-4 w-[36rem]">{props.title}</span>
<span <span
className={ className={
"relative px-2 py-1 text-sm font-bold rounded-md text-nowrap text-white " + "relative px-2 py-1 text-sm font-bold rounded-md text-nowrap text-white " +
props.versionClass || "" props.versionClass || ""
} }
> >
{props.version} {props.version}
</span> </span>
</p> </p>
); );
} }

View File

@ -1,10 +1,12 @@
export default function AboutLayout({ children }: { children: React.ReactNode }) { export default function AboutLayout({ children }: { children: React.ReactNode }) {
return ( return (
<div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]"> <div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]">
<main className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4 <main
pt-12 px-3 md:px-0"> className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4
{children} pt-12 px-3 md:px-0"
</main> >
</div> {children}
); </main>
</div>
);
} }

View File

@ -1,17 +1,15 @@
import LICENSE from "lib/license.txt?raw"; import LICENSE from "lib/license.txt?raw";
export default function LicensePage() { export default function LicensePage() {
return ( return (
<div className="dark:bg-[rgb(23,25,29)] dark:text-white min-h-screen w-screen overflow-x-hidden"> <div className="dark:bg-[rgb(23,25,29)] dark:text-white min-h-screen w-screen overflow-x-hidden">
<main <main
className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4 className="relative h-full w-full md:w-3/4 lg:w-1/2 left-0 md:left-[12.5%] lg:left-1/4
pt-12" pt-12"
> >
<h1 className="text-4xl font-bold mb-6">LICENSE</h1> <h1 className="text-4xl font-bold mb-6">LICENSE</h1>
<div className="font-mono text-justify whitespace-break-spaces"> <div className="font-mono text-justify whitespace-break-spaces">{LICENSE}</div>
{LICENSE} </main>
</div> </div>
</main> );
</div>
);
} }

View File

@ -8,21 +8,21 @@ import EngineSelector from "components/engineSelector";
import OneSearch from "components/onesearch/onesearch"; import OneSearch from "components/onesearch/onesearch";
export default function Homepage() { export default function Homepage() {
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const setBgFocus = useSetAtom(bgFocusAtom); const setBgFocus = useSetAtom(bgFocusAtom);
return ( return (
<div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]"> <div className="h-screen w-screen overflow-x-hidden bg-white dark:bg-[rgb(23,25,29)]">
<Background /> <Background />
<EngineSelector <EngineSelector
className="absolute top-20 lg:top-44 short:top-0 translate-x-[-50%] translate-y-[-0.2rem] 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 left-1/2 w-11/12 sm:w-[700px] text:black text-right
dark:text-white text-shadow-lg z-10" dark:text-white text-shadow-lg z-10"
/> />
<Search onFocus={() => setBgFocus(true)} /> <Search onFocus={() => setBgFocus(true)} />
<Time showSecond={settings.timeShowSecond} /> <Time showSecond={settings.timeShowSecond} />
<OneSearch /> <OneSearch />
</div> </div>
); );
} }

View File

@ -1,6 +1,6 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {}
} }
}; };

133
server.ts
View File

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

View File

@ -6,26 +6,26 @@ import AboutPage from "pages/about";
import LicensePage from "pages/about/license"; import LicensePage from "pages/about/license";
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <Homepage /> element: <Homepage />
}, },
{ {
path: "about", path: "about",
element: <AboutPage />, element: <AboutPage />,
children: [ children: [
{ {
path: "license", path: "license",
element: <LicensePage /> element: <LicensePage />
} }
] ]
} }
]); ]);
export function App() { export function App() {
return ( return (
<div className="relative bg-white dark:bg-black dark:text-white min-h-screen w-screen"> <div className="relative bg-white dark:bg-black dark:text-white min-h-screen w-screen">
<RouterProvider router={router} /> <RouterProvider router={router} />
</div> </div>
); );
} }

View File

@ -1,65 +1,65 @@
import * as en from "i18n/en.json" import * as en from "i18n/en.json";
import * as zh from "i18n/zh.json" import * as zh from "i18n/zh.json";
import * as ja from "i18n/ja.json" import * as ja from "i18n/ja.json";
import * as ar from "i18n/ar.json" import * as ar from "i18n/ar.json";
import * as de from "i18n/de.json" import * as de from "i18n/de.json";
import * as es from "i18n/es.json" import * as es from "i18n/es.json";
import * as fr from "i18n/fr.json" import * as fr from "i18n/fr.json";
import * as it from "i18n/it.json" import * as it from "i18n/it.json";
import * as ko from "i18n/ko.json" import * as ko from "i18n/ko.json";
import * as pt from "i18n/pt.json" import * as pt from "i18n/pt.json";
import * as ru from "i18n/ru.json" import * as ru from "i18n/ru.json";
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-i18next"; import { initReactI18next } from "react-i18next";
import LanguageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from "i18next-browser-languagedetector";
import ICU from 'i18next-icu'; import ICU from "i18next-icu";
i18n.use(initReactI18next) // passes i18n down to react-i18next i18n.use(initReactI18next) // passes i18n down to react-i18next
.use(LanguageDetector) .use(LanguageDetector)
.use(ICU) .use(ICU)
.init({ .init({
resources: { resources: {
en: { en: {
translation: en translation: en
}, },
zh: { zh: {
translation: zh translation: zh
}, },
ja: { ja: {
translation: ja translation: ja
}, },
ar: { ar: {
translation: ar translation: ar
}, },
de: { de: {
translation: de translation: de
}, },
es: { es: {
translation: es translation: es
}, },
fr: { fr: {
translation: fr translation: fr
}, },
it: { it: {
translation: it translation: it
}, },
ko: { ko: {
translation: ko translation: ko
}, },
pt: { pt: {
translation: pt translation: pt
}, },
ru: { ru: {
translation: ru translation: ru
} }
}, },
fallbackLng: "en", fallbackLng: "en",
interpolation: { interpolation: {
escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape escapeValue: false // react already safes from xss => https://www.i18next.com/translation-function/interpolation#unescape
}, },
detection: { detection: {
order: ['navigator'], order: ["navigator"],
caches: [] caches: []
} }
}); });

View File

@ -7,9 +7,9 @@ import { NextUIProvider } from "@nextui-org/react";
const app = createRoot(document.getElementById("root")!); const app = createRoot(document.getElementById("root")!);
app.render( app.render(
<StrictMode> <StrictMode>
<NextUIProvider> <NextUIProvider>
<App /> <App />
</NextUIProvider> </NextUIProvider>
</StrictMode> </StrictMode>
); );

View File

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

View File

@ -2,76 +2,80 @@ import { describe, expect, test } from "@jest/globals";
import validLink, { validTLD } from "../lib/url/validLink"; import validLink, { validTLD } from "../lib/url/validLink";
describe("Check if a string is an accessible domain/URL/IP", () => { describe("Check if a string is an accessible domain/URL/IP", () => {
test("Plain, full URL", () => { test("Plain, full URL", () => {
// Plain form // Plain form
expect(validLink("http://example.com")).toBe(true); expect(validLink("http://example.com")).toBe(true);
// With https and path // With https and path
expect(validLink("https://jestjs.io/docs/getting-started/")).toBe(true); expect(validLink("https://jestjs.io/docs/getting-started/")).toBe(true);
// With anchor // With anchor
expect(validLink("https://difftastic.wilfred.me.uk/zh-CN/git.html#git-difftool")).toBe(true); expect(validLink("https://difftastic.wilfred.me.uk/zh-CN/git.html#git-difftool")).toBe(
// With params true
expect(validLink("https://www.bilibili.com/list/ml2252204359?oid=990610203&bvid=BV1sx4y1g7Hh")).toBe(true); );
}); // With params
test("Punycode URL", () => { expect(
expect(validLink("https://原神大学.com/")).toBe(true); validLink("https://www.bilibili.com/list/ml2252204359?oid=990610203&bvid=BV1sx4y1g7Hh")
expect(validLink("中国原神大学.com")).toBe(true); ).toBe(true);
}); });
test("Invalid TLD with protocol", () => { test("Punycode URL", () => {
expect(validLink("https://www.example.notexist")).toBe(true); expect(validLink("https://原神大学.com/")).toBe(true);
}); expect(validLink("中国原神大学.com")).toBe(true);
test("Invalid TLD with no protocol", () => { });
expect(validLink("www.example.notexist")).toBe(false); test("Invalid TLD with protocol", () => {
}); expect(validLink("https://www.example.notexist")).toBe(true);
test("IPv4 without protocol", () => { });
expect(validLink("127.0.0.1")).toBe(true); test("Invalid TLD with no protocol", () => {
}); expect(validLink("www.example.notexist")).toBe(false);
test("IPv6 without protocol", () => { });
expect(validLink("[::]")).toBe(true); test("IPv4 without protocol", () => {
}); expect(validLink("127.0.0.1")).toBe(true);
test("special test for 铜锣湾.chn.moe", () => { });
expect(validLink("铜锣湾.chn.moe")).toBe(true); test("IPv6 without protocol", () => {
}); expect(validLink("[::]")).toBe(true);
test("Not a valid host/URL.", () => { });
expect(validLink("weather")).toBe(false); test("special test for 铜锣湾.chn.moe", () => {
}); expect(validLink("铜锣湾.chn.moe")).toBe(true);
});
test("Not a valid host/URL.", () => {
expect(validLink("weather")).toBe(false);
});
}); });
// Reference: https://www.iana.org/domains/root/db // Reference: https://www.iana.org/domains/root/db
describe("Check if the given TLD exist and assigned.", () => { describe("Check if the given TLD exist and assigned.", () => {
test("Valid normal TLD", () => { test("Valid normal TLD", () => {
expect(validTLD("example.com")).toBe(true); expect(validTLD("example.com")).toBe(true);
expect(validTLD("example.top")).toBe(true); expect(validTLD("example.top")).toBe(true);
expect(validTLD("example.net")).toBe(true); expect(validTLD("example.net")).toBe(true);
expect(validTLD("example.org")).toBe(true); expect(validTLD("example.org")).toBe(true);
}); });
test("Valid new TLDs", () => { test("Valid new TLDs", () => {
// they really exist! // they really exist!
expect(validTLD("example.foo")).toBe(true); expect(validTLD("example.foo")).toBe(true);
expect(validTLD("example.bar")).toBe(true); expect(validTLD("example.bar")).toBe(true);
expect(validTLD('example.zip')).toBe(true); expect(validTLD("example.zip")).toBe(true);
}); });
test("Exist but not assigned TLD", () => { test("Exist but not assigned TLD", () => {
expect(validTLD("example.active")).toBe(false); expect(validTLD("example.active")).toBe(false);
expect(validTLD("example.off")).toBe(false); expect(validTLD("example.off")).toBe(false);
}); });
test("with dot", () => { test("with dot", () => {
expect(validTLD("example.com")).toBe(true); expect(validTLD("example.com")).toBe(true);
expect(validTLD("example.us")).toBe(true); expect(validTLD("example.us")).toBe(true);
expect(validTLD("example.cn")).toBe(true); expect(validTLD("example.cn")).toBe(true);
expect(validTLD("example.io")).toBe(true); expect(validTLD("example.io")).toBe(true);
}); });
test("Punycode TLDs", () => { test("Punycode TLDs", () => {
expect(validTLD("example.中国")).toBe(true); expect(validTLD("example.中国")).toBe(true);
expect(validTLD("example.РФ")).toBe(true); expect(validTLD("example.РФ")).toBe(true);
expect(validTLD("example.कॉम")).toBe(true); expect(validTLD("example.कॉम")).toBe(true);
expect(validTLD("example.ایران")).toBe(true); expect(validTLD("example.ایران")).toBe(true);
expect(validTLD("example.இலங்கை")).toBe(true); expect(validTLD("example.இலங்கை")).toBe(true);
expect(validTLD("example.გე")).toBe(true); expect(validTLD("example.გე")).toBe(true);
expect(validTLD("example.ポイント")).toBe(true); expect(validTLD("example.ポイント")).toBe(true);
}); });
test("Punycode TLDs but not assigned", () => { test("Punycode TLDs but not assigned", () => {
expect(validTLD("example.テスト")).toBe(false); expect(validTLD("example.テスト")).toBe(false);
expect(validTLD("example.परीक्षा")).toBe(false); expect(validTLD("example.परीक्षा")).toBe(false);
expect(validTLD("example.测试")).toBe(false); expect(validTLD("example.测试")).toBe(false);
}); });
}); });

View File

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

View File

@ -1,11 +1,11 @@
{ {
"files": [], "files": [],
"references": [ "references": [
{ {
"path": "./tsconfig.app.json" "path": "./tsconfig.app.json"
}, },
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
} }
] ]
} }

View File

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

View File

@ -1,13 +1,9 @@
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc"; import react from "@vitejs/plugin-react-swc";
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from "vite-tsconfig-paths";
import { chunkSplitPlugin } from 'vite-plugin-chunk-split'; import { chunkSplitPlugin } from "vite-plugin-chunk-split";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [react(), tsconfigPaths(), chunkSplitPlugin()]
react(),
tsconfigPaths(),
chunkSplitPlugin()
]
}); });