271 lines
6.7 KiB
TypeScript
271 lines
6.7 KiB
TypeScript
import { create } from 'zustand'
|
|
import useAdb from './useAdb'
|
|
import { toast } from '@/components/ui/use-toast'
|
|
import { type LinuxFileType } from '@yume-chan/adb'
|
|
import { fromUnixTime } from 'date-fns'
|
|
import { Consumable, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
|
import JSZip from 'jszip'
|
|
|
|
export interface AndroidFile {
|
|
filename: string
|
|
type: LinuxFileType
|
|
size: bigint
|
|
dateModified: Date
|
|
}
|
|
|
|
interface FileManagerAndroidHook {
|
|
rootPath: string
|
|
currentPath: string
|
|
pushPath: (path: string) => void
|
|
popPath: () => void
|
|
setCurrentPath: (path: string) => void
|
|
scanPath: (path?: string) => Promise<AndroidFile[] | undefined>
|
|
pushFile: (file: File, targetPath: string) => Promise<void>
|
|
pushFiles: (files: File[], targetPath: string) => Promise<void>
|
|
createDirectory: (dirName: string) => Promise<void>
|
|
delete: (filename: string) => Promise<void>
|
|
rename: (filename: string, newName: string) => Promise<void>
|
|
download: (files: AndroidFile[]) => Promise<void>
|
|
}
|
|
|
|
const useFileManager = create<FileManagerAndroidHook>((set, get) => ({
|
|
rootPath: '/mnt/sdcard/coffeevending',
|
|
currentPath: '',
|
|
pushPath(path) {
|
|
set({ currentPath: get().currentPath + '/' + path })
|
|
console.log('currentPath', get().currentPath)
|
|
},
|
|
popPath() {
|
|
set({ currentPath: get().currentPath.slice(0, get().currentPath.lastIndexOf('/')) })
|
|
console.log('currentPath', get().currentPath)
|
|
},
|
|
setCurrentPath(path) {
|
|
set({ currentPath: path })
|
|
},
|
|
async scanPath(path) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const sync = await adb.sync()
|
|
try {
|
|
console.log('scanning path', get().rootPath + path)
|
|
const entries = await sync.readdir(get().rootPath + path)
|
|
return entries.reduce<AndroidFile[]>((acc, entry) => {
|
|
if (entry.name === '.' || entry.name === '..') {
|
|
return acc
|
|
}
|
|
|
|
return [
|
|
...acc,
|
|
{
|
|
filename: entry.name,
|
|
type: entry.type,
|
|
size: entry.size,
|
|
dateModified: fromUnixTime(Number(entry.mtime))
|
|
}
|
|
]
|
|
}, [])
|
|
} finally {
|
|
await sync.dispose()
|
|
}
|
|
},
|
|
async pushFile(file, targetPath) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const buffer = await file.arrayBuffer()
|
|
const sync = await adb.sync()
|
|
try {
|
|
await sync.write({
|
|
filename: targetPath + '/' + file.name,
|
|
file: new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new Consumable(new Uint8Array(buffer)))
|
|
controller.close()
|
|
}
|
|
})
|
|
})
|
|
} finally {
|
|
await sync.dispose()
|
|
}
|
|
},
|
|
async pushFiles(files, targetPath) {
|
|
for (const file of files) {
|
|
await get().pushFile(file, targetPath)
|
|
}
|
|
},
|
|
async createDirectory(name) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const process = await adb.subprocess.spawn('mkdir ' + get().rootPath + '/' + name)
|
|
process.stderr.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.error(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
process.stdout.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.log(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
if ((await process.exit) != 0) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to create directory',
|
|
description: 'Please try again'
|
|
})
|
|
}
|
|
},
|
|
async delete(filename) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const process = await adb.subprocess.spawn('rm ' + get().rootPath + '/' + filename)
|
|
process.stderr.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.error(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
process.stdout.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.log(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
if ((await process.exit) != 0) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to delete file',
|
|
description: 'Please try again'
|
|
})
|
|
}
|
|
},
|
|
async rename(filename, newName) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const process = await adb.subprocess.spawn(
|
|
'mv ' + get().rootPath + '/' + filename + ' ' + get().rootPath + '/' + newName
|
|
)
|
|
process.stderr.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.error(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
process.stdout.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
console.log(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
if ((await process.exit) != 0) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to rename file',
|
|
description: 'Please try again'
|
|
})
|
|
}
|
|
},
|
|
async download(files) {
|
|
const adb = useAdb.getState().adb
|
|
if (!adb) {
|
|
toast({
|
|
duration: 3000,
|
|
variant: 'destructive',
|
|
title: 'Failed to connect to device',
|
|
description: 'Please connect Adb first'
|
|
})
|
|
return
|
|
}
|
|
|
|
const sync = await adb.sync()
|
|
try {
|
|
const zip = new JSZip()
|
|
for (const file of files) {
|
|
const buffer: Uint8Array[] = []
|
|
const fileStream = sync.read(get().rootPath + '/' + file.filename)
|
|
await fileStream.pipeTo(
|
|
new WritableStream({
|
|
write(chunk) {
|
|
buffer.push(chunk)
|
|
}
|
|
})
|
|
)
|
|
|
|
zip.file(file.filename, new Blob(buffer))
|
|
}
|
|
|
|
const blob = await zip.generateAsync({ type: 'blob' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'download.zip'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
} finally {
|
|
await sync.dispose()
|
|
}
|
|
}
|
|
}))
|
|
|
|
export default useFileManager
|