feat: add back install apk page

This commit is contained in:
Simon Chan 2021-10-13 17:05:41 +08:00
parent ab32a8ee87
commit 20b914fe6f
10 changed files with 192 additions and 69 deletions

View file

@ -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<AdbBackend[]>([]);
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' }}
>
<Dropdown
disabled={!!device.current || backendOptions.length === 0}
disabled={!!global.device || backendOptions.length === 0}
label="Available devices"
placeholder="No available devices"
options={backendOptions}
@ -161,7 +161,7 @@ function _Connect(): JSX.Element | null {
onChange={handleSelectedBackendChange}
/>
{!device.current ? (
{!global.device ? (
<Stack horizontal tokens={CommonStackTokens}>
<StackItem grow shrink>
<PrimaryButton

View file

@ -3,7 +3,7 @@ import { AdbDemoModeMobileDataType, AdbDemoModeMobileDataTypes, AdbDemoModeSigna
import { autorun, makeAutoObservable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
import { CSSProperties, useCallback } from 'react';
import { device } from "../state";
import { global } from "../state";
const SignalStrengthOptions =
Object.values(AdbDemoModeSignalStrength)
@ -65,7 +65,7 @@ class DemoModeState {
makeAutoObservable(this);
reaction(
() => 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 = ({
<div style={{ padding: 12, overflow: 'hidden auto', ...style }}>
<Toggle
label="Allowed"
disabled={!device.current}
disabled={!global.device}
checked={state.allowed}
onChange={handleAllowedChange}
/>

View file

@ -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<string, string> = {
@ -52,7 +52,7 @@ const DeviceInfo: NextPage = () => {
</MessageBar>
<span>
<span>Protocol Version: </span>
<code>{device.current?.protocolVersion?.toString(16).padStart(8, '0')}</code>
<code>{global.device?.protocolVersion?.toString(16).padStart(8, '0')}</code>
</span>
<Separator />
@ -60,21 +60,21 @@ const DeviceInfo: NextPage = () => {
<code>ro.product.name</code>
<span> field in Android Build Props</span>
</MessageBar>
<span>Product Name: {device.current?.product}</span>
<span>Product Name: {global.device?.product}</span>
<Separator />
<MessageBar>
<code>ro.product.model</code>
<span> field in Android Build Props</span>
</MessageBar>
<span>Model Name: {device.current?.model}</span>
<span>Model Name: {global.device?.model}</span>
<Separator />
<MessageBar>
<code>ro.product.device</code>
<span> field in Android Build Props</span>
</MessageBar>
<span>Device Name: {device.current?.device}</span>
<span>Device Name: {global.device?.device}</span>
<Separator />
<MessageBar>
@ -87,7 +87,7 @@ const DeviceInfo: NextPage = () => {
</MessageBar>
<span>
<span>Features: </span>
{device.current?.features?.map((feature, index) => (
{global.device?.features?.map((feature, index) => (
<span key={feature}>
{index !== 0 && (<span>, </span>)}
<span>{feature}</span>

View file

@ -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<string | undefined>();
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;

View file

@ -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<HTMLCanvasElement | null>(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();
},
},

123
apps/demo/pages/install.tsx Normal file
View file

@ -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 (
<Stack {...RouteStackProps}>
<Head>
<title>Install APK - WebADB</title>
</Head>
<Stack horizontal>
<DefaultButton
disabled={!global.device || state.installing}
text="Open"
onClick={() => state.install()}
/>
</Stack>
{state.progress && (
<ProgressIndicator
styles={{ root: { width: 300 } }}
label={state.progress.filename}
percentComplete={state.progress.value}
description={Stage[state.progress.stage]}
/>
)}
</Stack>
);
};
export default observer(Install);

View file

@ -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}`);

View file

@ -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 && (
<TextField
disabled={!!state.serviceListenAddresses}
value={state.servicePort}

View file

@ -1,19 +1,19 @@
import { Adb } from "@yume-chan/adb";
import { makeAutoObservable } from 'mobx';
export class Device {
current: Adb | undefined;
export class GlobalState {
device: Adb | undefined;
constructor() {
makeAutoObservable(this);
}
setCurrent(device: Adb | undefined) {
this.current = device;
this.device = device;
device?.onDisconnected(() => {
this.setCurrent(undefined);
});
}
}
export const device = new Device();
export const global = new GlobalState();

View file

@ -1,2 +1,2 @@
export * from './device';
export * from './global';
export * from './logger';