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"
}`}
>
-
+
{
-
@@ -309,7 +227,9 @@ export const Sidebar = () => {