diff --git a/.vscode/settings.json b/.vscode/settings.json index 42e7a6ac..e48b9306 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,34 +1,30 @@ { "cSpell.words": [ "ADB's", - "Backends", - "CLASSPATH", - "CLSE", - "CNXN", - "Callout", - "Deserialization", - "GPRS", - "Genymobile's", - "HSPA", - "Muxer", - "PKCS", - "RSASSA", - "Remux", - "Remuxer", - "Scrcpy", - "WRTE", "addrs", "allowlist", "arraybuffer", + "autorun", + "Backends", "brotli", + "Callout", "carriernetworkchange", + "CLASSPATH", + "CLSE", + "CNXN", + "cybojenix", + "Deserialization", + "endregion", "fluentui", "genymobile", + "Genymobile's", "getprop", + "GPRS", "hardcode", "hardcodes", "hhmm", "hisi", + "HSPA", "jmuxer", "keyof", "killforward", @@ -38,13 +34,19 @@ "localabstract", "lstat", "mitm", + "Muxer", "nosim", "opendir", + "PKCS", "pluggable", "powersave", "reimplement", "reimplemented", + "Remux", + "Remuxer", + "RSASSA", "runtimes", + "Scrcpy", "scrollback", "sendrecv", "streamsaver", @@ -63,6 +65,7 @@ "webusb", "wifi", "wirelessly", + "WRTE", "yume", "zstd" ], diff --git a/apps/demo/.eslintrc.json b/apps/demo/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/apps/demo/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/demo/.gitignore b/apps/demo/.gitignore new file mode 100644 index 00000000..1437c53f --- /dev/null +++ b/apps/demo/.gitignore @@ -0,0 +1,34 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel diff --git a/apps/demo/README.md b/apps/demo/README.md index 11c6a27f..c87e0421 100644 --- a/apps/demo/README.md +++ b/apps/demo/README.md @@ -1,19 +1,34 @@ -# WebADB Demo +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -Demo of `@yume-chan/adb` and `@yume-chan/adb-backend-webusb`. +## Getting Started -## Start +First, run the development server: -From root folder: - -```sh -npm run start:demo +```bash +npm run dev +# or +yarn dev ``` -From this folder: +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -```sh -npm start -``` +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. -Then navigate to `http://localhost:9000`. +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/apps/demo/src/components/command-bar.tsx b/apps/demo/components/command-bar.tsx similarity index 100% rename from apps/demo/src/components/command-bar.tsx rename to apps/demo/components/command-bar.tsx diff --git a/apps/demo/src/components/connect.tsx b/apps/demo/components/connect.tsx similarity index 74% rename from apps/demo/src/components/connect.tsx rename to apps/demo/components/connect.tsx index dff31fe6..416cc18c 100644 --- a/apps/demo/src/components/connect.tsx +++ b/apps/demo/components/connect.tsx @@ -1,30 +1,19 @@ import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, StackItem, TooltipHost } from '@fluentui/react'; -import { Adb, AdbBackend, AdbLogger } from '@yume-chan/adb'; +import { Adb, AdbBackend } from '@yume-chan/adb'; import AdbWebUsbBackend, { AdbWebCredentialStore, AdbWebUsbBackendWatcher } from '@yume-chan/adb-backend-webusb'; import AdbWsBackend from '@yume-chan/adb-backend-ws'; +import { observer } from 'mobx-react-lite'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; -import { CommonStackTokens } from '../styles'; -import { withDisplayName } from '../utils'; +import { device, logger } from '../state'; +import { CommonStackTokens } from '../utils'; import { ErrorDialogContext } from './error-dialog'; const DropdownStyles = { dropdown: { width: '100%' } }; -interface ConnectProps { - device: Adb | undefined; - - logger?: AdbLogger; - - onDeviceChange: (device: Adb | undefined) => void; -} - const CredentialStore = new AdbWebCredentialStore(); -export const Connect = withDisplayName('Connect')(({ - device, - logger, - onDeviceChange, -}: ConnectProps): JSX.Element | null => { - const supported = AdbWebUsbBackend.isSupported(); +function _Connect(): JSX.Element | null { + const [supported, setSupported] = useState(true); const { show: showErrorDialog } = useContext(ErrorDialogContext); @@ -37,29 +26,39 @@ export const Connect = withDisplayName('Connect')(({ setUsbBackendList(backendList); return backendList; }, []); - useEffect(() => { - if (!supported) { - showErrorDialog('Your browser does not support WebUSB standard, which is required for this site to work.\n\nLatest version of Google Chrome (for Windows, macOS, Linux and Android), Microsoft Edge (for Windows and macOS), or other Chromium-based browsers should work.'); - return; - } - updateUsbBackendList(); + useEffect( + () => { + // Only run on client + const supported = AdbWebUsbBackend.isSupported(); + setSupported(supported); - const watcher = new AdbWebUsbBackendWatcher(async (serial?: string) => { - const list = await updateUsbBackendList(); - - if (serial) { - setSelectedBackend(list.find(backend => backend.serial === serial)); + if (!supported) { + showErrorDialog('Your browser does not support WebUSB standard, which is required for this site to work.\n\nLatest version of Google Chrome, Microsoft Edge, or other Chromium-based browsers are required.'); return; } - }); - return () => watcher.dispose(); - }, []); + + updateUsbBackendList(); + + const watcher = new AdbWebUsbBackendWatcher(async (serial?: string) => { + const list = await updateUsbBackendList(); + + if (serial) { + setSelectedBackend(list.find(backend => backend.serial === serial)); + return; + } + }); + + return () => watcher.dispose(); + }, + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + [] + ); const [wsBackendList, setWsBackendList] = useState([]); useEffect(() => { const intervalId = setInterval(async () => { - if (connecting || device) { + if (connecting || device.current) { return; } @@ -78,10 +77,10 @@ export const Connect = withDisplayName('Connect')(({ return () => { clearInterval(intervalId); }; - }, [connecting, device]); + }, [connecting]); const handleSelectedBackendChange = ( - _e: React.FormEvent, + e: React.FormEvent, option?: IDropdownOption, ) => { setSelectedBackend(option?.data as AdbBackend); @@ -91,18 +90,18 @@ export const Connect = withDisplayName('Connect')(({ const backend = await AdbWebUsbBackend.requestDevice(); setSelectedBackend(backend); await updateUsbBackendList(); - }, []); + }, [updateUsbBackendList]); const connect = useCallback(async () => { try { if (selectedBackend) { - const device = new Adb(selectedBackend, logger); + const adb = new Adb(selectedBackend, logger.logger); try { setConnecting(true); - await device.connect(CredentialStore); - onDeviceChange(device); + await adb.connect(CredentialStore); + device.setCurrent(adb); } catch (e) { - device.dispose(); + adb.dispose(); throw e; } } @@ -111,20 +110,15 @@ export const Connect = withDisplayName('Connect')(({ } finally { setConnecting(false); } - }, [selectedBackend, logger, onDeviceChange]); + }, [showErrorDialog, selectedBackend]); const disconnect = useCallback(async () => { try { - await device!.dispose(); - onDeviceChange(undefined); + await device.current!.dispose(); + device.setCurrent(undefined); } catch (e: any) { showErrorDialog(e.message); } - }, [device]); - useEffect(() => { - return device?.onDisconnected(() => { - onDeviceChange(undefined); - }); - }, [device, onDeviceChange]); + }, [showErrorDialog]); const backendList = useMemo( () => ([] as AdbBackend[]).concat(usbBackendList, wsBackendList), @@ -157,7 +151,7 @@ export const Connect = withDisplayName('Connect')(({ tokens={{ childrenGap: 8, padding: '0 0 8px 8px' }} > - {!device ? ( + {!device.current ? ( ); -}); +}; + +export const Connect = observer(_Connect); diff --git a/apps/demo/src/components/demo-mode.tsx b/apps/demo/components/demo-mode.tsx similarity index 99% rename from apps/demo/src/components/demo-mode.tsx rename to apps/demo/components/demo-mode.tsx index 8849d980..4e1dbde1 100644 --- a/apps/demo/src/components/demo-mode.tsx +++ b/apps/demo/components/demo-mode.tsx @@ -21,16 +21,14 @@ function useDemoModeSetting( if (enabled) { onChange(value); } - }, [enabled]); + }, [enabled, onChange, value]); const handleChange = useCallback((value: T) => { setValue(value); - if (enabled) { - onChange(value); - } else { + if (!enabled) { setEnabled(true); } - }, [enabled, onChange]); + }, [enabled, setEnabled]); return [value, handleChange]; } diff --git a/apps/demo/src/components/device-view.tsx b/apps/demo/components/device-view.tsx similarity index 100% rename from apps/demo/src/components/device-view.tsx rename to apps/demo/components/device-view.tsx diff --git a/apps/demo/src/components/error-dialog.tsx b/apps/demo/components/error-dialog.tsx similarity index 98% rename from apps/demo/src/components/error-dialog.tsx rename to apps/demo/components/error-dialog.tsx index 60b538cb..a8bcf814 100644 --- a/apps/demo/src/components/error-dialog.tsx +++ b/apps/demo/components/error-dialog.tsx @@ -20,7 +20,7 @@ export const ErrorDialogProvider = withDisplayName('ErrorDialogProvider')((props setErrorMessage(message); showErrorDialog(); } - }), []); + }), [showErrorDialog]); return ( diff --git a/apps/demo/src/components/external-link.tsx b/apps/demo/components/external-link.tsx similarity index 99% rename from apps/demo/src/components/external-link.tsx rename to apps/demo/components/external-link.tsx index 7c6b1fb5..d33024a8 100644 --- a/apps/demo/src/components/external-link.tsx +++ b/apps/demo/components/external-link.tsx @@ -4,11 +4,8 @@ import { withDisplayName } from '../utils/with-display-name'; export interface ExternalLinkProps { href: string; - spaceBefore?: boolean; - spaceAfter?: boolean; - children?: ReactNode; } diff --git a/apps/demo/src/components/index.ts b/apps/demo/components/index.ts similarity index 88% rename from apps/demo/src/components/index.ts rename to apps/demo/components/index.ts index 13d2845e..51f7c6e3 100644 --- a/apps/demo/src/components/index.ts +++ b/apps/demo/components/index.ts @@ -5,4 +5,3 @@ export * from './device-view'; export * from './error-dialog'; export * from './external-link'; export * from './logger'; -export * from './router'; diff --git a/apps/demo/src/components/logger.tsx b/apps/demo/components/logger.tsx similarity index 85% rename from apps/demo/src/components/logger.tsx rename to apps/demo/components/logger.tsx index b6180f87..8e919127 100644 --- a/apps/demo/src/components/logger.tsx +++ b/apps/demo/components/logger.tsx @@ -1,8 +1,9 @@ import { IconButton, IListProps, List, mergeStyles, mergeStyleSets, Stack } from '@fluentui/react'; -import { AdbLogger, AdbPacket, AdbPacketInit } from '@yume-chan/adb'; +import { AdbPacketInit } from '@yume-chan/adb'; import { decodeUtf8 } from '@yume-chan/adb-backend-webusb'; -import { DisposableList, EventEmitter } from '@yume-chan/event'; +import { DisposableList } from '@yume-chan/event'; import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { logger } from "../state"; import { withDisplayName } from '../utils'; import { CommandBar } from './command-bar'; @@ -22,28 +23,6 @@ const classNames = mergeStyleSets({ }, }); -export class AdbEventLogger { - private readonly _logger: AdbLogger; - public get logger() { return this._logger; } - - private readonly _incomingPacketEvent = new EventEmitter(); - public get onIncomingPacket() { return this._incomingPacketEvent.event; } - - private readonly _outgoingPacketEvent = new EventEmitter(); - public get onOutgoingPacket() { return this._outgoingPacketEvent.event; } - - public constructor() { - this._logger = { - onIncomingPacket: (packet) => { - this._incomingPacketEvent.fire(packet); - }, - onOutgoingPacket: (packet) => { - this._outgoingPacketEvent.fire(packet); - }, - }; - } -} - function serializePacket(packet: AdbPacketInit) { const command = decodeUtf8(new Uint32Array([packet.command]).buffer); @@ -77,7 +56,6 @@ const LoggerLine = withDisplayName('LoggerLine')(({ packet }: { packet: [string, export interface LoggerContextValue { visible: boolean; - onVisibleChange: React.Dispatch>; } @@ -119,8 +97,6 @@ export const ToggleLogger = withDisplayName('ToggleLogger')(() => { export interface LoggerProps { className?: string; - - logger: AdbEventLogger; } function shouldVirtualize(props: IListProps<[string, AdbPacketInit]>) { @@ -139,7 +115,6 @@ function renderCell(item?: [string, AdbPacketInit]) { export const Logger = withDisplayName('Logger')(({ className, - logger, }: LoggerProps) => { const contextValue = useContext(LoggerContext); const [packets, setPackets] = useState<[string, AdbPacketInit][]>([]); @@ -162,7 +137,7 @@ export const Logger = withDisplayName('Logger')(({ }); })); return disposables.dispose; - }, [logger]); + }, []); useLayoutEffect(() => { const scroller = scrollerRef.current; diff --git a/apps/demo/next-env.d.ts b/apps/demo/next-env.d.ts new file mode 100644 index 00000000..534a39ea --- /dev/null +++ b/apps/demo/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/demo/next.config.js b/apps/demo/next.config.js new file mode 100644 index 00000000..4dd22138 --- /dev/null +++ b/apps/demo/next.config.js @@ -0,0 +1,20 @@ +const withMDX = require('@next/mdx')({ + extension: /\.mdx?$/, + options: { + // Disable MDX createElement hack + // because we don't need rendering custom elements + renderer: ` + import React from 'react'; + const mdx = (name, props, ...children) => { + return React.createElement(name, props, ...children); + } + `, + } +}); + +/** @type {import('next').NextConfig} */ +module.exports = withMDX({ + pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'], + reactStrictMode: true, + productionBrowserSourceMaps: true, +}); diff --git a/apps/demo/package.json b/apps/demo/package.json index 98f22870..9912053a 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -1,69 +1,45 @@ { "name": "demo", + "version": "0.1.0", "private": true, - "version": "0.0.9", - "description": "Demo of `@yume-chan/adb` and `@yume-chan/adb-backend-webusb`.", - "author": "Simon Chan ", - "homepage": "https://github.com/yume-chan/ya-webadb#readme", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/yume-chan/ya-webadb.git" - }, "scripts": { - "start": "webpack serve", - "build": "webpack --mode production", - "build:dev": "webpack" - }, - "bugs": { - "url": "https://github.com/yume-chan/ya-webadb/issues" - }, - "devDependencies": { - "@types/jest": "^26.0.23", - "@types/react": "^17.0.0", - "@types/react-dom": "^17.0.0", - "@types/react-router-dom": "^5.1.6", - "clean-webpack-plugin": "^4.0.0", - "copy-webpack-plugin": "^8.0.0", - "css-loader": "^5.2.4", - "file-loader": "^6.2.0", - "html-webpack-plugin": "^5.3.1", - "mini-css-extract-plugin": "^1.4.0", - "source-map-loader": "^2.0.1", - "ts-loader": "^9.1.2", - "tslib": "^2.3.1", - "typescript": "^4.4.3", - "webpack": "^5.28.0", - "webpack-bundle-analyzer": "^4.4.0", - "webpack-cli": "^4.7.0", - "webpack-dev-server": "^3.11.1", - "webpackbar": "^5.0.0-3", - "worker-loader": "^3.0.7", - "@yume-chan/ts-package-builder": "^1.0.0" + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" }, "dependencies": { - "@fluentui/font-icons-mdl2": "^8.1.9", - "@fluentui/react": "^8.29.1", - "@fluentui/react-file-type-icons": "^8.2.3", - "@fluentui/react-hooks": "^8.2.7", - "@types/node": "^16.9.1", + "@fluentui/font-icons-mdl2": "^8.1.14", + "@fluentui/react": "^8.36.3", + "@fluentui/react-file-type-icons": "^8.4.3", + "@fluentui/react-hooks": "^8.3.4", "@yume-chan/adb": "^0.0.9", "@yume-chan/adb-backend-webusb": "^0.0.9", "@yume-chan/adb-backend-ws": "^0.0.9", "@yume-chan/async": "^2.1.4", "@yume-chan/event": "^0.0.9", "@yume-chan/struct": "^0.0.9", - "path-browserify": "^1.0.1", + "mobx": "^6.3.3", + "mobx-react-lite": "^3.2.1", + "next": "11.1.2", "react": "^17.0.2", "react-dom": "^17.0.2", - "react-router-dom": "^5.2.0", "streamsaver": "^2.0.5", - "tinyh264": "^0.0.6", + "tinyh264": "^0.0.7", "xterm": "^4.14.1", "xterm-addon-fit": "^0.5.0", "xterm-addon-search": "^0.8.1", "xterm-addon-webgl": "^0.11.2", "yuv-buffer": "^1.0.0", - "yuv-canvas": "^1.2.6" + "yuv-canvas": "^1.2.7" + }, + "devDependencies": { + "@mdx-js/loader": "^1.6.22", + "@next/mdx": "^11.1.2", + "@types/react": "17.0.27", + "copy-webpack-plugin": "^9.0.1", + "eslint": "7.32.0", + "eslint-config-next": "^11.1.3-canary.52", + "typescript": "^4.4.3" } } diff --git a/apps/demo/pages/_app.tsx b/apps/demo/pages/_app.tsx new file mode 100644 index 00000000..74e5ea02 --- /dev/null +++ b/apps/demo/pages/_app.tsx @@ -0,0 +1,114 @@ +import { ActionButton, IComponentAsProps, IconButton, INavButtonProps, INavLink, initializeIcons, Link as FluentLink, mergeStyles, mergeStyleSets, Nav, Stack, StackItem } from "@fluentui/react"; +import type { AppProps } from 'next/app'; +import { useRouter } from 'next/router'; +import Link from 'next/link'; +import React, { useCallback, useEffect, useState } from "react"; +import { Connect, ErrorDialogProvider, Logger, ToggleLogger } from "../components"; +import '../styles/globals.css'; + +initializeIcons(); + +const ROUTES = [ + { + url: '/', + name: 'README', + }, + { + url: '/device-info', + name: 'Device Info', + }, + { + url: '/file-manager', + name: 'File Manager', + }, +]; + +function NavLink({ link, defaultRender: DefaultRender, ...props }: IComponentAsProps) { + if (!link) { + return null; + } + + return ( + + + + ); +} + +function MyApp({ Component, pageProps }: AppProps) { + const classNames = mergeStyleSets({ + 'title-container': { + borderBottom: '1px solid rgb(243, 242, 241)', + }, + title: { + padding: '4px 0', + fontSize: 20, + textAlign: 'center', + }, + 'left-column': { + width: 250, + paddingRight: 8, + borderRight: '1px solid rgb(243, 242, 241)', + overflow: 'auto', + }, + 'right-column': { + borderLeft: '1px solid rgb(243, 242, 241)', + } + }); + + const [leftPanelVisible, setLeftPanelVisible] = useState(false); + const toggleLeftPanel = useCallback(() => { + setLeftPanelVisible(value => !value); + }, []); + useEffect(() => { + setLeftPanelVisible(innerWidth > 650); + }, []); + + const router = useRouter(); + + return ( + + + + + + +
WebADB Demo
+
+ + +
+ + + + + +