Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
4bc857bacThis file is pinned to the selected release. Download the original to open it in your design tool.
import type { AppBarMode } from 'chat-status-bar'
import type { DrawerButtonViewSpec } from 'drawer'
import type { FaceState } from 'face-state'
import {
FaceMainTemplate,
FaceView,
type FaceViewBehavior,
type FaceViewParams,
type FaceViewTemplateCtor,
} from 'face-view'
import type { HandAnimationName } from 'hands'
import {
MINI_APP_BAR_HEIGHT,
type MiniAppContext,
type MiniAppDefinition,
type MiniAppInstance,
MiniAppRegistry,
type MiniAppRegistryCapability,
} from 'mini-app'
import { createMiniAppViewport, MiniAppLauncher } from 'mini-app-launcher'
import type {
ApplicationDictionary,
Application as PiuApplication,
Container as PiuContainer,
Content as PiuContent,
} from 'piu/MC'
import { Application, Container } from 'piu/MC'
export type AppControllerParams = FaceViewParams
type GlobalWithApplication = typeof globalThis & {
application?: PiuApplication
}
type MiniAppScreen = 'face' | 'launcher' | 'app'
type ActiveMiniApp = {
definition: MiniAppDefinition
content: PiuContainer
dispose?: () => void
token: object
}
type MiniAppContentBehavior = {
onDispose?: (content: PiuContainer) => void
}
type AppBarBehavior = {
onAppBarMode?: (content: PiuContent, mode: AppBarMode) => void
onMiniAppAvailability?: (content: PiuContent, available: boolean) => void
}
function unwrapMiniAppInstance(created: PiuContainer | MiniAppInstance): MiniAppInstance {
if (created instanceof Container) return { content: created }
if (created && created.content instanceof Container) return created
throw new TypeError('mini app create must return a Piu Container or MiniAppInstance')
}
export class AppController extends Behavior {
#application: PiuApplication | null = null
#activeMiniApp: ActiveMiniApp | null = null
#drawerActionKeys = new Set<string>()
#miniAppRegistry = new MiniAppRegistry()
#miniAppScreen: MiniAppScreen = 'face'
#view: PiuContainer | null = null
#viewBehavior: FaceViewBehavior | null = null
onCreate(application: PiuApplication, data: AppControllerParams) {
this.#application = application
const main = data.main ?? new FaceMainTemplate(data, { anchor: 'MAIN' })
const viewData: FaceViewParams = { ...data, main }
this.showView(FaceView, viewData)
this.#miniAppRegistry.subscribe(() => this.#onMiniAppRegistryChanged())
this.#setAppBarMode({ kind: 'face' })
this.#syncMiniAppAvailability()
}
get application(): PiuApplication {
return this.#application as PiuApplication
}
get miniApps(): MiniAppRegistryCapability {
return this.#miniAppRegistry
}
showView(ViewTemplate: FaceViewTemplateCtor, data: FaceViewParams) {
const app = this.#application
if (!app) return
const view = new ViewTemplate(data)
app.empty()
app.add(view)
this.#view = view
this.#viewBehavior = view.behavior as FaceViewBehavior
}
update(_interval: number, faceState: FaceState): void {
if (!this.#view || !this.#viewBehavior) return
this.#viewBehavior.onFaceUpdate?.(this.#view, faceState)
}
addEffect(effect: PiuContent, key?: string): void {
this.#viewBehavior?.addEffect?.(effect, key)
}
removeEffect(effect: PiuContent): void {
this.#viewBehavior?.removeEffect?.(effect)
}
removeEffectByKey(key: string): void {
this.#viewBehavior?.removeEffectByKey?.(key)
}
setFace(face: PiuContainer): void {
this.#viewBehavior?.setFace?.(face)
}
setHandAnimation(animation: HandAnimationName): void {
this.#viewBehavior?.setHandAnimation?.(animation)
}
setFaceMotionEnabled(enabled: boolean): void {
this.#viewBehavior?.setFaceMotionEnabled?.(enabled)
}
setMain(content: PiuContainer): void {
this.#viewBehavior?.setMain?.(content)
}
showFace(): void {
if (this.#activeMiniApp) {
this.exitMiniApp()
return
}
this.#miniAppScreen = 'face'
this.#viewBehavior?.showFace?.()
this.#setAppBarMode({ kind: 'face' })
}
showMiniAppLauncher(): void {
if (this.#miniAppRegistry.list().length === 0) return
if (this.#activeMiniApp) this.exitMiniApp()
this.closeDrawer()
this.#miniAppScreen = 'launcher'
this.#renderMiniAppLauncher()
}
launchMiniApp(id: string): boolean {
const definition = this.#miniAppRegistry.get(id)
const app = this.#application
if (!definition || !app || !this.#viewBehavior) return false
if (this.#activeMiniApp) this.exitMiniApp()
const token = {}
let closeRequested = false
const context: MiniAppContext = Object.freeze({
width: app.width || 320,
height: Math.max(0, (app.height || 240) - MINI_APP_BAR_HEIGHT),
close: () => {
if (this.#activeMiniApp?.token === token) this.exitMiniApp()
else closeRequested = true
},
})
let instance: MiniAppInstance | null = null
let dispose: (() => void) | undefined
try {
instance = unwrapMiniAppInstance(definition.create(context))
dispose = instance.dispose?.bind(instance)
if (closeRequested) {
this.#disposeMiniAppContent(instance.content, dispose)
this.showFace()
return true
}
const viewport = createMiniAppViewport(instance.content)
this.#activeMiniApp = {
definition,
content: instance.content,
dispose,
token,
}
this.#miniAppScreen = 'app'
this.#setAppBarMode({ kind: 'app', title: definition.title })
this.#viewBehavior.setMain(viewport)
return true
} catch (error) {
trace(`[MiniApp] launch failed id=${id} error=${String(error)}\n`)
if (instance) this.#disposeMiniAppContent(instance.content, dispose)
this.#activeMiniApp = null
this.#miniAppScreen = 'launcher'
this.#renderMiniAppLauncher()
return false
}
}
exitMiniApp(): void {
if (!this.#activeMiniApp) return
const active = this.#activeMiniApp
this.#activeMiniApp = null
this.#miniAppScreen = 'face'
this.#viewBehavior?.showFace?.()
this.#setAppBarMode({ kind: 'face' })
this.#disposeMiniAppContent(active.content, active.dispose)
}
setDrawerButtons(buttons: DrawerButtonViewSpec[]): void {
this.#viewBehavior?.setDrawerButtons?.(buttons)
}
addDrawerButton(button: DrawerButtonViewSpec): void {
this.#viewBehavior?.addDrawerButton?.(button)
}
removeDrawerButton(key: string): void {
this.#viewBehavior?.removeDrawerButton?.(key)
}
setDrawerButtonState(key: string, active: boolean): void {
this.#viewBehavior?.setDrawerButtonState?.(key, active)
}
openDrawer(): void {
this.#viewBehavior?.openDrawer?.()
}
closeDrawer(): void {
this.#viewBehavior?.closeDrawer?.()
}
toggleDrawer(): void {
this.#viewBehavior?.toggleDrawer?.()
}
bindDrawerAction(key: string, callback: (value?: string) => void): boolean {
const target = this as unknown as Record<string, unknown>
const ownsKey = this.#drawerActionKeys.has(key)
if (!ownsKey && typeof target[key] === 'function') {
trace(`[AppController] drawer action key collision: ${key}\n`)
return false
}
target[key] = (_content: PiuContent, value?: string) => callback(value)
this.#drawerActionKeys.add(key)
return true
}
unbindDrawerAction(key: string): void {
if (!this.#drawerActionKeys.has(key)) return
delete (this as unknown as Record<string, unknown>)[key]
this.#drawerActionKeys.delete(key)
}
onDrawerToggle(): void {
trace('[AppController] onDrawerToggle\n')
this.toggleDrawer()
}
onDrawerOpen(): void {
trace('[AppController] onDrawerOpen\n')
this.openDrawer()
}
onDrawerClose(): void {
trace('[AppController] onDrawerClose\n')
this.closeDrawer()
this.#application?.distribute('onAppBarReveal')
}
onFaceTouch(): void {
trace('[AppController] onFaceTouch\n')
this.#application?.distribute('onAppBarReveal')
}
onMiniAppLauncher(): void {
this.showMiniAppLauncher()
}
onMiniAppBack(): void {
if (this.#miniAppScreen === 'app') this.exitMiniApp()
else if (this.#miniAppScreen === 'launcher') this.showFace()
}
#renderMiniAppLauncher(): void {
const launcher = new MiniAppLauncher({
apps: this.#miniAppRegistry.list(),
onLaunch: (id) => this.launchMiniApp(id),
})
this.#viewBehavior?.setMain(createMiniAppViewport(launcher))
this.#setAppBarMode({ kind: 'launcher', title: 'ミニアプリ' })
}
#onMiniAppRegistryChanged(): void {
this.#syncMiniAppAvailability()
if (this.#activeMiniApp && !this.#miniAppRegistry.get(this.#activeMiniApp.definition.id)) {
this.exitMiniApp()
return
}
if (this.#miniAppScreen === 'launcher') {
if (this.#miniAppRegistry.list().length === 0) this.showFace()
else this.#renderMiniAppLauncher()
}
}
#syncMiniAppAvailability(): void {
const appBar = this.#viewBehavior?.appBar
if (!appBar) return
const behavior = appBar.behavior as AppBarBehavior | undefined
behavior?.onMiniAppAvailability?.(appBar, this.#miniAppRegistry.list().length > 0)
}
#setAppBarMode(mode: AppBarMode): void {
const appBar = this.#viewBehavior?.appBar
if (!appBar) return
const behavior = appBar.behavior as AppBarBehavior | undefined
behavior?.onAppBarMode?.(appBar, mode)
}
#disposeMiniAppContent(content: PiuContainer, dispose?: () => void): void {
try {
;(content.behavior as MiniAppContentBehavior | undefined)?.onDispose?.(content)
} catch (error) {
trace(`[MiniApp] onDispose failed error=${String(error)}\n`)
}
try {
dispose?.()
} catch (error) {
trace(`[MiniApp] dispose failed error=${String(error)}\n`)
}
}
}
export function createAppControllerApplication(
data: AppControllerParams,
dictionary: Omit<ApplicationDictionary, 'Behavior' | 'contents'> = {},
): AppController {
const existingApplication = (globalThis as GlobalWithApplication).application
if (existingApplication) {
const controller = new AppController()
existingApplication.empty()
existingApplication.behavior = controller
controller.onCreate(existingApplication, data)
return controller
}
const application = new Application(data, {
...dictionary,
contents: [],
Behavior: AppController,
})
return application.behavior as AppController
}