add enjoy app

This commit is contained in:
an-lee
2024-01-09 15:19:32 +08:00
parent b88c52d5d8
commit aebd9ee213
434 changed files with 34955 additions and 62 deletions

View File

@@ -0,0 +1,37 @@
import { t } from "i18next";
import { Button, useToast } from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { useState, useContext } from "react";
import { LoaderIcon } from "lucide-react";
export const About = () => {
const { version } = useContext(AppSettingsProviderContext);
const [checking, setChecking] = useState<boolean>(false);
const { toast } = useToast();
const checkUpdate = () => {
setChecking(true);
setTimeout(() => {
setChecking(false);
toast({
description: t("alreadyLatestVersion"),
});
}, 1000);
};
return (
<>
<div className="font-semibold mb-4 capitilized">{t("about")}</div>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("currentVersion")}</div>
<div className="text-sm text-muted-foreground mb-2">v{version}</div>
</div>
<Button disabled={checking} onClick={checkUpdate}>
{checking && <LoaderIcon className="animate-spin mr-1 w-4 h-4" />}
{t("checkUpdate")}
</Button>
</div>
</>
);
};

View File

@@ -0,0 +1,43 @@
import { t } from "i18next";
import { Button, Separator } from "@renderer/components/ui";
import { ResetAllButton } from "@renderer/components";
import { InfoIcon } from "lucide-react";
export const AdvancedSettings = () => {
return (
<>
<div className="font-semibold mb-4 capitilized">
{t("advancedSettings")}
</div>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("resetAll")}</div>
<div className="text-sm text-muted-foreground mb-2">
{t("logoutAndRemoveAllPersonalData")}
</div>
</div>
<div className="">
<div className="mb-2 flex justify-end">
<ResetAllButton>
<Button
variant="secondary"
className="text-destructive"
size="sm"
>
{t("reset")}
</Button>
</ResetAllButton>
</div>
<div className="text-xs text-muted-foreground">
<InfoIcon className="mr-1 w-3 h-3 inline" />
<span>{t("relaunchIsNeededAfterChanged")}</span>
</div>
</div>
</div>
<Separator />
</>
);
};

View File

@@ -0,0 +1,327 @@
import { t } from "i18next";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
Avatar,
AvatarImage,
AvatarFallback,
Button,
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogDescription,
DialogFooter,
Input,
Label,
Separator,
useToast,
} from "@renderer/components/ui";
import { WhisperModelOptions } from "@renderer/components";
import {
AppSettingsProviderContext,
AISettingsProviderContext,
} from "@renderer/context";
import { useContext, useState, useRef, useEffect } from "react";
import { redirect } from "react-router-dom";
import { InfoIcon } from "lucide-react";
export const BasicSettings = () => {
return (
<div className="">
<div className="font-semibold mb-4 capitilized">{t("basicSettings")}</div>
<UserSettings />
<Separator />
<LibraryPathSettings />
<Separator />
<WhisperSettings />
<Separator />
<OpenaiSettings />
<Separator />
<GoogleGenerativeAiSettings />
<Separator />
</div>
);
};
const UserSettings = () => {
const { user, logout } = useContext(AppSettingsProviderContext);
if (!user) return null;
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="flex items-center space-x-2">
<Avatar>
<AvatarImage src={user.avatarUrl} />
<AvatarFallback className="text-xl">
{user.name[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="">
<div className="text-sm font-semibold">{user.name}</div>
<div className="text-xs text-muted-foreground">{user.id}</div>
</div>
</div>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" className="text-destructive" size="sm">
{t("logout")}
</Button>
</AlertDialogTrigger>
<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("/");
}}
>
{t("logout")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
const LibraryPathSettings = () => {
const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext);
const handleChooseLibraryPath = async () => {
const filePaths = await EnjoyApp.dialog.showOpenDialog({
properties: ["openDirectory"],
});
if (filePaths) {
EnjoyApp.settings.setLibrary(filePaths[0]);
const _library = await EnjoyApp.settings.getLibrary();
if (_library !== libraryPath) {
EnjoyApp.app.relaunch();
}
}
};
const openLibraryPath = async () => {
if (libraryPath) {
await EnjoyApp.shell.openPath(libraryPath);
}
};
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("libraryPath")}</div>
<div className="text-sm text-muted-foreground mb-2">{libraryPath}</div>
</div>
<div className="">
<div className="flex items-center justify-end space-x-2 mb-2">
<Button variant="secondary" size="sm" onClick={openLibraryPath}>
{t("open")}
</Button>
<Button variant="default" size="sm" onClick={handleChooseLibraryPath}>
{t("edit")}
</Button>
</div>
<div className="text-xs text-muted-foreground">
<InfoIcon className="mr-1 w-3 h-3 inline" />
<span>{t("relaunchIsNeededAfterChanged")}</span>
</div>
</div>
</div>
);
};
const WhisperSettings = () => {
const { whisperModel, whisperModelsPath } = useContext(
AppSettingsProviderContext
);
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("sttAiModel")}</div>
<div className="text-sm text-muted-foreground">{whisperModel}</div>
</div>
<Dialog>
<DialogTrigger asChild>
<Button size="sm">{t("edit")}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>{t("sttAiModel")}</DialogHeader>
<DialogDescription>
{t("chooseAIModelDependingOnYourHardware")}
</DialogDescription>
<WhisperModelOptions />
<DialogFooter>
<div className="text-xs opacity-70 flex items-start">
<InfoIcon className="mr-1.5 w-4 h-4" />
<span className="flex-1">
{t("yourModelsWillBeDownloadedTo", {
path: whisperModelsPath,
})}
</span>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
const OpenaiSettings = () => {
const { openai, setOpenai } = useContext(AISettingsProviderContext);
const [editing, setEditing] = useState(false);
const ref = useRef<HTMLInputElement>();
const { toast } = useToast();
const handleSave = () => {
if (!ref.current) return;
setOpenai({
key: ref.current.value,
});
setEditing(false);
toast({
title: t("success"),
description: t("openaiKeySaved"),
});
};
useEffect(() => {
if (editing) {
ref.current?.focus();
}
}, [editing]);
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">Open AI</div>
<div className="text-sm text-muted-foreground">
<div className="flex items-center space-x-4">
<Label>{t("key")}:</Label>
<Input
ref={ref}
type="password"
defaultValue={openai?.key}
placeholder="sk-*********"
disabled={!editing}
className="focus-visible:outline-0 focus-visible:ring-0 shadow-none"
/>
{editing && (
<Button
size="sm"
className="min-w-max text-md"
onClick={handleSave}
>
{t("save")}
</Button>
)}
</div>
</div>
</div>
<div className="">
<Button
variant={editing ? "secondary" : "default"}
size="sm"
onClick={() => setEditing(!editing)}
>
{editing ? t("cancel") : t("edit")}
</Button>
</div>
</div>
);
};
const GoogleGenerativeAiSettings = () => {
const { googleGenerativeAi, setGoogleGenerativeAi } = useContext(
AISettingsProviderContext
);
const [editing, setEditing] = useState(false);
const ref = useRef<HTMLInputElement>();
const { toast } = useToast();
const handleSave = () => {
if (!ref.current) return;
setGoogleGenerativeAi({
key: ref.current.value,
});
setEditing(false);
toast({
title: t("success"),
description: t("googleGenerativeAiKeySaved"),
});
};
useEffect(() => {
if (editing) {
ref.current?.focus();
}
}, [editing]);
return (
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">Google Generative AI</div>
<div className="text-sm text-muted-foreground">
<div className="flex items-center space-x-4">
<Label>{t("key")}:</Label>
<Input
ref={ref}
type="password"
defaultValue={googleGenerativeAi?.key}
placeholder="*********"
disabled={!editing}
className="focus-visible:outline-0 focus-visible:ring-0 shadow-none"
/>
{editing && (
<Button
size="sm"
className="min-w-max text-md"
onClick={handleSave}
>
{t("save")}
</Button>
)}
</div>
</div>
</div>
<div className="">
<Button
variant={editing ? "secondary" : "default"}
size="sm"
onClick={() => setEditing(!editing)}
>
{editing ? t("cancel") : t("edit")}
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,4 @@
export * from './preferences';
export * from './basic-settings';
export * from './advanced-settings';
export * from './about';

View File

@@ -0,0 +1,53 @@
import { t } from "i18next";
import { Button, ScrollArea } from "@renderer/components/ui";
import { BasicSettings, AdvancedSettings, About } from "@renderer/components";
import { useState } from "react";
export const Preferences = () => {
const TABS = [
{
value: "basic",
label: t("basicSettings"),
component: () => <BasicSettings />,
},
{
value: "advanced",
label: t("advancedSettings"),
component: () => <AdvancedSettings />,
},
{
value: "about",
label: t("about"),
component: () => <About />,
},
];
const [activeTab, setActiveTab] = useState<string>("basic");
return (
<div className="grid grid-cols-5">
<ScrollArea className="col-span-1 h-full bg-muted/50 p-4">
<div className="py-2 text-muted-foreground mb-4">
{t("sidebar.preferences")}
</div>
{TABS.map((tab) => (
<Button
key={tab.value}
variant={activeTab === tab.value ? "default" : "ghost"}
size="sm"
className={`capitilized w-full justify-start mb-2 ${
activeTab === tab.value ? "" : "hover:bg-muted"
}`}
onClick={() => setActiveTab(tab.value)}
>
<span className="text-sm">{tab.label}</span>
</Button>
))}
</ScrollArea>
<ScrollArea className="col-span-4 p-6">
{TABS.find((tab) => tab.value === activeTab)?.component()}
</ScrollArea>
</div>
);
};