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('')