feat(ui): wire Logo native loading progress

This commit is contained in:
2026-09-06 00:00:19 +08:00
parent 41fcd65464
commit 09e80e5052
6 changed files with 421 additions and 86 deletions
@@ -1,4 +1,4 @@
import { _decorator, Component, Label, Node, Prefab, Sprite, UITransform, instantiate, isValid, sys } from 'cc';
import { _decorator, assetManager, Component, Label, Node, Prefab, ProgressBar, Sprite, instantiate, isValid, sys } from 'cc';
import { DEBUG } from 'cc/env';
import { HttpConfigFetcher } from '../../framework/config/remote-config-fetcher.ts';
import type { NativeHost } from '../../framework/config/sources/native-settings.ts';
@@ -6,7 +6,7 @@ import type { RuntimeConfig } from '../../framework/config/runtime-config.ts';
import type { PageModel } from '../../framework/presentation/ui-contracts.ts';
import { createLocalLoginHost, type LocalLoginHost } from './local-login-host.ts';
import type { LocalLoginEvidence } from './local-login-transport.ts';
import { CocosLocalLoginView, createCocosFrameScheduler, requireNode } from './local-login-view.ts';
import { CocosLocalLoginView, createAssetLoadTask, createCocosFrameScheduler, requireNode } from './local-login-view.ts';
const { ccclass, property } = _decorator;
const MINIMUM_LOADING_MS = 500;
@@ -26,7 +26,7 @@ export class LocalPlatformLogin extends Component {
private host: LocalLoginHost | null = null;
private view: CocosLocalLoginView | null = null;
private spinner: Node | null = null;
private resourceProgress: ProgressBar | null = null;
private loginInstance: Node | null = null;
private stopped = false;
private firstDrawAt: number | null = null;
@@ -50,11 +50,11 @@ export class LocalPlatformLogin extends Component {
resultLabel: this.required(this.resultLabel, 'resultLabel') });
this.required(this.loadingPrefab, 'loadingPrefab');
this.required(this.loginPrefab, 'loginPrefab');
this.spinner = requireNode(loadingRoot, 'Layer614_Loading/group-40/载入动画');
const logo = requireNode(loadingRoot, 'Layer1_Logo');
this.resourceProgress = requireNode(logo, 'group-1/Spirit254').getComponent(ProgressBar);
if (this.resourceProgress === null) throw new Error('Layer1_Logo/group-1/Spirit254 requires ProgressBar');
for (const path of ['group-1/Spirit3103', 'group-1/Spirit3104', 'group-32']) requireNode(logo, path).active = false;
this.validateSprites(loadingRoot);
if (this.spinner.getComponent(UITransform) === null || this.spinner.getComponent(Sprite) === null) {
throw new Error('Loading spinner requires UITransform and Sprite');
}
stopButton.on(Node.EventType.TOUCH_END, this.stopTouch);
this.view.activate('loading');
const firstDraw = this.nextDraw().then(() => {
@@ -118,11 +118,21 @@ export class LocalPlatformLogin extends Component {
private async loadResources(): Promise<void> {
if (this.stopped) throw new Error('Local login resources cancelled');
const prefab = this.required(this.loginPrefab, 'loginPrefab');
const serializedPrefab = this.required(this.loginPrefab, 'loginPrefab');
const loading = this.required(this.loadingPrefab, 'loadingPrefab');
if (!isValid(prefab) || !isValid(prefab.data) || !isValid(loading) || !isValid(loading.data)) {
if (!isValid(serializedPrefab) || !isValid(serializedPrefab.data) || !isValid(loading) || !isValid(loading.data)) {
throw new Error('Local login serialized Prefab dependency is invalid');
}
const progress = this.required(this.resourceProgress, 'resource ProgressBar');
const task = createAssetLoadTask<Prefab>((onProgress, onComplete) => {
assetManager.loadAny(serializedPrefab.uuid, onProgress, (error, asset) => onComplete(error, asset as Prefab));
}, value => { progress.progress = value; });
this.pending.add(task.cancel);
let prefab: Prefab;
try { prefab = await task.promise; }
finally { this.pending.delete(task.cancel); }
if (this.stopped) throw new Error('Local login resources cancelled');
if (!isValid(prefab) || !isValid(prefab.data)) throw new Error('Loaded login Prefab dependency is invalid');
const instance = instantiate(prefab);
this.loginInstance = instance;
instance.parent = this.required(this.loginRoot, 'loginRoot');
@@ -164,10 +174,6 @@ export class LocalPlatformLogin extends Component {
});
}
update(deltaSeconds: number): void {
if (!this.stopped && this.spinner !== null && this.spinner.activeInHierarchy) this.spinner.angle -= deltaSeconds * 180;
}
private release(): void {
this.stopped = true;
for (const cancel of [...this.pending]) cancel();
@@ -206,6 +212,6 @@ export class LocalPlatformLogin extends Component {
getDiagnostics() {
return Object.freeze({ events: Object.freeze([...this.evidence]), snapshot: this.host?.getSnapshot(),
pageModel: this.pageModel, stopped: this.stopped, pendingCount: this.pending.size,
firstDrawAt: this.firstDrawAt, spinnerAngle: this.spinner?.angle });
firstDrawAt: this.firstDrawAt, resourceProgress: this.resourceProgress?.progress });
}
}
@@ -6,6 +6,48 @@ interface ViewReferences {
statusOverlay: Node; statusLabel: Label; resultLabel: Label;
}
export interface AssetLoadTask<T> {
readonly promise: Promise<T>;
cancel(): void;
}
type AssetLoadStarter<T> = (
onProgress: (completed: number, total: number) => void,
onComplete: (error: unknown, asset?: T) => void,
) => void;
/** Owns loader callback lifetime so stop/failure cannot revive the login flow. */
export function createAssetLoadTask<T>(start: AssetLoadStarter<T>, setProgress: (progress: number) => void): AssetLoadTask<T> {
let settled = false;
let resolveTask!: (asset: T) => void;
let rejectTask!: (error: unknown) => void;
const promise = new Promise<T>((resolve, reject) => { resolveTask = resolve; rejectTask = reject; });
const reject = (error: unknown) => {
if (settled) return;
settled = true;
rejectTask(error);
};
try {
setProgress(0);
start((completed, total) => {
if (settled) return;
if (!Number.isInteger(completed) || !Number.isInteger(total) || completed < 0 || total <= 0 || completed > total) {
reject(new Error(`Invalid asset load progress: completed=${completed}, total=${total}`));
return;
}
try { setProgress(completed / total); }
catch (error) { reject(error); }
}, (error, asset) => {
if (settled) return;
if (error !== null && error !== undefined) { reject(error); return; }
if (asset === null || asset === undefined) { reject(new Error('Asset loader completed without an asset')); return; }
settled = true;
resolveTask(asset);
});
} catch (error) { reject(error); }
return { promise, cancel() { reject(new Error('Asset load cancelled')); } };
}
export function requireNode(root: Node, path: string): Node {
const node = root.getChildByPath(path);
if (node === null) throw new Error(`Local login binding missing node: ${path}`);