Add download file
This commit is contained in:
parent
ac0f5bbeea
commit
92fbf7eed6
5 changed files with 279 additions and 88 deletions
|
|
@ -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 (
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex w-full p-5">
|
||||
<ToolBar />
|
||||
<ToolBar manager={manager} device={device} adb={adb} setAdb={setAdb} setDevice={setDevice} />
|
||||
</div>
|
||||
<Tabs defaultValue="scrcpy" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="scrcpy">Scrcpy</TabsTrigger>
|
||||
<TabsTrigger value="shell">Shell</TabsTrigger>
|
||||
<TabsTrigger value="file-manager">File Manager</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="scrcpy">
|
||||
<ScrcpyTab />
|
||||
<ScrcpyTab adb={adb} />
|
||||
</TabsContent>
|
||||
<TabsContent value="shell">
|
||||
<ShellTab />
|
||||
<ShellTab adb={adb} />
|
||||
</TabsContent>
|
||||
<TabsContent value="file-manager">
|
||||
<FileManagerTab adb={adb} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<Toaster />
|
||||
|
|
|
|||
|
|
@ -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<FileManagerTabProps> = ({ adb }) => {
|
||||
const [path, setPath] = useState<string>('')
|
||||
|
||||
const [files, setFiles] = useState<filesType>()
|
||||
|
||||
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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>File Manager</CardTitle>
|
||||
<CardDescription>Manage files in Android</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-end py-3 w-full">
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<Button variant={'outline'} onClick={refresh}>
|
||||
<ReloadIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex space-x-5 w-96">
|
||||
<Input placeholder="folder to download" value={path} onChange={e => setPath(e.target.value)} />
|
||||
<Button variant={'default'} onClick={download}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full max-h-96 overflow-y-auto">{files && <FileTree files={files} />}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const FileTree = ({ files }: { files: filesType }) => {
|
||||
return (
|
||||
<div className="pl-5">
|
||||
{files.files?.map((file, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<span>{file.file}</span>
|
||||
<div>{file.files && <FileTree files={file} />}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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<ScrcpyTabProps> = memo(({ adb }) => {
|
||||
const logcatRef = useRef<HTMLDivElement>(null)
|
||||
const scrcpyScreenRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
|
|
@ -31,15 +32,11 @@ export const ScrcpyTab: React.FC = () => {
|
|||
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
|
||||
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
|
||||
|
||||
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 = () => {
|
|||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null)
|
||||
interface ShellTabProps {
|
||||
adb: Adb | undefined
|
||||
}
|
||||
|
||||
export const ShellTab: React.FC<ShellTabProps> = memo(({ adb }) => {
|
||||
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
|
||||
const [terminal, setTerminal] = useState<Terminal | undefined>()
|
||||
|
||||
const [reader, setReader] = useState<WritableStream<Uint8Array> | 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<Uint8Array>({
|
||||
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<Uint8Array>({
|
||||
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 (
|
||||
<Card>
|
||||
|
|
@ -80,21 +89,46 @@ export const ShellTab: React.FC = () => {
|
|||
<CardContent>
|
||||
<div className="flex items-center justify-end py-3 w-full">
|
||||
<div>
|
||||
<Button
|
||||
variant={'destructive'}
|
||||
onClick={() => {
|
||||
process?.stderr.cancel()
|
||||
process?.stdin.close()
|
||||
process?.stdout.cancel()
|
||||
process?.kill()
|
||||
}}
|
||||
>
|
||||
<Button variant={'destructive'} onClick={killProcess}>
|
||||
Kill Shell
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-[800px] bg-slate-700" ref={shellRef}></div>
|
||||
{terminal ? (
|
||||
<ShellTerminal terminal={terminal} />
|
||||
) : (
|
||||
<div className="w-full h-[800px] bg-slate-700 flex justify-center items-center">
|
||||
<h1>No Connection ADB</h1>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
|
||||
interface ShellTerminalProps {
|
||||
terminal: Terminal
|
||||
}
|
||||
|
||||
const ShellTerminal: React.FC<ShellTerminalProps> = ({ terminal }) => {
|
||||
const shellRef = useRef<HTMLDivElement>(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 <div className="w-full h-[800px] bg-slate-700" ref={shellRef}></div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ToolBarProps> = ({ manager, adb, device, setAdb, setDevice }) => {
|
||||
const [name, setName] = useState<string>('')
|
||||
const [resolution, setResolution] = useState<string>('')
|
||||
const [version, setVersion] = useState<string>('')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue