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
This commit is contained in:
@@ -343,7 +343,9 @@
|
|||||||
"logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings",
|
"logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings",
|
||||||
"hotkeys": "Hotkeys",
|
"hotkeys": "Hotkeys",
|
||||||
"player": "Player",
|
"player": "Player",
|
||||||
|
"quit": "Quit",
|
||||||
"quitApp": "Quit APP",
|
"quitApp": "Quit APP",
|
||||||
|
"quitAppDescription": "Are you sure to quit Enjoy app?",
|
||||||
"openPreferences": "Open preferences",
|
"openPreferences": "Open preferences",
|
||||||
"openCopilot": "Open copilot",
|
"openCopilot": "Open copilot",
|
||||||
"playOrPause": "Play or pause",
|
"playOrPause": "Play or pause",
|
||||||
@@ -929,5 +931,7 @@
|
|||||||
"compressMediaBeforeAdding": "Compress media before adding",
|
"compressMediaBeforeAdding": "Compress media before adding",
|
||||||
"keepOriginalMedia": "Keep original media",
|
"keepOriginalMedia": "Keep original media",
|
||||||
"myPronunciation": "My pronunciation",
|
"myPronunciation": "My pronunciation",
|
||||||
"originalPronunciation": "Original pronunciation"
|
"originalPronunciation": "Original pronunciation",
|
||||||
|
"reloadApp": "reload app",
|
||||||
|
"exit": "Exit"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,9 @@
|
|||||||
"hotkeys": "快捷键",
|
"hotkeys": "快捷键",
|
||||||
"system": "系统",
|
"system": "系统",
|
||||||
"player": "播放器",
|
"player": "播放器",
|
||||||
|
"quit": "退出",
|
||||||
"quitApp": "退出应用",
|
"quitApp": "退出应用",
|
||||||
|
"quitAppDescription": "确定要退出 Enjoy 应用吗?",
|
||||||
"openPreferences": "打开设置",
|
"openPreferences": "打开设置",
|
||||||
"openCopilot": "打开 Copilot",
|
"openCopilot": "打开 Copilot",
|
||||||
"playOrPause": "播放/暂停",
|
"playOrPause": "播放/暂停",
|
||||||
@@ -929,5 +931,7 @@
|
|||||||
"compressMediaBeforeAdding": "添加前压缩媒体",
|
"compressMediaBeforeAdding": "添加前压缩媒体",
|
||||||
"keepOriginalMedia": "保存原始媒体",
|
"keepOriginalMedia": "保存原始媒体",
|
||||||
"myPronunciation": "我的发音",
|
"myPronunciation": "我的发音",
|
||||||
"originalPronunciation": "原始发音"
|
"originalPronunciation": "原始发音",
|
||||||
|
"reloadApp": "重载应用",
|
||||||
|
"exit": "退出"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,12 @@
|
|||||||
.scroll {
|
.scroll {
|
||||||
@apply scrollbar-thin scrollbar-thumb-primary scrollbar-track-secondary;
|
@apply scrollbar-thin scrollbar-thumb-primary scrollbar-track-secondary;
|
||||||
}
|
}
|
||||||
|
.draggable-region {
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
.non-draggable-region {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import dict from "./dict";
|
|||||||
import mdict from "./mdict";
|
import mdict from "./mdict";
|
||||||
import decompresser from "./decompresser";
|
import decompresser from "./decompresser";
|
||||||
import { UserSetting } from "@main/db/models";
|
import { UserSetting } from "@main/db/models";
|
||||||
|
import { platform } from "os";
|
||||||
|
import { t } from "i18next";
|
||||||
|
|
||||||
const __filename = url.fileURLToPath(import.meta.url);
|
const __filename = url.fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -520,6 +522,14 @@ ${log}
|
|||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
},
|
},
|
||||||
|
frame: false,
|
||||||
|
titleBarStyle: "hidden",
|
||||||
|
titleBarOverlay: process.platform === "darwin",
|
||||||
|
trafficLightPosition: {
|
||||||
|
x: 10,
|
||||||
|
y: 8,
|
||||||
|
},
|
||||||
|
useContentSize: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on("ready-to-show", () => {
|
mainWindow.on("ready-to-show", () => {
|
||||||
@@ -527,7 +537,62 @@ ${log}
|
|||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on("resize", () => {
|
mainWindow.on("resize", () => {
|
||||||
mainWindow.webContents.send("window-on-resize", mainWindow.getBounds());
|
mainWindow.webContents.send("window-on-change", {
|
||||||
|
event: "resize",
|
||||||
|
state: mainWindow.getBounds(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on("enter-full-screen", () => {
|
||||||
|
mainWindow.webContents.send("window-on-change", { event: "enter-full-screen" });
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on("leave-full-screen", () => {
|
||||||
|
mainWindow.webContents.send("window-on-change", { event: "leave-full-screen" });
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on("maximize", () => {
|
||||||
|
mainWindow.webContents.send("window-on-change", { event: "maximize" });
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on("unmaximize", () => {
|
||||||
|
mainWindow.webContents.send("window-on-change", { event: "unmaximize" });
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-is-maximized", () => {
|
||||||
|
return mainWindow.isMaximized();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-toggle-maximized", () => {
|
||||||
|
if (mainWindow.isMaximized()) {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
} else {
|
||||||
|
mainWindow.maximize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-maximize", () => {
|
||||||
|
mainWindow.maximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-unmaximize", () => {
|
||||||
|
mainWindow.unmaximize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-fullscreen", () => {
|
||||||
|
mainWindow.setFullScreen(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-unfullscreen", () => {
|
||||||
|
mainWindow.setFullScreen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-minimize", () => {
|
||||||
|
mainWindow.minimize();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("window-close", () => {
|
||||||
|
app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(() => {
|
mainWindow.webContents.setWindowOpenHandler(() => {
|
||||||
@@ -571,7 +636,42 @@ ${log}
|
|||||||
// mainWindow.webContents.openDevTools();
|
// mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
Menu.setApplicationMenu(null);
|
if (platform() === "darwin") {
|
||||||
|
const menu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: "about" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "hide" },
|
||||||
|
{ role: "unhide" },
|
||||||
|
{ type: "separator" },
|
||||||
|
{ role: "quit" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "&Help",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Check for Updates...",
|
||||||
|
click: () => {
|
||||||
|
shell.openExternal("https://1000h.org/enjoy-app/install.html");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Report Issue...",
|
||||||
|
click: () => {
|
||||||
|
shell.openExternal(`${REPO_URL}/issues/new`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
} else {
|
||||||
|
Menu.setApplicationMenu(null);
|
||||||
|
}
|
||||||
|
|
||||||
main.win = mainWindow;
|
main.win = mainWindow;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -54,14 +54,49 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
|||||||
version,
|
version,
|
||||||
},
|
},
|
||||||
window: {
|
window: {
|
||||||
onResize: (
|
isFullScreen: () => {
|
||||||
|
return ipcRenderer.invoke("window-is-full-screen");
|
||||||
|
},
|
||||||
|
toggleFullscreen: () => {
|
||||||
|
return ipcRenderer.invoke("window-fullscreen");
|
||||||
|
},
|
||||||
|
isMaximized: () => {
|
||||||
|
return ipcRenderer.invoke("window-is-maximized");
|
||||||
|
},
|
||||||
|
toggleMaximized: () => {
|
||||||
|
return ipcRenderer.invoke("window-toggle-maximized");
|
||||||
|
},
|
||||||
|
maximize: () => {
|
||||||
|
return ipcRenderer.invoke("window-maximize");
|
||||||
|
},
|
||||||
|
unmaximize: () => {
|
||||||
|
return ipcRenderer.invoke("window-unmaximize");
|
||||||
|
},
|
||||||
|
fullscreen: () => {
|
||||||
|
return ipcRenderer.invoke("window-fullscreen");
|
||||||
|
},
|
||||||
|
unfullscreen: () => {
|
||||||
|
return ipcRenderer.invoke("window-unfullscreen");
|
||||||
|
},
|
||||||
|
minimize: () => {
|
||||||
|
return ipcRenderer.invoke("window-minimize");
|
||||||
|
},
|
||||||
|
close: () => {
|
||||||
|
return ipcRenderer.invoke("window-close");
|
||||||
|
},
|
||||||
|
onChange: (
|
||||||
callback: (
|
callback: (
|
||||||
event: IpcRendererEvent,
|
event: IpcRendererEvent,
|
||||||
bounds: { x: number; y: number; width: number; height: number }
|
state: { event: string; state: any }
|
||||||
) => void
|
) => void
|
||||||
) => ipcRenderer.on("window-on-resize", callback),
|
) => ipcRenderer.on("window-on-change", callback),
|
||||||
removeListeners: () => {
|
removeListener: (
|
||||||
ipcRenderer.removeAllListeners("window-on-resize");
|
listener: (event: IpcRendererEvent, ...args: any[]) => void
|
||||||
|
) => {
|
||||||
|
ipcRenderer.removeListener("window-on-change", listener);
|
||||||
|
},
|
||||||
|
removeAllListeners: () => {
|
||||||
|
ipcRenderer.removeAllListeners("window-on-change");
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ export const ChatSession = (props: {
|
|||||||
|
|
||||||
if (!chatId) {
|
if (!chatId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-[calc(100vh-2rem)]">
|
||||||
<span className="text-muted-foreground">{t("noChatSelected")}</span>
|
<span className="text-muted-foreground">{t("noChatSelected")}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-screen relative">
|
<ScrollArea className="h-[calc(100vh-2rem)] relative">
|
||||||
<ChatSessionProvider chatId={chatId}>
|
<ChatSessionProvider chatId={chatId}>
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
sidePanelCollapsed={sidePanelCollapsed}
|
sidePanelCollapsed={sidePanelCollapsed}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const CopilotSession = () => {
|
|||||||
const { currentChat } = useContext(CopilotProviderContext);
|
const { currentChat } = useContext(CopilotProviderContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-screen relative">
|
<ScrollArea className="h-[calc(100vh-2rem)] relative">
|
||||||
{currentChat?.id ? (
|
{currentChat?.id ? (
|
||||||
<ChatSessionProvider chatId={currentChat.id}>
|
<ChatSessionProvider chatId={currentChat.id}>
|
||||||
<CopilotHeader />
|
<CopilotHeader />
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ export * from "./users";
|
|||||||
export * from "./videos";
|
export * from "./videos";
|
||||||
export * from "./widgets";
|
export * from "./widgets";
|
||||||
export * from "./login";
|
export * from "./login";
|
||||||
|
export * from "./layouts";
|
||||||
|
|||||||
2
enjoy/src/renderer/components/layouts/index.ts
Normal file
2
enjoy/src/renderer/components/layouts/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./layout";
|
||||||
|
export * from "./title-bar";
|
||||||
66
enjoy/src/renderer/components/layouts/layout.tsx
Normal file
66
enjoy/src/renderer/components/layouts/layout.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
AppSettingsProviderContext,
|
||||||
|
CopilotProviderContext,
|
||||||
|
} from "@renderer/context";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { CopilotSession, TitleBar, Sidebar } from "@renderer/components";
|
||||||
|
import {
|
||||||
|
ResizableHandle,
|
||||||
|
ResizablePanel,
|
||||||
|
ResizablePanelGroup,
|
||||||
|
} from "@renderer/components/ui";
|
||||||
|
|
||||||
|
export const Layout = () => {
|
||||||
|
const { initialized } = useContext(AppSettingsProviderContext);
|
||||||
|
const { active, setActive } = useContext(CopilotProviderContext);
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col">
|
||||||
|
<TitleBar />
|
||||||
|
<ResizablePanelGroup
|
||||||
|
direction="horizontal"
|
||||||
|
className="flex-1 h-full w-full"
|
||||||
|
data-testid="layout-home"
|
||||||
|
>
|
||||||
|
<ResizablePanel id="main-panel" order={1} minSize={50}>
|
||||||
|
<div className="flex flex-start">
|
||||||
|
<Sidebar />
|
||||||
|
<div className="flex-1 border-l overflow-x-hidden overflow-y-auto h-[calc(100vh-2rem)]">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
{active && (
|
||||||
|
<>
|
||||||
|
<ResizableHandle />
|
||||||
|
<ResizablePanel
|
||||||
|
id="copilot-panel"
|
||||||
|
order={2}
|
||||||
|
collapsible={true}
|
||||||
|
defaultSize={30}
|
||||||
|
maxSize={50}
|
||||||
|
minSize={15}
|
||||||
|
onCollapse={() => setActive(false)}
|
||||||
|
>
|
||||||
|
<div className="h-[calc(100vh-2rem)]">
|
||||||
|
<CopilotSession />
|
||||||
|
</div>
|
||||||
|
</ResizablePanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ResizablePanelGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col w-full">
|
||||||
|
<TitleBar />
|
||||||
|
<div className="flex-1 h-[calc(100vh-2rem)] overflow-y-auto">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
275
enjoy/src/renderer/components/layouts/title-bar.tsx
Normal file
275
enjoy/src/renderer/components/layouts/title-bar.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
import {
|
||||||
|
AppSettingsProviderContext,
|
||||||
|
CopilotProviderContext,
|
||||||
|
} from "@/renderer/context";
|
||||||
|
import { ChevronRightIcon } from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogAction,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from "@renderer/components/ui";
|
||||||
|
import { IpcRendererEvent } from "electron/renderer";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import {
|
||||||
|
ExternalLinkIcon,
|
||||||
|
HelpCircleIcon,
|
||||||
|
LightbulbIcon,
|
||||||
|
LightbulbOffIcon,
|
||||||
|
MaximizeIcon,
|
||||||
|
MenuIcon,
|
||||||
|
MinimizeIcon,
|
||||||
|
MinusIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
XIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const TitleBar = () => {
|
||||||
|
const [isMaximized, setIsMaximized] = useState(false);
|
||||||
|
const [isFullScreen, setIsFullScreen] = useState(false);
|
||||||
|
const [platform, setPlatform] = useState<"darwin" | "win32" | "linux">();
|
||||||
|
|
||||||
|
const { EnjoyApp, setDisplayPreferences, initialized } = useContext(
|
||||||
|
AppSettingsProviderContext
|
||||||
|
);
|
||||||
|
const { active, setActive } = useContext(CopilotProviderContext);
|
||||||
|
|
||||||
|
const onWindowChange = (
|
||||||
|
_event: IpcRendererEvent,
|
||||||
|
state: { event: string }
|
||||||
|
) => {
|
||||||
|
if (state.event === "maximize") {
|
||||||
|
setIsMaximized(true);
|
||||||
|
} else if (state.event === "unmaximize") {
|
||||||
|
setIsMaximized(false);
|
||||||
|
} else if (state.event === "enter-full-screen") {
|
||||||
|
setIsFullScreen(true);
|
||||||
|
} else if (state.event === "leave-full-screen") {
|
||||||
|
setIsFullScreen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
EnjoyApp.window.onChange(onWindowChange);
|
||||||
|
EnjoyApp.app.getPlatformInfo().then((info) => {
|
||||||
|
setPlatform(info.platform as "darwin" | "win32" | "linux");
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
EnjoyApp.window.removeListener(onWindowChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-50 h-8 w-full bg-muted draggable-region flex items-center justify-between border-b">
|
||||||
|
<div className="flex items-center px-2">
|
||||||
|
{platform === "darwin" && !isFullScreen && <div className="w-16"></div>}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
<img src="./assets/icon.png" alt="Enjoy" className="size-5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" side="bottom">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => EnjoyApp.app.reload()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="capitalize">{t("reloadApp")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => EnjoyApp.window.close()}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="capitalize">{t("exit")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
{initialized && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-primary/10"
|
||||||
|
onClick={() => setDisplayPreferences(true)}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-primary/10"
|
||||||
|
>
|
||||||
|
<HelpCircleIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="w-[--radix-dropdown-menu-trigger-width]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
EnjoyApp.shell.openExternal("https://1000h.org/enjoy-app/")
|
||||||
|
}
|
||||||
|
className="flex justify-between space-x-4"
|
||||||
|
>
|
||||||
|
<span className="min-w-fit capitalize">{t("userGuide")}</span>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger className="non-draggable-region">
|
||||||
|
<span className="capitalize">
|
||||||
|
{t("feedback")}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
EnjoyApp.shell.openExternal(
|
||||||
|
"https://mixin.one/codes/f6ff96b8-54fb-4ad8-a6d4-5a5bdb1df13e"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="flex justify-between space-x-4"
|
||||||
|
>
|
||||||
|
<span>Mixin</span>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
EnjoyApp.shell.openExternal(
|
||||||
|
"https://github.com/zuodaotech/everyone-can-use-english/discussions"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="flex justify-between space-x-4"
|
||||||
|
>
|
||||||
|
<span>Github</span>
|
||||||
|
<ExternalLinkIcon className="size-4" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
EnjoyApp.shell.openExternal(
|
||||||
|
"https://1000h.org/enjoy-app/install.html"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="capitalize">{t("checkUpdate")}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{initialized && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className={`size-8 rounded-none non-draggable-region hover:bg-primary/10 ${
|
||||||
|
active ? "bg-primary/10" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setActive(!active)}
|
||||||
|
>
|
||||||
|
{active ? (
|
||||||
|
<LightbulbIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<LightbulbOffIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{platform !== "darwin" && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-primary/10"
|
||||||
|
onClick={() => EnjoyApp.window.minimize()}
|
||||||
|
>
|
||||||
|
<MinusIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-primary/10"
|
||||||
|
onClick={() => EnjoyApp.window.toggleMaximized()}
|
||||||
|
>
|
||||||
|
{isMaximized ? (
|
||||||
|
<MinimizeIcon className="size-4" />
|
||||||
|
) : (
|
||||||
|
<MaximizeIcon className="size-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8 rounded-none non-draggable-region hover:bg-destructive"
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>{t("quitApp")}</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
{t("quitAppDescription")}
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
onClick={() => EnjoyApp.window.close()}
|
||||||
|
>
|
||||||
|
{t("quit")}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -38,7 +38,7 @@ export const LoginForm = () => {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<div className="px-4 py-2 border rounded-lg w-full max-w-sm">
|
<div className="px-4 py-2 border rounded-lg w-full max-w-sm m-auto">
|
||||||
<UserSettings />
|
<UserSettings />
|
||||||
<Separator />
|
<Separator />
|
||||||
<LanguageSettings />
|
<LanguageSettings />
|
||||||
|
|||||||
@@ -448,10 +448,8 @@ export const MediaCurrentRecording = () => {
|
|||||||
debouncedCalContainerSize();
|
debouncedCalContainerSize();
|
||||||
});
|
});
|
||||||
observer.observe(ref.current);
|
observer.observe(ref.current);
|
||||||
EnjoyApp.window.onResize(debouncedCalContainerSize);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
EnjoyApp.window.removeListeners();
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [ref, player]);
|
}, [ref, player]);
|
||||||
|
|||||||
@@ -144,10 +144,7 @@ export const MediaWaveform = () => {
|
|||||||
});
|
});
|
||||||
observer.observe(ref.current);
|
observer.observe(ref.current);
|
||||||
|
|
||||||
EnjoyApp.window.onResize(debouncedCalContainerSize);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
EnjoyApp.window.removeListeners();
|
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
};
|
};
|
||||||
}, [ref, wavesurfer]);
|
}, [ref, wavesurfer]);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
export * from "./db-state";
|
export * from "./db-state";
|
||||||
export * from "./layout";
|
|
||||||
export * from "./loader-spin";
|
export * from "./loader-spin";
|
||||||
export * from "./markdown-wrapper";
|
export * from "./markdown-wrapper";
|
||||||
export * from "./no-records-found";
|
export * from "./no-records-found";
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
import { Sidebar } from "./sidebar";
|
|
||||||
import { Outlet, Link } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
AppSettingsProviderContext,
|
|
||||||
CopilotProviderContext,
|
|
||||||
DbProviderContext,
|
|
||||||
} from "@renderer/context";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import { Button } from "@renderer/components/ui/button";
|
|
||||||
import { DbState, CopilotSession } from "@renderer/components";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from "@renderer/components/ui";
|
|
||||||
|
|
||||||
export const Layout = () => {
|
|
||||||
const { initialized } = useContext(AppSettingsProviderContext);
|
|
||||||
const db = useContext(DbProviderContext);
|
|
||||||
const { active, setActive } = useContext(CopilotProviderContext);
|
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="h-screen flex justify-center items-center"
|
|
||||||
date-testid="layout-onboarding"
|
|
||||||
>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="text-lg mb-6">
|
|
||||||
{t("welcomeTo")} <span className="font-semibold">Enjoy App</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="">
|
|
||||||
<Link data-testid="landing-button" to="/landing" replace>
|
|
||||||
<Button size="lg">{t("startToUse")}</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else if (db.state === "connected") {
|
|
||||||
return (
|
|
||||||
<ResizablePanelGroup
|
|
||||||
direction="horizontal"
|
|
||||||
className="h-screen w-full"
|
|
||||||
data-testid="layout-home"
|
|
||||||
>
|
|
||||||
<ResizablePanel id="main-panel" order={1} minSize={50}>
|
|
||||||
<div className="flex flex-start">
|
|
||||||
<Sidebar />
|
|
||||||
<div className="flex-1 border-l overflow-x-hidden overflow-y-auto h-screen">
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
{active && (
|
|
||||||
<>
|
|
||||||
<ResizableHandle />
|
|
||||||
<ResizablePanel
|
|
||||||
id="copilot-panel"
|
|
||||||
order={2}
|
|
||||||
collapsible={true}
|
|
||||||
defaultSize={30}
|
|
||||||
maxSize={50}
|
|
||||||
minSize={15}
|
|
||||||
onCollapse={() => setActive(false)}
|
|
||||||
>
|
|
||||||
<div className="h-screen">
|
|
||||||
<CopilotSession />
|
|
||||||
</div>
|
|
||||||
</ResizablePanel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ResizablePanelGroup>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="h-screen w-screen flex justify-center items-center"
|
|
||||||
data-testid="layout-db-error"
|
|
||||||
>
|
|
||||||
<DbState />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
DialogContent,
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
Separator,
|
Separator,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@@ -30,8 +24,6 @@ import {
|
|||||||
BotIcon,
|
BotIcon,
|
||||||
UsersRoundIcon,
|
UsersRoundIcon,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
HelpCircleIcon,
|
|
||||||
ExternalLinkIcon,
|
|
||||||
NotebookPenIcon,
|
NotebookPenIcon,
|
||||||
SpeechIcon,
|
SpeechIcon,
|
||||||
GraduationCapIcon,
|
GraduationCapIcon,
|
||||||
@@ -40,7 +32,6 @@ import {
|
|||||||
PanelLeftCloseIcon,
|
PanelLeftCloseIcon,
|
||||||
ChevronsUpDownIcon,
|
ChevronsUpDownIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
ChevronRightIcon,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useLocation, Link, useNavigate } from "react-router-dom";
|
import { useLocation, Link, useNavigate } from "react-router-dom";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
@@ -53,7 +44,8 @@ import { useState } from "react";
|
|||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const activeTab = location.pathname;
|
const activeTab = location.pathname;
|
||||||
const { EnjoyApp, cable } = useContext(AppSettingsProviderContext);
|
const { EnjoyApp, cable, displayPreferences, setDisplayPreferences } =
|
||||||
|
useContext(AppSettingsProviderContext);
|
||||||
const [isOpen, setIsOpen] = useState(true);
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -83,7 +75,7 @@ export const Sidebar = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`h-[100vh] transition-all relative ${
|
className={`h-[calc(100vh-2rem)] pt-8 transition-all relative draggable-region ${
|
||||||
isOpen ? "w-48" : "w-12"
|
isOpen ? "w-48" : "w-12"
|
||||||
}`}
|
}`}
|
||||||
data-testid="sidebar"
|
data-testid="sidebar"
|
||||||
@@ -93,7 +85,7 @@ export const Sidebar = () => {
|
|||||||
isOpen ? "w-48" : "w-12"
|
isOpen ? "w-48" : "w-12"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<ScrollArea className="w-full h-full pb-12">
|
<ScrollArea className="w-full h-full pb-12 pt-8">
|
||||||
<SidebarHeader isOpen={isOpen} />
|
<SidebarHeader isOpen={isOpen} />
|
||||||
<div className="grid gap-2 mb-4">
|
<div className="grid gap-2 mb-4">
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
@@ -194,28 +186,30 @@ export const Sidebar = () => {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<Dialog>
|
<div className="px-1 non-draggable-region">
|
||||||
<DialogTrigger asChild>
|
<Button
|
||||||
<div className="px-1">
|
size="sm"
|
||||||
<Button
|
variant={displayPreferences ? "default" : "ghost"}
|
||||||
size="sm"
|
id="preferences-button"
|
||||||
variant="ghost"
|
className={`w-full ${
|
||||||
id="preferences-button"
|
isOpen ? "justify-start" : "justify-center"
|
||||||
className={`w-full ${
|
}`}
|
||||||
isOpen ? "justify-start" : "justify-center"
|
data-tooltip-id="global-tooltip"
|
||||||
}`}
|
data-tooltip-content={t("sidebar.preferences")}
|
||||||
data-tooltip-id="global-tooltip"
|
data-tooltip-place="right"
|
||||||
data-tooltip-content={t("sidebar.preferences")}
|
onClick={() => setDisplayPreferences(true)}
|
||||||
data-tooltip-place="right"
|
>
|
||||||
>
|
<SettingsIcon className="size-4" />
|
||||||
<SettingsIcon className="size-4" />
|
{isOpen && (
|
||||||
{isOpen && (
|
<span className="ml-2"> {t("sidebar.preferences")} </span>
|
||||||
<span className="ml-2"> {t("sidebar.preferences")} </span>
|
)}
|
||||||
)}
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={displayPreferences}
|
||||||
|
onOpenChange={setDisplayPreferences}
|
||||||
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0"
|
className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0"
|
||||||
@@ -226,82 +220,6 @@ export const Sidebar = () => {
|
|||||||
<Preferences />
|
<Preferences />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<div className="px-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className={`w-full ${
|
|
||||||
isOpen ? "justify-start" : "justify-center"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<HelpCircleIcon className="size-4" />
|
|
||||||
{isOpen && (
|
|
||||||
<>
|
|
||||||
<span className="ml-2"> {t("sidebar.help")} </span>
|
|
||||||
<ChevronRightIcon className="w-4 h-4 ml-auto" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
|
|
||||||
<DropdownMenuContent
|
|
||||||
className="w-[--radix-dropdown-menu-trigger-width]"
|
|
||||||
align="start"
|
|
||||||
side="top"
|
|
||||||
>
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
EnjoyApp.shell.openExternal(
|
|
||||||
"https://1000h.org/enjoy-app/"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex justify-between space-x-4"
|
|
||||||
>
|
|
||||||
<span className="min-w-fit">{t("userGuide")}</span>
|
|
||||||
<ExternalLinkIcon className="size-4" />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
|
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuSub>
|
|
||||||
<DropdownMenuSubTrigger>
|
|
||||||
{t("feedback")}
|
|
||||||
</DropdownMenuSubTrigger>
|
|
||||||
<DropdownMenuPortal>
|
|
||||||
<DropdownMenuSubContent>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
EnjoyApp.shell.openExternal(
|
|
||||||
"https://mixin.one/codes/f6ff96b8-54fb-4ad8-a6d4-5a5bdb1df13e"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex justify-between space-x-4"
|
|
||||||
>
|
|
||||||
<span>Mixin</span>
|
|
||||||
<ExternalLinkIcon className="size-4" />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() =>
|
|
||||||
EnjoyApp.shell.openExternal(
|
|
||||||
"https://github.com/zuodaotech/everyone-can-use-english/discussions"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="flex justify-between space-x-4"
|
|
||||||
>
|
|
||||||
<span>Github</span>
|
|
||||||
<ExternalLinkIcon className="size-4" />
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuSubContent>
|
|
||||||
</DropdownMenuPortal>
|
|
||||||
</DropdownMenuSub>
|
|
||||||
</DropdownMenuGroup>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
@@ -309,7 +227,9 @@ export const Sidebar = () => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={`w-full ${isOpen ? "justify-start" : "justify-center"}`}
|
className={`w-full non-draggable-region ${
|
||||||
|
isOpen ? "justify-start" : "justify-center"
|
||||||
|
}`}
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
@@ -343,7 +263,7 @@ const SidebarItem = (props: {
|
|||||||
data-tooltip-content={tooltip}
|
data-tooltip-content={tooltip}
|
||||||
data-tooltip-place="right"
|
data-tooltip-place="right"
|
||||||
data-testid={testid}
|
data-testid={testid}
|
||||||
className="block px-1"
|
className="block px-1 non-draggable-region"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -362,8 +282,12 @@ const SidebarHeader = (props: { isOpen: boolean }) => {
|
|||||||
const { user, logout } = useContext(AppSettingsProviderContext);
|
const { user, logout } = useContext(AppSettingsProviderContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-3 px-1 sticky top-0 bg-muted z-10">
|
<div className="py-3 px-1 sticky top-0 bg-muted z-10 non-draggable-region">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import { DbProviderContext } from "@renderer/context";
|
|||||||
import { UserSettingKeyEnum } from "@/types/enums";
|
import { UserSettingKeyEnum } from "@/types/enums";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogTrigger,
|
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
@@ -16,10 +15,10 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
Button,
|
|
||||||
} from "@renderer/components/ui";
|
} from "@renderer/components/ui";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { redirect } from "react-router-dom";
|
import { redirect } from "react-router-dom";
|
||||||
|
import { Preferences } from "@renderer/components";
|
||||||
|
|
||||||
type AppSettingsProviderState = {
|
type AppSettingsProviderState = {
|
||||||
webApi: Client;
|
webApi: Client;
|
||||||
@@ -50,6 +49,8 @@ type AppSettingsProviderState = {
|
|||||||
setRecorderConfig?: (config: RecorderConfigType) => Promise<void>;
|
setRecorderConfig?: (config: RecorderConfigType) => Promise<void>;
|
||||||
// remote config
|
// remote config
|
||||||
ipaMappings?: { [key: string]: string };
|
ipaMappings?: { [key: string]: string };
|
||||||
|
displayPreferences?: boolean;
|
||||||
|
setDisplayPreferences?: (display: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnjoyApp = window.__ENJOY_APP__;
|
const EnjoyApp = window.__ENJOY_APP__;
|
||||||
@@ -87,6 +88,7 @@ export const AppSettingsProvider = ({
|
|||||||
IPA_MAPPINGS
|
IPA_MAPPINGS
|
||||||
);
|
);
|
||||||
const [loggingOut, setLoggingOut] = useState<boolean>(false);
|
const [loggingOut, setLoggingOut] = useState<boolean>(false);
|
||||||
|
const [displayPreferences, setDisplayPreferences] = useState<boolean>(false);
|
||||||
|
|
||||||
const db = useContext(DbProviderContext);
|
const db = useContext(DbProviderContext);
|
||||||
|
|
||||||
@@ -344,12 +346,14 @@ export const AppSettingsProvider = ({
|
|||||||
setProxy: setProxyConfigHandler,
|
setProxy: setProxyConfigHandler,
|
||||||
vocabularyConfig,
|
vocabularyConfig,
|
||||||
setVocabularyConfig: setVocabularyConfigHandler,
|
setVocabularyConfig: setVocabularyConfigHandler,
|
||||||
initialized: Boolean(db.state === "connected" && libraryPath),
|
initialized: Boolean(user && db.state === "connected" && libraryPath),
|
||||||
ahoy,
|
ahoy,
|
||||||
cable,
|
cable,
|
||||||
recorderConfig,
|
recorderConfig,
|
||||||
setRecorderConfig: setRecorderConfigHandler,
|
setRecorderConfig: setRecorderConfigHandler,
|
||||||
ipaMappings,
|
ipaMappings,
|
||||||
|
displayPreferences,
|
||||||
|
setDisplayPreferences,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col relative">
|
<div className="h-[calc(100vh-2rem)] flex flex-col relative">
|
||||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
|
|||||||
@@ -231,9 +231,9 @@ export default () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="conversation-page"
|
data-testid="conversation-page"
|
||||||
className="h-screen px-4 py-6 lg:px-8 flex flex-col"
|
className="h-[calc(100vh-2rem)] px-4 py-4 lg:px-8 flex flex-col"
|
||||||
>
|
>
|
||||||
<div className="h-[calc(100vh-3rem)] relative w-full max-w-screen-md mx-auto flex flex-col">
|
<div className="h-[calc(100vh-5rem)] relative w-full max-w-screen-md mx-auto flex flex-col">
|
||||||
<div className="flex items-center justify-center py-2 relative">
|
<div className="flex items-center justify-center py-2 relative">
|
||||||
<div className="cursor-pointer h-6 opacity-50 hover:opacity-100">
|
<div className="cursor-pointer h-6 opacity-50 hover:opacity-100">
|
||||||
<Link className="flex items-center" to="/conversations">
|
<Link className="flex items-center" to="/conversations">
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const DocumentComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col relative">
|
<div className="h-[calc(100vh-2rem)] flex flex-col relative">
|
||||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
|
|||||||
@@ -1,35 +1,65 @@
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useContext } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { Button } from "@renderer/components/ui";
|
import { Button } from "@renderer/components/ui";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, Navigate } from "react-router-dom";
|
||||||
import { LoginForm } from "@renderer/components";
|
import { DbState, LoginForm } from "@renderer/components";
|
||||||
import { AppSettingsProviderContext } from "@renderer/context";
|
import {
|
||||||
|
AppSettingsProviderContext,
|
||||||
|
DbProviderContext,
|
||||||
|
} from "@renderer/context";
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { initialized, EnjoyApp } = useContext(AppSettingsProviderContext);
|
const { initialized, EnjoyApp, user } = useContext(
|
||||||
|
AppSettingsProviderContext
|
||||||
|
);
|
||||||
|
const [started, setStarted] = useState(false);
|
||||||
|
const db = useContext(DbProviderContext);
|
||||||
|
|
||||||
|
if (initialized) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user && db.state === "error") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center h-full"
|
||||||
|
date-testid="layout-db-error"
|
||||||
|
>
|
||||||
|
<DbState />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-center items-center h-full"
|
||||||
|
date-testid="layout-onboarding"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg mb-6">
|
||||||
|
{t("welcomeTo")} <span className="font-semibold">Enjoy App</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="">
|
||||||
|
<Button size="lg" onClick={() => setStarted(true)}>
|
||||||
|
{t("startToUse")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full px-4 py-6 lg:px-8 flex flex-col">
|
<div className="w-full h-full px-4 py-6 lg:px-8 flex flex-col gap-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-lg font-mono py-6">{t("login")}</div>
|
<div className="text-lg font-mono py-4">{t("login")}</div>
|
||||||
<div className="text-sm opacity-70">{t("loginBeforeYouStart")}</div>
|
<div className="text-sm opacity-70">{t("loginBeforeYouStart")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex justify-center items-center">
|
<div className="flex-1 flex justify-center">
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto">
|
|
||||||
<div className="flex mb-4 justify-end space-x-4">
|
|
||||||
{initialized ? (
|
|
||||||
<Link data-testid="start-to-use-button" to="/" replace>
|
|
||||||
<Button className="w-24">{t("startToUse")}</Button>
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Button className="w-24" onClick={() => EnjoyApp.app.reload()}>
|
|
||||||
{t("reload")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
19
enjoy/src/renderer/pages/protected-page.tsx
Normal file
19
enjoy/src/renderer/pages/protected-page.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Navigate } from "react-router-dom";
|
||||||
|
import { AppSettingsProviderContext } from "../context";
|
||||||
|
import { useContext } from "react";
|
||||||
|
|
||||||
|
export const ProtectedPage = ({
|
||||||
|
children,
|
||||||
|
redirectPath = "/landing",
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
redirectPath?: string;
|
||||||
|
}) => {
|
||||||
|
const { initialized } = useContext(AppSettingsProviderContext);
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return <Navigate to={redirectPath} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ import { t } from "i18next";
|
|||||||
import nlp from "compromise";
|
import nlp from "compromise";
|
||||||
import paragraphs from "compromise-paragraphs";
|
import paragraphs from "compromise-paragraphs";
|
||||||
import { useDebounce } from "@uidotdev/usehooks";
|
import { useDebounce } from "@uidotdev/usehooks";
|
||||||
|
import { type IpcRendererEvent } from "electron/renderer";
|
||||||
nlp.plugin(paragraphs);
|
nlp.plugin(paragraphs);
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@@ -154,6 +155,15 @@ export default () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onWindowChange = (
|
||||||
|
event: IpcRendererEvent,
|
||||||
|
state: { event: string }
|
||||||
|
) => {
|
||||||
|
if (state.event === "resize") {
|
||||||
|
setWebviewRect(containerRef.current.getBoundingClientRect());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef?.current) return;
|
if (!containerRef?.current) return;
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
@@ -172,12 +182,10 @@ export default () => {
|
|||||||
if (!containerRef?.current) return;
|
if (!containerRef?.current) return;
|
||||||
|
|
||||||
setWebviewRect(containerRef.current.getBoundingClientRect());
|
setWebviewRect(containerRef.current.getBoundingClientRect());
|
||||||
EnjoyApp.window.onResize(() => {
|
EnjoyApp.window.onChange((_event, state) => onWindowChange(_event, state));
|
||||||
setWebviewRect(containerRef.current.getBoundingClientRect());
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
EnjoyApp.window.removeListeners();
|
EnjoyApp.window.removeListener(onWindowChange);
|
||||||
};
|
};
|
||||||
}, [containerRef?.current]);
|
}, [containerRef?.current]);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export default () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col relative">
|
<div className="h-[calc(100vh-2rem)] flex flex-col relative">
|
||||||
<Breadcrumb className="px-4 pt-3 pb-2">
|
<Breadcrumb className="px-4 pt-3 pb-2">
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import Courses from "./pages/courses/index";
|
|||||||
import Course from "./pages/courses/show";
|
import Course from "./pages/courses/show";
|
||||||
import Chapter from "./pages/courses/chapter";
|
import Chapter from "./pages/courses/chapter";
|
||||||
import Chats from "./pages/chats";
|
import Chats from "./pages/chats";
|
||||||
|
import { ProtectedPage } from "./pages/protected-page";
|
||||||
|
|
||||||
export default createHashRouter([
|
export default createHashRouter([
|
||||||
{
|
{
|
||||||
@@ -32,96 +33,191 @@ export default createHashRouter([
|
|||||||
element: <Layout />,
|
element: <Layout />,
|
||||||
errorElement: <ErrorPage />,
|
errorElement: <ErrorPage />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Home /> },
|
{ path: "/landing", element: <Landing /> },
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Home />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/chats",
|
path: "/chats",
|
||||||
element: <Chats />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Chats />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/courses",
|
path: "/courses",
|
||||||
element: <Courses />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Courses />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/courses/:id",
|
path: "/courses/:id",
|
||||||
element: <Course />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Course />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/courses/:id/chapters/:sequence",
|
path: "/courses/:id/chapters/:sequence",
|
||||||
element: <Chapter />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Chapter />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/community",
|
path: "/community",
|
||||||
element: <Community />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Community />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/users/:id",
|
path: "/users/:id",
|
||||||
element: <User />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<User />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/profile",
|
path: "/profile",
|
||||||
element: <Profile />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Profile />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/conversations",
|
path: "/conversations",
|
||||||
element: <Conversations />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Conversations />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/conversations/:id",
|
path: "/conversations/:id",
|
||||||
element: <Conversation />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Conversation />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/pronunciation_assessments",
|
path: "/pronunciation_assessments",
|
||||||
element: <PronunciationAssessmentsIndex />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<PronunciationAssessmentsIndex />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/pronunciation_assessments/new",
|
path: "/pronunciation_assessments/new",
|
||||||
element: <PronunciationAssessmentsNew />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<PronunciationAssessmentsNew />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/vocabulary",
|
path: "/vocabulary",
|
||||||
element: <Vocabulary />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Vocabulary />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/audios",
|
path: "/audios",
|
||||||
element: <Audios />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Audios />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/audios/:id",
|
path: "/audios/:id",
|
||||||
element: <Audio />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Audio />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/videos",
|
path: "/videos",
|
||||||
element: <Videos />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Videos />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/videos/:id",
|
path: "/videos/:id",
|
||||||
element: <Video />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Video />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/documents",
|
path: "/documents",
|
||||||
element: <Documents />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Documents />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/documents/:id",
|
path: "/documents/:id",
|
||||||
element: <Document />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Document />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/stories",
|
path: "/stories",
|
||||||
element: <Stories />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Stories />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/stories/:id",
|
path: "/stories/:id",
|
||||||
element: <Story />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Story />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/stories/preview/:uri",
|
path: "/stories/preview/:uri",
|
||||||
element: <StoryPreview />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<StoryPreview />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/notes",
|
path: "/notes",
|
||||||
element: <Notes />,
|
element: (
|
||||||
|
<ProtectedPage>
|
||||||
|
<Notes />
|
||||||
|
</ProtectedPage>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ path: "/landing", element: <Landing /> },
|
|
||||||
]);
|
]);
|
||||||
|
|||||||
17
enjoy/src/types/enjoy-app.d.ts
vendored
17
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -17,8 +17,21 @@ type EnjoyAppType = {
|
|||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
window: {
|
window: {
|
||||||
onResize: (callback: (event, bounds: any) => void) => void;
|
onChange: (
|
||||||
removeListeners: () => void;
|
callback: (event, state: { event: string; state: any }) => void
|
||||||
|
) => void;
|
||||||
|
toggleMaximized: () => Promise<void>;
|
||||||
|
isMaximized: () => Promise<boolean>;
|
||||||
|
maximize: () => Promise<void>;
|
||||||
|
unmaximize: () => Promise<void>;
|
||||||
|
fullscreen: () => Promise<void>;
|
||||||
|
unfullscreen: () => Promise<void>;
|
||||||
|
minimize: () => Promise<void>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
removeListener: (
|
||||||
|
listener: (event: IpcRendererEvent, ...args: any[]) => void
|
||||||
|
) => void;
|
||||||
|
removeAllListeners: () => void;
|
||||||
};
|
};
|
||||||
system: {
|
system: {
|
||||||
preferences: {
|
preferences: {
|
||||||
|
|||||||
Reference in New Issue
Block a user