feat: scrcpy
- add: scrcpy for previewing android screen. - change: notification of android connection fail to dialog. - wip: firmware packing (on file listing issue) - add: reject button on announcement dialog with callback. Signed-off-by: pakintada@gmail.com <Pakin>
This commit is contained in:
parent
e2aa2848e8
commit
1156b5e6c3
17 changed files with 2080 additions and 57 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<!-- navbar select menus -->
|
||||
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||
<script lang="ts">
|
||||
import { logger } from '$lib/core/utils/logger';
|
||||
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import AppAccountSelect from '$lib/components/app-account-select.svelte';
|
||||
|
|
@ -87,7 +88,7 @@
|
|||
|
||||
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||
addNotification(
|
||||
'ERR:Device is already in use by another program, please close the program and try again'
|
||||
'ERR:Device is already in use by another program, check if machine has been installed `adb`, kill the process and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
450
src/routes/(authed)/firmware/+page.svelte
Normal file
450
src/routes/(authed)/firmware/+page.svelte
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import { logger } from '$lib/core/utils/logger';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import { Progress } from '$lib/components/ui/progress/index.js';
|
||||
|
||||
import {
|
||||
firmwareStore,
|
||||
FIRMWARE_COUNTRIES,
|
||||
formatFileSize,
|
||||
formatBuildDate,
|
||||
statusBadgeVariant,
|
||||
statusLabel,
|
||||
type FirmwareFile,
|
||||
type FirmwareBuildStatus
|
||||
} from '$lib/core/stores/firmwareStore';
|
||||
|
||||
import {
|
||||
ArchiveIcon,
|
||||
Download,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
HardDrive,
|
||||
LoaderCircle,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Clock
|
||||
} from '@lucide/svelte/icons';
|
||||
import { auth as authStore } from '$lib/core/stores/auth';
|
||||
import { sendMessage } from '$lib/core/handlers/ws_messageSender';
|
||||
import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte';
|
||||
import { GlobalEventBus } from '$lib/core/utils/eventBus';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let selectedCountry = $state<string>(FIRMWARE_COUNTRIES[0].code);
|
||||
let buildDialogOpen = $state(false);
|
||||
let buildVersion = $state('');
|
||||
let buildNotes = $state('');
|
||||
let isRequesting = $state(false);
|
||||
|
||||
// All countries state from the store
|
||||
let allCountries = $derived($firmwareStore);
|
||||
|
||||
// Currently selected country state
|
||||
let currentCountryState = $derived(allCountries[selectedCountry] ?? null);
|
||||
|
||||
// Files for the current country
|
||||
let currentFiles = $derived(currentCountryState?.files ?? []);
|
||||
|
||||
// Is a build currently in progress for this country?
|
||||
let isBuildInProgress = $derived(
|
||||
currentCountryState?.buildStatus === 'queued' || currentCountryState?.buildStatus === 'building'
|
||||
);
|
||||
|
||||
// Options per country
|
||||
let increaseVersion: boolean = $state(false);
|
||||
let saveThisVersionBuildDate: boolean = $state(false);
|
||||
let saveThisVersionAsLatest: boolean = $state(false);
|
||||
|
||||
// Subscriptions
|
||||
let unsubscribe_fw_version: any;
|
||||
|
||||
// Latest Versions, allow refresh by re-entering page or from refresh
|
||||
let latest_versions: {
|
||||
[key: string]: {
|
||||
version: string;
|
||||
date: string;
|
||||
};
|
||||
} = $state({});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function openBuildDialog() {
|
||||
// Pre-fill version with a suggestion
|
||||
buildVersion = `v1.${new Date().getMonth() + 1}.${new Date().getDate()}`;
|
||||
buildNotes = '';
|
||||
buildDialogOpen = true;
|
||||
}
|
||||
|
||||
async function confirmRequestBuild() {
|
||||
if (!buildVersion.trim()) {
|
||||
addNotification('ERR:Version is required');
|
||||
return;
|
||||
}
|
||||
|
||||
let requester = get(authStore)?.uid ?? '';
|
||||
|
||||
isRequesting = true;
|
||||
try {
|
||||
firmwareStore.requestBuild({
|
||||
country: selectedCountry,
|
||||
version: buildVersion.trim(),
|
||||
notes: buildNotes.trim() || undefined,
|
||||
requester
|
||||
});
|
||||
addNotification(`INFO:Firmware build requested for ${selectedCountry.toUpperCase()}`);
|
||||
buildDialogOpen = false;
|
||||
} catch (e) {
|
||||
addNotification('ERR:Failed to request firmware build');
|
||||
logger.error('firmware build request failed', e);
|
||||
} finally {
|
||||
isRequesting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(file: FirmwareFile) {
|
||||
if (file.status !== 'ready' || !file.downloadUrl) {
|
||||
addNotification('ERR:Firmware file is not ready for download');
|
||||
return;
|
||||
}
|
||||
|
||||
// In production, this would be a real URL or trigger a WS download.
|
||||
// For now, just notify.
|
||||
addNotification(`INFO:Downloading ${file.filename}…`);
|
||||
logger.info('download firmware', file.filename, formatFileSize(file.size));
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
firmwareStore.refreshFiles(selectedCountry);
|
||||
addNotification('INFO:Refreshed firmware list');
|
||||
}
|
||||
|
||||
function statusIcon(status: FirmwareBuildStatus) {
|
||||
switch (status) {
|
||||
case 'ready':
|
||||
return CheckCircle2;
|
||||
case 'building':
|
||||
return LoaderCircle;
|
||||
case 'queued':
|
||||
return Clock;
|
||||
case 'failed':
|
||||
return AlertCircle;
|
||||
default:
|
||||
return HardDrive;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
logger.info('firmware page mounted');
|
||||
unsubscribe_fw_version = GlobalEventBus.on('firmware-versions', (data: any) => {
|
||||
latest_versions = data;
|
||||
logger.info(`firmware versions: ${JSON.stringify(latest_versions)}`);
|
||||
});
|
||||
|
||||
sendMessage({
|
||||
type: 'firmware-versions',
|
||||
payload: {}
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
unsubscribe_fw_version?.();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mx-8 flex flex-col gap-6 p-4">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<ArchiveIcon class="h-8 w-8 text-muted-foreground" />
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold">Firmware Requests</h1>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Request and download firmware builds per country
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button variant="outline" onclick={handleRefresh} disabled={isBuildInProgress}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
<Button onclick={openBuildDialog} disabled={isBuildInProgress}>
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Request Build
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Country Tabs + Content -->
|
||||
<Tabs.Root bind:value={selectedCountry}>
|
||||
<Tabs.List class="flex w-full flex-wrap gap-1">
|
||||
{#each FIRMWARE_COUNTRIES as country (country.code)}
|
||||
<Tabs.Trigger value={country.code} class="flex-1">
|
||||
{country.label}
|
||||
{#if allCountries[country.code]?.buildStatus === 'building'}
|
||||
<LoaderCircle class="ml-1 h-3 w-3 animate-spin" />
|
||||
{:else if allCountries[country.code]?.buildStatus === 'ready'}
|
||||
<CheckCircle2 class="ml-1 h-3 w-3 text-emerald-500" />
|
||||
{/if}
|
||||
</Tabs.Trigger>
|
||||
{/each}
|
||||
</Tabs.List>
|
||||
|
||||
{#each FIRMWARE_COUNTRIES as country (country.code)}
|
||||
<Tabs.Content value={country.code}>
|
||||
{@render countryPanel(country.code)}
|
||||
</Tabs.Content>
|
||||
{/each}
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
|
||||
<!-- Country Panel Snippet -->
|
||||
{#snippet countryPanel(countryCode: string)}
|
||||
{@const state = allCountries[countryCode]}
|
||||
{@const files = state?.files ?? []}
|
||||
{@const buildInProgress = state?.buildStatus === 'queued' || state?.buildStatus === 'building'}
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Build Status Card -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title class="flex items-center gap-2">
|
||||
{#if buildInProgress}
|
||||
<LoaderCircle class="h-5 w-5 animate-spin text-blue-500" />
|
||||
{:else if state?.buildStatus === 'ready'}
|
||||
<CheckCircle2 class="h-5 w-5 text-emerald-500" />
|
||||
{:else if state?.buildStatus === 'failed'}
|
||||
<AlertCircle class="h-5 w-5 text-red-500" />
|
||||
{:else}
|
||||
<HardDrive class="h-5 w-5 text-muted-foreground" />
|
||||
{/if}
|
||||
{state?.label ?? countryCode}
|
||||
</Card.Title>
|
||||
<Card.Description>Firmware build status and available files</Card.Description>
|
||||
</div>
|
||||
<Badge variant={statusBadgeVariant(state?.buildStatus ?? 'idle')}>
|
||||
{statusLabel(state?.buildStatus ?? 'idle')}
|
||||
</Badge>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if buildInProgress}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Building firmware…</span>
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>~{state?.files[0]?.version ?? ''}</span
|
||||
>
|
||||
</div>
|
||||
<Progress value={50} class="h-2" />
|
||||
</div>
|
||||
{:else if files.length === 0}
|
||||
<div class="flex flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<ArchiveIcon class="h-12 w-12 text-muted-foreground/40" />
|
||||
<div>
|
||||
<p class="text-sm font-medium">No firmware builds yet</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Click "Request Build" to create a new firmware for {state?.label}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- File Table -->
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Head class="w-[40%]">Filename</Table.Head>
|
||||
<Table.Head class="w-[15%]">Version</Table.Head>
|
||||
<Table.Head class="w-[15%]">Size</Table.Head>
|
||||
<Table.Head class="w-[15%]">Built</Table.Head>
|
||||
<Table.Head class="w-[10%]">Status</Table.Head>
|
||||
<Table.Head class="w-[5%] text-right">Download</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each files as file (file.id)}
|
||||
{@const StatusIcon = statusIcon(file.status)}
|
||||
<Table.Row>
|
||||
<Table.Cell class="font-mono text-sm">
|
||||
{file.filename}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-mono text-xs text-muted-foreground">
|
||||
{file.version}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-mono text-sm">
|
||||
{formatFileSize(file.size)}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-xs text-muted-foreground">
|
||||
{formatBuildDate(file.builtAt)}
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<StatusIcon
|
||||
class="h-3.5 w-3.5 {file.status === 'building'
|
||||
? 'animate-spin text-blue-500'
|
||||
: file.status === 'ready'
|
||||
? 'text-emerald-500'
|
||||
: file.status === 'failed'
|
||||
? 'text-red-500'
|
||||
: 'text-muted-foreground'}"
|
||||
/>
|
||||
<span class="text-xs">{statusLabel(file.status)}</span>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
{#if file.status === 'ready' && file.downloadUrl}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onclick={() => handleDownload(file)}
|
||||
title="Download"
|
||||
>
|
||||
<Download class="h-4 w-4" />
|
||||
</Button>
|
||||
{:else if file.status === 'building'}
|
||||
<Spinner class="scale-75" />
|
||||
{:else}
|
||||
<span class="text-xs text-muted-foreground/50">—</span>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Request Build Dialog -->
|
||||
<Dialog.Root bind:open={buildDialogOpen}>
|
||||
<Dialog.Content class="sm:max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Request Firmware Build</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Request a new firmware build for
|
||||
<span class="font-semibold">{allCountries[selectedCountry]?.label ?? selectedCountry}</span
|
||||
>. You will be notified when the build is ready or need confirmation.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="fw-version">Latest Firmware Version</Label>
|
||||
<Input
|
||||
id="fw-version"
|
||||
bind:value={latest_versions[selectedCountry].version}
|
||||
placeholder="10.00"
|
||||
class="font-mono"
|
||||
disabled={true}
|
||||
/>
|
||||
{#if latest_versions[selectedCountry].version == 'NO_DATA'}
|
||||
<p class="font-sans text-[12px]">
|
||||
Note: this will use latest version provided from source.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="fw-date">Latest Firmware Release Date</Label>
|
||||
<Input
|
||||
id="fw-date"
|
||||
bind:value={latest_versions[selectedCountry].date}
|
||||
placeholder="..."
|
||||
class="font-mono"
|
||||
disabled={true}
|
||||
/>
|
||||
|
||||
{#if latest_versions[selectedCountry].date == 'NO_DATA'}
|
||||
<p class="font-sans text-[12px]">Note: this will forcefully save today as build date.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- -->
|
||||
<div class="grid gap-2">
|
||||
<!-- <Label for="fw-version">Firmware Version</Label>
|
||||
<Input id="fw-version" bind:value={buildVersion} placeholder="v1.2.3" class="font-mono" /> -->
|
||||
<Label>
|
||||
<Checkbox
|
||||
id="ch-set-new-version"
|
||||
checked={increaseVersion}
|
||||
onchange={(e) => {
|
||||
const checkbox = e.target as HTMLInputElement | null;
|
||||
if (checkbox) {
|
||||
increaseVersion = checkbox.checked;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>Set to new version (Optional)</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<!-- <Label for="fw-notes">Notes (optional)</Label>
|
||||
<Input id="fw-notes" bind:value={buildNotes} placeholder="Bug fixes, new features, etc." /> -->
|
||||
<Label>
|
||||
<Checkbox
|
||||
id="ch-save-new-version-bdate"
|
||||
checked={saveThisVersionBuildDate}
|
||||
onchange={(e) => {
|
||||
const checkbox = e.target as HTMLInputElement | null;
|
||||
if (checkbox) {
|
||||
saveThisVersionBuildDate = checkbox.checked;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>Save this version's build date (Optional)</span>
|
||||
</Label>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<!-- <Label for="fw-notes">Notes (optional)</Label>
|
||||
<Input id="fw-notes" bind:value={buildNotes} placeholder="Bug fixes, new features, etc." /> -->
|
||||
<Label>
|
||||
<Checkbox
|
||||
id="ch-save-new-version"
|
||||
checked={saveThisVersionAsLatest}
|
||||
onchange={(e) => {
|
||||
const checkbox = e.target as HTMLInputElement | null;
|
||||
if (checkbox) {
|
||||
saveThisVersionAsLatest = checkbox.checked;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>Save this version as latest (Optional)</span>
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (buildDialogOpen = false)}>Cancel</Button>
|
||||
<Button onclick={confirmRequestBuild} disabled={isRequesting || !buildVersion.trim()}>
|
||||
{#if isRequesting}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
Requesting…
|
||||
{:else}
|
||||
Request Build
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">import { logger } from '$lib/core/utils/logger';
|
||||
<script lang="ts">
|
||||
import { logger } from '$lib/core/utils/logger';
|
||||
|
||||
import { auth } from '$lib/core/stores/auth';
|
||||
import { addNotification } from '$lib/core/stores/noti';
|
||||
|
|
@ -6,9 +7,20 @@
|
|||
import * as Select from '$lib/components/ui/select/index.js';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import Spinner from '$lib/components/ui/spinner/spinner.svelte';
|
||||
import { Upload, X, CheckCircle, AlertCircle, RefreshCw, Play, Video, MonitorPlay, Eye } from '@lucide/svelte/icons';
|
||||
import {
|
||||
Upload,
|
||||
X,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
RefreshCw,
|
||||
Play,
|
||||
Video,
|
||||
MonitorPlay,
|
||||
Eye
|
||||
} from '@lucide/svelte/icons';
|
||||
import * as adb from '$lib/core/adb/adb';
|
||||
import { AdbInstance } from '../../../state.svelte';
|
||||
import ScrcpyDialog from '$lib/components/scrcpy-dialog.svelte';
|
||||
|
||||
const UPLOAD_PROXY_ENDPOINT = '/api/adv-upload';
|
||||
const LIST_PROXY_ENDPOINT = '/api/adv-list';
|
||||
|
|
@ -78,11 +90,15 @@
|
|||
let pushProgress = $state({ current: 0, total: 0, name: '', percent: 0 });
|
||||
let connecting = $state(false);
|
||||
|
||||
let showScreenMirror = $state(false);
|
||||
|
||||
const anyBusy = $derived(
|
||||
uploadingAll || pushingToMachine || slots.some((s) => s.status === 'uploading')
|
||||
);
|
||||
// How many of the 21 slot names currently have a video on the server.
|
||||
const onServerCount = $derived(slots.filter((s) => serverFilesByName[s.name] !== undefined).length);
|
||||
const onServerCount = $derived(
|
||||
slots.filter((s) => serverFilesByName[s.name] !== undefined).length
|
||||
);
|
||||
|
||||
// Left-accent colour that encodes a slot's state at a glance.
|
||||
function slotAccent(s: Slot): string {
|
||||
|
|
@ -382,7 +398,9 @@
|
|||
await autoLoadAllVideos();
|
||||
} catch (error) {
|
||||
logger.error('[Adv] list server videos error:', error);
|
||||
addNotification(`ERR:Load videos failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||
addNotification(
|
||||
`ERR:Load videos failed: ${error instanceof Error ? error.message : 'unknown'}`
|
||||
);
|
||||
} finally {
|
||||
loadingServerVideos = false;
|
||||
}
|
||||
|
|
@ -421,7 +439,9 @@
|
|||
videoBlobUrls = { ...videoBlobUrls, [name]: URL.createObjectURL(blob) };
|
||||
} catch (error) {
|
||||
logger.error('[Adv] fetch server video error:', error);
|
||||
addNotification(`ERR:Load "${name}" failed: ${error instanceof Error ? error.message : 'unknown'}`);
|
||||
addNotification(
|
||||
`ERR:Load "${name}" failed: ${error instanceof Error ? error.message : 'unknown'}`
|
||||
);
|
||||
} finally {
|
||||
loadingVideoName = null;
|
||||
}
|
||||
|
|
@ -448,7 +468,9 @@
|
|||
<div class="mx-auto flex max-w-6xl flex-col gap-4 px-8 py-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
||||
<div
|
||||
class="flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10 text-primary"
|
||||
>
|
||||
<Video class="h-6 w-6" />
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -459,7 +481,8 @@
|
|||
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Badge variant={isAdbConnected ? 'default' : 'secondary'} class="gap-1">
|
||||
<span class="h-1.5 w-1.5 rounded-full {isAdbConnected ? 'bg-white' : 'bg-muted-foreground'}"
|
||||
<span
|
||||
class="h-1.5 w-1.5 rounded-full {isAdbConnected ? 'bg-white' : 'bg-muted-foreground'}"
|
||||
></span>
|
||||
{isAdbConnected ? 'Machine connected' : 'Machine offline'}
|
||||
</Badge>
|
||||
|
|
@ -490,6 +513,10 @@
|
|||
{/if}
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" onclick={() => (showScreenMirror = true)}>
|
||||
Show Android Screen
|
||||
</Button>
|
||||
|
||||
<Select.Root type="single" bind:value={selectedCountry}>
|
||||
<Select.Trigger class="h-9 w-40">
|
||||
{COUNTRIES.find((c) => c.value === selectedCountry)?.label || 'Country'}
|
||||
|
|
@ -546,7 +573,7 @@
|
|||
</div>
|
||||
{#if onServerCount > 0}
|
||||
<Badge
|
||||
class="gap-1 whitespace-nowrap border-green-500/40 bg-green-500/15 text-green-600 hover:bg-green-500/15 dark:text-green-400"
|
||||
class="gap-1 border-green-500/40 bg-green-500/15 whitespace-nowrap text-green-600 hover:bg-green-500/15 dark:text-green-400"
|
||||
>
|
||||
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span>
|
||||
{onServerCount} on server
|
||||
|
|
@ -622,7 +649,9 @@
|
|||
{slot.width ?? '?'}×{slot.height ?? '?'} · exp {MENU_SPEC.width}×{MENU_SPEC.height}
|
||||
</Badge>
|
||||
{/if}
|
||||
<span class="text-[10px] text-muted-foreground">{formatBytes(slot.file.size)}</span>
|
||||
<span class="text-[10px] text-muted-foreground"
|
||||
>{formatBytes(slot.file.size)}</span
|
||||
>
|
||||
</div>
|
||||
{#if slot.error}
|
||||
<p
|
||||
|
|
@ -809,4 +838,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ScrcpyDialog bind:open={showScreenMirror} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,12 @@
|
|||
import { deviceCredentialManager } from '$lib/core/adb/deviceCredManager';
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { adbWriter, isAdbWriterAlive, wasAndroidSocketEverAlive, sendToAndroid } from '$lib/core/stores/adbWriter';
|
||||
import {
|
||||
adbWriter,
|
||||
isAdbWriterAlive,
|
||||
wasAndroidSocketEverAlive,
|
||||
sendToAndroid
|
||||
} from '$lib/core/stores/adbWriter';
|
||||
import { adbReconnect } from '$lib/core/stores/adbReconnectStore';
|
||||
import { AdbInstance } from '../../../state.svelte';
|
||||
import {
|
||||
|
|
@ -227,7 +232,14 @@
|
|||
await loadBrewDataFromConnectedAdb();
|
||||
}
|
||||
} catch (e: any) {
|
||||
addNotification(`ERROR:${e}`);
|
||||
// addNotification(`ERROR:${e}`);
|
||||
GlobalEventBus.emit('announce', {
|
||||
title: 'Device is busy',
|
||||
subtitle: 'Expect `adb` or some related program has been using the device.',
|
||||
message: 'Help: kill the process & try again',
|
||||
buttonText: 'Ok',
|
||||
type: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -274,7 +286,7 @@
|
|||
}
|
||||
if (e instanceof AdbDaemonWebUsbDevice.DeviceBusyError) {
|
||||
addNotification(
|
||||
'ERR:Device is already in use by another program, please close the program and try again'
|
||||
'ERR:Device is already in use by another program, check if machine has been installed `adb`, kill the process and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue