mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(web): start a new web app
This commit is contained in:
parent
c30db6b694
commit
3e5d180699
19 changed files with 2830 additions and 210 deletions
24
apps/web/.gitignore
vendored
Normal file
24
apps/web/.gitignore
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
|
||||
dist
|
||||
.solid
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
netlify
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
*.launch
|
||||
.settings/
|
||||
|
||||
# Temp
|
||||
gitignore
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
30
apps/web/README.md
Normal file
30
apps/web/README.md
Normal file
|
@ -0,0 +1,30 @@
|
|||
# SolidStart
|
||||
|
||||
Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com);
|
||||
|
||||
## Creating a project
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm init solid@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm init solid@latest my-app
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Solid apps are built with _adapters_, which optimise your project for deployment to different environments.
|
||||
|
||||
By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different adapter, add it to the `devDependencies` in `package.json` and specify in your `vite.config.js`.
|
35
apps/web/package.json
Normal file
35
apps/web/package.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@yume-chan/tango-web",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "solid-start dev",
|
||||
"build": "solid-start build",
|
||||
"start": "solid-start start"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.2.1",
|
||||
"esbuild": "^0.14.54",
|
||||
"postcss": "^8.4.21",
|
||||
"solid-start-node": "^0.2.19",
|
||||
"typescript": "^5.0.3",
|
||||
"vite": "^4.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solidjs/meta": "^0.28.2",
|
||||
"@solidjs/router": "^0.8.2",
|
||||
"@solid-devtools/overlay": "^0.6.0",
|
||||
"@yume-chan/adb": "workspace:^0.0.19",
|
||||
"@yume-chan/adb-credential-web": "workspace:^0.0.19",
|
||||
"@yume-chan/adb-daemon-webusb": "workspace:^0.0.19",
|
||||
"@yume-chan/async": "^2.2.0",
|
||||
"@yume-chan/stream-extra": "workspace:^0.0.19",
|
||||
"@yume-chan/struct": "workspace:^0.0.19",
|
||||
"solid-js": "^1.7.2",
|
||||
"solid-start": "^0.2.26",
|
||||
"undici": "^5.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8"
|
||||
}
|
||||
}
|
BIN
apps/web/public/favicon.ico
Normal file
BIN
apps/web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 664 B |
64
apps/web/public/logo.hlsl
Normal file
64
apps/web/public/logo.hlsl
Normal file
|
@ -0,0 +1,64 @@
|
|||
vec2 project(vec2 a, vec2 b) {
|
||||
return a * dot(a, b) / dot(a, a);
|
||||
}
|
||||
|
||||
vec4 drawCircle(vec2 coord, vec2 c, float r, float rr, float as, float ae, vec3 color1, vec3 color2) {
|
||||
vec2 delta = coord - c;
|
||||
|
||||
float d = length(delta);
|
||||
if (d < r - 15.0) {
|
||||
return vec4(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
float a = atan(delta.y, delta.x) + 3.14;
|
||||
if (as < ae ? a > as && a < ae : a > as || a < ae) {
|
||||
if (d < r) {
|
||||
return vec4(0, 0, 0, 1);
|
||||
} else if (d < rr) {
|
||||
float crossLength = sqrt(pow(rr, 2.0) * 2.0);
|
||||
float p = length(project(vec2(-1,1),delta)-(vec2(-1,1)*crossLength)) / (crossLength * 2.0);
|
||||
return vec4(mix(color1, color2, p), 1);
|
||||
} else if (d < rr + 15.0) {
|
||||
return vec4(0, 0, 0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return vec4(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
vec3 parseColor(int color) {
|
||||
return vec3(float((color >> 16) & 0xff) / 255.0, float((color >> 8) & 0xff) / 255.0, float((color >> 0) & 0xff) / 255.0);
|
||||
}
|
||||
|
||||
void mainImage( out vec4 fragColor, in vec2 fragCoord )
|
||||
{
|
||||
vec2 c1 = vec2(0.6, 0.5) * iResolution.xy;
|
||||
float r1 = 0.3 * iResolution.y;
|
||||
float rr1 = 0.2 * iResolution.y;
|
||||
float as1 = 225.0 / 180.0 * 3.14;
|
||||
float ae1 = 135.0 / 180.0 * 3.14;
|
||||
vec2 c2 = vec2(0.45, 0.5) * iResolution.xy;
|
||||
float r2 = 0.3 * iResolution.y;
|
||||
float rr2 = 0.2 * iResolution.y;
|
||||
float as2 = 315.0 / 180.0 * 3.14;
|
||||
float ae2 = 270.0 / 180.0 * 3.14;
|
||||
float w = 11.0;
|
||||
vec3 color11= parseColor(0xD9D9D9);
|
||||
vec3 color12= parseColor(0x898989);
|
||||
vec3 color21 = parseColor(0x2D6AF6);
|
||||
vec3 color22 = parseColor(0xA3BFFF);
|
||||
|
||||
vec4 color;
|
||||
if (fragCoord.y < iResolution.y / 2.0) {
|
||||
color = drawCircle(fragCoord, c1, rr1, r1, as1, ae1, color11, color12);
|
||||
if (color.a == 0.0) {
|
||||
color = drawCircle(fragCoord, c2, rr2, r2, as2, ae2, color21, color22);
|
||||
}
|
||||
} else {
|
||||
color = drawCircle(fragCoord, c2, rr2, r2, as2, ae2, color21, color22);
|
||||
if (color.a == 0.0) {
|
||||
color = drawCircle(fragCoord, c1, rr1, r1, as1, ae1, color11, color12);
|
||||
}
|
||||
}
|
||||
fragColor = color;
|
||||
}
|
20
apps/web/src/components/Counter.css
Normal file
20
apps/web/src/components/Counter.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.increment {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
padding: 1em 2em;
|
||||
color: #335d92;
|
||||
background-color: rgba(68, 107, 158, 0.1);
|
||||
border-radius: 2em;
|
||||
border: 2px solid rgba(68, 107, 158, 0);
|
||||
outline: none;
|
||||
width: 200px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.increment:focus {
|
||||
border: 2px solid #335d92;
|
||||
}
|
||||
|
||||
.increment:active {
|
||||
background-color: rgba(68, 107, 158, 0.2);
|
||||
}
|
11
apps/web/src/components/Counter.tsx
Normal file
11
apps/web/src/components/Counter.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createSignal } from "solid-js";
|
||||
import "./Counter.css";
|
||||
|
||||
export default function Counter() {
|
||||
const [count, setCount] = createSignal(0);
|
||||
return (
|
||||
<button class="increment" onClick={() => setCount(count() + 1)}>
|
||||
Clicks: {count()}
|
||||
</button>
|
||||
);
|
||||
}
|
353
apps/web/src/components/worker.ts
Normal file
353
apps/web/src/components/worker.ts
Normal file
|
@ -0,0 +1,353 @@
|
|||
import {
|
||||
AdbDaemonTransport,
|
||||
AdbPacketData,
|
||||
AdbPacketInit,
|
||||
AdbSocket,
|
||||
} from "@yume-chan/adb";
|
||||
import AdbWebCredentialStore from "@yume-chan/adb-credential-web";
|
||||
import { PromiseResolver } from "@yume-chan/async";
|
||||
import {
|
||||
Consumable,
|
||||
ConsumableWritableStream,
|
||||
PushReadableStream,
|
||||
PushReadableStreamController,
|
||||
ReadableWritablePair,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
|
||||
const CredentialStore = new AdbWebCredentialStore();
|
||||
|
||||
const transports = new Map<string, AdbDaemonTransport>();
|
||||
|
||||
class RetryError extends Error {
|
||||
public constructor() {
|
||||
super("Retry");
|
||||
}
|
||||
}
|
||||
|
||||
class SharedWorkerDaemonConnection
|
||||
implements ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>
|
||||
{
|
||||
#serial: string;
|
||||
get serial() {
|
||||
return this.#serial;
|
||||
}
|
||||
|
||||
#port = new PromiseResolver<MessagePort>();
|
||||
|
||||
#readable: PushReadableStream<AdbPacketData>;
|
||||
#readableController!: PushReadableStreamController<AdbPacketData>;
|
||||
get readable() {
|
||||
return this.#readable;
|
||||
}
|
||||
|
||||
#writable: WritableStream<Consumable<AdbPacketInit>>;
|
||||
#writePromise: PromiseResolver<void> | undefined;
|
||||
get writable() {
|
||||
return this.#writable;
|
||||
}
|
||||
|
||||
public constructor(serial: string) {
|
||||
this.#serial = serial;
|
||||
this.#readable = new PushReadableStream((controller) => {
|
||||
this.#readableController = controller;
|
||||
});
|
||||
this.#writable = new WritableStream({
|
||||
write: async (chunk) => {
|
||||
console.log("out begin", chunk);
|
||||
while (true) {
|
||||
try {
|
||||
this.#writePromise = new PromiseResolver();
|
||||
const port = await this.#port.promise;
|
||||
console.log("out port", port);
|
||||
port.postMessage({
|
||||
type: "data",
|
||||
payload: chunk.value,
|
||||
});
|
||||
await this.#writePromise.promise;
|
||||
this.#writePromise = undefined;
|
||||
chunk.consume();
|
||||
console.log("out finish");
|
||||
return;
|
||||
} catch (e) {
|
||||
if (e instanceof RetryError) {
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public attach(port: MessagePort) {
|
||||
this.#port.resolve(port);
|
||||
port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| { type: "data"; payload: AdbPacketData }
|
||||
| { type: "ack" }
|
||||
| { type: "close" };
|
||||
switch (message.type) {
|
||||
case "data":
|
||||
console.log("in", message.payload);
|
||||
await this.#readableController.enqueue(message.payload);
|
||||
port.postMessage({ type: "ack" });
|
||||
break;
|
||||
case "ack":
|
||||
this.#writePromise!.resolve();
|
||||
break;
|
||||
case "close":
|
||||
this.#readableController.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public detach() {
|
||||
this.#port = new PromiseResolver();
|
||||
this.#writePromise?.reject(new RetryError());
|
||||
}
|
||||
}
|
||||
|
||||
class SharedWorkerSocketOwner {
|
||||
#port: MessagePort;
|
||||
|
||||
#socket: AdbSocket;
|
||||
#writer: WritableStreamDefaultWriter<Consumable<Uint8Array>>;
|
||||
#readAbortController = new AbortController();
|
||||
#pendingAck: PromiseResolver<void> | undefined;
|
||||
|
||||
constructor(port: MessagePort, socket: AdbSocket) {
|
||||
this.#port = port;
|
||||
this.#socket = socket;
|
||||
this.#writer = socket.writable.getWriter();
|
||||
|
||||
socket.readable
|
||||
.pipeTo(
|
||||
new WritableStream({
|
||||
write: async (chunk) => {
|
||||
this.#pendingAck = new PromiseResolver();
|
||||
console.log("socket in write", socket.service, chunk);
|
||||
port.postMessage({ type: "data", payload: chunk });
|
||||
await this.#pendingAck.promise;
|
||||
this.#pendingAck = undefined;
|
||||
console.log("socket in write done");
|
||||
},
|
||||
close: () => {
|
||||
console.log("socket in close", socket.service);
|
||||
port.postMessage({ type: "close" });
|
||||
},
|
||||
}),
|
||||
{
|
||||
signal: this.#readAbortController.signal,
|
||||
}
|
||||
)
|
||||
.catch((e) => {
|
||||
if (this.#readAbortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
|
||||
port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| {
|
||||
type: "data";
|
||||
payload: Uint8Array;
|
||||
}
|
||||
| { type: "ack" }
|
||||
| { type: "close" };
|
||||
switch (message.type) {
|
||||
case "data":
|
||||
await this.write(message.payload);
|
||||
break;
|
||||
case "ack":
|
||||
this.ack();
|
||||
break;
|
||||
case "close":
|
||||
this.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async write(payload: Uint8Array) {
|
||||
await ConsumableWritableStream.write(this.#writer, payload);
|
||||
this.#port.postMessage({ type: "ack" });
|
||||
}
|
||||
|
||||
public ack() {
|
||||
this.#pendingAck!.resolve();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.#writer.releaseLock();
|
||||
this.#socket.close();
|
||||
this.#port.close();
|
||||
}
|
||||
}
|
||||
|
||||
class SharedWorkerTransportOwner {
|
||||
#port: MessagePort;
|
||||
|
||||
#transport: AdbDaemonTransport;
|
||||
|
||||
constructor(port: MessagePort, transport: AdbDaemonTransport) {
|
||||
this.#port = port;
|
||||
this.#transport = transport;
|
||||
|
||||
transport.disconnected.then(() => {
|
||||
port.postMessage({ type: "close" });
|
||||
});
|
||||
|
||||
port.onmessage = async (event) => {
|
||||
const message = event.data as {
|
||||
type: "connect";
|
||||
id: number;
|
||||
service: string;
|
||||
};
|
||||
switch (message.type) {
|
||||
case "connect":
|
||||
await this.connect(message.id, message.service);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async connect(id: number, service: string) {
|
||||
try {
|
||||
const socket = await this.#transport.connect(service);
|
||||
const channel = new MessageChannel();
|
||||
const server = new SharedWorkerSocketOwner(channel.port2, socket);
|
||||
this.#port.postMessage(
|
||||
{
|
||||
type: "connect",
|
||||
id: id,
|
||||
result: true,
|
||||
port: channel.port1,
|
||||
},
|
||||
[channel.port1]
|
||||
);
|
||||
} catch {
|
||||
this.#port.postMessage({
|
||||
type: "connect",
|
||||
id: id,
|
||||
result: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare interface SharedWorkerGlobalScope {
|
||||
onconnect: (e: MessageEvent) => void;
|
||||
}
|
||||
|
||||
const clientToConnections = new Map<
|
||||
MessagePort,
|
||||
Set<SharedWorkerDaemonConnection>
|
||||
>();
|
||||
const serialToConnection = new Map<string, SharedWorkerDaemonConnection>();
|
||||
|
||||
async function connect(port: MessagePort, serial: string) {
|
||||
const channel = new MessageChannel();
|
||||
|
||||
const messageResolver = new PromiseResolver<{
|
||||
type: "connect";
|
||||
result: boolean;
|
||||
}>();
|
||||
channel.port2.onmessage = async (event) => {
|
||||
messageResolver.resolve(event.data);
|
||||
};
|
||||
|
||||
port.postMessage({ type: "connect", serial, port: channel.port1 }, [
|
||||
channel.port1,
|
||||
]);
|
||||
|
||||
const message = await messageResolver.promise;
|
||||
switch (message.type) {
|
||||
case "connect":
|
||||
if (!message.result) {
|
||||
throw new Error("Failed to connect");
|
||||
}
|
||||
|
||||
if (serialToConnection.has(serial)) {
|
||||
const connection = serialToConnection.get(serial)!;
|
||||
console.log("switch", connection, "to", port);
|
||||
connection.attach(channel.port2);
|
||||
clientToConnections.get(port)!.add(connection);
|
||||
} else {
|
||||
const connection = new SharedWorkerDaemonConnection(serial);
|
||||
connection.attach(channel.port2);
|
||||
serialToConnection.set(serial, connection);
|
||||
clientToConnections.get(port)!.add(connection);
|
||||
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
serial,
|
||||
connection,
|
||||
credentialStore: CredentialStore,
|
||||
});
|
||||
transports.set(transport.serial, transport);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unknown message type");
|
||||
}
|
||||
}
|
||||
|
||||
(globalThis as unknown as SharedWorkerGlobalScope).onconnect = (e) => {
|
||||
const port = e.ports[0]!;
|
||||
clientToConnections.set(port, new Set());
|
||||
|
||||
port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| {
|
||||
type: "query";
|
||||
id: number;
|
||||
serial: string;
|
||||
}
|
||||
| { type: "disconnect" };
|
||||
switch (message.type) {
|
||||
case "query":
|
||||
if (!transports.has(message.serial)) {
|
||||
await connect(port, message.serial);
|
||||
}
|
||||
|
||||
const transport = transports.get(message.serial)!;
|
||||
const channel = new MessageChannel();
|
||||
port.postMessage(
|
||||
{
|
||||
type: "query-success",
|
||||
id: message.id,
|
||||
serial: message.serial,
|
||||
product: transport.banner.product,
|
||||
model: transport.banner.model,
|
||||
device: transport.banner.device,
|
||||
features: transport.banner.features,
|
||||
maxPayloadSize: transport.maxPayloadSize,
|
||||
port: channel.port1,
|
||||
},
|
||||
[channel.port1]
|
||||
);
|
||||
new SharedWorkerTransportOwner(channel.port2, transport);
|
||||
break;
|
||||
case "disconnect":
|
||||
for (const connection of clientToConnections.get(port)!) {
|
||||
connection.detach();
|
||||
}
|
||||
let nextClient: MessagePort;
|
||||
for (const client of clientToConnections.keys()) {
|
||||
if (client !== port) {
|
||||
nextClient = client;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
Array.from(clientToConnections.get(port)!, (connection) =>
|
||||
connect(nextClient, connection.serial)
|
||||
)
|
||||
);
|
||||
clientToConnections.delete(port);
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
3
apps/web/src/entry-client.tsx
Normal file
3
apps/web/src/entry-client.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { mount, StartClient } from "solid-start/entry-client";
|
||||
|
||||
mount(() => <StartClient />, document);
|
9
apps/web/src/entry-server.tsx
Normal file
9
apps/web/src/entry-server.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import {
|
||||
createHandler,
|
||||
renderAsync,
|
||||
StartServer,
|
||||
} from "solid-start/entry-server";
|
||||
|
||||
export default createHandler(
|
||||
renderAsync((event) => <StartServer event={event} />)
|
||||
);
|
40
apps/web/src/root.css
Normal file
40
apps/web/src/root.css
Normal file
|
@ -0,0 +1,40 @@
|
|||
body {
|
||||
font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
main {
|
||||
text-align: center;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #335d92;
|
||||
text-transform: uppercase;
|
||||
font-size: 4rem;
|
||||
font-weight: 100;
|
||||
line-height: 1.1;
|
||||
margin: 4rem auto;
|
||||
max-width: 14rem;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 14rem;
|
||||
margin: 2rem auto;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
h1 {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
42
apps/web/src/root.tsx
Normal file
42
apps/web/src/root.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
// @refresh reload
|
||||
import { Suspense } from "solid-js";
|
||||
import {
|
||||
A,
|
||||
Body,
|
||||
ErrorBoundary,
|
||||
FileRoutes,
|
||||
Head,
|
||||
Html,
|
||||
Meta,
|
||||
Routes,
|
||||
Scripts,
|
||||
Title,
|
||||
} from "solid-start";
|
||||
import "./root.css";
|
||||
|
||||
export default function Root() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<Title>SolidStart - Bare</Title>
|
||||
<Meta charset="utf-8" />
|
||||
<Meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1"
|
||||
/>
|
||||
</Head>
|
||||
<Body>
|
||||
<Suspense>
|
||||
<ErrorBoundary>
|
||||
<A href="/">Index</A>
|
||||
<A href="/about">About</A>
|
||||
<Routes>
|
||||
<FileRoutes />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
<Scripts />
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
19
apps/web/src/routes/[...404].tsx
Normal file
19
apps/web/src/routes/[...404].tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Title } from "solid-start";
|
||||
import { HttpStatusCode } from "solid-start/server";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main>
|
||||
<Title>Not Found</Title>
|
||||
<HttpStatusCode code={404} />
|
||||
<h1>Page Not Found</h1>
|
||||
<p>
|
||||
Visit{" "}
|
||||
<a href="https://start.solidjs.com" target="_blank">
|
||||
start.solidjs.com
|
||||
</a>{" "}
|
||||
to learn how to build SolidStart apps.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
441
apps/web/src/routes/index.tsx
Normal file
441
apps/web/src/routes/index.tsx
Normal file
|
@ -0,0 +1,441 @@
|
|||
import {
|
||||
Adb,
|
||||
AdbBanner,
|
||||
AdbFeature,
|
||||
AdbIncomingSocketHandler,
|
||||
AdbPacketData,
|
||||
AdbPacketInit,
|
||||
AdbSocket,
|
||||
AdbTransport,
|
||||
} from "@yume-chan/adb";
|
||||
import {
|
||||
ADB_DEFAULT_DEVICE_FILTER,
|
||||
AdbDaemonWebUsbDeviceManager,
|
||||
} from "@yume-chan/adb-daemon-webusb";
|
||||
import { AsyncOperationManager, PromiseResolver } from "@yume-chan/async";
|
||||
import {
|
||||
Consumable,
|
||||
ConsumableWritableStream,
|
||||
PushReadableStream,
|
||||
PushReadableStreamController,
|
||||
WritableStream,
|
||||
} from "@yume-chan/stream-extra";
|
||||
import { createSignal } from "solid-js";
|
||||
import { Title } from "solid-start";
|
||||
|
||||
class SharedWorkerSocket implements AdbSocket {
|
||||
#port: MessagePort;
|
||||
|
||||
#service: string;
|
||||
get service() {
|
||||
return this.#service;
|
||||
}
|
||||
|
||||
#readable: PushReadableStream<Uint8Array>;
|
||||
#readableController!: PushReadableStreamController<Uint8Array>;
|
||||
get readable() {
|
||||
return this.#readable;
|
||||
}
|
||||
|
||||
#writable: WritableStream<Consumable<Uint8Array>>;
|
||||
#pendingWrite: PromiseResolver<void> | undefined;
|
||||
get writable() {
|
||||
return this.#writable;
|
||||
}
|
||||
|
||||
public constructor(port: MessagePort, service: string) {
|
||||
this.#port = port;
|
||||
this.#service = service;
|
||||
|
||||
this.#readable = new PushReadableStream((controller) => {
|
||||
this.#readableController = controller;
|
||||
});
|
||||
this.#writable = new WritableStream({
|
||||
write: async (chunk) => {
|
||||
this.#pendingWrite = new PromiseResolver();
|
||||
port.postMessage({
|
||||
type: "data",
|
||||
payload: chunk.value,
|
||||
});
|
||||
await this.#pendingWrite.promise;
|
||||
this.#pendingWrite = undefined;
|
||||
chunk.consume();
|
||||
},
|
||||
});
|
||||
|
||||
this.#port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| {
|
||||
type: "data";
|
||||
payload: Uint8Array;
|
||||
}
|
||||
| { type: "ack" }
|
||||
| { type: "close" };
|
||||
switch (message.type) {
|
||||
case "data":
|
||||
console.log("socket in", this.#service, message.payload);
|
||||
await this.#readableController.enqueue(message.payload);
|
||||
port.postMessage({ type: "ack" });
|
||||
break;
|
||||
case "ack":
|
||||
this.#pendingWrite!.resolve();
|
||||
break;
|
||||
case "close":
|
||||
this.#readableController.close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.#port.postMessage({
|
||||
type: "close",
|
||||
});
|
||||
this.#port.close();
|
||||
}
|
||||
}
|
||||
|
||||
class SharedWorkerTransport implements AdbTransport {
|
||||
#serial: string;
|
||||
get serial() {
|
||||
return this.#serial;
|
||||
}
|
||||
|
||||
#maxPayloadSize: number;
|
||||
get maxPayloadSize() {
|
||||
return this.#maxPayloadSize;
|
||||
}
|
||||
|
||||
#banner: AdbBanner;
|
||||
get banner() {
|
||||
return this.#banner;
|
||||
}
|
||||
|
||||
#port: MessagePort;
|
||||
#operations = new AsyncOperationManager();
|
||||
|
||||
#reverseTunnels = new Map<string, AdbIncomingSocketHandler>();
|
||||
|
||||
#disconnected = new PromiseResolver<void>();
|
||||
get disconnected() {
|
||||
return this.#disconnected.promise;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
serial: string,
|
||||
maxPayloadSize: number,
|
||||
banner: AdbBanner,
|
||||
port: MessagePort
|
||||
) {
|
||||
this.#serial = serial;
|
||||
this.#maxPayloadSize = maxPayloadSize;
|
||||
this.#banner = banner;
|
||||
this.#port = port;
|
||||
this.#port.onmessage = async (event) => {
|
||||
const message = event.data;
|
||||
switch (message.type) {
|
||||
case "connect":
|
||||
if (message.result) {
|
||||
this.#operations.resolve(message.id, message.port);
|
||||
} else {
|
||||
this.#operations.reject(
|
||||
message.id,
|
||||
new Error("failed to connect")
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "reverse-tunnel":
|
||||
{
|
||||
const handler = this.#reverseTunnels.get(
|
||||
message.address
|
||||
);
|
||||
if (!handler) {
|
||||
break;
|
||||
}
|
||||
const socket = new SharedWorkerSocket(
|
||||
message.port,
|
||||
message.address
|
||||
);
|
||||
await handler(socket);
|
||||
}
|
||||
break;
|
||||
case "add-reverse-tunnel":
|
||||
if (message.result) {
|
||||
this.#operations.resolve(message.id, message.address);
|
||||
} else {
|
||||
this.#operations.reject(
|
||||
message.id,
|
||||
new Error(message.error)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "remove-reverse-tunnel":
|
||||
case "clear-reverse-tunnels":
|
||||
if (message.result) {
|
||||
this.#operations.resolve(message.id, undefined);
|
||||
} else {
|
||||
this.#operations.reject(
|
||||
message.id,
|
||||
new Error(message.error)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "close":
|
||||
this.#disconnected.resolve();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public async connect(service: string): Promise<AdbSocket> {
|
||||
const [id, promise] = this.#operations.add<MessagePort>();
|
||||
this.#port.postMessage({
|
||||
type: "connect",
|
||||
id,
|
||||
service,
|
||||
});
|
||||
const port = await promise;
|
||||
return new SharedWorkerSocket(port, service);
|
||||
}
|
||||
|
||||
public async addReverseTunnel(
|
||||
handler: AdbIncomingSocketHandler,
|
||||
address?: string
|
||||
): Promise<string> {
|
||||
const [id, promise] = this.#operations.add<string>();
|
||||
this.#port.postMessage({
|
||||
type: "add-reverse-tunnel",
|
||||
id,
|
||||
address,
|
||||
});
|
||||
address = await promise;
|
||||
this.#reverseTunnels.set(address, handler);
|
||||
return address;
|
||||
}
|
||||
|
||||
public async removeReverseTunnel(address: string): Promise<void> {
|
||||
const [id, promise] = this.#operations.add<void>();
|
||||
this.#port.postMessage({
|
||||
type: "remove-reverse-tunnel",
|
||||
id,
|
||||
address,
|
||||
});
|
||||
await promise;
|
||||
this.#reverseTunnels.delete(address);
|
||||
}
|
||||
|
||||
public async clearReverseTunnels(): Promise<void> {
|
||||
const [id, promise] = this.#operations.add<void>();
|
||||
this.#port.postMessage({
|
||||
type: "clear-reverse-tunnels",
|
||||
id,
|
||||
});
|
||||
await promise;
|
||||
this.#reverseTunnels.clear();
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.#port.postMessage({
|
||||
type: "close",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class SharedWorkerDaemonConnectionOwner {
|
||||
#port: MessagePort;
|
||||
#connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>>;
|
||||
#writer: WritableStreamDefaultWriter<Consumable<AdbPacketInit>>;
|
||||
#pendingWrite: PromiseResolver<void> | undefined;
|
||||
|
||||
constructor(
|
||||
port: MessagePort,
|
||||
connection: ReadableWritablePair<
|
||||
AdbPacketData,
|
||||
Consumable<AdbPacketInit>
|
||||
>
|
||||
) {
|
||||
this.#port = port;
|
||||
this.#connection = connection;
|
||||
this.#writer = connection.writable.getWriter();
|
||||
|
||||
this.#connection.readable.pipeTo(
|
||||
new WritableStream<AdbPacketData>({
|
||||
write: async (chunk) => {
|
||||
console.log("connection in", chunk);
|
||||
this.#pendingWrite = new PromiseResolver();
|
||||
this.#port.postMessage({
|
||||
type: "data",
|
||||
payload: chunk,
|
||||
});
|
||||
await this.#pendingWrite.promise;
|
||||
this.#pendingWrite = undefined;
|
||||
},
|
||||
close: () => {
|
||||
this.#port.postMessage({
|
||||
type: "close",
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.#port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| { type: "data"; payload: AdbPacketInit }
|
||||
| { type: "ack" };
|
||||
switch (message.type) {
|
||||
case "data":
|
||||
console.log("connection out", message.payload);
|
||||
await ConsumableWritableStream.write(
|
||||
this.#writer,
|
||||
message.payload
|
||||
);
|
||||
this.#port.postMessage({
|
||||
type: "ack",
|
||||
});
|
||||
break;
|
||||
case "ack":
|
||||
this.#pendingWrite!.resolve();
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [adb, setAdb] = createSignal<Adb>();
|
||||
|
||||
let operations = new AsyncOperationManager();
|
||||
let port!: MessagePort;
|
||||
if (typeof window !== "undefined") {
|
||||
const worker = new SharedWorker(
|
||||
new URL("../components/worker.ts", import.meta.url),
|
||||
{
|
||||
type: "module",
|
||||
}
|
||||
);
|
||||
port = worker.port;
|
||||
port.onmessage = async (event) => {
|
||||
const message = event.data as
|
||||
| {
|
||||
type: "query-success";
|
||||
id: number;
|
||||
serial: string;
|
||||
product: string | undefined;
|
||||
model: string | undefined;
|
||||
device: string | undefined;
|
||||
features: AdbFeature[];
|
||||
maxPayloadSize: number;
|
||||
port: MessagePort;
|
||||
}
|
||||
| { type: "query-error"; id: number; error: string }
|
||||
| {
|
||||
type: "connect";
|
||||
serial: string;
|
||||
port: MessagePort;
|
||||
};
|
||||
switch (message.type) {
|
||||
case "query-success":
|
||||
operations.resolve(
|
||||
message.id,
|
||||
new SharedWorkerTransport(
|
||||
message.serial,
|
||||
message.maxPayloadSize,
|
||||
new AdbBanner(
|
||||
message.product,
|
||||
message.model,
|
||||
message.device,
|
||||
message.features
|
||||
),
|
||||
message.port
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "query-error":
|
||||
operations.reject(message.id, new Error(message.error));
|
||||
break;
|
||||
case "connect":
|
||||
{
|
||||
const [device] =
|
||||
await AdbDaemonWebUsbDeviceManager.BROWSER!.getDevices(
|
||||
[
|
||||
{
|
||||
...ADB_DEFAULT_DEVICE_FILTER,
|
||||
serialNumber: message.serial,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (!device) {
|
||||
message.port.postMessage({
|
||||
type: "connect",
|
||||
result: false,
|
||||
});
|
||||
message.port.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const connection = await device.connect();
|
||||
new SharedWorkerDaemonConnectionOwner(
|
||||
message.port,
|
||||
connection
|
||||
);
|
||||
message.port.postMessage({
|
||||
type: "connect",
|
||||
result: true,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", () => {
|
||||
port.postMessage({
|
||||
type: "disconnect",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const handleClick = async () => {
|
||||
const device =
|
||||
await AdbDaemonWebUsbDeviceManager.BROWSER!.requestDevice();
|
||||
if (!device) {
|
||||
return;
|
||||
}
|
||||
const [id, promise] = operations.add<SharedWorkerTransport>();
|
||||
port.postMessage({
|
||||
type: "query",
|
||||
id,
|
||||
serial: device.serial,
|
||||
});
|
||||
const transport = await promise;
|
||||
const adb = new Adb(transport);
|
||||
setAdb(adb);
|
||||
|
||||
setInterval(async () => {
|
||||
const model = await adb.getProp("ro.product.model");
|
||||
console.log("model:", model);
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
const _adb = adb();
|
||||
if (!_adb) {
|
||||
return;
|
||||
}
|
||||
await _adb.close();
|
||||
setAdb(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Title>Tango</Title>
|
||||
<div>{adb() ? "connected" : "disconnected"}</div>
|
||||
{adb() ? (
|
||||
<>
|
||||
<button onClick={handleDisconnect}>Disconnect</button>
|
||||
</>
|
||||
) : (
|
||||
<button onClick={handleClick}>Connect</button>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
17
apps/web/tsconfig.json
Normal file
17
apps/web/tsconfig.json
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"jsxImportSource": "solid-js",
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"types": ["solid-start/env"],
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
6
apps/web/vite.config.ts
Normal file
6
apps/web/vite.config.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import solid from "solid-start/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
});
|
1920
common/config/rush/pnpm-lock.yaml
generated
1920
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||
{
|
||||
"pnpmShrinkwrapHash": "c4be1a3333837f03459388b246f781f6e6d5938e",
|
||||
"pnpmShrinkwrapHash": "a14edaaec87943bdc5bdf6114640f71dcf626d70",
|
||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||
}
|
||||
|
|
|
@ -502,6 +502,10 @@
|
|||
{
|
||||
"packageName": "@yume-chan/adb-cli",
|
||||
"projectFolder": "apps/cli"
|
||||
},
|
||||
{
|
||||
"packageName": "@yume-chan/tango-web",
|
||||
"projectFolder": "apps/web"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue