Loading apps/demo/src/components/scrcpy/state.tsx +27 −21 Original line number Diff line number Diff line Loading @@ -67,13 +67,15 @@ export class ScrcpyPageState { async pushServer() { const serverBuffer = await fetchServer(); await new ReadableStream<Uint8Array>({ await AdbScrcpyClient.pushServer( GLOBAL_STATE.device!, new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(serverBuffer); controller.close(); }, }).pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!)); }) ); } decoder: H264Decoder | undefined = undefined; Loading Loading @@ -186,12 +188,16 @@ export class ScrcpyPageState { ); try { await new ReadableStream<Uint8Array>({ await AdbScrcpyClient.pushServer( GLOBAL_STATE.device!, new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(serverBuffer); controller.close(); }, }) // In fact `pushServer` will pipe the stream through a ChunkStream, // but without this pipeThrough, the progress will not be updated. .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( new ProgressStream( Loading @@ -200,7 +206,7 @@ export class ScrcpyPageState { }) ) ) .pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!)); ); runInAction(() => { this.serverUploadSpeed = Loading apps/demo/src/components/terminal.tsx +33 −23 Original line number Diff line number Diff line Loading @@ -2,21 +2,22 @@ import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb"; import { AutoDisposable } from "@yume-chan/event"; import { AbortController, WritableStream } from '@yume-chan/stream-extra'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { SearchAddon } from 'xterm-addon-search'; import { WebglAddon } from 'xterm-addon-webgl'; import { AbortController, WritableStream } from "@yume-chan/stream-extra"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import { SearchAddon } from "xterm-addon-search"; import { WebglAddon } from "xterm-addon-webgl"; export class AdbTerminal extends AutoDisposable { private element = document.createElement('div'); private element = document.createElement("div"); public terminal: Terminal = new Terminal({ allowProposedApi: true, allowTransparency: true, cursorStyle: 'bar', cursorStyle: "bar", cursorBlink: true, fontFamily: '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"', fontFamily: '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"', letterSpacing: 1, scrollback: 9000, smoothScrollDuration: 50, Loading @@ -29,7 +30,9 @@ export class AdbTerminal extends AutoDisposable { private _socket: AdbSubprocessProtocol | undefined; private _socketAbortController: AbortController | undefined; public get socket() { return this._socket; } public get socket() { return this._socket; } public set socket(value) { if (this._socket) { // Remove event listeners Loading @@ -46,19 +49,26 @@ export class AdbTerminal extends AutoDisposable { this._socketAbortController = new AbortController(); // pty mode only has one stream value.stdout.pipeTo(new WritableStream<Uint8Array>({ value.stdout .pipeTo( new WritableStream<Uint8Array>({ write: (chunk) => { this.terminal.write(chunk); }, }), { }), { signal: this._socketAbortController.signal, }); } ) .catch(() => {}); const _writer = value.stdin.getWriter(); this.addDisposable(this.terminal.onData(data => { this.addDisposable( this.terminal.onData((data) => { const buffer = encodeUtf8(data); _writer.write(buffer); })); }) ); this.fit(); } Loading @@ -67,9 +77,9 @@ export class AdbTerminal extends AutoDisposable { public constructor() { super(); this.element.style.width = '100%'; this.element.style.height = '100%'; this.element.style.overflow = 'hidden'; this.element.style.width = "100%"; this.element.style.height = "100%"; this.element.style.overflow = "hidden"; this.terminal.loadAddon(this.searchAddon); this.terminal.loadAddon(this.fitAddon); Loading apps/demo/src/pages/device-info.tsx +13 −10 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ import { Stack, TooltipHost, } from "@fluentui/react"; import { AdbFeatures } from "@yume-chan/adb"; import { AdbFeature } from "@yume-chan/adb"; import { observer } from "mobx-react-lite"; import type { NextPage } from "next"; import Head from "next/head"; Loading @@ -14,25 +14,28 @@ import { GLOBAL_STATE } from "../state"; import { Icons, RouteStackProps } from "../utils"; const KNOWN_FEATURES: Record<string, string> = { [AdbFeatures.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`, [AdbFeature.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`, // 'cmd': '', [AdbFeatures.StatV2]: [AdbFeature.StatV2]: '"sync" command now supports "STA2" (returns more information of a file than old "STAT") and "LST2" (returns information of a directory) sub command', [AdbFeatures.ListV2]: [AdbFeature.ListV2]: '"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"', [AdbFeatures.FixedPushMkdir]: [AdbFeature.FixedPushMkdir]: "Android 9 (P) introduced a bug that pushing files to a non-existing directory would fail. This feature indicates it's fixed (Android 10)", // 'apex': '', // 'abb': '', // 'fixed_push_symlink_timestamp': '', abb_exec: 'Support "exec" command which can stream stdin into child process', [AdbFeature.AbbExec]: 'Supports "abb_exec" variant that can be used to install App faster', // 'remount_shell': '', // 'track_app': '', // 'sendrecv_v2': '', // 'sendrecv_v2_brotli': '', // 'sendrecv_v2_lz4': '', // 'sendrecv_v2_zstd': '', sendrecv_v2_brotli: 'Supports "brotli" compression algorithm when pushing/pulling files', sendrecv_v2_lz4: 'Supports "lz4" compression algorithm when pushing/pulling files', sendrecv_v2_zstd: 'Supports "zstd" compression algorithm when pushing/pulling files', // 'sendrecv_v2_dry_run_send': '', }; Loading apps/demo/src/pages/file-manager.tsx +12 −20 Original line number Diff line number Diff line Loading @@ -29,13 +29,7 @@ import { } from "@fluentui/react-file-type-icons"; import { useConst } from "@fluentui/react-hooks"; import { getIcon } from "@fluentui/style-utilities"; import { ADB_SYNC_MAX_PACKET_SIZE, AdbFeatures, LinuxFileType, type AdbSyncEntry, } from "@yume-chan/adb"; import { ChunkStream } from "@yume-chan/stream-extra"; import { AdbFeature, LinuxFileType, type AdbSyncEntry } from "@yume-chan/adb"; import { action, autorun, Loading Loading @@ -270,6 +264,7 @@ class FileManagerState { const bSortKey = b[this.sortKey]!; if (aSortKey === bSortKey) { // use name as tie breaker result = compareCaseInsensitively(a.name!, b.name!); } else if (typeof aSortKey === "string") { result = compareCaseInsensitively( Loading @@ -277,7 +272,8 @@ class FileManagerState { bSortKey as string ); } else { result = aSortKey < bSortKey ? -1 : 1; result = (aSortKey as number) < (bSortKey as number) ? -1 : 1; } } Loading Loading @@ -391,7 +387,7 @@ class FileManagerState { }, ]; if (GLOBAL_STATE.device?.features?.includes(AdbFeatures.ListV2)) { if (GLOBAL_STATE.device?.supportsFeature(AdbFeature.ListV2)) { list.push( { key: "ctime", Loading Loading @@ -574,21 +570,17 @@ class FileManagerState { ); try { await createFileStream(file) .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( await sync.write( itemPath, createFileStream(file).pipeThrough( new ProgressStream( action((uploaded) => { this.uploadedSize = uploaded; }) ) ) .pipeTo( sync.write( itemPath, ), (LinuxFileType.File << 12) | 0o666, file.lastModified / 1000 ) ); runInAction(() => { Loading apps/demo/src/pages/install.tsx +61 −9 Original line number Diff line number Diff line import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react"; import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb"; import { ChunkStream } from "@yume-chan/stream-extra"; import { Checkbox, PrimaryButton, ProgressIndicator, Stack, } from "@fluentui/react"; import { PackageManager, PackageManagerInstallOptions, } from "@yume-chan/android-bin"; import { WritableStream } from "@yume-chan/stream-extra"; import { action, makeAutoObservable, observable, runInAction } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; Loading Loading @@ -38,10 +46,17 @@ class InstallPageState { progress: Progress | undefined = undefined; log: string = ""; options: Partial<PackageManagerInstallOptions> = { bypassLowTargetSdkBlock: false, }; constructor() { makeAutoObservable(this, { progress: observable.ref, install: false, options: observable.deep, }); } Loading @@ -60,11 +75,14 @@ class InstallPageState { totalSize: file.size, value: 0, }; this.log = ""; }); await createFileStream(file) .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( const pm = new PackageManager(GLOBAL_STATE.device!); const start = Date.now(); const log = await pm.installStream( file.size, createFileStream(file).pipeThrough( new ProgressStream( action((uploaded) => { if (uploaded !== file.size) { Loading @@ -87,7 +105,24 @@ class InstallPageState { }) ) ) .pipeTo(GLOBAL_STATE.device!.install()); ); const elapsed = Date.now() - start; await log.pipeTo( new WritableStream({ write: action((chunk) => { this.log += chunk; }), }) ); const transferRate = ( file.size / (elapsed / 1000) / 1024 / 1024 ).toFixed(2); this.log += `Install finished in ${elapsed}ms at ${transferRate}MB/s`; runInAction(() => { this.progress = { Loading @@ -112,9 +147,24 @@ const Install: NextPage = () => { </Head> <Stack horizontal> <DefaultButton <Checkbox label="--bypass-low-target-sdk-block (Android 14)" checked={state.options.bypassLowTargetSdkBlock} onChange={(_, checked) => { if (checked === undefined) { return; } runInAction(() => { state.options.bypassLowTargetSdkBlock = checked; }); }} /> </Stack> <Stack horizontal> <PrimaryButton disabled={!GLOBAL_STATE.device || state.installing} text="Open" text="Browse APK" onClick={state.install} /> </Stack> Loading @@ -127,6 +177,8 @@ const Install: NextPage = () => { description={Stage[state.progress.stage]} /> )} {state.log && <pre>{state.log}</pre>} </Stack> ); }; Loading Loading
apps/demo/src/components/scrcpy/state.tsx +27 −21 Original line number Diff line number Diff line Loading @@ -67,13 +67,15 @@ export class ScrcpyPageState { async pushServer() { const serverBuffer = await fetchServer(); await new ReadableStream<Uint8Array>({ await AdbScrcpyClient.pushServer( GLOBAL_STATE.device!, new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(serverBuffer); controller.close(); }, }).pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!)); }) ); } decoder: H264Decoder | undefined = undefined; Loading Loading @@ -186,12 +188,16 @@ export class ScrcpyPageState { ); try { await new ReadableStream<Uint8Array>({ await AdbScrcpyClient.pushServer( GLOBAL_STATE.device!, new ReadableStream<Uint8Array>({ start(controller) { controller.enqueue(serverBuffer); controller.close(); }, }) // In fact `pushServer` will pipe the stream through a ChunkStream, // but without this pipeThrough, the progress will not be updated. .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( new ProgressStream( Loading @@ -200,7 +206,7 @@ export class ScrcpyPageState { }) ) ) .pipeTo(AdbScrcpyClient.pushServer(GLOBAL_STATE.device!)); ); runInAction(() => { this.serverUploadSpeed = Loading
apps/demo/src/components/terminal.tsx +33 −23 Original line number Diff line number Diff line Loading @@ -2,21 +2,22 @@ import { AdbSubprocessProtocol, encodeUtf8 } from "@yume-chan/adb"; import { AutoDisposable } from "@yume-chan/event"; import { AbortController, WritableStream } from '@yume-chan/stream-extra'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { SearchAddon } from 'xterm-addon-search'; import { WebglAddon } from 'xterm-addon-webgl'; import { AbortController, WritableStream } from "@yume-chan/stream-extra"; import { Terminal } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import { SearchAddon } from "xterm-addon-search"; import { WebglAddon } from "xterm-addon-webgl"; export class AdbTerminal extends AutoDisposable { private element = document.createElement('div'); private element = document.createElement("div"); public terminal: Terminal = new Terminal({ allowProposedApi: true, allowTransparency: true, cursorStyle: 'bar', cursorStyle: "bar", cursorBlink: true, fontFamily: '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"', fontFamily: '"Cascadia Code", Consolas, monospace, "Source Han Sans SC", "Microsoft YaHei"', letterSpacing: 1, scrollback: 9000, smoothScrollDuration: 50, Loading @@ -29,7 +30,9 @@ export class AdbTerminal extends AutoDisposable { private _socket: AdbSubprocessProtocol | undefined; private _socketAbortController: AbortController | undefined; public get socket() { return this._socket; } public get socket() { return this._socket; } public set socket(value) { if (this._socket) { // Remove event listeners Loading @@ -46,19 +49,26 @@ export class AdbTerminal extends AutoDisposable { this._socketAbortController = new AbortController(); // pty mode only has one stream value.stdout.pipeTo(new WritableStream<Uint8Array>({ value.stdout .pipeTo( new WritableStream<Uint8Array>({ write: (chunk) => { this.terminal.write(chunk); }, }), { }), { signal: this._socketAbortController.signal, }); } ) .catch(() => {}); const _writer = value.stdin.getWriter(); this.addDisposable(this.terminal.onData(data => { this.addDisposable( this.terminal.onData((data) => { const buffer = encodeUtf8(data); _writer.write(buffer); })); }) ); this.fit(); } Loading @@ -67,9 +77,9 @@ export class AdbTerminal extends AutoDisposable { public constructor() { super(); this.element.style.width = '100%'; this.element.style.height = '100%'; this.element.style.overflow = 'hidden'; this.element.style.width = "100%"; this.element.style.height = "100%"; this.element.style.overflow = "hidden"; this.terminal.loadAddon(this.searchAddon); this.terminal.loadAddon(this.fitAddon); Loading
apps/demo/src/pages/device-info.tsx +13 −10 Original line number Diff line number Diff line Loading @@ -5,7 +5,7 @@ import { Stack, TooltipHost, } from "@fluentui/react"; import { AdbFeatures } from "@yume-chan/adb"; import { AdbFeature } from "@yume-chan/adb"; import { observer } from "mobx-react-lite"; import type { NextPage } from "next"; import Head from "next/head"; Loading @@ -14,25 +14,28 @@ import { GLOBAL_STATE } from "../state"; import { Icons, RouteStackProps } from "../utils"; const KNOWN_FEATURES: Record<string, string> = { [AdbFeatures.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`, [AdbFeature.ShellV2]: `"shell" command now supports separating child process's stdout and stderr, and returning exit code`, // 'cmd': '', [AdbFeatures.StatV2]: [AdbFeature.StatV2]: '"sync" command now supports "STA2" (returns more information of a file than old "STAT") and "LST2" (returns information of a directory) sub command', [AdbFeatures.ListV2]: [AdbFeature.ListV2]: '"sync" command now supports "LST2" sub command which returns more information when listing a directory than old "LIST"', [AdbFeatures.FixedPushMkdir]: [AdbFeature.FixedPushMkdir]: "Android 9 (P) introduced a bug that pushing files to a non-existing directory would fail. This feature indicates it's fixed (Android 10)", // 'apex': '', // 'abb': '', // 'fixed_push_symlink_timestamp': '', abb_exec: 'Support "exec" command which can stream stdin into child process', [AdbFeature.AbbExec]: 'Supports "abb_exec" variant that can be used to install App faster', // 'remount_shell': '', // 'track_app': '', // 'sendrecv_v2': '', // 'sendrecv_v2_brotli': '', // 'sendrecv_v2_lz4': '', // 'sendrecv_v2_zstd': '', sendrecv_v2_brotli: 'Supports "brotli" compression algorithm when pushing/pulling files', sendrecv_v2_lz4: 'Supports "lz4" compression algorithm when pushing/pulling files', sendrecv_v2_zstd: 'Supports "zstd" compression algorithm when pushing/pulling files', // 'sendrecv_v2_dry_run_send': '', }; Loading
apps/demo/src/pages/file-manager.tsx +12 −20 Original line number Diff line number Diff line Loading @@ -29,13 +29,7 @@ import { } from "@fluentui/react-file-type-icons"; import { useConst } from "@fluentui/react-hooks"; import { getIcon } from "@fluentui/style-utilities"; import { ADB_SYNC_MAX_PACKET_SIZE, AdbFeatures, LinuxFileType, type AdbSyncEntry, } from "@yume-chan/adb"; import { ChunkStream } from "@yume-chan/stream-extra"; import { AdbFeature, LinuxFileType, type AdbSyncEntry } from "@yume-chan/adb"; import { action, autorun, Loading Loading @@ -270,6 +264,7 @@ class FileManagerState { const bSortKey = b[this.sortKey]!; if (aSortKey === bSortKey) { // use name as tie breaker result = compareCaseInsensitively(a.name!, b.name!); } else if (typeof aSortKey === "string") { result = compareCaseInsensitively( Loading @@ -277,7 +272,8 @@ class FileManagerState { bSortKey as string ); } else { result = aSortKey < bSortKey ? -1 : 1; result = (aSortKey as number) < (bSortKey as number) ? -1 : 1; } } Loading Loading @@ -391,7 +387,7 @@ class FileManagerState { }, ]; if (GLOBAL_STATE.device?.features?.includes(AdbFeatures.ListV2)) { if (GLOBAL_STATE.device?.supportsFeature(AdbFeature.ListV2)) { list.push( { key: "ctime", Loading Loading @@ -574,21 +570,17 @@ class FileManagerState { ); try { await createFileStream(file) .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( await sync.write( itemPath, createFileStream(file).pipeThrough( new ProgressStream( action((uploaded) => { this.uploadedSize = uploaded; }) ) ) .pipeTo( sync.write( itemPath, ), (LinuxFileType.File << 12) | 0o666, file.lastModified / 1000 ) ); runInAction(() => { Loading
apps/demo/src/pages/install.tsx +61 −9 Original line number Diff line number Diff line import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react"; import { ADB_SYNC_MAX_PACKET_SIZE } from "@yume-chan/adb"; import { ChunkStream } from "@yume-chan/stream-extra"; import { Checkbox, PrimaryButton, ProgressIndicator, Stack, } from "@fluentui/react"; import { PackageManager, PackageManagerInstallOptions, } from "@yume-chan/android-bin"; import { WritableStream } from "@yume-chan/stream-extra"; import { action, makeAutoObservable, observable, runInAction } from "mobx"; import { observer } from "mobx-react-lite"; import { NextPage } from "next"; Loading Loading @@ -38,10 +46,17 @@ class InstallPageState { progress: Progress | undefined = undefined; log: string = ""; options: Partial<PackageManagerInstallOptions> = { bypassLowTargetSdkBlock: false, }; constructor() { makeAutoObservable(this, { progress: observable.ref, install: false, options: observable.deep, }); } Loading @@ -60,11 +75,14 @@ class InstallPageState { totalSize: file.size, value: 0, }; this.log = ""; }); await createFileStream(file) .pipeThrough(new ChunkStream(ADB_SYNC_MAX_PACKET_SIZE)) .pipeThrough( const pm = new PackageManager(GLOBAL_STATE.device!); const start = Date.now(); const log = await pm.installStream( file.size, createFileStream(file).pipeThrough( new ProgressStream( action((uploaded) => { if (uploaded !== file.size) { Loading @@ -87,7 +105,24 @@ class InstallPageState { }) ) ) .pipeTo(GLOBAL_STATE.device!.install()); ); const elapsed = Date.now() - start; await log.pipeTo( new WritableStream({ write: action((chunk) => { this.log += chunk; }), }) ); const transferRate = ( file.size / (elapsed / 1000) / 1024 / 1024 ).toFixed(2); this.log += `Install finished in ${elapsed}ms at ${transferRate}MB/s`; runInAction(() => { this.progress = { Loading @@ -112,9 +147,24 @@ const Install: NextPage = () => { </Head> <Stack horizontal> <DefaultButton <Checkbox label="--bypass-low-target-sdk-block (Android 14)" checked={state.options.bypassLowTargetSdkBlock} onChange={(_, checked) => { if (checked === undefined) { return; } runInAction(() => { state.options.bypassLowTargetSdkBlock = checked; }); }} /> </Stack> <Stack horizontal> <PrimaryButton disabled={!GLOBAL_STATE.device || state.installing} text="Open" text="Browse APK" onClick={state.install} /> </Stack> Loading @@ -127,6 +177,8 @@ const Install: NextPage = () => { description={Stage[state.progress.stage]} /> )} {state.log && <pre>{state.log}</pre>} </Stack> ); }; Loading