capacitor-lottie-splash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ import type { PluginListenerHandle } from '@capacitor/core';
2
+ export type SplashRenderer = 'ios' | 'android' | 'web';
3
+ export type SplashPhase = 'preparing' | 'playing' | 'holding' | 'hiding' | 'hidden' | 'recovery';
4
+ export type PlaybackOutcome = 'completed' | 'reduced-motion' | 'static-fallback';
5
+ export type SplashTheme = 'light' | 'dark';
6
+ export interface SplashAssets {
7
+ /** A local URL or path to a bundled Lottie composition. */
8
+ animationPath?: string;
9
+ /** Parsed composition data. Prefer this for browser bundlers. */
10
+ animationData?: Record<string, unknown>;
11
+ /** A bundled static poster used while the composition is unavailable. */
12
+ posterPath?: string;
13
+ }
14
+ export interface SplashShowOptions {
15
+ /** The host's already-resolved appearance preference for this presentation. */
16
+ darkMode: boolean;
17
+ assets?: SplashAssets;
18
+ /** Used by the app adapter when a web presentation is deliberately requested. */
19
+ renderer?: Extract<SplashRenderer, 'web'>;
20
+ }
21
+ /** Persist the launch appearance used by Android's system-owned starting window. */
22
+ export interface SplashLaunchThemeOptions {
23
+ darkMode: boolean;
24
+ }
25
+ export interface Presentation {
26
+ presentationId: string;
27
+ renderer: SplashRenderer;
28
+ adopted: boolean;
29
+ }
30
+ export interface PlaybackResult {
31
+ presentationId: string;
32
+ outcome: PlaybackOutcome;
33
+ reason?: string;
34
+ }
35
+ export interface SplashState {
36
+ presentationId?: string;
37
+ phase: SplashPhase;
38
+ visible: boolean;
39
+ playbackSettled: boolean;
40
+ appReady: boolean;
41
+ theme: SplashTheme;
42
+ }
43
+ export interface SplashCapabilities {
44
+ apiVersion: 1;
45
+ pluginVersion: string;
46
+ assetSetVersion?: string;
47
+ coldStartInstalled: boolean;
48
+ }
49
+ export interface StateChangeEvent extends SplashState {
50
+ generation: number;
51
+ }
52
+ export interface LottieSplashPlugin {
53
+ show(options?: SplashShowOptions): Promise<Presentation>;
54
+ setLaunchTheme(options: SplashLaunchThemeOptions): Promise<void>;
55
+ waitForCompletion(options: {
56
+ presentationId: string;
57
+ }): Promise<PlaybackResult>;
58
+ hide(options: {
59
+ presentationId: string;
60
+ }): Promise<void>;
61
+ getState(): Promise<SplashState>;
62
+ getCapabilities(): Promise<SplashCapabilities>;
63
+ addListener(eventName: 'stateChange', listenerFunc: (event: StateChangeEvent) => void): Promise<PluginListenerHandle>;
64
+ }
65
+ //# sourceMappingURL=definitions.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=definitions.js.map
@@ -0,0 +1,5 @@
1
+ import type { LottieSplashPlugin } from './definitions';
2
+ declare const LottieSplash: LottieSplashPlugin;
3
+ export * from './definitions';
4
+ export { LottieSplash };
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { registerPlugin } from '@capacitor/core';
2
+ const LottieSplash = registerPlugin('LottieSplash', {
3
+ web: () => import('./web').then(module => new module.LottieSplashWeb())
4
+ });
5
+ export * from './definitions';
6
+ export { LottieSplash };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ import type { PlaybackOutcome, PlaybackResult, SplashState, SplashTheme } from './definitions';
2
+ export interface StateMachineSnapshot extends SplashState {
3
+ generation: number;
4
+ }
5
+ export declare class SplashStateMachine {
6
+ private generation;
7
+ private playbackResult?;
8
+ private snapshot;
9
+ start(presentationId: string, theme: SplashTheme): StateMachineSnapshot;
10
+ playing(presentationId: string): StateMachineSnapshot;
11
+ settlePlayback(presentationId: string, outcome: PlaybackOutcome, reason?: string): StateMachineSnapshot;
12
+ requestHide(presentationId: string): StateMachineSnapshot;
13
+ recovery(presentationId: string): StateMachineSnapshot;
14
+ hidden(presentationId: string): StateMachineSnapshot;
15
+ getPlaybackResult(presentationId: string): PlaybackResult | undefined;
16
+ current(): StateMachineSnapshot;
17
+ private requireCurrent;
18
+ }
19
+ //# sourceMappingURL=state-machine.d.ts.map
@@ -0,0 +1,79 @@
1
+ export class SplashStateMachine {
2
+ generation = 0;
3
+ playbackResult;
4
+ snapshot = {
5
+ generation: 0,
6
+ phase: 'hidden',
7
+ visible: false,
8
+ playbackSettled: false,
9
+ appReady: false,
10
+ theme: 'light'
11
+ };
12
+ start(presentationId, theme) {
13
+ if (this.snapshot.visible)
14
+ return this.current();
15
+ this.generation += 1;
16
+ this.playbackResult = undefined;
17
+ this.snapshot = {
18
+ generation: this.generation,
19
+ presentationId,
20
+ phase: 'preparing',
21
+ visible: true,
22
+ playbackSettled: false,
23
+ appReady: false,
24
+ theme
25
+ };
26
+ return this.current();
27
+ }
28
+ playing(presentationId) {
29
+ this.requireCurrent(presentationId);
30
+ if (!this.snapshot.playbackSettled)
31
+ this.snapshot.phase = 'playing';
32
+ return this.current();
33
+ }
34
+ settlePlayback(presentationId, outcome, reason) {
35
+ this.requireCurrent(presentationId);
36
+ this.playbackResult = { presentationId, outcome, reason };
37
+ this.snapshot.playbackSettled = true;
38
+ this.snapshot.phase = this.snapshot.appReady ? 'hiding' : 'holding';
39
+ return this.current();
40
+ }
41
+ requestHide(presentationId) {
42
+ this.requireCurrent(presentationId);
43
+ this.snapshot.appReady = true;
44
+ if (this.snapshot.playbackSettled)
45
+ this.snapshot.phase = 'hiding';
46
+ return this.current();
47
+ }
48
+ recovery(presentationId) {
49
+ this.requireCurrent(presentationId);
50
+ if (!this.snapshot.appReady)
51
+ this.snapshot.phase = 'recovery';
52
+ return this.current();
53
+ }
54
+ hidden(presentationId) {
55
+ this.requireCurrent(presentationId);
56
+ this.snapshot = {
57
+ generation: this.generation,
58
+ phase: 'hidden',
59
+ visible: false,
60
+ playbackSettled: true,
61
+ appReady: true,
62
+ theme: this.snapshot.theme
63
+ };
64
+ return this.current();
65
+ }
66
+ getPlaybackResult(presentationId) {
67
+ this.requireCurrent(presentationId);
68
+ return this.playbackResult;
69
+ }
70
+ current() {
71
+ return { ...this.snapshot };
72
+ }
73
+ requireCurrent(presentationId) {
74
+ if (!this.snapshot.presentationId || this.snapshot.presentationId !== presentationId) {
75
+ throw new Error(`Stale splash presentation: ${presentationId}`);
76
+ }
77
+ }
78
+ }
79
+ //# sourceMappingURL=state-machine.js.map
@@ -0,0 +1,31 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { LottieSplashPlugin, PlaybackResult, Presentation, SplashCapabilities, SplashLaunchThemeOptions, SplashShowOptions, SplashState } from './definitions';
3
+ export declare class LottieSplashWeb extends WebPlugin implements LottieSplashPlugin {
4
+ private readonly stateMachine;
5
+ private playbackPromise?;
6
+ private resolvePlayback?;
7
+ private overlay?;
8
+ private progressTimer?;
9
+ private recoveryTimer?;
10
+ show(options: SplashShowOptions): Promise<Presentation>;
11
+ setLaunchTheme(_options: SplashLaunchThemeOptions): Promise<void>;
12
+ waitForCompletion(options: {
13
+ presentationId: string;
14
+ }): Promise<PlaybackResult>;
15
+ hide(options: {
16
+ presentationId: string;
17
+ }): Promise<void>;
18
+ getState(): Promise<SplashState>;
19
+ getCapabilities(): Promise<SplashCapabilities>;
20
+ private createOverlay;
21
+ private playComposition;
22
+ private settle;
23
+ private showLoadingRail;
24
+ private showRecovery;
25
+ private removeOverlay;
26
+ private ensureCurrent;
27
+ private emitState;
28
+ private prefersReducedMotion;
29
+ private backgroundFor;
30
+ }
31
+ //# sourceMappingURL=web.d.ts.map
@@ -0,0 +1,175 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import { SplashStateMachine } from './state-machine';
3
+ const RECOVERY_TIMEOUT_MS = 15_000;
4
+ const PROGRESS_DELAY_MS = 1_000;
5
+ export class LottieSplashWeb extends WebPlugin {
6
+ stateMachine = new SplashStateMachine();
7
+ playbackPromise;
8
+ resolvePlayback;
9
+ overlay;
10
+ progressTimer;
11
+ recoveryTimer;
12
+ async show(options) {
13
+ const state = this.stateMachine.current();
14
+ if (state.visible && state.presentationId)
15
+ return { presentationId: state.presentationId, renderer: 'web', adopted: true };
16
+ const presentationId = crypto.randomUUID();
17
+ this.stateMachine.start(presentationId, options.darkMode ? 'dark' : 'light');
18
+ this.emitState();
19
+ this.playbackPromise = new Promise(resolve => { this.resolvePlayback = resolve; });
20
+ this.createOverlay(options, presentationId);
21
+ return { presentationId, renderer: 'web', adopted: false };
22
+ }
23
+ async setLaunchTheme(_options) {
24
+ // Browser builds have no OS-owned launch window to configure.
25
+ }
26
+ async waitForCompletion(options) {
27
+ this.ensureCurrent(options.presentationId);
28
+ return this.playbackPromise ?? Promise.reject(new Error('No active splash playback'));
29
+ }
30
+ async hide(options) {
31
+ const state = this.stateMachine.requestHide(options.presentationId);
32
+ this.emitState();
33
+ if (!state.playbackSettled)
34
+ await this.waitForCompletion(options);
35
+ this.removeOverlay(options.presentationId);
36
+ }
37
+ async getState() {
38
+ const { generation: _generation, ...state } = this.stateMachine.current();
39
+ return state;
40
+ }
41
+ async getCapabilities() {
42
+ return { apiVersion: 1, pluginVersion: '0.1.0', coldStartInstalled: false };
43
+ }
44
+ createOverlay(options, presentationId) {
45
+ const overlay = document.createElement('section');
46
+ overlay.setAttribute('role', 'status');
47
+ overlay.setAttribute('aria-live', 'polite');
48
+ overlay.setAttribute('aria-label', 'Loading application');
49
+ overlay.dataset.presentationId = presentationId;
50
+ Object.assign(overlay.style, {
51
+ position: 'fixed', inset: '0', zIndex: '2147483647', display: 'grid', placeItems: 'center',
52
+ background: this.backgroundFor(this.stateMachine.current().theme), overflow: 'hidden'
53
+ });
54
+ document.body.append(overlay);
55
+ this.overlay = overlay;
56
+ // A generic package cannot ship HeadCount assets. The host supplies an approved local
57
+ // composition; absent or failed assets leave the opaque branded background in place.
58
+ if (!options.assets?.animationData && !options.assets?.animationPath) {
59
+ this.settle(presentationId, 'static-fallback', 'No local animation asset supplied');
60
+ return;
61
+ }
62
+ if (this.prefersReducedMotion()) {
63
+ this.settle(presentationId, 'reduced-motion', 'Reduced motion is enabled');
64
+ return;
65
+ }
66
+ this.stateMachine.playing(presentationId);
67
+ this.emitState();
68
+ void this.playComposition(options, presentationId);
69
+ }
70
+ async playComposition(options, presentationId) {
71
+ try {
72
+ const lottie = await import('lottie-web');
73
+ const stage = document.createElement('div');
74
+ stage.setAttribute('aria-hidden', 'true');
75
+ stage.style.cssText = 'position:relative;z-index:1;width:min(35vw,35vh);aspect-ratio:1/1;';
76
+ this.overlay?.append(stage);
77
+ const animation = lottie.default.loadAnimation({
78
+ container: stage,
79
+ renderer: 'svg',
80
+ loop: false,
81
+ autoplay: false,
82
+ path: options.assets?.animationPath,
83
+ animationData: options.assets?.animationData
84
+ });
85
+ // Host compositions begin at frame zero. Starting at a fixed legacy offset would
86
+ // bypass their opening motion (for example, HeadCount's scanner sequence).
87
+ animation.addEventListener('DOMLoaded', () => {
88
+ if (this.stateMachine.current().presentationId === presentationId)
89
+ animation.goToAndPlay(0, true);
90
+ });
91
+ animation.addEventListener('complete', () => this.settle(presentationId, 'completed'));
92
+ animation.addEventListener('data_failed', () => this.settle(presentationId, 'static-fallback', 'Animation data failed to load'));
93
+ }
94
+ catch (error) {
95
+ this.settle(presentationId, 'static-fallback', error instanceof Error ? error.message : 'Animation renderer failed');
96
+ }
97
+ }
98
+ settle(presentationId, outcome, reason) {
99
+ try {
100
+ const state = this.stateMachine.settlePlayback(presentationId, outcome, reason);
101
+ const result = this.stateMachine.getPlaybackResult(presentationId);
102
+ if (result)
103
+ this.resolvePlayback?.(result);
104
+ this.emitState();
105
+ if (!state.appReady) {
106
+ this.progressTimer = window.setTimeout(() => this.showLoadingRail(presentationId), PROGRESS_DELAY_MS);
107
+ this.recoveryTimer = window.setTimeout(() => this.showRecovery(presentationId), RECOVERY_TIMEOUT_MS);
108
+ }
109
+ }
110
+ catch {
111
+ // A stale renderer callback is intentionally ignored.
112
+ }
113
+ }
114
+ showLoadingRail(presentationId) {
115
+ const state = this.stateMachine.current();
116
+ if (state.presentationId !== presentationId || state.appReady || state.phase !== 'holding')
117
+ return;
118
+ const rail = document.createElement('div');
119
+ rail.dataset.splashLoadingRail = 'true';
120
+ rail.setAttribute('aria-label', 'Application is still loading');
121
+ const trackColor = state.theme === 'dark' ? 'rgba(255,255,255,.18)' : 'rgba(0,0,0,.14)';
122
+ rail.style.cssText = `position:absolute;top:calc(50% + min(22vw,22vh));left:50%;width:104px;height:3px;overflow:hidden;border-radius:2px;transform:translate(-50%,-50%);background:${trackColor};`;
123
+ const segment = document.createElement('div');
124
+ segment.style.cssText = 'width:37px;height:100%;border-radius:inherit;background:rgba(255,77,77,.9);animation:capacitor-lottie-splash-rail 1.15s ease-in-out infinite;';
125
+ const style = document.createElement('style');
126
+ style.textContent = '@keyframes capacitor-lottie-splash-rail{from{transform:translateX(-37px)}to{transform:translateX(104px)}}';
127
+ rail.append(segment);
128
+ this.overlay?.append(style, rail);
129
+ }
130
+ showRecovery(presentationId) {
131
+ let state;
132
+ try {
133
+ state = this.stateMachine.recovery(presentationId);
134
+ }
135
+ catch {
136
+ return;
137
+ }
138
+ if (state.presentationId !== presentationId || state.appReady)
139
+ return;
140
+ this.overlay?.querySelector('[data-splash-loading-rail]')?.remove();
141
+ const recovery = document.createElement('div');
142
+ recovery.style.cssText = 'position:absolute;bottom:12%;display:grid;gap:12px;place-items:center;color:inherit;font:500 16px system-ui;';
143
+ recovery.textContent = 'Still starting…';
144
+ const retry = document.createElement('button');
145
+ retry.type = 'button';
146
+ retry.textContent = 'Try again';
147
+ retry.onclick = () => window.location.reload();
148
+ recovery.append(retry);
149
+ this.overlay?.append(recovery);
150
+ this.emitState();
151
+ }
152
+ removeOverlay(presentationId) {
153
+ window.clearTimeout(this.progressTimer);
154
+ window.clearTimeout(this.recoveryTimer);
155
+ this.overlay?.remove();
156
+ this.overlay = undefined;
157
+ this.stateMachine.hidden(presentationId);
158
+ this.emitState();
159
+ }
160
+ ensureCurrent(presentationId) {
161
+ const state = this.stateMachine.current();
162
+ if (state.presentationId !== presentationId)
163
+ throw new Error(`Stale splash presentation: ${presentationId}`);
164
+ }
165
+ emitState() {
166
+ this.notifyListeners('stateChange', this.stateMachine.current());
167
+ }
168
+ prefersReducedMotion() {
169
+ return window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
170
+ }
171
+ backgroundFor(theme) {
172
+ return theme === 'dark' ? 'linear-gradient(to right,#1e1e1e,#252525)' : 'linear-gradient(to right,#f0f0f0,#f5f5f5)';
173
+ }
174
+ }
175
+ //# sourceMappingURL=web.js.map