diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 5124e906..60480c45 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -343,7 +343,9 @@ "logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings", "hotkeys": "Hotkeys", "player": "Player", + "quit": "Quit", "quitApp": "Quit APP", + "quitAppDescription": "Are you sure to quit Enjoy app?", "openPreferences": "Open preferences", "openCopilot": "Open copilot", "playOrPause": "Play or pause", @@ -929,5 +931,7 @@ "compressMediaBeforeAdding": "Compress media before adding", "keepOriginalMedia": "Keep original media", "myPronunciation": "My pronunciation", - "originalPronunciation": "Original pronunciation" + "originalPronunciation": "Original pronunciation", + "reloadApp": "reload app", + "exit": "Exit" } diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index 4fbb8f7e..25ccd18d 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -344,7 +344,9 @@ "hotkeys": "快捷键", "system": "系统", "player": "播放器", + "quit": "退出", "quitApp": "退出应用", + "quitAppDescription": "确定要退出 Enjoy 应用吗?", "openPreferences": "打开设置", "openCopilot": "打开 Copilot", "playOrPause": "播放/暂停", @@ -929,5 +931,7 @@ "compressMediaBeforeAdding": "添加前压缩媒体", "keepOriginalMedia": "保存原始媒体", "myPronunciation": "我的发音", - "originalPronunciation": "原始发音" + "originalPronunciation": "原始发音", + "reloadApp": "重载应用", + "exit": "退出" } diff --git a/enjoy/src/index.css b/enjoy/src/index.css index 7ba598ee..c5c6a517 100644 --- a/enjoy/src/index.css +++ b/enjoy/src/index.css @@ -94,6 +94,12 @@ .scroll { @apply scrollbar-thin scrollbar-thumb-primary scrollbar-track-secondary; } + .draggable-region { + -webkit-app-region: drag; + } + .non-draggable-region { + -webkit-app-region: no-drag; + } } body { diff --git a/enjoy/src/main/window.ts b/enjoy/src/main/window.ts index bd21e9f2..d60ed448 100644 --- a/enjoy/src/main/window.ts +++ b/enjoy/src/main/window.ts @@ -25,6 +25,8 @@ import dict from "./dict"; import mdict from "./mdict"; import decompresser from "./decompresser"; import { UserSetting } from "@main/db/models"; +import { platform } from "os"; +import { t } from "i18next"; const __filename = url.fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -520,6 +522,14 @@ ${log} preload: path.join(__dirname, "preload.js"), spellcheck: false, }, + frame: false, + titleBarStyle: "hidden", + titleBarOverlay: process.platform === "darwin", + trafficLightPosition: { + x: 10, + y: 8, + }, + useContentSize: true, }); mainWindow.on("ready-to-show", () => { @@ -527,7 +537,62 @@ ${log} }); 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(() => { @@ -571,7 +636,42 @@ ${log} // 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; }; diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index ceeab03b..d5d15990 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -54,14 +54,49 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { version, }, 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: ( event: IpcRendererEvent, - bounds: { x: number; y: number; width: number; height: number } + state: { event: string; state: any } ) => void - ) => ipcRenderer.on("window-on-resize", callback), - removeListeners: () => { - ipcRenderer.removeAllListeners("window-on-resize"); + ) => ipcRenderer.on("window-on-change", callback), + removeListener: ( + listener: (event: IpcRendererEvent, ...args: any[]) => void + ) => { + ipcRenderer.removeListener("window-on-change", listener); + }, + removeAllListeners: () => { + ipcRenderer.removeAllListeners("window-on-change"); }, }, system: { diff --git a/enjoy/src/renderer/components/chats/chat-session.tsx b/enjoy/src/renderer/components/chats/chat-session.tsx index 85299bab..bef2c947 100644 --- a/enjoy/src/renderer/components/chats/chat-session.tsx +++ b/enjoy/src/renderer/components/chats/chat-session.tsx @@ -12,14 +12,14 @@ export const ChatSession = (props: { if (!chatId) { return ( -
+
{t("noChatSelected")}
); } return ( - + { const { currentChat } = useContext(CopilotProviderContext); return ( - + {currentChat?.id ? ( diff --git a/enjoy/src/renderer/components/index.ts b/enjoy/src/renderer/components/index.ts index 00b8e1c5..89cf5cd4 100644 --- a/enjoy/src/renderer/components/index.ts +++ b/enjoy/src/renderer/components/index.ts @@ -20,3 +20,4 @@ export * from "./users"; export * from "./videos"; export * from "./widgets"; export * from "./login"; +export * from "./layouts"; diff --git a/enjoy/src/renderer/components/layouts/index.ts b/enjoy/src/renderer/components/layouts/index.ts new file mode 100644 index 00000000..325b6fdc --- /dev/null +++ b/enjoy/src/renderer/components/layouts/index.ts @@ -0,0 +1,2 @@ +export * from "./layout"; +export * from "./title-bar"; diff --git a/enjoy/src/renderer/components/layouts/layout.tsx b/enjoy/src/renderer/components/layouts/layout.tsx new file mode 100644 index 00000000..ece45090 --- /dev/null +++ b/enjoy/src/renderer/components/layouts/layout.tsx @@ -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 ( +
+ + + +
+ +
+ +
+
+
+ {active && ( + <> + + setActive(false)} + > +
+ +
+
+ + )} +
+
+ ); + } else { + return ( +
+ +
+ +
+
+ ); + } +}; diff --git a/enjoy/src/renderer/components/layouts/title-bar.tsx b/enjoy/src/renderer/components/layouts/title-bar.tsx new file mode 100644 index 00000000..b3c275e0 --- /dev/null +++ b/enjoy/src/renderer/components/layouts/title-bar.tsx @@ -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 ( +
+
+ {platform === "darwin" && !isFullScreen &&
} + + + + + + EnjoyApp.app.reload()} + className="cursor-pointer" + > + {t("reloadApp")} + + + EnjoyApp.window.close()} + className="cursor-pointer" + > + {t("exit")} + + + + + {initialized && ( + + )} + + + + + + + + + + EnjoyApp.shell.openExternal("https://1000h.org/enjoy-app/") + } + className="flex justify-between space-x-4" + > + {t("userGuide")} + + + + + + + + + {t("feedback")} + + + + + + EnjoyApp.shell.openExternal( + "https://mixin.one/codes/f6ff96b8-54fb-4ad8-a6d4-5a5bdb1df13e" + ) + } + className="flex justify-between space-x-4" + > + Mixin + + + + EnjoyApp.shell.openExternal( + "https://github.com/zuodaotech/everyone-can-use-english/discussions" + ) + } + className="flex justify-between space-x-4" + > + Github + + + + + + + + + EnjoyApp.shell.openExternal( + "https://1000h.org/enjoy-app/install.html" + ) + } + className="cursor-pointer" + > + {t("checkUpdate")} + + + +
+ +
+
+ {initialized && ( + + )} +
+ + {platform !== "darwin" && ( +
+ + + + + + + + + {t("quitApp")} + + {t("quitAppDescription")} + + + + {t("cancel")} + EnjoyApp.window.close()} + > + {t("quit")} + + + + +
+ )} +
+
+ ); +}; diff --git a/enjoy/src/renderer/components/login/login-form.tsx b/enjoy/src/renderer/components/login/login-form.tsx index d8784050..caf8f2d0 100644 --- a/enjoy/src/renderer/components/login/login-form.tsx +++ b/enjoy/src/renderer/components/login/login-form.tsx @@ -38,7 +38,7 @@ export const LoginForm = () => { if (user) { return ( -
+
diff --git a/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx b/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx index e4595ff5..b1d022e3 100644 --- a/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx +++ b/enjoy/src/renderer/components/medias/media-bottom-panel/media-current-recording.tsx @@ -448,10 +448,8 @@ export const MediaCurrentRecording = () => { debouncedCalContainerSize(); }); observer.observe(ref.current); - EnjoyApp.window.onResize(debouncedCalContainerSize); return () => { - EnjoyApp.window.removeListeners(); observer.disconnect(); }; }, [ref, player]); diff --git a/enjoy/src/renderer/components/medias/media-bottom-panel/media-waveform.tsx b/enjoy/src/renderer/components/medias/media-bottom-panel/media-waveform.tsx index 42e3c180..711a4547 100644 --- a/enjoy/src/renderer/components/medias/media-bottom-panel/media-waveform.tsx +++ b/enjoy/src/renderer/components/medias/media-bottom-panel/media-waveform.tsx @@ -144,10 +144,7 @@ export const MediaWaveform = () => { }); observer.observe(ref.current); - EnjoyApp.window.onResize(debouncedCalContainerSize); - return () => { - EnjoyApp.window.removeListeners(); observer.disconnect(); }; }, [ref, wavesurfer]); diff --git a/enjoy/src/renderer/components/misc/index.ts b/enjoy/src/renderer/components/misc/index.ts index 51e2b49b..6a01dbda 100644 --- a/enjoy/src/renderer/components/misc/index.ts +++ b/enjoy/src/renderer/components/misc/index.ts @@ -1,5 +1,4 @@ export * from "./db-state"; -export * from "./layout"; export * from "./loader-spin"; export * from "./markdown-wrapper"; export * from "./no-records-found"; diff --git a/enjoy/src/renderer/components/misc/layout.tsx b/enjoy/src/renderer/components/misc/layout.tsx deleted file mode 100644 index 05b3bc1e..00000000 --- a/enjoy/src/renderer/components/misc/layout.tsx +++ /dev/null @@ -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 ( -
-
-
- {t("welcomeTo")} Enjoy App -
- -
- - - -
-
-
- ); - } else if (db.state === "connected") { - return ( - - -
- -
- -
-
-
- {active && ( - <> - - setActive(false)} - > -
- -
-
- - )} -
- ); - } else { - return ( -
- -
- ); - } -}; diff --git a/enjoy/src/renderer/components/misc/sidebar.tsx b/enjoy/src/renderer/components/misc/sidebar.tsx index 18f08816..62843bd1 100644 --- a/enjoy/src/renderer/components/misc/sidebar.tsx +++ b/enjoy/src/renderer/components/misc/sidebar.tsx @@ -1,17 +1,11 @@ import { Button, Dialog, - DialogTrigger, DialogContent, ScrollArea, DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuSub, - DropdownMenuPortal, - DropdownMenuSubContent, - DropdownMenuSubTrigger, DropdownMenuItem, Separator, DialogTitle, @@ -30,8 +24,6 @@ import { BotIcon, UsersRoundIcon, LucideIcon, - HelpCircleIcon, - ExternalLinkIcon, NotebookPenIcon, SpeechIcon, GraduationCapIcon, @@ -40,7 +32,6 @@ import { PanelLeftCloseIcon, ChevronsUpDownIcon, LogOutIcon, - ChevronRightIcon, } from "lucide-react"; import { useLocation, Link, useNavigate } from "react-router-dom"; import { t } from "i18next"; @@ -53,7 +44,8 @@ import { useState } from "react"; export const Sidebar = () => { const location = useLocation(); const activeTab = location.pathname; - const { EnjoyApp, cable } = useContext(AppSettingsProviderContext); + const { EnjoyApp, cable, displayPreferences, setDisplayPreferences } = + useContext(AppSettingsProviderContext); const [isOpen, setIsOpen] = useState(true); useEffect(() => { @@ -83,7 +75,7 @@ export const Sidebar = () => { return (
{ isOpen ? "w-48" : "w-12" }`} > - +
{ - - -
- -
-
+
+ +
+ { - - - -
- -
-
- - - - - EnjoyApp.shell.openExternal( - "https://1000h.org/enjoy-app/" - ) - } - className="flex justify-between space-x-4" - > - {t("userGuide")} - - - - - - - - {t("feedback")} - - - - - EnjoyApp.shell.openExternal( - "https://mixin.one/codes/f6ff96b8-54fb-4ad8-a6d4-5a5bdb1df13e" - ) - } - className="flex justify-between space-x-4" - > - Mixin - - - - EnjoyApp.shell.openExternal( - "https://github.com/zuodaotech/everyone-can-use-english/discussions" - ) - } - className="flex justify-between space-x-4" - > - Github - - - - - - - -
@@ -309,7 +227,9 @@ export const Sidebar = () => { +
+
+
+ ); + } return ( -
+
-
{t("login")}
+
{t("login")}
{t("loginBeforeYouStart")}
-
+
-
-
- {initialized ? ( - - - - ) : ( - - )} -
-
); }; diff --git a/enjoy/src/renderer/pages/protected-page.tsx b/enjoy/src/renderer/pages/protected-page.tsx new file mode 100644 index 00000000..4753ea18 --- /dev/null +++ b/enjoy/src/renderer/pages/protected-page.tsx @@ -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 ; + } + + return children; +}; diff --git a/enjoy/src/renderer/pages/story-preview.tsx b/enjoy/src/renderer/pages/story-preview.tsx index 7a4b15a2..96721424 100644 --- a/enjoy/src/renderer/pages/story-preview.tsx +++ b/enjoy/src/renderer/pages/story-preview.tsx @@ -14,6 +14,7 @@ import { t } from "i18next"; import nlp from "compromise"; import paragraphs from "compromise-paragraphs"; import { useDebounce } from "@uidotdev/usehooks"; +import { type IpcRendererEvent } from "electron/renderer"; nlp.plugin(paragraphs); export default () => { @@ -154,6 +155,15 @@ export default () => { setLoading(false); }; + const onWindowChange = ( + event: IpcRendererEvent, + state: { event: string } + ) => { + if (state.event === "resize") { + setWebviewRect(containerRef.current.getBoundingClientRect()); + } + }; + useEffect(() => { if (!containerRef?.current) return; if (!url) return; @@ -172,12 +182,10 @@ export default () => { if (!containerRef?.current) return; setWebviewRect(containerRef.current.getBoundingClientRect()); - EnjoyApp.window.onResize(() => { - setWebviewRect(containerRef.current.getBoundingClientRect()); - }); + EnjoyApp.window.onChange((_event, state) => onWindowChange(_event, state)); return () => { - EnjoyApp.window.removeListeners(); + EnjoyApp.window.removeListener(onWindowChange); }; }, [containerRef?.current]); diff --git a/enjoy/src/renderer/pages/video.tsx b/enjoy/src/renderer/pages/video.tsx index d60d5aa1..93bb1f0a 100644 --- a/enjoy/src/renderer/pages/video.tsx +++ b/enjoy/src/renderer/pages/video.tsx @@ -25,7 +25,7 @@ export default () => { const navigate = useNavigate(); return ( -
+
diff --git a/enjoy/src/renderer/router.tsx b/enjoy/src/renderer/router.tsx index f5c4df86..851e4708 100644 --- a/enjoy/src/renderer/router.tsx +++ b/enjoy/src/renderer/router.tsx @@ -25,6 +25,7 @@ import Courses from "./pages/courses/index"; import Course from "./pages/courses/show"; import Chapter from "./pages/courses/chapter"; import Chats from "./pages/chats"; +import { ProtectedPage } from "./pages/protected-page"; export default createHashRouter([ { @@ -32,96 +33,191 @@ export default createHashRouter([ element: , errorElement: , children: [ - { index: true, element: }, + { path: "/landing", element: }, + { + index: true, + element: ( + + + + ), + }, { path: "/chats", - element: , + element: ( + + + + ), }, { path: "/courses", - element: , + element: ( + + + + ), }, { path: "/courses/:id", - element: , + element: ( + + + + ), }, { path: "/courses/:id/chapters/:sequence", - element: , + element: ( + + + + ), }, { path: "/community", - element: , + element: ( + + + + ), }, { path: "/users/:id", - element: , + element: ( + + + + ), }, { path: "/profile", - element: , + element: ( + + + + ), }, { path: "/conversations", - element: , + element: ( + + + + ), }, { path: "/conversations/:id", - element: , + element: ( + + + + ), }, { path: "/pronunciation_assessments", - element: , + element: ( + + + + ), }, { path: "/pronunciation_assessments/new", - element: , + element: ( + + + + ), }, { path: "/vocabulary", - element: , + element: ( + + + + ), }, { path: "/audios", - element: , + element: ( + + + + ), }, { path: "/audios/:id", - element: