Supra_App/src/lib/components/dashboard-quick-adb.svelte

441 lines
12 KiB
Svelte
Raw Normal View History

2026-02-17 14:30:02 +07:00
<script lang="ts">
import { Button, type ButtonVariant } from './ui/button';
import * as Card from './ui/card/index';
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
import Label from '$lib/components/ui/label/label.svelte';
import { LockIcon, UnlockIcon, Circle, AlertCircleIcon } from '@lucide/svelte/icons';
import * as adb from '$lib/core/adb/adb';
import { machineInfoStore } from '$lib/core/stores/machineInfoStore';
import { toast } from 'svelte-sonner';
import { handleIncomingMessages } from '$lib/core/handlers/messageHandler';
import { auth as authStore } from '$lib/core/stores/auth';
import { get } from 'svelte/store';
import {
AdbDaemonWebUsbDevice,
AdbDaemonWebUsbDeviceManager
} from '@yume-chan/adb-daemon-webusb';
2026-02-17 14:30:02 +07:00
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { onMount } from 'svelte';
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
import { file } from 'zod/mini';
import { addNotification } from '$lib/core/stores/noti';
2026-02-17 14:30:02 +07:00
let { enableComponent = true } = $props();
// connection button states
let connectionButtonText = $state('Connect');
let connectionButtonDisable = $state(false);
let connectionButtonVariant: ButtonVariant = $state('default');
let connectDeviceOk = $state(false);
let hasStoredDevice = $state(false);
let openAppBrewWhenConnected = $state(true);
let hasOpenedBrewOnce = $state(false);
// progress
let showLoadProgress = $state(false);
//
let recipe: any | undefined = $state(undefined);
let machineStatus: string = $state('Ok');
// machineInfoStore.subscribe((mInfo) => {});
const essentialFiles = ['/sdcard/coffeevending/versions/'];
async function loadEssentialFiles() {
showLoadProgress = true;
machineStatus = 'Loading infos ...';
let instance = adb.getAdbInstance();
if (instance) {
// check country
let country = await adb.pull('/sdcard/coffeevending/country/short');
machineStatus = `Found country: ${country}`;
// check dev
let devMode = await adb.pull('/sdcard/coffeevending/CURR_TEST');
if (devMode?.includes('1')) {
machineStatus = `Dev mode enabled`;
} else {
machineStatus = `Dev mode disabled`;
}
// check .bid
let boxid = await adb.pull('/sdcard/coffeevending/.bid');
if (boxid) {
machineStatus = `Box id is ${boxid}`;
} else {
machineStatus = 'No box id';
}
machineInfoStore.set({
boxId: boxid,
versions: {
firmware: '',
brew: '',
xmlengine: '',
netcore: '',
devbox: ''
},
devMode: devMode?.includes('1') ?? false,
country: country ?? '',
status: '',
errors: []
});
handleIncomingMessages(
JSON.stringify({
type: 'chat',
payload: `${new Date().toLocaleTimeString()}: ${get(authStore)?.displayName} has connected to ${boxid}`
})
);
} else {
machineStatus = 'Instance lost, try disconnect and re-connect again';
toast.error('Unexpected Error');
}
showLoadProgress = false;
}
async function openBrewApp() {
try {
let instance = adb.getAdbInstance();
if (instance) {
try {
// bypass
await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass');
} catch (e) {}
let result = await adb.executeCmd(
'am start -n com.forthvending.coffeemain/com.forthvending.coffeemain.MainActivity'
);
if (result?.output) {
toast.success('Open app success!');
machineStatus = 'open app success, check the screen and put the password';
} else if (result?.error) {
// case usb connection cutoff
if (result.error === 'ExactReadableEndedError') {
toast.warning('Connection unstable');
machineStatus = 'app maybe opened, check the screen';
} else {
throw new Error(`Exit ${result.exitCode}. ${result.error}`);
}
} else {
throw new Error('Instance not found or error while executing');
}
hasOpenedBrewOnce = true;
try {
// bypass
await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass');
} catch (e) {}
setTimeout(async () => {
try {
// bypass
await adb.executeCmd('input tap 336 795');
} catch (e) {}
}, 3000);
}
} catch (e: any) {
machineStatus = 'Cannot open brew app';
toast.error('Error while trying to open brew app, please check the screen. ', {
description: e.toString()
});
}
}
async function testPushPullFile() {
try {
let instance = adb.getAdbInstance();
if (instance) {
await adb.push(
'/sdcard/coffeevending/test.json',
JSON.stringify({ test: new Date().toLocaleTimeString() })
);
let result = await adb.pull('/sdcard/coffeevending/test.json');
if (result) {
if (result === '') {
console.log('push pull not ok, get empty');
} else {
console.log('push pull ok', result);
}
} else {
console.log('push pull not ok', result);
}
// clean file
await adb.executeCmd('rm /sdcard/coffeevending/test.json');
2026-02-17 14:30:02 +07:00
}
} catch (error) {
console.log('test push file failed', error);
}
}
async function connectAdb() {
connectionButtonText = '...';
// lock away no spam
connectionButtonDisable = true;
connectionButtonVariant = 'outline';
try {
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
await adb.connnectViaWebUSB();
let instance = adb.getAdbInstance();
if (instance) {
await loadEssentialFiles();
if (openAppBrewWhenConnected) await openBrewApp();
}
} catch (e: any) {
if (e.message === 'CREDENTIAL_EXPIRED') {
try {
await deviceCredentialManager.clearAllCredentials();
hasStoredDevice = false;
} catch (ignored) {}
}
console.log('error on quick adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Retry';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
connectionButtonDisable = false;
}
async function disconnectAdb() {
// clean up no password flag
// return to engine
2026-02-17 14:30:02 +07:00
await adb.disconnect();
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
handleIncomingMessages(
JSON.stringify({
type: 'chat',
payload: `${new Date().toLocaleTimeString()}: ${get(authStore)?.displayName} has disconnected!`
})
);
}
function checkDeviceConnection() {
try {
let instance = adb.getAdbInstance();
if (instance) {
// ready
connectionButtonText = 'Disconnect';
connectionButtonVariant = 'destructive';
connectDeviceOk = true;
} else {
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
} catch (e: any) {
console.log('error on quick adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Retry';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
// connectionButtonDisable = false;
2026-02-17 14:30:02 +07:00
}
async function checkStoredCredentials() {
try {
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
hasStoredDevice = false;
return;
}
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER.getDevices();
if (!devices || devices.length === 0) {
hasStoredDevice = false;
return;
}
const credentialStore = new AdbWebCredentialStore();
let hasKeys = false;
try {
for await (const key of credentialStore.iterateKeys()) {
hasKeys = true;
break;
}
} catch (e) {
console.log('check stored error', e);
}
hasStoredDevice = devices.length > 0 && hasKeys;
} catch (e) {
console.error('check stored error', e);
hasStoredDevice = false;
}
}
async function tryAutoConnect(): Promise<boolean> {
try {
connectionButtonText = '...';
// lock away no spam
connectionButtonDisable = true;
connectionButtonVariant = 'outline';
if (!AdbDaemonWebUsbDeviceManager.BROWSER) {
throw new Error('WebUSB not supported, try using fallback or different browser');
}
const devices = await AdbDaemonWebUsbDeviceManager.BROWSER?.getDevices();
if (!devices || devices.length == 0) {
throw new Error('No device found');
}
if (devices.length > 1) {
throw new Error('Too many connected devices');
}
const device = devices[0];
const credStore = new AdbWebCredentialStore();
try {
await adb.connectDeviceByCred(device, credStore);
return true;
} catch (e: any) {
if (e.message === 'CREDENTIAL_EXPIRED') {
try {
await deviceCredentialManager.clearAllCredentials();
hasStoredDevice = false;
} catch (ignored) {}
}
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
addNotification(
'ERR:Device is already in use by another program, please close the program and try again'
);
}
2026-02-17 14:30:02 +07:00
return false;
}
} catch (e: any) {
console.log('error on auto connect adb: ', e);
toast.error(`Machine Connection Error`, {
description: e.toString()
});
connectionButtonText = 'Connect';
connectionButtonVariant = 'default';
connectDeviceOk = false;
}
connectionButtonDisable = false;
return false;
}
// update every 1s
setInterval(async function () {
checkDeviceConnection();
}, 1000);
onMount(async () => {
await checkStoredCredentials();
if (!connectDeviceOk && !adb.getAdbInstance()) await tryAutoConnect();
2026-02-17 14:30:02 +07:00
});
</script>
<div class="p-4">
<Card.Root class="h-full bg-muted/50">
<Card.Header>
<Card.Title>Machine Shortcuts</Card.Title>
<Card.Description>Shortcuts for machine i.e. connect or hotfix</Card.Description>
{#if enableComponent}
<Card.Action>
<Button
variant={connectionButtonVariant}
disabled={connectionButtonDisable}
onclick={connectDeviceOk ? disconnectAdb : connectAdb}>{connectionButtonText}</Button
>
</Card.Action>
{/if}
</Card.Header>
<Card.Content>
{#if enableComponent}
<!-- -->
<div class="items-center space-y-8">
<!-- <div class="flex w-full items-center gap-2">
<UnlockIcon />
<span>This feature is enabled </span>
</div> -->
<div class="flex w-full items-center justify-between gap-2">
<p>
Device: {connectDeviceOk ? 'Online' : 'Offline'}
</p>
<Circle
class=""
color={connectDeviceOk ? 'green' : 'red'}
fill={connectDeviceOk ? 'green' : 'red'}
size={16}
/>
</div>
<div class="flex w-full items-start gap-2">
<Checkbox
id="open_brew_now"
checked={openAppBrewWhenConnected}
onCheckedChange={() => {
openAppBrewWhenConnected = !openAppBrewWhenConnected;
}}
/>
<div class="grid gap-2">
<Label for="open_brew_now">Show brew app when connected</Label>
<p class="text-sm text-muted-foreground">
Immediately try to open brew app when first connected
</p>
</div>
</div>
<!-- open app brew manual -->
<div class="flex w-full items-center gap-2">
<Button variant="default" onclick={openBrewApp} disabled={!connectDeviceOk}
>Open Brew App</Button
>
<Button variant="default" disabled={true}>Refresh Infos</Button>
<!-- test push file -->
<Button variant="default" onclick={testPushPullFile} disabled={!connectDeviceOk}
>Test Push</Button
>
</div>
</div>
{:else}
<!-- show lock -->
<div class="flex w-full items-center justify-center gap-2">
<LockIcon /> <span>This feature is not enabled </span>
</div>
{/if}
</Card.Content>
<Card.Footer>
<!-- Display status -->
{#if connectDeviceOk}
{#if showLoadProgress}
<Spinner />
{/if}
<p class="mx-4 font-mono text-sm text-muted-foreground">{machineStatus}</p>
{/if}
</Card.Footer>
</Card.Root>
</div>