diff --git a/apps/demo/components/connect.tsx b/apps/demo/components/connect.tsx index 416cc18c..01098fa7 100644 --- a/apps/demo/components/connect.tsx +++ b/apps/demo/components/connect.tsx @@ -4,7 +4,7 @@ import AdbWebUsbBackend, { AdbWebCredentialStore, AdbWebUsbBackendWatcher } from import AdbWsBackend from '@yume-chan/adb-backend-ws'; import { observer } from 'mobx-react-lite'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { device, logger } from '../state'; +import { global, logger } from '../state'; import { CommonStackTokens } from '../utils'; import { ErrorDialogContext } from './error-dialog'; @@ -58,7 +58,7 @@ function _Connect(): JSX.Element | null { const [wsBackendList, setWsBackendList] = useState([]); useEffect(() => { const intervalId = setInterval(async () => { - if (connecting || device.current) { + if (connecting || global.device) { return; } @@ -99,7 +99,7 @@ function _Connect(): JSX.Element | null { try { setConnecting(true); await adb.connect(CredentialStore); - device.setCurrent(adb); + global.setCurrent(adb); } catch (e) { adb.dispose(); throw e; @@ -113,8 +113,8 @@ function _Connect(): JSX.Element | null { }, [showErrorDialog, selectedBackend]); const disconnect = useCallback(async () => { try { - await device.current!.dispose(); - device.setCurrent(undefined); + await global.device!.dispose(); + global.setCurrent(undefined); } catch (e: any) { showErrorDialog(e.message); } @@ -151,7 +151,7 @@ function _Connect(): JSX.Element | null { tokens={{ childrenGap: 8, padding: '0 0 8px 8px' }} > - {!device.current ? ( + {!global.device ? ( device.current, + () => global.device, async (device) => { if (device) { const allowed = await device.demoMode.getAllowed(); @@ -120,21 +120,21 @@ const FEATURES: FeatureDefinition[][] = [ max: 100, step: 1, initial: 100, - onChange: (value) => device.current!.demoMode.setBatteryLevel(value as number), + onChange: (value) => global.device!.demoMode.setBatteryLevel(value as number), }, { key: 'batteryCharging', label: 'Battery Charging', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setBatteryCharging(value as boolean), + onChange: (value) => global.device!.demoMode.setBatteryCharging(value as boolean), }, { key: 'powerSaveMode', label: 'Power Save Mode', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setPowerSaveMode(value as boolean), + onChange: (value) => global.device!.demoMode.setPowerSaveMode(value as boolean), }, ], [ @@ -144,14 +144,14 @@ const FEATURES: FeatureDefinition[][] = [ type: 'select', options: SignalStrengthOptions, initial: AdbDemoModeSignalStrength.Level4, - onChange: (value) => device.current!.demoMode.setWifiSignalStrength(value as AdbDemoModeSignalStrength), + onChange: (value) => global.device!.demoMode.setWifiSignalStrength(value as AdbDemoModeSignalStrength), }, { key: 'airplaneMode', label: 'Airplane Mode', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setAirplaneMode(value as boolean), + onChange: (value) => global.device!.demoMode.setAirplaneMode(value as boolean), }, { key: 'mobileDataType', @@ -159,7 +159,7 @@ const FEATURES: FeatureDefinition[][] = [ type: 'select', options: MobileDataTypeOptions, initial: 'lte', - onChange: (value) => device.current!.demoMode.setMobileDataType(value as AdbDemoModeMobileDataType), + onChange: (value) => global.device!.demoMode.setMobileDataType(value as AdbDemoModeMobileDataType), }, { key: 'mobileSignalStrength', @@ -167,7 +167,7 @@ const FEATURES: FeatureDefinition[][] = [ type: 'select', options: SignalStrengthOptions, initial: AdbDemoModeSignalStrength.Level4, - onChange: (value) => device.current!.demoMode.setMobileSignalStrength(value as AdbDemoModeSignalStrength), + onChange: (value) => global.device!.demoMode.setMobileSignalStrength(value as AdbDemoModeSignalStrength), }, ], [ @@ -177,42 +177,42 @@ const FEATURES: FeatureDefinition[][] = [ type: 'select', options: StatusBarModeOptions, initial: 'transparent', - onChange: (value) => device.current!.demoMode.setStatusBarMode(value as AdbDemoModeStatusBarMode), + onChange: (value) => global.device!.demoMode.setStatusBarMode(value as AdbDemoModeStatusBarMode), }, { key: 'vibrateMode', label: 'Vibrate Mode Indicator', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setVibrateModeEnabled(value as boolean), + onChange: (value) => global.device!.demoMode.setVibrateModeEnabled(value as boolean), }, { key: 'bluetoothConnected', label: 'Bluetooth Indicator', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setBluetoothConnected(value as boolean), + onChange: (value) => global.device!.demoMode.setBluetoothConnected(value as boolean), }, { key: 'locatingIcon', label: 'Locating Icon', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setLocatingIcon(value as boolean), + onChange: (value) => global.device!.demoMode.setLocatingIcon(value as boolean), }, { key: 'alarmIcon', label: 'Alarm Icon', type: 'boolean', initial: false, - onChange: (value) => device.current!.demoMode.setAlarmIcon(value as boolean), + onChange: (value) => global.device!.demoMode.setAlarmIcon(value as boolean), }, { key: 'notificationsVisibility', label: 'Notifications Visibility', type: 'boolean', initial: true, - onChange: (value) => device.current!.demoMode.setNotificationsVisibility(value as boolean), + onChange: (value) => global.device!.demoMode.setNotificationsVisibility(value as boolean), }, { key: 'hour', @@ -222,7 +222,7 @@ const FEATURES: FeatureDefinition[][] = [ max: 23, step: 1, initial: 12, - onChange: (value) => device.current!.demoMode.setTime(value as number, state.features.get('minute') as number | undefined ?? 34) + onChange: (value) => global.device!.demoMode.setTime(value as number, state.features.get('minute') as number | undefined ?? 34) }, { key: 'minute', @@ -232,7 +232,7 @@ const FEATURES: FeatureDefinition[][] = [ max: 59, step: 1, initial: 34, - onChange: (value) => device.current!.demoMode.setTime(state.features.get('hour') as number | undefined ?? 34, value as number) + onChange: (value) => global.device!.demoMode.setTime(state.features.get('hour') as number | undefined ?? 34, value as number) }, ], ]; @@ -306,7 +306,7 @@ const DemoModeBase = ({ style, }: DemoModeProps) => { const handleAllowedChange = useCallback(async (e, value?: boolean) => { - await device.current!.demoMode.setAllowed(value!); + await global.device!.demoMode.setAllowed(value!); runInAction(() => { state.allowed = value!; state.enabled = false; @@ -314,7 +314,7 @@ const DemoModeBase = ({ }, []); const handleEnabledChange = useCallback(async (e, value?: boolean) => { - await device.current!.demoMode.setEnabled(value!); + await global.device!.demoMode.setEnabled(value!); runInAction(() => state.enabled = value!); }, []); @@ -322,7 +322,7 @@ const DemoModeBase = ({
diff --git a/apps/demo/pages/device-info.tsx b/apps/demo/pages/device-info.tsx index a0922337..8b62d780 100644 --- a/apps/demo/pages/device-info.tsx +++ b/apps/demo/pages/device-info.tsx @@ -5,7 +5,7 @@ import type { NextPage } from 'next'; import Head from 'next/head'; import React from "react"; import { ExternalLink } from "../components"; -import { device } from '../state'; +import { global } from '../state'; import { RouteStackProps } from "../utils"; const KNOWN_FEATURES: Record = { @@ -52,7 +52,7 @@ const DeviceInfo: NextPage = () => { Protocol Version: - {device.current?.protocolVersion?.toString(16).padStart(8, '0')} + {global.device?.protocolVersion?.toString(16).padStart(8, '0')} @@ -60,21 +60,21 @@ const DeviceInfo: NextPage = () => { ro.product.name field in Android Build Props - Product Name: {device.current?.product} + Product Name: {global.device?.product} ro.product.model field in Android Build Props - Model Name: {device.current?.model} + Model Name: {global.device?.model} ro.product.device field in Android Build Props - Device Name: {device.current?.device} + Device Name: {global.device?.device} @@ -87,7 +87,7 @@ const DeviceInfo: NextPage = () => { Features: - {device.current?.features?.map((feature, index) => ( + {global.device?.features?.map((feature, index) => ( {index !== 0 && (, )} {feature} diff --git a/apps/demo/pages/file-manager.tsx b/apps/demo/pages/file-manager.tsx index 29d8fd90..f48c4674 100644 --- a/apps/demo/pages/file-manager.tsx +++ b/apps/demo/pages/file-manager.tsx @@ -10,7 +10,7 @@ import Router, { useRouter } from "next/router"; import path from 'path'; import React, { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react'; import { CommandBar, ErrorDialogContext } from '../components'; -import { device } from '../state'; +import { global } from '../state'; import { asyncEffect, chunkFile, formatSize, formatSpeed, pickFile, RouteStackProps, useSpeed } from '../utils'; let StreamSaver: typeof import('streamsaver'); @@ -96,7 +96,7 @@ class FileManagerState { items: observable.shallow, }); reaction( - () => device.current, + () => global.device, () => this.loadFiles(), { fireImmediately: true }, ); @@ -264,13 +264,13 @@ class FileManagerState { runInAction(() => this.items = []); - if (!device.current) { + if (!global.device) { return; } runInAction(() => this.loading = true); - const sync = await device.current.sync(); + const sync = await global.device.sync(); const items: ListItem[] = []; const linkItems: AdbSyncEntryResponse[] = []; @@ -365,7 +365,7 @@ const FileManager: NextPage = (): JSX.Element | null => { const [previewUrl, setPreviewUrl] = useState(); const previewImage = useCallback(async (path: string) => { - const sync = await device.current!.sync(); + const sync = await global.device!.sync(); try { const readableStream = createReadableStreamFromBufferIterator(sync.read(path)); const response = new Response(readableStream); @@ -413,7 +413,7 @@ const FileManager: NextPage = (): JSX.Element | null => { const [uploadTotalSize, setUploadTotalSize] = useState(0); const [debouncedUploadedSize, uploadSpeed] = useSpeed(uploadedSize, uploadTotalSize); const upload = useCallback(async (file: File) => { - const sync = await device.current!.sync(); + const sync = await global.device!.sync(); try { const itemPath = path.resolve(state.path!, file.name); setUploading(true); @@ -445,7 +445,7 @@ const FileManager: NextPage = (): JSX.Element | null => { key: 'upload', text: 'Upload', iconProps: { iconName: 'Upload' }, - disabled: !device.current, + disabled: !global.device, onClick() { (async () => { const files = await pickFile({ multiple: true }); @@ -467,7 +467,7 @@ const FileManager: NextPage = (): JSX.Element | null => { iconProps: { iconName: 'Download' }, onClick() { (async () => { - const sync = await device.current!.sync(); + const sync = await global.device!.sync(); try { const itemPath = path.resolve(state.path, selectedItems[0].name!); const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath)); @@ -495,7 +495,7 @@ const FileManager: NextPage = (): JSX.Element | null => { (async () => { try { for (const item of selectedItems) { - const output = await device.current!.rm(path.resolve(state.path, item.name!)); + const output = await global.device!.rm(path.resolve(state.path, item.name!)); if (output) { showErrorDialog(output); return; diff --git a/apps/demo/pages/framebuffer.tsx b/apps/demo/pages/framebuffer.tsx index 78f4ac41..f5e5802f 100644 --- a/apps/demo/pages/framebuffer.tsx +++ b/apps/demo/pages/framebuffer.tsx @@ -6,7 +6,7 @@ import { NextPage } from "next"; import Head from "next/head"; import React, { useCallback, useContext, useEffect, useRef } from 'react'; import { CommandBar, DemoMode, DeviceView, ErrorDialogContext } from '../components'; -import { device } from "../state"; +import { global } from "../state"; import { RouteStackProps } from "../utils"; class FrameBufferState { @@ -38,13 +38,13 @@ const FrameBuffer: NextPage = (): JSX.Element | null => { const canvasRef = useRef(null); const capture = useCallback(async () => { - if (!device.current) { + if (!global.device) { return; } try { const start = window.performance.now(); - const framebuffer = await device.current.framebuffer(); + const framebuffer = await global.device.framebuffer(); const end = window.performance.now(); console.log('time', end - start); state.setImage(framebuffer); @@ -68,7 +68,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => { const commandBarItems = computed(() => [ { key: 'start', - disabled: !device.current, + disabled: !global.device, iconProps: { iconName: 'Camera' }, text: 'Capture', onClick: capture, @@ -87,7 +87,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => { const url = canvas.toDataURL(); const a = document.createElement('a'); a.href = url; - a.download = `Screenshot of ${device.current!.name}.png`; + a.download = `Screenshot of ${global.device!.name}.png`; a.click(); }, }, diff --git a/apps/demo/pages/install.tsx b/apps/demo/pages/install.tsx new file mode 100644 index 00000000..174a87f6 --- /dev/null +++ b/apps/demo/pages/install.tsx @@ -0,0 +1,123 @@ +import { DefaultButton, ProgressIndicator, Stack } from "@fluentui/react"; +import { AdbSyncMaxPacketSize } from "@yume-chan/adb"; +import { makeAutoObservable, observable, runInAction } from "mobx"; +import { observer } from "mobx-react-lite"; +import { NextPage } from "next"; +import Head from "next/head"; +import React from "react"; +import { global } from "../state"; +import { chunkFile, pickFile, RouteStackProps } from "../utils"; + +enum Stage { + Uploading, + + Installing, + + Completed, +} + +interface Progress { + filename: string; + + stage: Stage; + + uploadedSize: number; + + totalSize: number; + + value: number | undefined; +} + +class InstallPageState { + installing = false; + + progress: Progress | undefined; + + constructor() { + makeAutoObservable(this, { + progress: observable.ref, + }); + } + + async install() { + const file = await pickFile({ accept: '.apk' }); + if (!file) { + return; + } + + runInAction(() => { + this.installing = true; + this.progress = { + filename: file.name, + stage: Stage.Uploading, + uploadedSize: 0, + totalSize: file.size, + value: 0, + }; + }); + + await global.device!.install(chunkFile(file, AdbSyncMaxPacketSize), uploaded => { + runInAction(() => { + if (uploaded !== file.size) { + this.progress = { + filename: file.name, + stage: Stage.Uploading, + uploadedSize: uploaded, + totalSize: file.size, + value: uploaded / file.size * 0.8, + }; + } else { + this.progress = { + filename: file.name, + stage: Stage.Installing, + uploadedSize: uploaded, + totalSize: file.size, + value: 0.8, + }; + } + }); + }); + + runInAction(() => { + this.progress = { + filename: file.name, + stage: Stage.Completed, + uploadedSize: file.size, + totalSize: file.size, + value: 1, + }; + this.installing = false; + }); + } +} + +const state = new InstallPageState(); + +const Install: NextPage = () => { + return ( + + + Install APK - WebADB + + + + state.install()} + /> + + + {state.progress && ( + + )} + + ); +}; + +export default observer(Install); diff --git a/apps/demo/pages/shell.tsx b/apps/demo/pages/shell.tsx index 833446ac..b1cbc12a 100644 --- a/apps/demo/pages/shell.tsx +++ b/apps/demo/pages/shell.tsx @@ -6,7 +6,7 @@ import Head from "next/head"; import React, { CSSProperties, useCallback, useContext, useEffect, useRef, useState } from 'react'; import 'xterm/css/xterm.css'; import { ErrorDialogContext } from '../components/error-dialog'; -import { device } from "../state"; +import { global } from "../state"; import { ResizeObserver, RouteStackProps } from '../utils'; let terminal: import('../components/terminal').AdbTerminal; @@ -43,9 +43,9 @@ const Shell: NextPage = (): JSX.Element | null => { const connectingRef = useRef(false); useEffect(() => { return reaction( - () => device.current, + () => global.device, async () => { - if (!device.current) { + if (!global.device) { terminal.socket = undefined; return; } @@ -56,7 +56,7 @@ const Shell: NextPage = (): JSX.Element | null => { try { connectingRef.current = true; - const socket = await device.current.childProcess.shell(); + const socket = await global.device.childProcess.shell(); terminal.socket = socket; } catch (e) { showErrorDialog(e instanceof Error ? e.message : `${e}`); diff --git a/apps/demo/pages/tcpip.tsx b/apps/demo/pages/tcpip.tsx index dc429b82..d4372e04 100644 --- a/apps/demo/pages/tcpip.tsx +++ b/apps/demo/pages/tcpip.tsx @@ -5,7 +5,7 @@ import { NextPage } from "next"; import Head from "next/head"; import React, { useCallback } from "react"; import { ExternalLink } from "../components"; -import { device } from "../state"; +import { global } from "../state"; import { asyncEffect, RouteStackProps } from "../utils"; class TcpIpState { @@ -18,7 +18,7 @@ class TcpIpState { constructor() { makeAutoObservable(this); reaction( - () => device.current, + () => global.device, () => this.queryInfo(), { fireImmediately: true } ); @@ -28,14 +28,14 @@ class TcpIpState { return [ { key: 'refresh', - disabled: !device.current, + disabled: !global.device, iconProps: { iconName: 'Refresh' }, text: 'Refresh', onClick: () => { this.queryInfo(); }, }, { key: 'apply', - disabled: !device.current, + disabled: !global.device, iconProps: { iconName: 'Save' }, text: 'Apply', onClick: () => { this.applyServicePort(); }, @@ -44,7 +44,7 @@ class TcpIpState { } queryInfo = asyncEffect(async (signal) => { - if (!device.current) { + if (!global.device) { runInAction(() => { this.serviceListenAddresses = undefined; this.servicePortEnabled = false; @@ -55,9 +55,9 @@ class TcpIpState { return; } - const serviceListenAddresses = await device.current.getProp('service.adb.listen_addrs'); - const servicePort = await device.current.getProp('service.adb.tcp.port'); - const persistPort = await device.current.getProp('persist.adb.tcp.port'); + const serviceListenAddresses = await global.device.getProp('service.adb.listen_addrs'); + const servicePort = await global.device.getProp('service.adb.tcp.port'); + const persistPort = await global.device.getProp('persist.adb.tcp.port'); if (signal.aborted) { return; @@ -85,14 +85,14 @@ class TcpIpState { }); async applyServicePort() { - if (!device.current) { + if (!global.device) { return; } if (state.servicePortEnabled) { - await device.current.tcpip.setPort(Number.parseInt(state.servicePort, 10)); + await global.device.tcpip.setPort(Number.parseInt(state.servicePort, 10)); } else { - await device.current.tcpip.disable(); + await global.device.tcpip.disable(); } } } @@ -158,12 +158,12 @@ const TcpIp: NextPage = () => { inlineLabel label="service.adb.tcp.port" checked={state.servicePortEnabled} - disabled={!device.current || !!state.serviceListenAddresses} + disabled={!global.device || !!state.serviceListenAddresses} onText="Enabled" offText="Disabled" onChange={handleServicePortEnabledChange} /> - {device && ( + {global && ( { this.setCurrent(undefined); }); } } -export const device = new Device(); +export const global = new GlobalState(); diff --git a/apps/demo/state/index.ts b/apps/demo/state/index.ts index 4acc9dc1..0033be22 100644 --- a/apps/demo/state/index.ts +++ b/apps/demo/state/index.ts @@ -1,2 +1,2 @@ -export * from './device'; +export * from './global'; export * from './logger';