Update Electron
This commit is contained in:
parent
cae6d582ac
commit
c84ee948f5
22 changed files with 763 additions and 152 deletions
|
|
@ -1,62 +1,120 @@
|
||||||
import { type BrowserWindow } from 'electron/main'
|
import { type BrowserWindow } from 'electron/main'
|
||||||
import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp'
|
import type { AdbPacketData, AdbPacketInit } from '@yume-chan/adb'
|
||||||
import type { AdbServerDevice, AdbServerTransport } from '@yume-chan/adb'
|
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||||
import { Adb, AdbServerClient } from '@yume-chan/adb'
|
import type { Consumable, ReadableWritablePair } from '@yume-chan/stream-extra'
|
||||||
import { DecodeUtf8Stream, WritableStream } from '@yume-chan/stream-extra'
|
import { WritableStream } from '@yume-chan/stream-extra'
|
||||||
|
import { AdbDaemonDirectSocketsDevice } from './adbaemonDirectSocketsDevice'
|
||||||
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||||
|
|
||||||
let adb: Adb | undefined
|
export function AdbTcpSocket(win: BrowserWindow | null, ipcMain: Electron.IpcMain) {
|
||||||
|
if (!win) return
|
||||||
|
|
||||||
export function AdbDaemon(_win: BrowserWindow | null, ipcMain: Electron.IpcMain) {
|
const adbConnectionList: { [key: string]: Adb } = {}
|
||||||
ipcMain.handle('adb', async () => {
|
|
||||||
await createConnection()
|
ipcMain.handle('adb:tcp:socket:getprop', async (_event, serial: string, key: string) => {
|
||||||
|
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||||
|
return adbConnectionList[serial].getProp(key)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.handle('adb:shell', async (_event, command: string) => {
|
ipcMain.handle('adb:tcp:socket:getDevices', () => {
|
||||||
if (!adb) {
|
return Object.keys(adbConnectionList).map(serial => {
|
||||||
return
|
return {
|
||||||
|
name: adbConnectionList[serial].banner.product,
|
||||||
|
serial: serial
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('adb:tcp:socket:connect', async (_event, { host, port }: { host: string; port: number }) => {
|
||||||
|
const device: AdbDaemonDirectSocketsDevice = new AdbDaemonDirectSocketsDevice({
|
||||||
|
host,
|
||||||
|
port
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Object.keys(adbConnectionList).includes(device.serial)) {
|
||||||
|
return Promise.resolve({ name: device.name, serial: device.serial })
|
||||||
}
|
}
|
||||||
|
|
||||||
const process = await adb.subprocess.shell(command)
|
try {
|
||||||
let result: string | null = null
|
const connection: ReadableWritablePair<AdbPacketData, Consumable<AdbPacketInit>> = await device.connect()
|
||||||
await process.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
|
||||||
new WritableStream({
|
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||||
write(chunk) {
|
|
||||||
result += chunk
|
const transport = await AdbDaemonTransport.authenticate({
|
||||||
}
|
serial: device.serial,
|
||||||
|
connection: connection,
|
||||||
|
credentialStore: credentialStore
|
||||||
})
|
})
|
||||||
)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createConnection() {
|
adbConnectionList[device.serial] = new Adb(transport)
|
||||||
const connector: AdbServerNodeTcpConnector = new AdbServerNodeTcpConnector({
|
|
||||||
host: 'localhost',
|
const productName = await adbConnectionList[device.serial].getProp('ro.product.name')
|
||||||
port: 5037
|
return { name: productName, serial: device.serial }
|
||||||
|
} catch (e) {
|
||||||
|
return Promise.reject(e)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Connecting to ADB server...')
|
ipcMain.handle(
|
||||||
connector.connect()
|
'adb:tcp:socket:shell',
|
||||||
|
async (_event, { serial, command, callbackId }: { serial: string; command: string; callbackId: string }) => {
|
||||||
|
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||||
|
|
||||||
const client: AdbServerClient = new AdbServerClient(connector)
|
const process = await adbConnectionList[serial].subprocess.shell(command)
|
||||||
|
|
||||||
const devices: AdbServerDevice[] = await client.getDevices()
|
await process.stdout
|
||||||
if (devices.length === 0) {
|
.pipeTo(
|
||||||
console.log('No device found')
|
new WritableStream({
|
||||||
return
|
write(chunk) {
|
||||||
}
|
win.webContents.send(callbackId, chunk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
process.stdin.close()
|
||||||
|
process.stderr.cancel()
|
||||||
|
process.stdout.cancel()
|
||||||
|
process.kill()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
console.log('Devices found:', devices.map(device => device.serial).join(', '))
|
ipcMain.handle(
|
||||||
|
'adb:tcp:socket:spawn',
|
||||||
|
async (_event, { serial, command, callbackId }: { serial: string; command: string; callbackId: string }) => {
|
||||||
|
if (!adbConnectionList[serial]) return Promise.reject('adb is not connected')
|
||||||
|
|
||||||
const device: AdbServerDevice | undefined = devices.find(device => device.serial === 'd')
|
const process = await adbConnectionList[serial].subprocess.spawn(command)
|
||||||
|
|
||||||
if (!device) {
|
let buffer: Uint8Array = new Uint8Array()
|
||||||
console.log('No device found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Device found:', device.serial)
|
const reader = process.stdout.getReader()
|
||||||
|
|
||||||
const transport: AdbServerTransport = await client.createTransport(device)
|
// eslint-disable-next-line no-constant-condition
|
||||||
adb = new Adb(transport)
|
while (true) {
|
||||||
|
const { value, done } = await reader.read()
|
||||||
|
if (done) break
|
||||||
|
const oldBuffer = buffer
|
||||||
|
buffer = new Uint8Array(value.length + buffer.length)
|
||||||
|
buffer.set(oldBuffer)
|
||||||
|
buffer.set(value, oldBuffer.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.releaseLock()
|
||||||
|
await reader.cancel()
|
||||||
|
|
||||||
|
await process.stdin.close()
|
||||||
|
await process.stderr.cancel()
|
||||||
|
await process.stdout.cancel()
|
||||||
|
|
||||||
|
await process.kill()
|
||||||
|
|
||||||
|
win.webContents.send(callbackId, buffer)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ipcMain.handle('adb:tcp:socket:disconnect', async (_event, serial: string) => {
|
||||||
|
await adbConnectionList[serial].close()
|
||||||
|
delete adbConnectionList[serial]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
57
client-electron/electron/adbaemonDirectSocketsDevice.ts
Normal file
57
client-electron/electron/adbaemonDirectSocketsDevice.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import type { AdbDaemonDevice } from '@yume-chan/adb'
|
||||||
|
import { AdbPacket, AdbPacketSerializeStream } from '@yume-chan/adb'
|
||||||
|
import {
|
||||||
|
StructDeserializeStream,
|
||||||
|
UnwrapConsumableStream,
|
||||||
|
WrapReadableStream,
|
||||||
|
WrapWritableStream
|
||||||
|
} from '@yume-chan/stream-extra'
|
||||||
|
import { TCPSocket } from './socketToTCPSocket'
|
||||||
|
|
||||||
|
export interface AdbDaemonDirectSocketDeviceOptions {
|
||||||
|
host: string
|
||||||
|
port?: number
|
||||||
|
name?: string
|
||||||
|
unref?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdbDaemonDirectSocketsDevice implements AdbDaemonDevice {
|
||||||
|
static isSupported(): boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
#options: AdbDaemonDirectSocketDeviceOptions
|
||||||
|
|
||||||
|
readonly serial: string
|
||||||
|
|
||||||
|
get host(): string {
|
||||||
|
return this.#options.host
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly port: number
|
||||||
|
|
||||||
|
get name(): string | undefined {
|
||||||
|
return this.#options.name
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(options: AdbDaemonDirectSocketDeviceOptions) {
|
||||||
|
this.#options = options
|
||||||
|
this.port = options.port ?? 5555
|
||||||
|
this.serial = `${this.host}:${this.port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect() {
|
||||||
|
const socket = new TCPSocket(this.host, this.port, {
|
||||||
|
noDelay: true,
|
||||||
|
unref: this.#options.unref
|
||||||
|
})
|
||||||
|
const { readable, writable } = await socket.opened
|
||||||
|
|
||||||
|
return {
|
||||||
|
readable: new WrapReadableStream(readable).pipeThrough(new StructDeserializeStream(AdbPacket)),
|
||||||
|
writable: new WrapWritableStream(writable)
|
||||||
|
.bePipedThroughFrom(new UnwrapConsumableStream())
|
||||||
|
.bePipedThroughFrom(new AdbPacketSerializeStream())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
client-electron/electron/electron-env.d.ts
vendored
8
client-electron/electron/electron-env.d.ts
vendored
|
|
@ -27,4 +27,12 @@ interface Window {
|
||||||
ipcRenderer: import('electron').IpcRenderer
|
ipcRenderer: import('electron').IpcRenderer
|
||||||
electronRuntime: boolean
|
electronRuntime: boolean
|
||||||
platform: NodeJS.Platform
|
platform: NodeJS.Platform
|
||||||
|
adbNativeTcpSocket: {
|
||||||
|
getProp(serial: string, key: string): Promise<string>
|
||||||
|
getDevices(): Promise<{ name: string; serial: string }[]>
|
||||||
|
connect(host: string, port: number): Promise<{ name: string; serial: string }>
|
||||||
|
shell(serial: string, command: string, callback: (chunk: Uint8Array) => void): Promise<void>
|
||||||
|
spawn(serial: string, command: string, callback: (chunk: Uint8Array) => void): Promise<void>
|
||||||
|
disconnect(): Promise<void>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { findCredentials, getPassword, setPassword, deletePassword } from '@postman/node-keytar'
|
import { findCredentials, getPassword, setPassword, deletePassword } from '@postman/node-keytar'
|
||||||
|
|
||||||
export function eventGetKeyChain(icpMain: Electron.IpcMain) {
|
export function eventGetKeyChain(icpMain: Electron.IpcMain) {
|
||||||
icpMain.handle('get-keyChain', async (_event, serviceName, account) => {
|
icpMain.handle('get-keyChain', async (_event, { serviceName, account }) => {
|
||||||
return getPassword(serviceName, account)
|
return getPassword(serviceName, account)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { app, BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import deeplink from './deeplink'
|
import deeplink from './deeplink'
|
||||||
import { eventGetKeyChain } from './keychain'
|
import { eventGetKeyChain } from './keychain'
|
||||||
import { AdbDaemon } from './adb'
|
import { AdbTcpSocket } from './adb'
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
//
|
//
|
||||||
|
|
@ -125,7 +125,7 @@ app.whenReady().then(() => {
|
||||||
deeplink(app, win, ipcMain, shell)
|
deeplink(app, win, ipcMain, shell)
|
||||||
|
|
||||||
// adb
|
// adb
|
||||||
AdbDaemon(win, ipcMain)
|
AdbTcpSocket(win, ipcMain)
|
||||||
|
|
||||||
//keychain
|
//keychain
|
||||||
eventGetKeyChain(ipcMain)
|
eventGetKeyChain(ipcMain)
|
||||||
|
|
@ -146,3 +146,8 @@ app.whenReady().then(() => {
|
||||||
callback({ responseHeaders: details.responseHeaders })
|
callback({ responseHeaders: details.responseHeaders })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
process.on('unhandledRejection', () => {
|
||||||
|
console.error('Unhandled Rejection ignoring')
|
||||||
|
// Application specific logging, throwing an error, or other logic here
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,38 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||||
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
|
contextBridge.exposeInMainWorld('ipcRenderer', withPrototype(ipcRenderer))
|
||||||
contextBridge.exposeInMainWorld('electronRuntime', true)
|
contextBridge.exposeInMainWorld('electronRuntime', true)
|
||||||
contextBridge.exposeInMainWorld('platform', process.platform)
|
contextBridge.exposeInMainWorld('platform', process.platform)
|
||||||
|
contextBridge.exposeInMainWorld('adbNativeTcpSocket', {
|
||||||
|
async getProp(serial: string, key: string) {
|
||||||
|
return ipcRenderer.invoke('adb:tcp:socket:getprop', serial, key)
|
||||||
|
},
|
||||||
|
getDevices() {
|
||||||
|
return ipcRenderer.invoke('adb:tcp:socket:getDevices')
|
||||||
|
},
|
||||||
|
async connect(host: string, port: number) {
|
||||||
|
return ipcRenderer.invoke('adb:tcp:socket:connect', { host, port })
|
||||||
|
},
|
||||||
|
async shell(serial: string, command: string, callback: (chunk: Uint8Array) => void) {
|
||||||
|
// generate a unique id for the callback
|
||||||
|
const callbackId = `adb:tcp:socket:shell:output:${Date.now()}`
|
||||||
|
|
||||||
|
ipcRenderer.on(callbackId, (_event, chunk: Uint8Array) => {
|
||||||
|
callback(chunk)
|
||||||
|
})
|
||||||
|
await ipcRenderer.invoke('adb:tcp:socket:shell', { serial, command, callbackId })
|
||||||
|
},
|
||||||
|
async spawn(serial: string, command: string, callback: (chunk: Uint8Array) => void) {
|
||||||
|
const callbackId = `adb:tcp:socket:spawn:output:${Date.now()}`
|
||||||
|
|
||||||
|
ipcRenderer.on(callbackId, (_event, chunk: Uint8Array) => {
|
||||||
|
callback(chunk)
|
||||||
|
})
|
||||||
|
|
||||||
|
await ipcRenderer.invoke('adb:tcp:socket:spawn', { serial, command, callbackId })
|
||||||
|
},
|
||||||
|
disconnect() {
|
||||||
|
return ipcRenderer.invoke('adb:tcp:socket:disconnect')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
|
// `exposeInMainWorld` can't detect attributes and methods of `prototype`, manually patching it.
|
||||||
function withPrototype(obj: Record<string, any>) {
|
function withPrototype(obj: Record<string, any>) {
|
||||||
|
|
@ -27,9 +59,7 @@ function withPrototype(obj: Record<string, any>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------- Preload scripts loading ---------
|
// --------- Preload scripts loading ---------
|
||||||
function domReady(
|
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
|
||||||
condition: DocumentReadyState[] = ['complete', 'interactive']
|
|
||||||
) {
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
if (condition.includes(document.readyState)) {
|
if (condition.includes(document.readyState)) {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
|
|
|
||||||
86
client-electron/electron/socketToTCPSocket.ts
Normal file
86
client-electron/electron/socketToTCPSocket.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { PromiseResolver } from '@yume-chan/async'
|
||||||
|
import { PushReadableStream, WritableStream, type ReadableStream } from '@yume-chan/stream-extra'
|
||||||
|
import { connect, type Socket } from 'node:net'
|
||||||
|
|
||||||
|
export interface TCPSocketOptions {
|
||||||
|
noDelay?: boolean
|
||||||
|
unref?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TCPSocketOpenInfo {
|
||||||
|
readable: ReadableStream<Uint8Array>
|
||||||
|
writable: WritableStream<Uint8Array>
|
||||||
|
|
||||||
|
remoteAddress: string
|
||||||
|
remotePort: number
|
||||||
|
|
||||||
|
localAddress: string
|
||||||
|
localPort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TCPSocket {
|
||||||
|
#socket: Socket
|
||||||
|
#opened = new PromiseResolver<TCPSocketOpenInfo>()
|
||||||
|
get opened(): Promise<TCPSocketOpenInfo> {
|
||||||
|
return this.#opened.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(remoteAddress: string, remotePort: number, options?: TCPSocketOptions) {
|
||||||
|
this.#socket = connect(remotePort, remoteAddress)
|
||||||
|
|
||||||
|
if (options?.noDelay) {
|
||||||
|
this.#socket.setNoDelay(true)
|
||||||
|
}
|
||||||
|
if (options?.unref) {
|
||||||
|
this.#socket.unref()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#socket.on('connect', () => {
|
||||||
|
const readable = new PushReadableStream<Uint8Array>(controller => {
|
||||||
|
this.#socket.on('data', async data => {
|
||||||
|
this.#socket.pause()
|
||||||
|
await controller.enqueue(data)
|
||||||
|
this.#socket.resume()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#socket.on('end', () => {
|
||||||
|
try {
|
||||||
|
controller.close()
|
||||||
|
} catch {
|
||||||
|
console.error('Controller already closed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
controller.abortSignal.addEventListener('abort', () => {
|
||||||
|
this.#socket.end()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#opened.resolve({
|
||||||
|
remoteAddress,
|
||||||
|
remotePort,
|
||||||
|
localAddress: this.#socket.localAddress!,
|
||||||
|
localPort: this.#socket.localPort!,
|
||||||
|
readable,
|
||||||
|
writable: new WritableStream({
|
||||||
|
write: async chunk => {
|
||||||
|
return new Promise<void>(resolve => {
|
||||||
|
if (!this.#socket.write(chunk)) {
|
||||||
|
this.#socket.once('drain', resolve)
|
||||||
|
} else {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
close: async () => {
|
||||||
|
this.#socket.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.#socket.on('error', error => {
|
||||||
|
this.#opened.reject(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
128
client-electron/package-lock.json
generated
128
client-electron/package-lock.json
generated
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@postman/node-keytar": "^7.9.3",
|
"@postman/node-keytar": "^7.9.3",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
|
@ -74,6 +75,7 @@
|
||||||
"@electron-forge/cli": "^7.2.0",
|
"@electron-forge/cli": "^7.2.0",
|
||||||
"@electron/rebuild": "^3.5.0",
|
"@electron/rebuild": "^3.5.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.17.20",
|
"@tanstack/eslint-plugin-query": "^5.17.20",
|
||||||
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
|
|
@ -3111,6 +3113,37 @@
|
||||||
"@babel/runtime": "^7.13.10"
|
"@babel/runtime": "^7.13.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-accordion": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collapsible": "1.0.3",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-arrow": {
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
||||||
|
|
@ -3190,6 +3223,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-collapsible": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collection": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||||
|
|
@ -4450,9 +4513,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.19.6",
|
"version": "20.11.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
||||||
"integrity": "sha512-X36s5CXMrrJOs2lQCdDF68apW4Rfx9ixYMawlepwmE4Anezv/AV2LSpKD1Ub8DAc+urp5bk0BGZ6NtmBitfnsg==",
|
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
|
@ -7454,6 +7517,15 @@
|
||||||
"integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==",
|
"integrity": "sha512-2k7t5PHvLsufpP6Zwk0nof62yLOsCf032wZx7/q0mv8gwlXjhcxI3lz6f0jBr0GrnWKcm3burXzI3t5IrcdUxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/electron/node_modules/@types/node": {
|
||||||
|
"version": "18.19.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.18.tgz",
|
||||||
|
"integrity": "sha512-80CP7B8y4PzZF0GWx15/gVWRrB5y/bIjNI84NK3cmQJu0WZwvmj2WMA5LcofQFVfLqqCSp545+U2LsrVzX36Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|
@ -15958,6 +16030,23 @@
|
||||||
"@babel/runtime": "^7.13.10"
|
"@babel/runtime": "^7.13.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-accordion": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-fDG7jcoNKVjSK6yfmuAs0EnPDro0WMXIhMtXdTBWqEioVW206ku+4Lw07e+13lUkFkpoEQ2PdeMIAGpdqEAmDg==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-collapsible": "1.0.3",
|
||||||
|
"@radix-ui/react-collection": "1.0.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-arrow": {
|
"@radix-ui/react-arrow": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
|
||||||
|
|
@ -15995,6 +16084,22 @@
|
||||||
"@radix-ui/react-use-size": "1.0.1"
|
"@radix-ui/react-use-size": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-collapsible": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-UBmVDkmR6IvDsloHVN+3rtx4Mi5TFvylYXpluuv0f37dtaz3H99bp8No0LGXRigVpl3UAT4l9j6bIchh42S/Gg==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-compose-refs": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-collection": {
|
"@radix-ui/react-collection": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||||
|
|
@ -16723,9 +16828,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.19.6",
|
"version": "20.11.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
|
||||||
"integrity": "sha512-X36s5CXMrrJOs2lQCdDF68apW4Rfx9ixYMawlepwmE4Anezv/AV2LSpKD1Ub8DAc+urp5bk0BGZ6NtmBitfnsg==",
|
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~5.26.4"
|
||||||
|
|
@ -18740,6 +18845,17 @@
|
||||||
"@electron/get": "^2.0.0",
|
"@electron/get": "^2.0.0",
|
||||||
"@types/node": "^18.11.18",
|
"@types/node": "^18.11.18",
|
||||||
"extract-zip": "^2.0.1"
|
"extract-zip": "^2.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": {
|
||||||
|
"version": "18.19.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.18.tgz",
|
||||||
|
"integrity": "sha512-80CP7B8y4PzZF0GWx15/gVWRrB5y/bIjNI84NK3cmQJu0WZwvmj2WMA5LcofQFVfLqqCSp545+U2LsrVzX36Zg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"electron-builder": {
|
"electron-builder": {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@postman/node-keytar": "^7.9.3",
|
"@postman/node-keytar": "^7.9.3",
|
||||||
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-avatar": "^1.0.4",
|
"@radix-ui/react-avatar": "^1.0.4",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
|
|
@ -86,6 +87,7 @@
|
||||||
"@electron-forge/cli": "^7.2.0",
|
"@electron-forge/cli": "^7.2.0",
|
||||||
"@electron/rebuild": "^3.5.0",
|
"@electron/rebuild": "^3.5.0",
|
||||||
"@tanstack/eslint-plugin-query": "^5.17.20",
|
"@tanstack/eslint-plugin-query": "^5.17.20",
|
||||||
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
DialogTrigger
|
DialogTrigger
|
||||||
} from './ui/dialog'
|
} from './ui/dialog'
|
||||||
import { Button } from './ui/button'
|
import { Button } from './ui/button'
|
||||||
import { CaretSortIcon, CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'
|
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons'
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
|
|
@ -27,9 +27,11 @@ import useAdb from '@/hooks/useAdb'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import type { AdbDaemonWebUsbConnection } from '@yume-chan/adb-daemon-webusb'
|
import type { AdbDaemonWebUsbConnection } from '@yume-chan/adb-daemon-webusb'
|
||||||
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb'
|
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb'
|
||||||
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
import { Adb, type AdbDaemonDevice, AdbDaemonTransport } from '@yume-chan/adb'
|
||||||
import { toast } from './ui/use-toast'
|
import { toast } from './ui/use-toast'
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||||
|
import { Input } from './ui/input'
|
||||||
|
import { IpcTcpTransport } from '@/lib/adb-tcp'
|
||||||
|
|
||||||
type PopoverTriggerProps = React.ComponentPropsWithoutRef<typeof PopoverTrigger>
|
type PopoverTriggerProps = React.ComponentPropsWithoutRef<typeof PopoverTrigger>
|
||||||
|
|
||||||
|
|
@ -38,7 +40,8 @@ interface TeamSwitcherProps extends PopoverTriggerProps {}
|
||||||
const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [showNewDeviceDialog, setShowNewDeviceDialog] = useState(false)
|
const [showNewDeviceDialog, setShowNewDeviceDialog] = useState(false)
|
||||||
const [connectedDevices, setConnectedDevices] = useState<AdbDaemonWebUsbDevice[]>([])
|
const [connectedUsbDevices, setConnectedUsbDevices] = useState<AdbDaemonDevice[]>([])
|
||||||
|
const [connectedTcpDevices, setConnectedTcpDevices] = useState<{ name: string; serial: string }[]>([])
|
||||||
|
|
||||||
const [newConnectionState, setNewConnectionState] = useState<'connection' | 'connecting'>('connection')
|
const [newConnectionState, setNewConnectionState] = useState<'connection' | 'connecting'>('connection')
|
||||||
const [selectedConnectionType, setSelectedConnectionType] = useState<string | undefined>()
|
const [selectedConnectionType, setSelectedConnectionType] = useState<string | undefined>()
|
||||||
|
|
@ -56,9 +59,11 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
const getDevices = async () => {
|
const getDevices = async () => {
|
||||||
const devices = await manager?.getDevices()
|
const usbDevices = await manager?.getDevices()
|
||||||
console.log(devices)
|
const tcpDevices = await window.adbNativeTcpSocket.getDevices()
|
||||||
setConnectedDevices(devices || [])
|
|
||||||
|
setConnectedUsbDevices(usbDevices || [])
|
||||||
|
setConnectedTcpDevices(tcpDevices || [])
|
||||||
}
|
}
|
||||||
getDevices()
|
getDevices()
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +99,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewUsbConnection() {
|
async function createNewUsbConnection() {
|
||||||
let selectedDevice: AdbDaemonWebUsbDevice | undefined = undefined
|
let selectedDevice: AdbDaemonDevice | undefined = undefined
|
||||||
if (!device) {
|
if (!device) {
|
||||||
console.log('no device')
|
console.log('no device')
|
||||||
|
|
||||||
|
|
@ -149,6 +154,24 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewTCPConnection() {
|
||||||
|
const device = await window.adbNativeTcpSocket.connect('192.168.11.196', 5555)
|
||||||
|
|
||||||
|
const transport = new IpcTcpTransport(device.serial, { product: device.name, model: device.name })
|
||||||
|
|
||||||
|
const adb = new Adb(transport)
|
||||||
|
|
||||||
|
setAdb(adb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectDeviceAdbTcp(device: { name: string; serial: string }) {
|
||||||
|
const transport = new IpcTcpTransport(device.serial, { product: device.name, model: device.name })
|
||||||
|
|
||||||
|
const adb = new Adb(transport)
|
||||||
|
|
||||||
|
setAdb(adb)
|
||||||
|
}
|
||||||
|
|
||||||
// async function connectAdbDaemon() {
|
// async function connectAdbDaemon() {
|
||||||
// if (!window.electronRuntime) {
|
// if (!window.electronRuntime) {
|
||||||
// toast({
|
// toast({
|
||||||
|
|
@ -168,7 +191,9 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
function onDisconnect() {
|
function onDisconnect() {
|
||||||
device?.raw.forget()
|
if (device instanceof AdbDaemonWebUsbDevice) {
|
||||||
|
device?.raw.forget()
|
||||||
|
}
|
||||||
setDevice(undefined)
|
setDevice(undefined)
|
||||||
|
|
||||||
adb?.close()
|
adb?.close()
|
||||||
|
|
@ -195,6 +220,12 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
<span className="font-medium">USB</span> -{' '}
|
<span className="font-medium">USB</span> -{' '}
|
||||||
<span className="text-muted-foreground">Connect device via USB</span>
|
<span className="text-muted-foreground">Connect device via USB</span>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
{window.electronRuntime && import.meta.env.DEV && (
|
||||||
|
<SelectItem value="tcp">
|
||||||
|
<span className="font-medium">TCP</span> -{' '}
|
||||||
|
<span className="text-muted-foreground">Connect device via TCP</span>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,23 +251,27 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
<DialogDescription>Connect a new device to manage your recipes or control your devices. </DialogDescription>
|
<DialogDescription>Connect a new device to manage your recipes or control your devices. </DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-4 py-2 pb-4">
|
{selectedConnectionType === 'usb' ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-4 py-2 pb-4">
|
||||||
{/* <Label htmlFor="plan">Connection type</Label>
|
<div className="space-y-2">
|
||||||
<Select onValueChange={setSelectedConnectionType}>
|
<span className="text-sm">Please plug in your device and click Connect.</span>
|
||||||
<SelectTrigger>
|
</div>
|
||||||
<SelectValue placeholder="Select connection type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="usb">
|
|
||||||
<span className="font-medium">USB</span> -{' '}
|
|
||||||
<span className="text-muted-foreground">Connect device via USB</span>
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select> */}
|
|
||||||
<span className="text-sm">Please connect your device via USB</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : selectedConnectionType === 'tcp' ? (
|
||||||
|
<div className="space-y-4 py-2 pb-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="plan">TCP Connection</Label>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Input type="text" placeholder="IP Address" className="flex-1" />
|
||||||
|
<Input type="text" placeholder="Port" className="flex-[0.5]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<span className="text-sm">Unknown connection type.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setNewConnectionState('connection')}>
|
<Button variant="outline" onClick={() => setNewConnectionState('connection')}>
|
||||||
|
|
@ -245,7 +280,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createNewUsbConnection()
|
selectedConnectionType === 'usb' ? createNewUsbConnection() : createNewTCPConnection()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Connect
|
Connect
|
||||||
|
|
@ -267,7 +302,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
aria-label="Select a Device"
|
aria-label="Select a Device"
|
||||||
className={cn('w-[400px] justify-between', className)}
|
className={cn('w-[400px] justify-between', className)}
|
||||||
>
|
>
|
||||||
{device ? device.name : 'Select a Device'}
|
{device ? device.name + ' [' + device.serial + ']' : 'Select a Device'}
|
||||||
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
@ -276,30 +311,50 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandInput placeholder="Search device..." />
|
<CommandInput placeholder="Search device..." />
|
||||||
<CommandEmpty>No device found.</CommandEmpty>
|
<CommandEmpty>No device found.</CommandEmpty>
|
||||||
<CommandGroup heading={'Devices'}>
|
<CommandGroup heading={'Devices [USB]'}>
|
||||||
{connectedDevices.length > 0 ? (
|
{connectedUsbDevices.length > 0 ? (
|
||||||
connectedDevices.map(device => (
|
connectedUsbDevices.map(device => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={device.serial}
|
key={device.serial}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
connectDeviceAdbUsb(device)
|
connectDeviceAdbUsb(device as AdbDaemonWebUsbDevice)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}}
|
}}
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
>
|
>
|
||||||
{device.name}
|
{device.name}
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
'ml-auto h-4 w-4',
|
|
||||||
device?.serial === device.serial ? 'opacity-100' : 'opacity-0'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm ml-2">Not found device connected.</span>
|
<span className="text-sm ml-2">Not found device connected.</span>
|
||||||
)}
|
)}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
{window.electronRuntime && import.meta.env.DEV && (
|
||||||
|
<CommandGroup heading={'Devices [TCP]'}>
|
||||||
|
{connectedTcpDevices.length > 0 ? (
|
||||||
|
connectedTcpDevices.map(device => (
|
||||||
|
<CommandItem
|
||||||
|
key={device.serial}
|
||||||
|
onSelect={() => {
|
||||||
|
connectDeviceAdbTcp(device)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
className="text-sm"
|
||||||
|
>
|
||||||
|
{device.name}
|
||||||
|
{/* <CheckIcon
|
||||||
|
className={cn(
|
||||||
|
'ml-auto h-4 w-4',
|
||||||
|
device?.serial === device.serial ? 'opacity-100' : 'opacity-0'
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</CommandItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<span className="text-sm ml-2">Not found device connected.</span>
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
|
|
|
||||||
55
client-electron/src/components/ui/accordion.tsx
Normal file
55
client-electron/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||||
|
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Accordion = AccordionPrimitive.Root
|
||||||
|
|
||||||
|
const AccordionItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AccordionItem.displayName = "AccordionItem"
|
||||||
|
|
||||||
|
const AccordionTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Header className="flex">
|
||||||
|
<AccordionPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||||
|
</AccordionPrimitive.Trigger>
|
||||||
|
</AccordionPrimitive.Header>
|
||||||
|
))
|
||||||
|
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const AccordionContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<AccordionPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||||
|
</AccordionPrimitive.Content>
|
||||||
|
))
|
||||||
|
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||||
|
|
@ -155,31 +155,7 @@ const useFileManager = create<FileManagerAndroidHook>((set, get) => ({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const process = await adb.subprocess.spawn('rm ' + get().rootPath + '/' + filename)
|
await adb.rm(filename, { recursive: true })
|
||||||
process.stderr.pipeTo(
|
|
||||||
new WritableStream({
|
|
||||||
write(chunk) {
|
|
||||||
console.error(chunk)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
process.stdout.pipeTo(
|
|
||||||
new WritableStream({
|
|
||||||
write(chunk) {
|
|
||||||
console.log(chunk)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
if ((await process.exit) != 0) {
|
|
||||||
toast({
|
|
||||||
duration: 3000,
|
|
||||||
variant: 'destructive',
|
|
||||||
title: 'Failed to delete file',
|
|
||||||
description: 'Please try again'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async rename(filename, newName) {
|
async rename(filename, newName) {
|
||||||
const adb = useAdb.getState().adb
|
const adb = useAdb.getState().adb
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,13 @@
|
||||||
import { type Adb } from '@yume-chan/adb'
|
import { type AdbDaemonDevice, type Adb } from '@yume-chan/adb'
|
||||||
import {
|
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'
|
||||||
type AdbDaemonWebUsbDevice,
|
|
||||||
AdbDaemonWebUsbDeviceManager
|
|
||||||
} from '@yume-chan/adb-daemon-webusb'
|
|
||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
|
|
||||||
interface ADB {
|
interface ADB {
|
||||||
adb: Adb | undefined
|
adb: Adb | undefined
|
||||||
manager: AdbDaemonWebUsbDeviceManager | undefined
|
manager: AdbDaemonWebUsbDeviceManager | undefined
|
||||||
device: AdbDaemonWebUsbDevice | undefined
|
device: AdbDaemonDevice | undefined
|
||||||
setAdb: (adb: Adb | undefined) => void
|
setAdb: (adb: Adb | undefined) => void
|
||||||
setDevice: (device: AdbDaemonWebUsbDevice | undefined) => void
|
setDevice: (device: AdbDaemonDevice | undefined) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAdb = create<ADB>(set => ({
|
const useAdb = create<ADB>(set => ({
|
||||||
|
|
|
||||||
129
client-electron/src/lib/adb-tcp.ts
Normal file
129
client-electron/src/lib/adb-tcp.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import type { AdbSocket, AdbTransport } from '@yume-chan/adb'
|
||||||
|
import { AdbBanner, encodeUtf8 } from '@yume-chan/adb'
|
||||||
|
import { PromiseResolver } from '@yume-chan/async'
|
||||||
|
import type { Consumable } from '@yume-chan/stream-extra'
|
||||||
|
import { ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||||
|
import type { ValueOrPromise } from '@yume-chan/struct'
|
||||||
|
|
||||||
|
export class IpcTcpSocket implements AdbSocket {
|
||||||
|
service: string
|
||||||
|
readable: ReadableStream<Uint8Array>
|
||||||
|
writable: WritableStream<Consumable<Uint8Array>>
|
||||||
|
|
||||||
|
_closed = false
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
constructor(serial: string, service: string, ...args: string[]) {
|
||||||
|
this.service = service
|
||||||
|
|
||||||
|
switch (service) {
|
||||||
|
case 'getprop':
|
||||||
|
this.readable = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
const result = await window.adbNativeTcpSocket.getProp(serial, args[0])
|
||||||
|
controller.enqueue(encodeUtf8(result))
|
||||||
|
controller.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'shell':
|
||||||
|
this.readable = new ReadableStream({
|
||||||
|
async start(controller) {
|
||||||
|
await window.adbNativeTcpSocket.shell(serial, args[0], chunk => {
|
||||||
|
controller.enqueue(chunk)
|
||||||
|
})
|
||||||
|
controller.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
this.readable = new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.writable = new WritableStream({
|
||||||
|
write(chunk) {
|
||||||
|
if (closed) {
|
||||||
|
throw new Error('Socket closed')
|
||||||
|
} else {
|
||||||
|
console.log(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
close(): ValueOrPromise<void> {
|
||||||
|
this._closed = true
|
||||||
|
}
|
||||||
|
get closed(): Promise<void> {
|
||||||
|
this._closed = true
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IpcTcpTransport implements AdbTransport {
|
||||||
|
constructor(
|
||||||
|
private _serial: string,
|
||||||
|
private _banner?: {
|
||||||
|
product?: string
|
||||||
|
model?: string
|
||||||
|
device?: string
|
||||||
|
}
|
||||||
|
) {}
|
||||||
|
|
||||||
|
get serial(): string {
|
||||||
|
return this._serial
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxPayloadSize(): number {
|
||||||
|
return 4 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
get banner(): AdbBanner {
|
||||||
|
if (this._banner) {
|
||||||
|
return new AdbBanner(this._banner.product, this._banner.model, this._banner.device, [])
|
||||||
|
}
|
||||||
|
return new AdbBanner('undefined', 'undefined', 'undefined', [])
|
||||||
|
}
|
||||||
|
|
||||||
|
#disconnected = new PromiseResolver<void>()
|
||||||
|
get disconnected(): Promise<void> {
|
||||||
|
return this.#disconnected.promise
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(service: string): AdbSocket {
|
||||||
|
const serviceSpl = service.split(':', 2)
|
||||||
|
console.log(service)
|
||||||
|
console.log(serviceSpl)
|
||||||
|
switch (serviceSpl[0]) {
|
||||||
|
case 'exec':
|
||||||
|
if (serviceSpl[1].startsWith('getprop')) {
|
||||||
|
const [func, key] = serviceSpl[1].split(' ', 2)
|
||||||
|
return new IpcTcpSocket(this._serial, func, key)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'shell':
|
||||||
|
return new IpcTcpSocket(this._serial, serviceSpl[0], serviceSpl[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown service: ${service}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
addReverseTunnel(): ValueOrPromise<string> {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
removeReverseTunnel(): ValueOrPromise<void> {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
clearReverseTunnels(): ValueOrPromise<void> {
|
||||||
|
throw new Error('Method not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): ValueOrPromise<void> {
|
||||||
|
this.#disconnected.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@ import axios, { type AxiosResponse } from 'axios'
|
||||||
|
|
||||||
const taoAxios = axios.create({
|
const taoAxios = axios.create({
|
||||||
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
||||||
|
headers: {
|
||||||
|
'Taobin-Client-Kind': window.electronRuntime ? 'electron' : 'web'
|
||||||
|
},
|
||||||
withCredentials: true
|
withCredentials: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,18 @@ const LoginPage: React.FC = () => {
|
||||||
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl + '&kind=electron'
|
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl + '&kind=electron'
|
||||||
)
|
)
|
||||||
|
|
||||||
window.ipcRenderer.on('loginSuccess', (_event, data) => {
|
window.ipcRenderer.on('loginSuccess', async (_event, data) => {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
|
|
||||||
const { accessToken, maxAge, refreshToken } = data
|
const { accessToken, refreshToken } = data
|
||||||
|
|
||||||
window.ipcRenderer.send('set-keyChain', {
|
await window.ipcRenderer.invoke('set-keyChain', {
|
||||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||||
password: accessToken + ';' + maxAge
|
password: accessToken
|
||||||
})
|
})
|
||||||
|
|
||||||
window.ipcRenderer.send('set-keyChain', {
|
await window.ipcRenderer.invoke('set-keyChain', {
|
||||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||||
password: refreshToken
|
password: refreshToken
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { type AndroidFile } from '@/models/android/schema'
|
import { type AndroidFile } from '@/models/android/schema'
|
||||||
import { File, Folder } from 'lucide-react'
|
import { File, Folder } from 'lucide-react'
|
||||||
|
import useAdb from '@/hooks/useAdb'
|
||||||
|
|
||||||
export const FileManagerTab: React.FC = () => {
|
export const FileManagerTab: React.FC = () => {
|
||||||
const { currentPath, pushPath, scanPath } = useFileManager(
|
const { currentPath, pushPath, scanPath } = useFileManager(
|
||||||
|
|
@ -23,6 +24,8 @@ export const FileManagerTab: React.FC = () => {
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const adb = useAdb(state => state.adb)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('scanning path', currentPath)
|
console.log('scanning path', currentPath)
|
||||||
scanPath(currentPath).then(files => {
|
scanPath(currentPath).then(files => {
|
||||||
|
|
@ -30,7 +33,7 @@ export const FileManagerTab: React.FC = () => {
|
||||||
setFiles(files)
|
setFiles(files)
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
}, [currentPath])
|
}, [currentPath, adb])
|
||||||
|
|
||||||
const [files, setFiles] = useState<AndroidFile[] | undefined>(undefined)
|
const [files, setFiles] = useState<AndroidFile[] | undefined>(undefined)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
const RecipeForm: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Recipe Form</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecipeForm
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||||
|
|
@ -36,7 +38,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col overflow-y-auto">
|
||||||
<div className="flex items-center p-2">
|
<div className="flex items-center p-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -159,16 +161,54 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
||||||
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
|
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{
|
<Accordion type="single" collapsible>
|
||||||
// list all recipes
|
{
|
||||||
recipe.recipes
|
// list all recipes
|
||||||
.filter(r => r.isUse)
|
recipe.recipes
|
||||||
.map((recipe, index) => (
|
.filter(r => r.isUse)
|
||||||
<div key={index}>
|
.map((recipe, index) => (
|
||||||
<span className="font-semibold">Recipe {index + 1}:</span> {recipe.materialPathId}
|
<AccordionItem key={index} value={'item-' + index}>
|
||||||
</div>
|
<AccordionTrigger>{recipe.materialPathId}</AccordionTrigger>
|
||||||
))
|
<AccordionContent>
|
||||||
}
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Is Use</TableHead>
|
||||||
|
<TableHead>Powder Gram</TableHead>
|
||||||
|
<TableHead>Powder Time</TableHead>
|
||||||
|
<TableHead>Syrup Gram</TableHead>
|
||||||
|
<TableHead>Syrup Time</TableHead>
|
||||||
|
<TableHead>Hot Water</TableHead>
|
||||||
|
<TableHead>Cold Water</TableHead>
|
||||||
|
<TableHead>Mix Order</TableHead>
|
||||||
|
<TableHead>String Param</TableHead>
|
||||||
|
<TableHead>Stir Time</TableHead>
|
||||||
|
<TableHead>Feed param</TableHead>
|
||||||
|
<TableHead>Feed pattern</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell className="font-medium">{JSON.stringify(recipe.isUse)}</TableCell>
|
||||||
|
<TableCell>{recipe.powderGram}</TableCell>
|
||||||
|
<TableCell>{recipe.powderTime}</TableCell>
|
||||||
|
<TableCell>{recipe.syrupGram}</TableCell>
|
||||||
|
<TableCell>{recipe.syrupTime}</TableCell>
|
||||||
|
<TableCell>{recipe.waterYield}</TableCell>
|
||||||
|
<TableCell>{recipe.waterCold}</TableCell>
|
||||||
|
<TableCell>{recipe.MixOrder}</TableCell>
|
||||||
|
<TableCell>{recipe.StringParam}</TableCell>
|
||||||
|
<TableCell>{recipe.stirTime}</TableCell>
|
||||||
|
<TableCell>{recipe.FeedParameter}</TableCell>
|
||||||
|
<TableCell>{recipe.FeedPattern}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
||||||
onLayout={(sizes: number[]) => {
|
onLayout={(sizes: number[]) => {
|
||||||
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
||||||
}}
|
}}
|
||||||
className="h-full max-h-[900px] items-stretch"
|
className="h-full max-h-screen items-stretch"
|
||||||
>
|
>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={defaultLayout[0]}
|
defaultSize={defaultLayout[0]}
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ func (ar *AuthRouter) Route(r chi.Router) {
|
||||||
// get userInfo info
|
// get userInfo info
|
||||||
userInfo, err := ar.oauth.GetUserInfo(r.Context(), &oauth2.Token{AccessToken: token})
|
userInfo, err := ar.oauth.GetUserInfo(r.Context(), &oauth2.Token{AccessToken: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error getting userInfo info", http.StatusBadRequest)
|
http.Error(w, "Error getting userInfo info", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ func (o *oauthService) GetUserInfo(ctx context.Context, token *oauth2.Token) (*m
|
||||||
}
|
}
|
||||||
|
|
||||||
if userInfo["error"] != nil {
|
if userInfo["error"] != nil {
|
||||||
return nil, errors.New("Error getting user info")
|
return nil, errors.New("error getting user info")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &models.User{
|
return &models.User{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue