From 92fbf7eed6134565fc2b81fd3ac2e2cc920c777a Mon Sep 17 00:00:00 2001 From: Kenta420 Date: Wed, 7 Feb 2024 18:25:11 +0700 Subject: [PATCH] Add download file --- client-electron/src/pages/android/android.tsx | 25 ++- .../android/components/file-manager-tab.tsx | 138 ++++++++++++++++ .../pages/android/components/scrcpy-tab.tsx | 30 ++-- .../pages/android/components/shell-tab.tsx | 148 +++++++++++------- .../src/pages/android/components/tool-bar.tsx | 26 +-- 5 files changed, 279 insertions(+), 88 deletions(-) create mode 100644 client-electron/src/pages/android/components/file-manager-tab.tsx diff --git a/client-electron/src/pages/android/android.tsx b/client-electron/src/pages/android/android.tsx index 45ef8bd..00a61fe 100644 --- a/client-electron/src/pages/android/android.tsx +++ b/client-electron/src/pages/android/android.tsx @@ -3,23 +3,40 @@ import { ScrcpyTab } from './components/scrcpy-tab' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Toaster } from '@/components/ui/toaster' import { ShellTab } from './components/shell-tab' +import useAdb from '@/hooks/useAdb' +import { useShallow } from 'zustand/react/shallow' +import { FileManagerTab } from './components/file-manager-tab' const AndroidPage: React.FC = () => { + const { manager, device, adb, setDevice, setAdb } = useAdb( + useShallow(state => ({ + manager: state.manager, + device: state.device, + adb: state.adb, + setDevice: state.setDevice, + setAdb: state.setAdb + })) + ) + return (
- +
- + Scrcpy Shell + File Manager - + - + + + + diff --git a/client-electron/src/pages/android/components/file-manager-tab.tsx b/client-electron/src/pages/android/components/file-manager-tab.tsx new file mode 100644 index 0000000..2ef6989 --- /dev/null +++ b/client-electron/src/pages/android/components/file-manager-tab.tsx @@ -0,0 +1,138 @@ +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { ReloadIcon } from '@radix-ui/react-icons' +import { LinuxFileType, type Adb } from '@yume-chan/adb' +import { WritableStream } from '@yume-chan/stream-extra' +import { useCallback, useState } from 'react' + +interface FileManagerTabProps { + adb: Adb | undefined +} + +type filesType = { + file: string + indent: number + blob?: Blob + files?: filesType[] +} + +export const FileManagerTab: React.FC = ({ adb }) => { + const [path, setPath] = useState('') + + const [files, setFiles] = useState() + + const download = useCallback(() => { + const readFiles = async (adb: Adb) => { + const sync = await adb.sync() + try { + const folder: filesType = { + file: 'test-pull-taobin_project', + indent: 0, + files: [] + } + + const TypeBlob: { + [key: string]: string + } = { + mp4: 'video/mp4', + png: 'image/png', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + gif: 'image/gif' + } + + // scan all files in the folder + const scanFiles = async (folder: filesType, path: string, indent: number) => { + const entries = await sync.readdir(path) + for (const entry of entries) { + if (entry.name.startsWith('.')) { + continue + } + if (entry.type === LinuxFileType.Directory) { + const newFolder: filesType = { + file: entry.name, + indent: indent, + files: [] + } + folder.files?.push(newFolder) + await scanFiles(newFolder, `${path}/${entry.name}`, indent + 1) + } else { + const buffer: Uint8Array[] = [] + const fileStream = sync.read(`${path}/${entry.name}`) + await fileStream.pipeTo( + new WritableStream({ + write(chunk) { + buffer.push(chunk) + } + }) + ) + + const blob = new Blob(buffer, { type: TypeBlob[entry.name.split('.').pop()!] }) + folder.files?.push({ + file: entry.name, + blob: blob, + indent: indent + }) + } + } + } + console.log('Scanning files...') + await scanFiles(folder, path, 1) + console.log('Scanned files') + setFiles(folder) + console.log(folder) + } finally { + await sync.dispose() + } + } + + if (adb) { + readFiles(adb) + } + }, [adb, path]) + + const refresh = useCallback(() => { + console.log('Refreshing...') + }, []) + + return ( + + + File Manager + Manage files in Android + + +
+
+ +
+
+ setPath(e.target.value)} /> + +
+
{files && }
+
+
+
+ ) +} + +const FileTree = ({ files }: { files: filesType }) => { + return ( +
+ {files.files?.map((file, index) => { + return ( +
+ {file.file} +
{file.files && }
+
+ ) + })} +
+ ) +} diff --git a/client-electron/src/pages/android/components/scrcpy-tab.tsx b/client-electron/src/pages/android/components/scrcpy-tab.tsx index 84ebcfe..5c49323 100644 --- a/client-electron/src/pages/android/components/scrcpy-tab.tsx +++ b/client-electron/src/pages/android/components/scrcpy-tab.tsx @@ -1,8 +1,7 @@ import { Button } from '@/components/ui/button' import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card' import { toast } from '@/components/ui/use-toast' -import useAdb from '@/hooks/useAdb' -import { type AdbSubprocessProtocol } from '@yume-chan/adb' +import { type Adb, type AdbSubprocessProtocol } from '@yume-chan/adb' import { AdbScrcpyClient, AdbScrcpyOptions1_22 } from '@yume-chan/adb-scrcpy' import { AndroidKeyCode, @@ -15,15 +14,17 @@ import { } from '@yume-chan/scrcpy' import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs' import { Consumable, WritableStream, ReadableStream, DecodeUtf8Stream } from '@yume-chan/stream-extra' -import { useCallback, useEffect, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useRef, useState } from 'react' import { Terminal } from 'xterm' import { FitAddon } from 'xterm-addon-fit' import 'xterm/css/xterm.css' -export const ScrcpyTab: React.FC = () => { - const adb = useAdb(state => state.adb) +interface ScrcpyTabProps { + adb: Adb | undefined +} +export const ScrcpyTab: React.FC = memo(({ adb }) => { const logcatRef = useRef(null) const scrcpyScreenRef = useRef(null) @@ -31,15 +32,11 @@ export const ScrcpyTab: React.FC = () => { const [client, setClient] = useState() const [decoder, setDecoder] = useState() + console.log('rendering scrcpy tab') + useEffect(() => { const startTerminal = async () => { if (logcatRef.current && adb) { - if (logcatRef.current.children.length > 0) { - // remove all children from the logcatRef - while (logcatRef.current.firstChild) { - logcatRef.current.removeChild(logcatRef.current.firstChild) - } - } const terminal: Terminal = new Terminal() const fitAddon = new FitAddon() terminal.loadAddon(fitAddon) @@ -68,12 +65,17 @@ export const ScrcpyTab: React.FC = () => { startTerminal() return () => { - logcatRef.current && logcatRef.current.firstChild && logcatRef.current.removeChild(logcatRef.current.firstChild) + console.log('cleaning up logcat') + + for (const child of logcatRef.current?.children || []) { + logcatRef.current?.removeChild(child) + } + process?.stderr.cancel() process?.stdout.cancel() process?.kill() } - }, [logcatRef, adb]) + }, [adb]) const connectScrcpy = useCallback(async () => { if (!adb) { @@ -319,4 +321,4 @@ export const ScrcpyTab: React.FC = () => { ) -} +}) diff --git a/client-electron/src/pages/android/components/shell-tab.tsx b/client-electron/src/pages/android/components/shell-tab.tsx index c31bf73..e2c961a 100644 --- a/client-electron/src/pages/android/components/shell-tab.tsx +++ b/client-electron/src/pages/android/components/shell-tab.tsx @@ -1,75 +1,84 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import useAdb from '@/hooks/useAdb' -import { encodeUtf8, type AdbSubprocessProtocol } from '@yume-chan/adb' +import { encodeUtf8, type AdbSubprocessProtocol, type Adb } from '@yume-chan/adb' import { Consumable, WritableStream } from '@yume-chan/stream-extra' -import { useEffect, useRef, useState } from 'react' +import { memo, useCallback, useEffect, useRef, useState } from 'react' import { Terminal } from 'xterm' import { FitAddon } from 'xterm-addon-fit' import 'xterm/css/xterm.css' -export const ShellTab: React.FC = () => { - const adb = useAdb(state => state.adb) - - const shellRef = useRef(null) +interface ShellTabProps { + adb: Adb | undefined +} +export const ShellTab: React.FC = memo(({ adb }) => { const [process, setProcess] = useState() + const [terminal, setTerminal] = useState() + + const [reader, setReader] = useState | undefined>() useEffect(() => { - const startTerminal = async () => { - if (shellRef.current && adb) { - if (shellRef.current.children.length > 0) { - // remove all children from the shellRef - while (shellRef.current.firstChild) { - shellRef.current.removeChild(shellRef.current.firstChild) - } + if (adb) { + console.log('adb is connected') + + console.log('creating terminal') + const terminal: Terminal = new Terminal() + terminal.options.cursorBlink = true + terminal.options.theme = { + background: '#1e1e1e', + foreground: '#d4d4d4' + } + + console.log('creating process') + + const _reader = new WritableStream({ + write(chunk) { + terminal.write(chunk) } - const terminal: Terminal = new Terminal() - const fitAddon = new FitAddon() - terminal.loadAddon(fitAddon) + }) - const process: AdbSubprocessProtocol = await adb.subprocess.shell( - '/data/data/com.termux/files/usr/bin/telnet localhost 45515' - ) - process.stdout.pipeTo( - new WritableStream({ - write(chunk) { - terminal.write(chunk) - } - }) - ) + adb.subprocess.shell('/data/data/com.termux/files/usr/bin/telnet localhost 45515').then(_process => { + _process.stdout.pipeTo(_reader) - const writer = process.stdin.getWriter() + const writer = _process.stdin.getWriter() terminal.onData(data => { const buffer = encodeUtf8(data) const consumable = new Consumable(buffer) writer.write(consumable) }) - terminal.options.cursorBlink = true - terminal.options.theme = { - background: '#1e1e1e', - foreground: '#d4d4d4' - } - - terminal.open(shellRef.current) - fitAddon.fit() - setProcess(process) + setReader(_reader) + setProcess(_process) + setTerminal(terminal) + }) + } else { + console.log('adb is not connected') + if (process) { + process?.stdout.cancel() + process?.stderr.cancel() + process?.stdin.close() + process?.kill() } - } - startTerminal() - return () => { - console.log('cleaning up shell') - shellRef.current && shellRef.current.firstChild && shellRef.current.removeChild(shellRef.current.firstChild) - process?.stderr.cancel() - process?.stdin.close() - process?.stdout.cancel() - process?.kill() + setProcess(undefined) + setTerminal(undefined) } - }, [shellRef, adb]) + }, [adb]) + + const killProcess = useCallback(() => { + console.log('killing shell') + console.log(process) + if (process && terminal) { + terminal.write('exit\n') + reader?.close() + process.stderr.cancel() + process.stdin.close() + process.stdout.cancel() + process.kill() + } + }, [process]) return ( @@ -80,21 +89,46 @@ export const ShellTab: React.FC = () => {
-
-
+ {terminal ? ( + + ) : ( +
+

No Connection ADB

+
+ )}
) +}) + +interface ShellTerminalProps { + terminal: Terminal +} + +const ShellTerminal: React.FC = ({ terminal }) => { + const shellRef = useRef(null) + + useEffect(() => { + console.log(shellRef.current) + // check if shellRef is have child remove all + if (shellRef.current && shellRef.current.children.length > 0) { + for (const child of shellRef.current.children) { + shellRef.current.removeChild(child) + } + } + + if (terminal && shellRef.current) { + const addon = new FitAddon() + terminal.loadAddon(addon) + terminal.open(shellRef.current) + addon.fit() + } + }, [terminal]) + + return
} diff --git a/client-electron/src/pages/android/components/tool-bar.tsx b/client-electron/src/pages/android/components/tool-bar.tsx index 96f74ca..ec5c6a9 100644 --- a/client-electron/src/pages/android/components/tool-bar.tsx +++ b/client-electron/src/pages/android/components/tool-bar.tsx @@ -9,24 +9,24 @@ import { DialogTrigger } from '@/components/ui/dialog' import { toast } from '@/components/ui/use-toast' -import useAdb from '@/hooks/useAdb' import { Adb, AdbDaemonTransport } from '@yume-chan/adb' import AdbWebCredentialStore from '@yume-chan/adb-credential-web' -import { ADB_DEFAULT_DEVICE_FILTER } from '@yume-chan/adb-daemon-webusb' +import { + ADB_DEFAULT_DEVICE_FILTER, + type AdbDaemonWebUsbDevice, + type AdbDaemonWebUsbDeviceManager +} from '@yume-chan/adb-daemon-webusb' import { useState } from 'react' -import { useShallow } from 'zustand/react/shallow' -export const ToolBar: React.FC = () => { - const { manager, device, adb, setDevice, setAdb } = useAdb( - useShallow(state => ({ - manager: state.manager, - device: state.device, - adb: state.adb, - setDevice: state.setDevice, - setAdb: state.setAdb - })) - ) +interface ToolBarProps { + manager: AdbDaemonWebUsbDeviceManager | undefined + device: AdbDaemonWebUsbDevice | undefined + adb: Adb | undefined + setDevice: (device: AdbDaemonWebUsbDevice | undefined) => void + setAdb: (adb: Adb | undefined) => void +} +export const ToolBar: React.FC = ({ manager, adb, device, setAdb, setDevice }) => { const [name, setName] = useState('') const [resolution, setResolution] = useState('') const [version, setVersion] = useState('')