Files
everyone-can-use-english/enjoy/src/renderer/context/app-settings-provider.tsx
an-lee 8bebf2072c Customize title bar (#1184)
* add basic title bar

* add title bar actions

* fix layout

* update title bar

* update layout

* fix title bar for macOS

* UI

* setup menu for macOS

* fix title bar logo
2024-11-17 16:02:17 +08:00

386 lines
11 KiB
TypeScript

import { createContext, useContext, useEffect, useState } from "react";
import { WEB_API_URL, LANGUAGES, IPA_MAPPINGS } from "@/constants";
import { Client } from "@/api";
import i18n from "@renderer/i18n";
import ahoy from "ahoy.js";
import { type Consumer, createConsumer } from "@rails/actioncable";
import { DbProviderContext } from "@renderer/context";
import { UserSettingKeyEnum } from "@/types/enums";
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogCancel,
AlertDialogAction,
} from "@renderer/components/ui";
import { t } from "i18next";
import { redirect } from "react-router-dom";
import { Preferences } from "@renderer/components";
type AppSettingsProviderState = {
webApi: Client;
apiUrl?: string;
setApiUrl?: (url: string) => Promise<void>;
user: UserType | null;
initialized: boolean;
version?: string;
latestVersion?: string;
libraryPath?: string;
login?: (user: UserType) => void;
logout?: () => void;
setLibraryPath?: (path: string) => Promise<void>;
EnjoyApp: EnjoyAppType;
language?: "en" | "zh-CN";
switchLanguage?: (language: "en" | "zh-CN") => void;
nativeLanguage?: string;
switchNativeLanguage?: (lang: string) => void;
learningLanguage?: string;
switchLearningLanguage?: (lang: string) => void;
proxy?: ProxyConfigType;
setProxy?: (config: ProxyConfigType) => Promise<void>;
vocabularyConfig?: VocabularyConfigType;
setVocabularyConfig?: (config: VocabularyConfigType) => Promise<void>;
cable?: Consumer;
ahoy?: typeof ahoy;
recorderConfig?: RecorderConfigType;
setRecorderConfig?: (config: RecorderConfigType) => Promise<void>;
// remote config
ipaMappings?: { [key: string]: string };
displayPreferences?: boolean;
setDisplayPreferences?: (display: boolean) => void;
};
const EnjoyApp = window.__ENJOY_APP__;
const initialState: AppSettingsProviderState = {
webApi: null,
user: null,
initialized: false,
EnjoyApp: EnjoyApp,
};
export const AppSettingsProviderContext =
createContext<AppSettingsProviderState>(initialState);
export const AppSettingsProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [version, setVersion] = useState<string>("");
const [latestVersion, setLatestVersion] = useState<string>("");
const [apiUrl, setApiUrl] = useState<string>(WEB_API_URL);
const [webApi, setWebApi] = useState<Client>(null);
const [cable, setCable] = useState<Consumer>();
const [user, setUser] = useState<UserType | null>(null);
const [libraryPath, setLibraryPath] = useState("");
const [language, setLanguage] = useState<"en" | "zh-CN">();
const [nativeLanguage, setNativeLanguage] = useState<string>("zh-CN");
const [learningLanguage, setLearningLanguage] = useState<string>("en-US");
const [vocabularyConfig, setVocabularyConfig] =
useState<VocabularyConfigType>(null);
const [proxy, setProxy] = useState<ProxyConfigType>();
const [recorderConfig, setRecorderConfig] = useState<RecorderConfigType>();
const [ipaMappings, setIpaMappings] = useState<{ [key: string]: string }>(
IPA_MAPPINGS
);
const [loggingOut, setLoggingOut] = useState<boolean>(false);
const [displayPreferences, setDisplayPreferences] = useState<boolean>(false);
const db = useContext(DbProviderContext);
const fetchLanguages = async () => {
const language = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.LANGUAGE
);
setLanguage((language as "en" | "zh-CN") || "en");
i18n.changeLanguage(language);
const _nativeLanguage =
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.NATIVE_LANGUAGE)) ||
"zh-CN";
setNativeLanguage(_nativeLanguage);
const _learningLanguage =
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.LEARNING_LANGUAGE)) ||
"en-US";
setLearningLanguage(_learningLanguage);
};
const switchLanguage = (language: "en" | "zh-CN") => {
EnjoyApp.userSettings
.set(UserSettingKeyEnum.LANGUAGE, language)
.then(() => {
i18n.changeLanguage(language);
setLanguage(language);
});
};
const switchNativeLanguage = (lang: string) => {
if (LANGUAGES.findIndex((l) => l.code == lang) < 0) return;
if (lang == learningLanguage) return;
setNativeLanguage(lang);
EnjoyApp.userSettings.set(UserSettingKeyEnum.NATIVE_LANGUAGE, lang);
};
const switchLearningLanguage = (lang: string) => {
if (LANGUAGES.findIndex((l) => l.code == lang) < 0) return;
if (lang == nativeLanguage) return;
EnjoyApp.userSettings.set(UserSettingKeyEnum.LEARNING_LANGUAGE, lang);
setLearningLanguage(lang);
};
const fetchVersion = async () => {
const version = EnjoyApp.app.version;
setVersion(version);
};
const fetchApiUrl = async () => {
const apiUrl = await EnjoyApp.app.apiUrl();
setApiUrl(apiUrl);
};
const autoLogin = async () => {
const currentUser = await EnjoyApp.appSettings.getUser();
if (!currentUser) return;
setUser(currentUser);
};
const login = async (user: UserType) => {
if (!user?.id) return;
setUser(user);
if (user.accessToken) {
// Set current user to App settings
EnjoyApp.appSettings.setUser({ id: user.id, name: user.name });
}
};
const logout = () => {
setUser(null);
EnjoyApp.appSettings.setUser(null);
};
const fetchLibraryPath = async () => {
const dir = await EnjoyApp.appSettings.getLibrary();
setLibraryPath(dir);
};
const setLibraryPathHandler = async (dir: string) => {
await EnjoyApp.appSettings.setLibrary(dir);
setLibraryPath(dir);
};
const fetchProxyConfig = async () => {
const config = await EnjoyApp.system.proxy.get();
setProxy(config);
EnjoyApp.system.proxy.refresh();
};
const setProxyConfigHandler = async (config: ProxyConfigType) => {
EnjoyApp.system.proxy.set(config).then(() => {
setProxy(config);
EnjoyApp.system.proxy.refresh();
});
};
const setApiUrlHandler = async (url: string) => {
EnjoyApp.appSettings.setApiUrl(url).then(() => {
EnjoyApp.app.reload();
});
};
const createCable = async (token: string) => {
if (!token) return;
const wsUrl = await EnjoyApp.app.wsUrl();
const consumer = createConsumer(wsUrl + "/cable?token=" + token);
setCable(consumer);
};
const fetchRecorderConfig = async () => {
const config = await EnjoyApp.userSettings.get(UserSettingKeyEnum.RECORDER);
if (config) {
setRecorderConfig(config);
} else {
const defaultConfig: RecorderConfigType = {
autoGainControl: true,
echoCancellation: true,
noiseSuppression: true,
sampleRate: 16000,
sampleSize: 16,
};
setRecorderConfigHandler(defaultConfig);
}
};
const setRecorderConfigHandler = async (config: RecorderConfigType) => {
return EnjoyApp.userSettings
.set(UserSettingKeyEnum.RECORDER, config)
.then(() => {
setRecorderConfig(config);
});
};
const fetchVocabularyConfig = async () => {
EnjoyApp.userSettings
.get(UserSettingKeyEnum.VOCABULARY)
.then((config) => {
setVocabularyConfig(config || { lookupOnMouseOver: true });
})
.catch((err) => {
console.error(err);
setVocabularyConfig({ lookupOnMouseOver: true });
});
};
const setVocabularyConfigHandler = async (config: VocabularyConfigType) => {
await EnjoyApp.userSettings.set(UserSettingKeyEnum.VOCABULARY, config);
setVocabularyConfig(config);
};
useEffect(() => {
if (db.state === "connected") {
fetchLanguages();
fetchVocabularyConfig();
fetchRecorderConfig();
}
}, [db.state]);
useEffect(() => {
autoLogin();
fetchVersion();
fetchLibraryPath();
fetchProxyConfig();
fetchApiUrl();
}, []);
useEffect(() => {
if (!apiUrl) return;
setWebApi(
new Client({
baseUrl: apiUrl,
accessToken: user?.accessToken,
locale: language,
onError: (err) => {
if (user && user.accessToken && err.status == 401) {
setUser({ ...user, accessToken: null });
}
},
})
);
}, [user?.accessToken, apiUrl, language]);
useEffect(() => {
if (!apiUrl) return;
ahoy.configure({
urlPrefix: apiUrl,
});
}, [apiUrl]);
useEffect(() => {
if (!webApi) return;
if (ipaMappings && latestVersion) return;
webApi.config("ipa_mappings").then((mappings) => {
if (mappings) setIpaMappings(mappings);
});
webApi.config("app_version").then((config) => {
if (config.version) setLatestVersion(config.version);
});
}, [webApi]);
useEffect(() => {
if (!user) return;
db.connect().then(async () => {
// Login via API, update profile to DB
if (user.accessToken) {
EnjoyApp.userSettings.set(UserSettingKeyEnum.PROFILE, user);
createCable(user.accessToken);
} else {
// Auto login from local settings, get full profile from DB
const profile = await EnjoyApp.userSettings.get(
UserSettingKeyEnum.PROFILE
);
setUser(profile);
EnjoyApp.appSettings.setUser({ id: profile.id, name: profile.name });
}
});
return () => {
db.disconnect();
setUser(null);
};
}, [user?.id]);
return (
<AppSettingsProviderContext.Provider
value={{
language,
switchLanguage,
nativeLanguage,
switchNativeLanguage,
learningLanguage,
switchLearningLanguage,
EnjoyApp,
version,
latestVersion,
webApi,
apiUrl,
setApiUrl: setApiUrlHandler,
user,
login,
logout: () => setLoggingOut(true),
libraryPath,
setLibraryPath: setLibraryPathHandler,
proxy,
setProxy: setProxyConfigHandler,
vocabularyConfig,
setVocabularyConfig: setVocabularyConfigHandler,
initialized: Boolean(user && db.state === "connected" && libraryPath),
ahoy,
cable,
recorderConfig,
setRecorderConfig: setRecorderConfigHandler,
ipaMappings,
displayPreferences,
setDisplayPreferences,
}}
>
{children}
<AlertDialog open={loggingOut} onOpenChange={setLoggingOut}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("logout")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("logoutConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={() => {
logout();
redirect("/landing");
}}
>
{t("logout")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</AppSettingsProviderContext.Provider>
);
};