Taobin-Recipe-Manager/client-electron/src/hooks/filemanager-android.ts
2024-02-19 18:07:10 +07:00

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