mirror of
https://github.com/moeru-ai/airi.git
synced 2026-10-30 19:34:10 +08:00
fix(stage-tamagotchi-kirie): connect Android QR scanner
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, OnboardingStepAnalyticsNotice, StartupOverlay, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useStartupResourceTimeout } from '@proj-airi/stage-ui/composables/use-startup-resource-timeout'
|
||||
import { getHostWebSocketConnector } from '@proj-airi/stage-ui/libs/host-websocket-bridge'
|
||||
import { usePiniaSynced } from '@proj-airi/stage-ui/libs/pinia'
|
||||
import { initializeAnalytics, isAnalyticsAvailableInBuild } from '@proj-airi/stage-ui/libs/product-signals'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
@@ -28,8 +29,6 @@ import { toast, Toaster } from 'vue-sonner'
|
||||
|
||||
import OnboardingPermissionsStep from './components/onboarding/step-permissions.vue'
|
||||
|
||||
import { getHostWebSocketConnector } from './modules/websocket-bridge'
|
||||
|
||||
const contextBridgeStore = useContextBridgeStore()
|
||||
const authStore = useAuthStore()
|
||||
const i18n = useI18n()
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
import { CapacitorBarcodeScanner, CapacitorBarcodeScannerTypeHint } from '@capacitor/barcode-scanner'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { parseServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr'
|
||||
import { probeServerChannelQrPayload } from '@proj-airi/stage-ui/libs/server-channel-qr-probe'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { GhostButton } from '@proj-airi/ui'
|
||||
import { shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { probeServerChannelQrPayload } from '../../../modules/server-channel-qr-probe'
|
||||
|
||||
const { t } = useI18n()
|
||||
const serverChannelStore = useModsServerChannelStore()
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import type { AiriAndroidBarcodeScanResult } from '../../shared/eventa'
|
||||
|
||||
import { airiAndroidBarcodeEventIds } from '../../shared/eventa'
|
||||
import { invokeAndroidEventa } from './android-permissions'
|
||||
|
||||
export async function scanAndroidBarcode(scanInstructions: string) {
|
||||
const result = await invokeAndroidEventa<AiriAndroidBarcodeScanResult>(
|
||||
airiAndroidBarcodeEventIds.scan,
|
||||
{ scanInstructions },
|
||||
)
|
||||
|
||||
if (result.error)
|
||||
throw new Error(result.error)
|
||||
if (!result.ScanResult)
|
||||
throw new Error('The barcode scanner returned no result')
|
||||
|
||||
return result.ScanResult
|
||||
}
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
import type {
|
||||
AiriAndroidPermission,
|
||||
AiriAndroidPermissionPayload,
|
||||
AiriAndroidPermissionSnapshot,
|
||||
} from '../../shared/eventa'
|
||||
|
||||
@@ -69,7 +68,7 @@ function getAndroidEventaChannel() {
|
||||
return channel
|
||||
}
|
||||
|
||||
function invokeAndroidEventa<Response>(eventId: string, content: AiriAndroidPermissionPayload): Promise<Response> {
|
||||
export function invokeAndroidEventa<Response>(eventId: string, content: unknown): Promise<Response> {
|
||||
const channel = getAndroidEventaChannel()
|
||||
const invokeId = crypto.randomUUID()
|
||||
return new Promise<Response>((resolve, reject) => {
|
||||
|
||||
+7
-3
@@ -8,8 +8,10 @@ import { computed, shallowRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ServerChannelQrCard from './server-channel-qr-card.vue'
|
||||
import ServerChannelQrScanner from './server-channel-qr-scanner.vue'
|
||||
|
||||
import { useServerChannelSettingsStore } from '../../../stores/settings/server-channel'
|
||||
import { isAndroidRenderer } from '../../../window-context'
|
||||
|
||||
const serverChannelSettingsStore = useServerChannelSettingsStore()
|
||||
const { authToken, hostname, lastApplyError, tlsConfig } = storeToRefs(serverChannelSettingsStore)
|
||||
@@ -29,7 +31,7 @@ const exposeToAll = computed({
|
||||
},
|
||||
})
|
||||
|
||||
const showDesktopServerControls = computed(() => isStageTamagotchi())
|
||||
const showDesktopServerControls = computed(() => isStageTamagotchi() && !isAndroidRenderer())
|
||||
const authTokenInput = shallowRef(authToken.value)
|
||||
const authTokenInputDebounced = refDebounced(authTokenInput, 500)
|
||||
const authTokenVisible = shallowRef(false)
|
||||
@@ -57,7 +59,7 @@ watch(authTokenInputDebounced, (value) => {
|
||||
>
|
||||
{{ lastApplyError }}
|
||||
</Callout>
|
||||
<ConnectionSettings server-address-disabled>
|
||||
<ConnectionSettings :server-address-disabled="!isAndroidRenderer()">
|
||||
<template #before-server-address>
|
||||
<FieldCheckbox
|
||||
v-if="showDesktopServerControls"
|
||||
@@ -109,12 +111,14 @@ watch(authTokenInputDebounced, (value) => {
|
||||
</div>
|
||||
|
||||
<FieldCheckbox
|
||||
v-if="!isAndroidRenderer()"
|
||||
v-model="websocketTlsEnabled"
|
||||
:label="t('settings.websocket-secure-enabled.title')"
|
||||
:description="t('settings.websocket-secure-enabled.description')"
|
||||
/>
|
||||
|
||||
<ServerChannelQrCard />
|
||||
<ServerChannelQrScanner v-if="isAndroidRenderer()" />
|
||||
<ServerChannelQrCard v-else />
|
||||
</template>
|
||||
</ConnectionSettings>
|
||||
</div>
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { parseServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr'
|
||||
import { probeServerChannelQrPayload } from '@proj-airi/stage-ui/libs/server-channel-qr-probe'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { GhostButton } from '@proj-airi/ui'
|
||||
import { shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { scanAndroidBarcode } from '../../../host-context/android-barcode-scanner'
|
||||
|
||||
const { t } = useI18n()
|
||||
const serverChannelStore = useModsServerChannelStore()
|
||||
const scanning = shallowRef(false)
|
||||
const errorMessage = shallowRef('')
|
||||
|
||||
async function scanServerChannelQrCode() {
|
||||
scanning.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const scanResult = await scanAndroidBarcode(t('settings.pages.connection.qr-scan.instructions'))
|
||||
const payload = parseServerChannelQrPayload(scanResult)
|
||||
const url = await probeServerChannelQrPayload(payload)
|
||||
|
||||
serverChannelStore.websocketAuthToken = payload.authToken
|
||||
serverChannelStore.websocketUrl = url
|
||||
toast.success(t('settings.pages.connection.qr-scan.success.title'), {
|
||||
description: t('settings.pages.connection.qr-scan.success.description', { url }),
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
errorMessage.value = errorMessageFrom(error) ?? t('settings.pages.connection.qr-scan.errors.failed')
|
||||
toast.error(t('settings.pages.connection.qr-scan.errors.title'), {
|
||||
description: errorMessage.value,
|
||||
})
|
||||
}
|
||||
finally {
|
||||
scanning.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start justify-between gap-3">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm text-neutral-900 font-medium dark:text-neutral-100">
|
||||
{{ t('settings.pages.connection.qr-scan.title') }}
|
||||
</div>
|
||||
<p class="m-0 text-xs text-neutral-500 leading-5 dark:text-neutral-400">
|
||||
{{ t('settings.pages.connection.qr-scan.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<GhostButton
|
||||
:loading="scanning"
|
||||
:label="t('settings.pages.connection.qr-scan.action')"
|
||||
@click="scanServerChannelQrCode"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -95,6 +95,20 @@ export const airiAndroidPermissionEventIds = {
|
||||
request: 'eventa:invoke:airi:android:permission:request',
|
||||
} as const
|
||||
|
||||
export interface AiriAndroidBarcodeScanPayload {
|
||||
scanInstructions: string
|
||||
}
|
||||
|
||||
export interface AiriAndroidBarcodeScanResult {
|
||||
ScanResult?: string
|
||||
error?: string
|
||||
format?: number
|
||||
}
|
||||
|
||||
export const airiAndroidBarcodeEventIds = {
|
||||
scan: 'eventa:invoke:airi:android:barcode:scan',
|
||||
} as const
|
||||
|
||||
export const electronStartTrackMousePosition = defineInvokeEventa('eventa:invoke:electron:start-tracking-mouse-position')
|
||||
export const electronStartDraggingWindow = defineInvokeEventa('eventa:invoke:electron:start-dragging-window')
|
||||
|
||||
|
||||
+16
-10
@@ -13,24 +13,29 @@ type HostBridgeEvent
|
||||
| { kind: 'error', id: string, message: string }
|
||||
| { kind: 'close', id: string, code?: number, reason?: string }
|
||||
|
||||
interface WebkitHostBridge {
|
||||
messageHandlers?: {
|
||||
airiHostBridge?: {
|
||||
postMessage: (payload: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
AiriHostBridge?: {
|
||||
postMessage: (payload: string) => void
|
||||
}
|
||||
webkit?: {
|
||||
messageHandlers?: {
|
||||
airiHostBridge?: {
|
||||
postMessage: (payload: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
__airiHostBridge?: {
|
||||
onNativeMessage?: (payload: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getWebkitHostBridge() {
|
||||
return (window as unknown as { webkit?: WebkitHostBridge }).webkit?.messageHandlers?.airiHostBridge
|
||||
}
|
||||
|
||||
const connections = new Map<string, HostBridgeConnection>()
|
||||
|
||||
function postBridgeMessage(command: HostBridgeCommand) {
|
||||
@@ -39,8 +44,9 @@ function postBridgeMessage(command: HostBridgeCommand) {
|
||||
return
|
||||
}
|
||||
|
||||
if (window.webkit?.messageHandlers?.airiHostBridge) {
|
||||
window.webkit.messageHandlers.airiHostBridge.postMessage(JSON.stringify(command))
|
||||
const webkitHostBridge = getWebkitHostBridge()
|
||||
if (webkitHostBridge) {
|
||||
webkitHostBridge.postMessage(JSON.stringify(command))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,7 +155,7 @@ function createCloseBeforeOpenError(event: Extract<HostBridgeEvent, { kind: 'clo
|
||||
}
|
||||
|
||||
export function getHostWebSocketConnector(url: string): ClientConnector<string> | undefined {
|
||||
if (!window.AiriHostBridge && !window.webkit?.messageHandlers?.airiHostBridge) {
|
||||
if (!window.AiriHostBridge && !getWebkitHostBridge()) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-chan
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { Client, createTextProtocolConnector, WebSocketEventSource } from '@proj-airi/server-sdk'
|
||||
|
||||
import { getHostWebSocketConnector } from './websocket-bridge'
|
||||
import { getHostWebSocketConnector } from './host-websocket-bridge'
|
||||
|
||||
export async function probeServerChannelQrPayload(payload: ServerChannelQrPayload) {
|
||||
if (!payload.urls.some(url => getHostWebSocketConnector(url))) {
|
||||
Reference in New Issue
Block a user