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 pushFile: (file: File, targetPath: string) => Promise pushFiles: (files: File[], targetPath: string) => Promise createDirectory: (dirName: string) => Promise delete: (filename: string) => Promise rename: (filename: string, newName: string) => Promise download: (files: AndroidFile[]) => Promise } const useFileManager = create((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((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