diff --git a/internal b/internal index ca43a373..c37d4ea1 160000 --- a/internal +++ b/internal @@ -1 +1 @@ -Subproject commit ca43a373355d179ef3f5314d042f09c91e1cb644 +Subproject commit c37d4ea1eed0ae3748a9a056a3ed277cf3cb709f diff --git a/packages/main/src/services/DownloadService.ts b/packages/main/src/services/DownloadService.ts index 9dcafd9c..085a07b3 100644 --- a/packages/main/src/services/DownloadService.ts +++ b/packages/main/src/services/DownloadService.ts @@ -168,6 +168,7 @@ export default class DownloadService extends EventEmitter { if (onMessage) { ptyProcess.onData((data) => { try { + this.emit("download-message", data); const message = stripAnsi(data); this.logger.debug("download message: ", message); onMessage(message); diff --git a/packages/main/src/windows/MainWindow.ts b/packages/main/src/windows/MainWindow.ts index 9fb9f8ec..7fcb95c4 100644 --- a/packages/main/src/windows/MainWindow.ts +++ b/packages/main/src/windows/MainWindow.ts @@ -43,6 +43,7 @@ export default class MainWindow extends Window { this.downloadService.on("download-failed", this.onDownloadFailed); this.downloadService.on("download-start", this.onDownloadStart); this.downloadService.on("download-stop", this.onDownloadStart); + this.downloadService.on("download-message", this.receiveMessage); this.store.onDidAnyChange(this.storeChange); app.on("second-instance", this.secondInstance); } @@ -140,6 +141,10 @@ export default class MainWindow extends Window { this.send("download-stop", id); }; + receiveMessage = (message: any) => { + this.send("download-message", message); + }; + send(channel: string, ...args: any[]) { if (!this.window) return; diff --git a/packages/renderer/package.json b/packages/renderer/package.json index dc765770..1c7e6581 100644 --- a/packages/renderer/package.json +++ b/packages/renderer/package.json @@ -15,6 +15,7 @@ "@reduxjs/toolkit": "^2.0.1", "ahooks": "^3.7.8", "antd": "^5.13.2", + "antd-style": "^3.6.1", "axios": "^1.6.5", "classnames": "^2.5.1", "dayjs": "^1.11.10", @@ -28,7 +29,8 @@ "react-redux": "^9.1.0", "react-router-dom": "^6.21.3", "sort-by": "^1.2.0", - "xgplayer": "^3.0.11" + "xgplayer": "^3.0.11", + "xterm": "^5.3.0" }, "devDependencies": { "@types/node": "^20.11.5", @@ -53,4 +55,4 @@ "vite": "^5.0.12" }, "license": "MIT" -} \ No newline at end of file +} diff --git a/packages/renderer/src/components/Terminal/index.tsx b/packages/renderer/src/components/Terminal/index.tsx new file mode 100644 index 00000000..ba828f39 --- /dev/null +++ b/packages/renderer/src/components/Terminal/index.tsx @@ -0,0 +1,52 @@ +import React, { FC, useEffect, useRef } from "react"; +import { useStyles } from "./style"; +import "xterm/css/xterm.css"; +import { Terminal as XTerminal } from "xterm"; +import classNames from "classnames"; +import { Button } from "antd"; +import { CloseOutlined } from "@ant-design/icons"; +import useElectron from "../../hooks/electron"; + +interface TerminalProps { + className?: string; + onClose?: () => void; +} + +const Terminal: FC = ({ className, onClose }) => { + const { styles } = useStyles(); + const terminalRef = useRef(null); + const { addIpcListener, removeIpcListener } = useElectron(); + + useEffect(() => { + const terminal = new XTerminal({ + rows: 10, + cols: 100, + fontFamily: "Consolas, 'Courier New', monospace", + }); + terminal.open(terminalRef.current); + + const onDownloadMessage = (_: unknown, message: string) => { + terminal.write(message); + }; + + addIpcListener("download-message", onDownloadMessage); + + return () => { + removeIpcListener("download-message", onDownloadMessage); + terminal.dispose(); + }; + }, []); + + return ( +
+
+ ); +}; + +export default Terminal; diff --git a/packages/renderer/src/components/Terminal/style.ts b/packages/renderer/src/components/Terminal/style.ts new file mode 100644 index 00000000..fb0af162 --- /dev/null +++ b/packages/renderer/src/components/Terminal/style.ts @@ -0,0 +1,18 @@ +import { createStyles } from "antd-style"; + +export const useStyles = createStyles(({ css }) => ({ + container: css` + position: relative; + `, + closeBtn: css` + position: absolute; + top: 5px; + right: 5px; + z-index: 10000; + color: #fff; + + &:hover { + color: #fff !important; + } + `, +})); diff --git a/packages/renderer/src/i18n/index.ts b/packages/renderer/src/i18n/index.ts index 226a7608..9f0ba211 100644 --- a/packages/renderer/src/i18n/index.ts +++ b/packages/renderer/src/i18n/index.ts @@ -121,6 +121,7 @@ http://example.com/xxx.m3u8`, Origin: http://www.example.com Referer: http://www.example.com`, cancle: "Cancle", + terminal: "Open Terminal", }, }, zh: { @@ -229,6 +230,7 @@ Referer: http://www.example.com`, light: "浅色", pleaseSelectTheme: "请选择主题", cancle: "取消", + terminal: "打开控制台", }, }, }, diff --git a/packages/renderer/src/nodes/HomePage/index.scss b/packages/renderer/src/nodes/HomePage/index.scss index 38ba0977..9e05701e 100644 --- a/packages/renderer/src/nodes/HomePage/index.scss +++ b/packages/renderer/src/nodes/HomePage/index.scss @@ -1,6 +1,8 @@ .home-page { .home-page-inner { padding-bottom: 10px; + display: flex; + flex-direction: column; .download-list { height: 100%; overflow: hidden; @@ -49,4 +51,7 @@ } } } + .home-page-terminal { + margin-top: 10px; + } } diff --git a/packages/renderer/src/nodes/HomePage/index.tsx b/packages/renderer/src/nodes/HomePage/index.tsx index 9b37cedb..b82ad607 100644 --- a/packages/renderer/src/nodes/HomePage/index.tsx +++ b/packages/renderer/src/nodes/HomePage/index.tsx @@ -24,6 +24,7 @@ import { SyncOutlined, MobileOutlined, MoreOutlined, + CodeOutlined, } from "@ant-design/icons"; import { useSelector } from "react-redux"; import { selectAppStore } from "../../store"; @@ -31,6 +32,7 @@ import DownloadFrom from "../../components/DownloadForm"; import dayjs from "dayjs"; import classNames from "classnames"; import { Trans, useTranslation } from "react-i18next"; +import Terminal from "../../components/Terminal"; const { Text } = Typography; @@ -81,8 +83,8 @@ const HomePage: FC = ({ filter = DownloadFilter.list }) => { {}, ); const [messageApi, contextHolder] = message.useMessage(); - const [baseUrl, setBaseUrl] = useState(""); + const [terminalVisible, setTerminalVisible] = useState(false); useAsyncEffect(async () => { const localIP = await getLocalIP(); @@ -211,6 +213,14 @@ const HomePage: FC = ({ filter = DownloadFilter.list }) => { ); }; + const showTerminal = () => { + setTerminalVisible(true); + }; + + const closeTerminal = () => { + setTerminalVisible(false); + }; + const renderActionButtons = ( dom: ReactNode, item: DownloadItem, @@ -229,6 +239,13 @@ const HomePage: FC = ({ filter = DownloadFilter.list }) => { } if (item.status === DownloadStatus.Downloading) { return [ +