@hkdigital/lib-sveltekit 0.1.20 → 0.1.22
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.
- package/dist/classes/svelte/image/ImageScene.svelte.d.ts +26 -54
- package/dist/classes/svelte/image/ImageScene.svelte.js +75 -208
- package/dist/classes/svelte/image/ImageScene.svelte.js__ +253 -0
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.d.ts +6 -5
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js +55 -39
- package/dist/classes/svelte/image/ImageVariantsLoader.svelte.js--responsive-loading-fails-fix-tried__ +184 -0
- package/dist/util/svelte/index.d.ts +1 -0
- package/dist/util/svelte/index.js +2 -0
- package/dist/util/svelte/loading/loading-tracker.svelte.d.ts +31 -0
- package/dist/util/svelte/loading/loading-tracker.svelte.js +108 -0
- package/dist/widgets/image-box/ImageBox.svelte +11 -7
- package/dist/widgets/image-box/ImageBox.svelte.d.ts +6 -4
- package/dist/widgets/presenter/Presenter.state.svelte.d.ts +17 -0
- package/dist/widgets/presenter/Presenter.state.svelte.js +28 -5
- package/package.json +1 -1
@@ -1,78 +1,50 @@
|
|
1
|
-
/**
|
2
|
-
* @typedef {object} SourceConfig
|
3
|
-
* // property ...
|
4
|
-
*/
|
5
|
-
/**
|
6
|
-
* @typedef {object} ImageSource
|
7
|
-
* @property {string} label
|
8
|
-
* @property {ImageLoader} imageLoader
|
9
|
-
* @property {ImageMeta} [imageMeta]
|
10
|
-
*/
|
11
1
|
export default class ImageScene {
|
12
|
-
state: string;
|
13
|
-
loaded: boolean;
|
14
|
-
destroy(): void;
|
15
2
|
/**
|
16
|
-
*
|
17
|
-
* - Uses an ImageLoader instance to load image data from network
|
3
|
+
* Define an image to be managed by this scene
|
18
4
|
*
|
19
|
-
* @param {object}
|
20
|
-
* @param {string}
|
21
|
-
* @param {ImageMeta|ImageMeta[]}
|
5
|
+
* @param {object} params
|
6
|
+
* @param {string} params.label - Unique identifier for the image
|
7
|
+
* @param {ImageMeta|ImageMeta[]} params.imageMeta - Image metadata (single or variants)
|
22
8
|
*/
|
23
9
|
defineImage({ label, imageMeta }: {
|
24
10
|
label: string;
|
25
11
|
imageMeta: ImageMeta | ImageMeta[];
|
26
12
|
}): void;
|
27
13
|
/**
|
28
|
-
* Start loading all
|
14
|
+
* Start loading all defined images
|
29
15
|
*/
|
30
16
|
load(): void;
|
31
17
|
/**
|
32
|
-
*
|
18
|
+
* Unload all images and free resources
|
33
19
|
*/
|
34
|
-
|
35
|
-
totalBytesLoaded: number;
|
36
|
-
totalSize: number;
|
37
|
-
sourcesLoaded: number;
|
38
|
-
numberOfSources: number;
|
39
|
-
};
|
20
|
+
unload(): void;
|
40
21
|
/**
|
41
|
-
* Get
|
22
|
+
* Get the image loader for a specific label
|
42
23
|
*
|
43
|
-
* @param {string} label
|
44
|
-
*
|
45
|
-
* @returns {ImageLoader}
|
24
|
+
* @param {string} label - Image identifier
|
25
|
+
* @returns {ImageLoader|null} The image loader or null if not found
|
46
26
|
*/
|
47
|
-
getImageLoader(label: string): ImageLoader;
|
27
|
+
getImageLoader(label: string): ImageLoader | null;
|
48
28
|
/**
|
49
|
-
*
|
50
|
-
*
|
51
|
-
* @param {string} label
|
52
|
-
*
|
53
|
-
* @returns {ImageMeta}
|
29
|
+
* Check if the scene is currently loading
|
54
30
|
*/
|
55
|
-
|
31
|
+
get loading(): boolean;
|
56
32
|
/**
|
57
|
-
*
|
58
|
-
*
|
59
|
-
* @param {string} label
|
60
|
-
*
|
61
|
-
* @note the objectURL should be revoked when no longer used
|
62
|
-
*
|
63
|
-
* @returns {string}
|
33
|
+
* Check if all images in the scene are loaded
|
64
34
|
*/
|
65
|
-
|
35
|
+
get loaded(): boolean;
|
36
|
+
/**
|
37
|
+
* Get the current loading progress
|
38
|
+
*/
|
39
|
+
get progress(): {
|
40
|
+
bytesLoaded: number;
|
41
|
+
size: number;
|
42
|
+
loaded: boolean;
|
43
|
+
};
|
66
44
|
#private;
|
67
45
|
}
|
68
|
-
export type ImageMeta = import("./typedef.js").ImageMeta;
|
69
46
|
/**
|
70
|
-
*
|
47
|
+
* A simple class to preload and manage images for a scene
|
71
48
|
*/
|
72
|
-
export type
|
73
|
-
|
74
|
-
label: string;
|
75
|
-
imageLoader: ImageLoader;
|
76
|
-
imageMeta?: ImageMeta;
|
77
|
-
};
|
78
|
-
import ImageLoader from './ImageLoader.svelte.js';
|
49
|
+
export type ImageMeta = import("../../../config/typedef.js").ImageMeta;
|
50
|
+
import { ImageLoader } from './index.js';
|
@@ -1,253 +1,120 @@
|
|
1
|
-
/** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
|
2
|
-
|
3
|
-
import * as expect from '../../../util/expect/index.js';
|
4
|
-
|
5
|
-
import {
|
6
|
-
LoadingStateMachine,
|
7
|
-
STATE_INITIAL,
|
8
|
-
STATE_LOADING,
|
9
|
-
STATE_UNLOADING,
|
10
|
-
STATE_LOADED,
|
11
|
-
STATE_CANCELLED,
|
12
|
-
STATE_ERROR,
|
13
|
-
LOAD,
|
14
|
-
// CANCEL,
|
15
|
-
ERROR,
|
16
|
-
LOADED,
|
17
|
-
UNLOAD,
|
18
|
-
INITIAL
|
19
|
-
} from '../loading-state-machine/index.js';
|
20
|
-
|
21
|
-
import ImageLoader from './ImageLoader.svelte.js';
|
22
|
-
|
23
|
-
/**
|
24
|
-
* @typedef {object} SourceConfig
|
25
|
-
* // property ...
|
26
|
-
*/
|
27
|
-
|
28
1
|
/**
|
29
|
-
*
|
30
|
-
*
|
31
|
-
* @
|
32
|
-
* @property {ImageMeta} [imageMeta]
|
2
|
+
* A simple class to preload and manage images for a scene
|
3
|
+
*
|
4
|
+
* @typedef {import('../../../config/typedef.js').ImageMeta} ImageMeta
|
33
5
|
*/
|
6
|
+
import { ImageLoader } from './index.js';
|
34
7
|
|
35
8
|
export default class ImageScene {
|
36
|
-
|
37
|
-
|
38
|
-
// @note this exported state is set by $effect's
|
39
|
-
state = $state(STATE_INITIAL);
|
40
|
-
|
41
|
-
// @note this exported state is set by $effect's
|
42
|
-
loaded = $derived.by(() => {
|
43
|
-
return this.state === STATE_LOADED;
|
44
|
-
});
|
45
|
-
|
46
|
-
/** @type {ImageSource[]} */
|
47
|
-
#imageSources = $state([]);
|
48
|
-
|
49
|
-
#progress = $derived.by(() => {
|
50
|
-
// console.log('update progress');
|
51
|
-
|
52
|
-
let totalSize = 0;
|
53
|
-
let totalBytesLoaded = 0;
|
54
|
-
let sourcesLoaded = 0;
|
55
|
-
|
56
|
-
const sources = this.#imageSources;
|
57
|
-
const numberOfSources = sources.length;
|
58
|
-
|
59
|
-
for (let j = 0; j < numberOfSources; j++) {
|
60
|
-
const source = sources[j];
|
61
|
-
const { imageLoader } = source;
|
62
|
-
|
63
|
-
const { bytesLoaded, size, loaded } = imageLoader.progress;
|
64
|
-
|
65
|
-
totalSize += size;
|
66
|
-
totalBytesLoaded += bytesLoaded;
|
67
|
-
|
68
|
-
if (loaded) {
|
69
|
-
sourcesLoaded++;
|
70
|
-
}
|
71
|
-
} // end for
|
72
|
-
|
73
|
-
return {
|
74
|
-
totalBytesLoaded,
|
75
|
-
totalSize,
|
76
|
-
sourcesLoaded,
|
77
|
-
numberOfSources
|
78
|
-
};
|
79
|
-
});
|
9
|
+
/** @type {Map<string, ImageLoader>} */
|
10
|
+
#loaders = $state.raw(new Map());
|
80
11
|
|
81
|
-
/**
|
82
|
-
|
83
|
-
*/
|
84
|
-
constructor() {
|
85
|
-
const state = this.#state;
|
86
|
-
|
87
|
-
$effect(() => {
|
88
|
-
if (state.current === STATE_LOADING) {
|
89
|
-
// console.log(
|
90
|
-
// 'progress',
|
91
|
-
// JSON.stringify($state.snapshot(this.#progress))
|
92
|
-
// );
|
93
|
-
|
94
|
-
const { sourcesLoaded, numberOfSources } = this.#progress;
|
95
|
-
|
96
|
-
if (sourcesLoaded === numberOfSources) {
|
97
|
-
// console.log(`All [${numberOfSources}] sources loaded`);
|
98
|
-
this.#state.send(LOADED);
|
99
|
-
}
|
100
|
-
}
|
101
|
-
});
|
12
|
+
/** @type {boolean} */
|
13
|
+
#loading = $state(false);
|
102
14
|
|
103
|
-
|
104
|
-
|
105
|
-
case STATE_LOADING:
|
106
|
-
{
|
107
|
-
// console.log('ImageScene:loading');
|
108
|
-
this.#startLoading();
|
109
|
-
}
|
110
|
-
break;
|
111
|
-
|
112
|
-
case STATE_UNLOADING:
|
113
|
-
{
|
114
|
-
// console.log('ImageScene:unloading');
|
115
|
-
// this.#startUnLoading();
|
116
|
-
}
|
117
|
-
break;
|
118
|
-
|
119
|
-
case STATE_LOADED:
|
120
|
-
{
|
121
|
-
// console.log('ImageScene:loaded');
|
122
|
-
// TODO
|
123
|
-
// this.#abortLoading = null;
|
124
|
-
}
|
125
|
-
break;
|
126
|
-
|
127
|
-
case STATE_CANCELLED:
|
128
|
-
{
|
129
|
-
// console.log('ImageScene:cancelled');
|
130
|
-
// TODO
|
131
|
-
}
|
132
|
-
break;
|
133
|
-
|
134
|
-
case STATE_ERROR:
|
135
|
-
{
|
136
|
-
console.log('ImageScene:error', state.error);
|
137
|
-
}
|
138
|
-
break;
|
139
|
-
} // end switch
|
140
|
-
|
141
|
-
this.state = state.current;
|
142
|
-
});
|
143
|
-
}
|
15
|
+
/** @type {boolean} */
|
16
|
+
#loaded = $state(false);
|
144
17
|
|
145
|
-
|
146
|
-
|
147
|
-
// TODO: Unload ImageLoaders?
|
148
|
-
}
|
18
|
+
/** @type {{ bytesLoaded: number, size: number, loaded: boolean }} */
|
19
|
+
#progress = $state({ bytesLoaded: 0, size: 0, loaded: false });
|
149
20
|
|
150
21
|
/**
|
151
|
-
*
|
152
|
-
* - Uses an ImageLoader instance to load image data from network
|
22
|
+
* Define an image to be managed by this scene
|
153
23
|
*
|
154
|
-
* @param {object}
|
155
|
-
* @param {string}
|
156
|
-
* @param {ImageMeta|ImageMeta[]}
|
24
|
+
* @param {object} params
|
25
|
+
* @param {string} params.label - Unique identifier for the image
|
26
|
+
* @param {ImageMeta|ImageMeta[]} params.imageMeta - Image metadata (single or variants)
|
157
27
|
*/
|
158
28
|
defineImage({ label, imageMeta }) {
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
const imageLoader = new ImageLoader({ imageMeta });
|
164
|
-
|
165
|
-
this.#imageSources.push({ label, imageLoader, imageMeta });
|
29
|
+
// Create loader for this image
|
30
|
+
const loader = new ImageLoader({ imageMeta });
|
31
|
+
this.#loaders.set(label, loader);
|
166
32
|
}
|
167
33
|
|
168
34
|
/**
|
169
|
-
* Start loading all
|
35
|
+
* Start loading all defined images
|
170
36
|
*/
|
171
37
|
load() {
|
172
|
-
this.#
|
38
|
+
if (this.#loading || this.#loaded) return;
|
173
39
|
|
174
|
-
|
40
|
+
this.#loading = true;
|
175
41
|
|
176
|
-
|
177
|
-
|
42
|
+
// Start loading all images
|
43
|
+
for (const loader of this.#loaders.values()) {
|
44
|
+
loader.load();
|
178
45
|
}
|
179
|
-
}
|
180
46
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
// for (const { audioLoader } of this.#memorySources) {
|
185
|
-
// audioLoader.load();
|
186
|
-
// }
|
187
|
-
}
|
47
|
+
// Track overall loading progress
|
48
|
+
$effect(() => {
|
49
|
+
if (this.#loaders.size === 0) return;
|
188
50
|
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
51
|
+
let totalBytesLoaded = 0;
|
52
|
+
let totalSize = 0;
|
53
|
+
let allLoaded = true;
|
54
|
+
|
55
|
+
for (const loader of this.#loaders.values()) {
|
56
|
+
const progress = loader.progress;
|
57
|
+
|
58
|
+
totalBytesLoaded += progress.bytesLoaded;
|
59
|
+
totalSize += progress.size || 0;
|
60
|
+
|
61
|
+
if (!progress.loaded) {
|
62
|
+
allLoaded = false;
|
63
|
+
}
|
200
64
|
}
|
201
|
-
}
|
202
65
|
|
203
|
-
|
66
|
+
this.#progress = {
|
67
|
+
bytesLoaded: totalBytesLoaded,
|
68
|
+
size: totalSize,
|
69
|
+
loaded: allLoaded
|
70
|
+
};
|
71
|
+
|
72
|
+
if (allLoaded && this.#loading) {
|
73
|
+
this.#loaded = true;
|
74
|
+
}
|
75
|
+
});
|
204
76
|
}
|
205
77
|
|
206
78
|
/**
|
207
|
-
*
|
79
|
+
* Unload all images and free resources
|
208
80
|
*/
|
209
|
-
|
210
|
-
|
81
|
+
unload() {
|
82
|
+
for (const loader of this.#loaders.values()) {
|
83
|
+
loader.unload();
|
84
|
+
}
|
85
|
+
|
86
|
+
this.#loading = false;
|
87
|
+
this.#loaded = false;
|
211
88
|
}
|
212
89
|
|
213
90
|
/**
|
214
|
-
* Get
|
215
|
-
*
|
216
|
-
* @param {string} label
|
91
|
+
* Get the image loader for a specific label
|
217
92
|
*
|
218
|
-
* @
|
93
|
+
* @param {string} label - Image identifier
|
94
|
+
* @returns {ImageLoader|null} The image loader or null if not found
|
219
95
|
*/
|
220
96
|
getImageLoader(label) {
|
221
|
-
|
222
|
-
|
223
|
-
return source.imageLoader;
|
97
|
+
return this.#loaders.get(label) || null;
|
224
98
|
}
|
225
99
|
|
226
100
|
/**
|
227
|
-
*
|
228
|
-
*
|
229
|
-
* @param {string} label
|
230
|
-
*
|
231
|
-
* @returns {ImageMeta}
|
101
|
+
* Check if the scene is currently loading
|
232
102
|
*/
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
return source.imageMeta;
|
103
|
+
get loading() {
|
104
|
+
return this.#loading;
|
237
105
|
}
|
238
106
|
|
239
107
|
/**
|
240
|
-
*
|
241
|
-
*
|
242
|
-
* @param {string} label
|
243
|
-
*
|
244
|
-
* @note the objectURL should be revoked when no longer used
|
245
|
-
*
|
246
|
-
* @returns {string}
|
108
|
+
* Check if all images in the scene are loaded
|
247
109
|
*/
|
248
|
-
|
249
|
-
|
110
|
+
get loaded() {
|
111
|
+
return this.#loaded;
|
112
|
+
}
|
250
113
|
|
251
|
-
|
114
|
+
/**
|
115
|
+
* Get the current loading progress
|
116
|
+
*/
|
117
|
+
get progress() {
|
118
|
+
return this.#progress;
|
252
119
|
}
|
253
120
|
}
|
@@ -0,0 +1,253 @@
|
|
1
|
+
/** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
|
2
|
+
|
3
|
+
import * as expect from '$lib/util/expect/index.js';
|
4
|
+
|
5
|
+
import {
|
6
|
+
LoadingStateMachine,
|
7
|
+
STATE_INITIAL,
|
8
|
+
STATE_LOADING,
|
9
|
+
STATE_UNLOADING,
|
10
|
+
STATE_LOADED,
|
11
|
+
STATE_CANCELLED,
|
12
|
+
STATE_ERROR,
|
13
|
+
LOAD,
|
14
|
+
// CANCEL,
|
15
|
+
ERROR,
|
16
|
+
LOADED,
|
17
|
+
UNLOAD,
|
18
|
+
INITIAL
|
19
|
+
} from '$lib/classes/svelte/loading-state-machine/index.js';
|
20
|
+
|
21
|
+
import ImageLoader from '$lib/classes/svelte/image/ImageLoader.svelte.js';
|
22
|
+
|
23
|
+
/**
|
24
|
+
* @typedef {object} SourceConfig
|
25
|
+
* // property ...
|
26
|
+
*/
|
27
|
+
|
28
|
+
/**
|
29
|
+
* @typedef {object} ImageSource
|
30
|
+
* @property {string} label
|
31
|
+
* @property {ImageLoader} imageLoader
|
32
|
+
* @property {ImageMeta} [imageMeta]
|
33
|
+
*/
|
34
|
+
|
35
|
+
export default class ImageScene {
|
36
|
+
#state = new LoadingStateMachine();
|
37
|
+
|
38
|
+
// @note this exported state is set by $effect's
|
39
|
+
state = $state(STATE_INITIAL);
|
40
|
+
|
41
|
+
// @note this exported state is set by $effect's
|
42
|
+
loaded = $derived.by(() => {
|
43
|
+
return this.state === STATE_LOADED;
|
44
|
+
});
|
45
|
+
|
46
|
+
/** @type {ImageSource[]} */
|
47
|
+
#imageSources = $state([]);
|
48
|
+
|
49
|
+
#progress = $derived.by(() => {
|
50
|
+
// console.log('update progress');
|
51
|
+
|
52
|
+
let totalSize = 0;
|
53
|
+
let totalBytesLoaded = 0;
|
54
|
+
let sourcesLoaded = 0;
|
55
|
+
|
56
|
+
const sources = this.#imageSources;
|
57
|
+
const numberOfSources = sources.length;
|
58
|
+
|
59
|
+
for (let j = 0; j < numberOfSources; j++) {
|
60
|
+
const source = sources[j];
|
61
|
+
const { imageLoader } = source;
|
62
|
+
|
63
|
+
const { bytesLoaded, size, loaded } = imageLoader.progress;
|
64
|
+
|
65
|
+
totalSize += size;
|
66
|
+
totalBytesLoaded += bytesLoaded;
|
67
|
+
|
68
|
+
if (loaded) {
|
69
|
+
sourcesLoaded++;
|
70
|
+
}
|
71
|
+
} // end for
|
72
|
+
|
73
|
+
return {
|
74
|
+
totalBytesLoaded,
|
75
|
+
totalSize,
|
76
|
+
sourcesLoaded,
|
77
|
+
numberOfSources
|
78
|
+
};
|
79
|
+
});
|
80
|
+
|
81
|
+
/**
|
82
|
+
* Construct ImageScene
|
83
|
+
*/
|
84
|
+
constructor() {
|
85
|
+
const state = this.#state;
|
86
|
+
|
87
|
+
$effect(() => {
|
88
|
+
if (state.current === STATE_LOADING) {
|
89
|
+
// console.log(
|
90
|
+
// 'progress',
|
91
|
+
// JSON.stringify($state.snapshot(this.#progress))
|
92
|
+
// );
|
93
|
+
|
94
|
+
const { sourcesLoaded, numberOfSources } = this.#progress;
|
95
|
+
|
96
|
+
if (sourcesLoaded === numberOfSources) {
|
97
|
+
// console.log(`All [${numberOfSources}] sources loaded`);
|
98
|
+
this.#state.send(LOADED);
|
99
|
+
}
|
100
|
+
}
|
101
|
+
});
|
102
|
+
|
103
|
+
$effect(() => {
|
104
|
+
switch (state.current) {
|
105
|
+
case STATE_LOADING:
|
106
|
+
{
|
107
|
+
// console.log('ImageScene:loading');
|
108
|
+
this.#startLoading();
|
109
|
+
}
|
110
|
+
break;
|
111
|
+
|
112
|
+
case STATE_UNLOADING:
|
113
|
+
{
|
114
|
+
// console.log('ImageScene:unloading');
|
115
|
+
// this.#startUnLoading();
|
116
|
+
}
|
117
|
+
break;
|
118
|
+
|
119
|
+
case STATE_LOADED:
|
120
|
+
{
|
121
|
+
// console.log('ImageScene:loaded');
|
122
|
+
// TODO
|
123
|
+
// this.#abortLoading = null;
|
124
|
+
}
|
125
|
+
break;
|
126
|
+
|
127
|
+
case STATE_CANCELLED:
|
128
|
+
{
|
129
|
+
// console.log('ImageScene:cancelled');
|
130
|
+
// TODO
|
131
|
+
}
|
132
|
+
break;
|
133
|
+
|
134
|
+
case STATE_ERROR:
|
135
|
+
{
|
136
|
+
console.log('ImageScene:error', state.error);
|
137
|
+
}
|
138
|
+
break;
|
139
|
+
} // end switch
|
140
|
+
|
141
|
+
this.state = state.current;
|
142
|
+
});
|
143
|
+
}
|
144
|
+
|
145
|
+
destroy() {
|
146
|
+
// TODO: disconnect all image sources?
|
147
|
+
// TODO: Unload ImageLoaders?
|
148
|
+
}
|
149
|
+
|
150
|
+
/**
|
151
|
+
* Add image source
|
152
|
+
* - Uses an ImageLoader instance to load image data from network
|
153
|
+
*
|
154
|
+
* @param {object} _
|
155
|
+
* @param {string} _.label
|
156
|
+
* @param {ImageMeta|ImageMeta[]} _.imageMeta
|
157
|
+
*/
|
158
|
+
defineImage({ label, imageMeta }) {
|
159
|
+
expect.notEmptyString(label);
|
160
|
+
|
161
|
+
// expect.notEmptyString(url);
|
162
|
+
|
163
|
+
const imageLoader = new ImageLoader({ imageMeta });
|
164
|
+
|
165
|
+
this.#imageSources.push({ label, imageLoader, imageMeta });
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Start loading all image sources
|
170
|
+
*/
|
171
|
+
load() {
|
172
|
+
this.#state.send(LOAD);
|
173
|
+
|
174
|
+
// FIXME: in unit test when moved to startloading it hangs!
|
175
|
+
|
176
|
+
for (const { imageLoader } of this.#imageSources) {
|
177
|
+
imageLoader.load();
|
178
|
+
}
|
179
|
+
}
|
180
|
+
|
181
|
+
async #startLoading() {
|
182
|
+
// console.log('#startLoading');
|
183
|
+
// FIXME: in unit test when moved to startloading it hangs!
|
184
|
+
// for (const { audioLoader } of this.#memorySources) {
|
185
|
+
// audioLoader.load();
|
186
|
+
// }
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* Get Image source
|
191
|
+
*
|
192
|
+
* @param {string} label
|
193
|
+
*
|
194
|
+
* @returns {ImageSource}
|
195
|
+
*/
|
196
|
+
#getImageSource(label) {
|
197
|
+
for (const source of this.#imageSources) {
|
198
|
+
if (label === source.label) {
|
199
|
+
return source;
|
200
|
+
}
|
201
|
+
}
|
202
|
+
|
203
|
+
throw new Error(`Source [${label}] has not been defined`);
|
204
|
+
}
|
205
|
+
|
206
|
+
/**
|
207
|
+
* Get image scene loading progress
|
208
|
+
*/
|
209
|
+
get progress() {
|
210
|
+
return this.#progress;
|
211
|
+
}
|
212
|
+
|
213
|
+
/**
|
214
|
+
* Get an image loader
|
215
|
+
*
|
216
|
+
* @param {string} label
|
217
|
+
*
|
218
|
+
* @returns {ImageLoader}
|
219
|
+
*/
|
220
|
+
getImageLoader(label) {
|
221
|
+
const source = this.#getImageSource(label);
|
222
|
+
|
223
|
+
return source.imageLoader;
|
224
|
+
}
|
225
|
+
|
226
|
+
/**
|
227
|
+
* Get object URL that can be used as src parameter of an HTML image
|
228
|
+
*
|
229
|
+
* @param {string} label
|
230
|
+
*
|
231
|
+
* @returns {ImageMeta}
|
232
|
+
*/
|
233
|
+
getImageMeta(label) {
|
234
|
+
const source = this.#getImageSource(label);
|
235
|
+
|
236
|
+
return source.imageMeta;
|
237
|
+
}
|
238
|
+
|
239
|
+
/**
|
240
|
+
* Get object URL that can be used as src parameter of an HTML image
|
241
|
+
*
|
242
|
+
* @param {string} label
|
243
|
+
*
|
244
|
+
* @note the objectURL should be revoked when no longer used
|
245
|
+
*
|
246
|
+
* @returns {string}
|
247
|
+
*/
|
248
|
+
getObjectURL(label) {
|
249
|
+
const source = this.#getImageSource(label);
|
250
|
+
|
251
|
+
return source.imageLoader.getObjectURL();
|
252
|
+
}
|
253
|
+
}
|
@@ -1,8 +1,5 @@
|
|
1
1
|
export default class ImageVariantsLoader {
|
2
|
-
|
3
|
-
* @param {ImageMeta[]} imagesMeta
|
4
|
-
*/
|
5
|
-
constructor(imagesMeta: ImageMeta[], { devicePixelRatio }?: {
|
2
|
+
constructor(imagesMeta: any, { devicePixelRatio }?: {
|
6
3
|
devicePixelRatio?: number;
|
7
4
|
});
|
8
5
|
/**
|
@@ -28,7 +25,11 @@ export default class ImageVariantsLoader {
|
|
28
25
|
* @returns {string|null}
|
29
26
|
*/
|
30
27
|
getObjectURL(): string | null;
|
31
|
-
get progress():
|
28
|
+
get progress(): {
|
29
|
+
bytesLoaded: any;
|
30
|
+
size: any;
|
31
|
+
loaded: boolean;
|
32
|
+
};
|
32
33
|
/**
|
33
34
|
* Get optimal image variant
|
34
35
|
*
|
@@ -1,11 +1,7 @@
|
|
1
1
|
/** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
|
2
2
|
|
3
|
-
// import * as expect from '../../../util/expect/index.js';
|
4
|
-
|
5
3
|
import { calculateEffectiveWidth } from '../../../util/image/index.js';
|
6
|
-
|
7
4
|
import { untrack } from 'svelte';
|
8
|
-
|
9
5
|
import ImageLoader from './ImageLoader.svelte.js';
|
10
6
|
|
11
7
|
export default class ImageVariantsLoader {
|
@@ -21,22 +17,32 @@ export default class ImageVariantsLoader {
|
|
21
17
|
/** @type {ImageLoader|null} */
|
22
18
|
#imageLoader = $state(null);
|
23
19
|
|
24
|
-
|
25
|
-
|
26
|
-
return this.#imageLoader.progress;
|
27
|
-
} else {
|
28
|
-
return { bytesLoaded: 0, size: 0, loaded: false };
|
29
|
-
}
|
30
|
-
});
|
20
|
+
/** @type {boolean} */
|
21
|
+
#isObjectUrlCreated = $state(false);
|
31
22
|
|
32
|
-
|
23
|
+
/** @type {boolean} */
|
24
|
+
#variantLoaded = $state(false);
|
25
|
+
|
26
|
+
/** @type {Object} */
|
27
|
+
#baseProgress = $state({ bytesLoaded: 0, size: 0, loaded: false });
|
33
28
|
|
34
|
-
/**
|
35
|
-
* @param {ImageMeta[]} imagesMeta
|
36
|
-
*/
|
37
29
|
constructor(imagesMeta, { devicePixelRatio = 1 } = {}) {
|
38
30
|
this.#devicePixelRatio = devicePixelRatio ?? 1;
|
39
31
|
this.#imagesMeta = [...imagesMeta].sort((a, b) => a.width - b.width);
|
32
|
+
|
33
|
+
// Track the imageLoader's progress
|
34
|
+
$effect(() => {
|
35
|
+
if (this.#imageLoader) {
|
36
|
+
// Store the base progress from the loader
|
37
|
+
this.#baseProgress = this.#imageLoader.progress;
|
38
|
+
|
39
|
+
// When the base image is loaded, we can say variant is loaded
|
40
|
+
// if an object URL has been created
|
41
|
+
if (this.#baseProgress.loaded && this.#isObjectUrlCreated) {
|
42
|
+
this.#variantLoaded = true;
|
43
|
+
}
|
44
|
+
}
|
45
|
+
});
|
40
46
|
}
|
41
47
|
|
42
48
|
/**
|
@@ -67,7 +73,11 @@ export default class ImageVariantsLoader {
|
|
67
73
|
) {
|
68
74
|
this.#imageVariant = newVariant;
|
69
75
|
|
70
|
-
//
|
76
|
+
// Reset our loaded flags when changing variants
|
77
|
+
this.#isObjectUrlCreated = false;
|
78
|
+
this.#variantLoaded = false;
|
79
|
+
|
80
|
+
// Clean up and create a new loader
|
71
81
|
if (this.#imageLoader?.initial) {
|
72
82
|
this.#imageLoader.unload();
|
73
83
|
}
|
@@ -81,7 +91,7 @@ export default class ImageVariantsLoader {
|
|
81
91
|
}
|
82
92
|
|
83
93
|
get loaded() {
|
84
|
-
return this.#
|
94
|
+
return this.#variantLoaded;
|
85
95
|
}
|
86
96
|
|
87
97
|
get variant() {
|
@@ -96,31 +106,38 @@ export default class ImageVariantsLoader {
|
|
96
106
|
* @returns {string|null}
|
97
107
|
*/
|
98
108
|
getObjectURL() {
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
//
|
110
|
-
|
111
|
-
|
112
|
-
//
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
109
|
+
if (!this.#imageLoader) {
|
110
|
+
return null;
|
111
|
+
}
|
112
|
+
|
113
|
+
const blob = this.#imageLoader.getBlob();
|
114
|
+
|
115
|
+
if (!blob) {
|
116
|
+
return null;
|
117
|
+
}
|
118
|
+
|
119
|
+
// Get the URL
|
120
|
+
const url = URL.createObjectURL(blob);
|
121
|
+
|
122
|
+
// Mark that we've successfully created an object URL
|
123
|
+
this.#isObjectUrlCreated = true;
|
124
|
+
|
125
|
+
// If the underlying loader is also loaded, we can consider
|
126
|
+
// the whole variant loaded
|
127
|
+
if (this.#baseProgress.loaded) {
|
128
|
+
this.#variantLoaded = true;
|
129
|
+
}
|
118
130
|
|
119
131
|
return url;
|
120
132
|
}
|
121
133
|
|
122
134
|
get progress() {
|
123
|
-
return
|
135
|
+
// Only return loaded:true in the progress when we're fully loaded
|
136
|
+
return {
|
137
|
+
bytesLoaded: this.#baseProgress.bytesLoaded,
|
138
|
+
size: this.#baseProgress.size,
|
139
|
+
loaded: this.#variantLoaded
|
140
|
+
};
|
124
141
|
}
|
125
142
|
|
126
143
|
/**
|
@@ -141,7 +158,6 @@ export default class ImageVariantsLoader {
|
|
141
158
|
const imagesMeta = this.#imagesMeta;
|
142
159
|
|
143
160
|
// Find the smallest image that's larger than our required width
|
144
|
-
|
145
161
|
const optimal = imagesMeta.find(
|
146
162
|
(current) => current.width >= requiredWidth
|
147
163
|
);
|
@@ -149,4 +165,4 @@ export default class ImageVariantsLoader {
|
|
149
165
|
// Fall back to the largest image if nothing is big enough
|
150
166
|
return optimal || imagesMeta[imagesMeta.length - 1];
|
151
167
|
}
|
152
|
-
}
|
168
|
+
}
|
@@ -0,0 +1,184 @@
|
|
1
|
+
/** @typedef {import('./typedef.js').ImageMeta} ImageMeta */
|
2
|
+
|
3
|
+
// import * as expect from '$lib/util/expect/index.js';
|
4
|
+
|
5
|
+
import { calculateEffectiveWidth } from '$lib/util/image/index.js';
|
6
|
+
|
7
|
+
import { untrack } from 'svelte';
|
8
|
+
|
9
|
+
import ImageLoader from './ImageLoader.svelte.js';
|
10
|
+
|
11
|
+
export default class ImageVariantsLoader {
|
12
|
+
/** @type {number} */
|
13
|
+
#devicePixelRatio;
|
14
|
+
|
15
|
+
/** @type {ImageMeta[]} */
|
16
|
+
#imagesMeta;
|
17
|
+
|
18
|
+
/** @type {ImageMeta|null} */
|
19
|
+
#imageVariant = $state(null);
|
20
|
+
|
21
|
+
/** @type {ImageLoader|null} */
|
22
|
+
#imageLoader = $state(null);
|
23
|
+
|
24
|
+
/** @type {boolean} */
|
25
|
+
#manuallyCheckedLoaded = $state(false);
|
26
|
+
|
27
|
+
// Create a custom progress object that we control fully
|
28
|
+
#customProgress = $state({
|
29
|
+
bytesLoaded: 0,
|
30
|
+
size: 0,
|
31
|
+
loaded: false
|
32
|
+
});
|
33
|
+
|
34
|
+
#progress = $derived.by(() => {
|
35
|
+
return this.#customProgress;
|
36
|
+
});
|
37
|
+
|
38
|
+
// Derive loaded state from our custom progress
|
39
|
+
#loaded = $derived.by(() => this.#customProgress.loaded);
|
40
|
+
|
41
|
+
/**
|
42
|
+
* @param {ImageMeta[]} imagesMeta
|
43
|
+
*/
|
44
|
+
constructor(imagesMeta, { devicePixelRatio = 1 } = {}) {
|
45
|
+
this.#devicePixelRatio = devicePixelRatio ?? 1;
|
46
|
+
this.#imagesMeta = [...imagesMeta].sort((a, b) => a.width - b.width);
|
47
|
+
|
48
|
+
// Track when the imageLoader's progress changes
|
49
|
+
$effect(() => {
|
50
|
+
if (this.#imageLoader) {
|
51
|
+
const loaderProgress = this.#imageLoader.progress;
|
52
|
+
|
53
|
+
// Update our custom progress with the loader's values,
|
54
|
+
// but maintain our own loaded state
|
55
|
+
this.#customProgress = {
|
56
|
+
bytesLoaded: loaderProgress.bytesLoaded,
|
57
|
+
size: loaderProgress.size,
|
58
|
+
loaded: this.#manuallyCheckedLoaded && loaderProgress.loaded
|
59
|
+
};
|
60
|
+
}
|
61
|
+
});
|
62
|
+
}
|
63
|
+
|
64
|
+
/**
|
65
|
+
* Set new optimal image variant or keep current
|
66
|
+
*
|
67
|
+
* @param {object} params
|
68
|
+
* @param {number} [params.containerWidth] Container width
|
69
|
+
* @param {number} [params.containerHeight] Container height
|
70
|
+
* @param {'cover'|'contain'|'fill'} [params.fit='contain'] Fit mode
|
71
|
+
*/
|
72
|
+
updateOptimalImageMeta({ containerWidth, containerHeight, fit = 'contain' }) {
|
73
|
+
const baseImage = this.#imagesMeta[0];
|
74
|
+
const imageAspectRatio = baseImage.width / baseImage.height;
|
75
|
+
|
76
|
+
const effectiveWidth = calculateEffectiveWidth({
|
77
|
+
containerWidth,
|
78
|
+
containerHeight,
|
79
|
+
imageAspectRatio,
|
80
|
+
fit
|
81
|
+
});
|
82
|
+
|
83
|
+
const newVariant = this.getOptimalImageMeta(effectiveWidth);
|
84
|
+
|
85
|
+
if (
|
86
|
+
!newVariant ||
|
87
|
+
!this.#imageVariant ||
|
88
|
+
newVariant.width > this.#imageVariant.width
|
89
|
+
) {
|
90
|
+
this.#imageVariant = newVariant;
|
91
|
+
|
92
|
+
// Reset loaded state when changing variants
|
93
|
+
this.#manuallyCheckedLoaded = false;
|
94
|
+
this.#customProgress = {
|
95
|
+
bytesLoaded: 0,
|
96
|
+
size: 0,
|
97
|
+
loaded: false
|
98
|
+
};
|
99
|
+
|
100
|
+
// Create and start loader here directly when variant changes
|
101
|
+
if (this.#imageLoader?.initial) {
|
102
|
+
this.#imageLoader.unload();
|
103
|
+
}
|
104
|
+
|
105
|
+
this.#imageLoader = new ImageLoader({
|
106
|
+
imageMeta: newVariant
|
107
|
+
});
|
108
|
+
|
109
|
+
this.#imageLoader.load();
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
get loaded() {
|
114
|
+
return this.#loaded;
|
115
|
+
}
|
116
|
+
|
117
|
+
get variant() {
|
118
|
+
return this.#imageVariant;
|
119
|
+
}
|
120
|
+
|
121
|
+
/**
|
122
|
+
* Get object URL that can be used as src parameter of an HTML image
|
123
|
+
*
|
124
|
+
* @note the objectURL should be revoked when no longer used
|
125
|
+
*
|
126
|
+
* @returns {string|null}
|
127
|
+
*/
|
128
|
+
getObjectURL() {
|
129
|
+
// First check if the loader is actually loaded
|
130
|
+
if (!this.#imageLoader?.loaded) {
|
131
|
+
return null;
|
132
|
+
}
|
133
|
+
|
134
|
+
const blob = this.#imageLoader.getBlob();
|
135
|
+
|
136
|
+
if (!blob) {
|
137
|
+
return null;
|
138
|
+
}
|
139
|
+
|
140
|
+
// Successfully got a blob, so we're definitely loaded
|
141
|
+
if (!this.#manuallyCheckedLoaded) {
|
142
|
+
this.#manuallyCheckedLoaded = true;
|
143
|
+
|
144
|
+
// Update our custom progress to indicate we're loaded
|
145
|
+
this.#customProgress = {
|
146
|
+
bytesLoaded: this.#customProgress.bytesLoaded,
|
147
|
+
size: this.#customProgress.size,
|
148
|
+
loaded: true
|
149
|
+
};
|
150
|
+
}
|
151
|
+
|
152
|
+
return URL.createObjectURL(blob);
|
153
|
+
}
|
154
|
+
|
155
|
+
get progress() {
|
156
|
+
return this.#progress;
|
157
|
+
}
|
158
|
+
|
159
|
+
/**
|
160
|
+
* Get optimal image variant
|
161
|
+
*
|
162
|
+
* @param {number} containerWidth
|
163
|
+
*
|
164
|
+
* @returns {ImageMeta|null}
|
165
|
+
*/
|
166
|
+
getOptimalImageMeta(containerWidth) {
|
167
|
+
if (!containerWidth) {
|
168
|
+
return null;
|
169
|
+
}
|
170
|
+
|
171
|
+
// Calculate the required width (container * DPR)
|
172
|
+
const requiredWidth = containerWidth * this.#devicePixelRatio;
|
173
|
+
|
174
|
+
const imagesMeta = this.#imagesMeta;
|
175
|
+
|
176
|
+
// Find the smallest image that's larger than our required width
|
177
|
+
const optimal = imagesMeta.find(
|
178
|
+
(current) => current.width >= requiredWidth
|
179
|
+
);
|
180
|
+
|
181
|
+
// Fall back to the largest image if nothing is big enough
|
182
|
+
return optimal || imagesMeta[imagesMeta.length - 1];
|
183
|
+
}
|
184
|
+
} // end class
|
@@ -0,0 +1,31 @@
|
|
1
|
+
/**
|
2
|
+
* Simple utility for tracking combined loading progress across multiple components
|
3
|
+
*
|
4
|
+
* @typedef {Object} LoadingProgress
|
5
|
+
* @property {number} bytesLoaded - Number of bytes loaded
|
6
|
+
* @property {number} size - Total size in bytes
|
7
|
+
* @property {boolean} loaded - Whether loading is complete
|
8
|
+
*/
|
9
|
+
/**
|
10
|
+
* Creates a loading tracker that combines progress from multiple sources
|
11
|
+
*
|
12
|
+
* @returns {Object} Loading tracker instance
|
13
|
+
*/
|
14
|
+
export function createTracker(): any;
|
15
|
+
/**
|
16
|
+
* Simple utility for tracking combined loading progress across multiple components
|
17
|
+
*/
|
18
|
+
export type LoadingProgress = {
|
19
|
+
/**
|
20
|
+
* - Number of bytes loaded
|
21
|
+
*/
|
22
|
+
bytesLoaded: number;
|
23
|
+
/**
|
24
|
+
* - Total size in bytes
|
25
|
+
*/
|
26
|
+
size: number;
|
27
|
+
/**
|
28
|
+
* - Whether loading is complete
|
29
|
+
*/
|
30
|
+
loaded: boolean;
|
31
|
+
};
|
@@ -0,0 +1,108 @@
|
|
1
|
+
/**
|
2
|
+
* Simple utility for tracking combined loading progress across multiple components
|
3
|
+
*
|
4
|
+
* @typedef {Object} LoadingProgress
|
5
|
+
* @property {number} bytesLoaded - Number of bytes loaded
|
6
|
+
* @property {number} size - Total size in bytes
|
7
|
+
* @property {boolean} loaded - Whether loading is complete
|
8
|
+
*/
|
9
|
+
|
10
|
+
/**
|
11
|
+
* Creates a loading tracker that combines progress from multiple sources
|
12
|
+
*
|
13
|
+
* @returns {Object} Loading tracker instance
|
14
|
+
*/
|
15
|
+
export function createTracker() {
|
16
|
+
// Store progress for each item by ID
|
17
|
+
const items = new Map();
|
18
|
+
|
19
|
+
// Computed values
|
20
|
+
let totalBytesLoaded = 0;
|
21
|
+
let totalSize = 0;
|
22
|
+
let allLoaded = false;
|
23
|
+
let progressPercent = 0;
|
24
|
+
|
25
|
+
/**
|
26
|
+
* Update the totals based on current items
|
27
|
+
*/
|
28
|
+
function updateTotals() {
|
29
|
+
let bytesLoaded = 0;
|
30
|
+
let size = 0;
|
31
|
+
let loaded = items.size > 0;
|
32
|
+
|
33
|
+
// Loop through all items and sum values
|
34
|
+
for (const progress of items.values()) {
|
35
|
+
bytesLoaded += progress.bytesLoaded || 0;
|
36
|
+
size += progress.size || 0;
|
37
|
+
|
38
|
+
if (!progress.loaded) {
|
39
|
+
loaded = false;
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
// Update state
|
44
|
+
totalBytesLoaded = bytesLoaded;
|
45
|
+
totalSize = size;
|
46
|
+
allLoaded = items.size > 0 ? loaded : false;
|
47
|
+
progressPercent = size > 0 ? Math.round((bytesLoaded / size) * 100) : 0;
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Track progress for a component
|
52
|
+
*
|
53
|
+
* @param {LoadingProgress} progress - The progress update
|
54
|
+
* @param {string|Symbol} id - The component identifier
|
55
|
+
*/
|
56
|
+
function track(progress, id) {
|
57
|
+
if (progress && id) {
|
58
|
+
items.set(id, progress);
|
59
|
+
updateTotals();
|
60
|
+
}
|
61
|
+
}
|
62
|
+
|
63
|
+
/**
|
64
|
+
* Remove an item from tracking
|
65
|
+
*
|
66
|
+
* @param {string|Symbol} id - Item identifier to remove
|
67
|
+
*/
|
68
|
+
function remove(id) {
|
69
|
+
if (items.has(id)) {
|
70
|
+
items.delete(id);
|
71
|
+
updateTotals();
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
/**
|
76
|
+
* Reset the tracker, clearing all items
|
77
|
+
*/
|
78
|
+
function reset() {
|
79
|
+
items.clear();
|
80
|
+
updateTotals();
|
81
|
+
}
|
82
|
+
|
83
|
+
// Return the public API
|
84
|
+
return {
|
85
|
+
track,
|
86
|
+
remove,
|
87
|
+
reset,
|
88
|
+
|
89
|
+
// Read-only properties
|
90
|
+
get state() {
|
91
|
+
return {
|
92
|
+
bytesLoaded: totalBytesLoaded,
|
93
|
+
size: totalSize,
|
94
|
+
loaded: allLoaded,
|
95
|
+
percent: progressPercent,
|
96
|
+
itemCount: items.size
|
97
|
+
};
|
98
|
+
},
|
99
|
+
|
100
|
+
get loaded() {
|
101
|
+
return allLoaded;
|
102
|
+
},
|
103
|
+
|
104
|
+
get percent() {
|
105
|
+
return progressPercent;
|
106
|
+
}
|
107
|
+
};
|
108
|
+
}
|
@@ -14,10 +14,11 @@
|
|
14
14
|
* overflow?: string,
|
15
15
|
* fit?: 'contain' | 'cover' | 'fill',
|
16
16
|
* position?: string,
|
17
|
-
* imageMeta
|
17
|
+
* imageMeta?: import('../../config/typedef.js').ImageMeta | import('../../config/typedef.js').ImageMeta[],
|
18
18
|
* imageLoader?: import('../../classes/svelte/image/index.js').ImageLoader,
|
19
19
|
* alt?: string,
|
20
|
-
*
|
20
|
+
* id?: string | Symbol,
|
21
|
+
* onProgress?: (progress: import('../../classes/svelte/network-loader/typedef.js').LoadingProgress, id?: string | Symbol) => void,
|
21
22
|
* [attr: string]: any
|
22
23
|
* }}
|
23
24
|
*/
|
@@ -42,6 +43,9 @@
|
|
42
43
|
// Accessibility
|
43
44
|
alt = '',
|
44
45
|
|
46
|
+
// Component identification
|
47
|
+
id = Symbol('ImageBox'),
|
48
|
+
|
45
49
|
// Events
|
46
50
|
onProgress,
|
47
51
|
|
@@ -49,9 +53,9 @@
|
|
49
53
|
...attrs
|
50
54
|
} = $props();
|
51
55
|
|
52
|
-
if (!imageMeta) {
|
53
|
-
|
54
|
-
}
|
56
|
+
// if (!imageMeta) {
|
57
|
+
// throw new Error('Missing [imageMeta]');
|
58
|
+
// }
|
55
59
|
|
56
60
|
/** @type {HTMLDivElement|undefined} */
|
57
61
|
let containerElem = $state();
|
@@ -87,11 +91,11 @@
|
|
87
91
|
|
88
92
|
// Report progress from variants loader
|
89
93
|
if (variantsLoader) {
|
90
|
-
onProgress(variantsLoader.progress);
|
94
|
+
onProgress(variantsLoader.progress, id);
|
91
95
|
}
|
92
96
|
// Report progress from single image loader
|
93
97
|
else if (imageLoader_) {
|
94
|
-
onProgress(imageLoader_.progress);
|
98
|
+
onProgress(imageLoader_.progress, id);
|
95
99
|
}
|
96
100
|
});
|
97
101
|
|
@@ -12,10 +12,11 @@ type ImageBox = {
|
|
12
12
|
overflow?: string;
|
13
13
|
fit?: "fill" | "contain" | "cover";
|
14
14
|
position?: string;
|
15
|
-
imageMeta
|
15
|
+
imageMeta?: ImageMeta | ImageMeta[];
|
16
16
|
imageLoader?: ImageLoader;
|
17
17
|
alt?: string;
|
18
|
-
|
18
|
+
id?: string | Symbol;
|
19
|
+
onProgress?: (progress: LoadingProgress, id?: string | Symbol) => void;
|
19
20
|
}>): void;
|
20
21
|
};
|
21
22
|
declare const ImageBox: import("svelte").Component<{
|
@@ -29,8 +30,9 @@ declare const ImageBox: import("svelte").Component<{
|
|
29
30
|
overflow?: string;
|
30
31
|
fit?: "contain" | "cover" | "fill";
|
31
32
|
position?: string;
|
32
|
-
imageMeta
|
33
|
+
imageMeta?: import("../../config/typedef.js").ImageMeta | import("../../config/typedef.js").ImageMeta[];
|
33
34
|
imageLoader?: import("../../classes/svelte/image/index.js").ImageLoader;
|
34
35
|
alt?: string;
|
35
|
-
|
36
|
+
id?: string | Symbol;
|
37
|
+
onProgress?: (progress: import("../../classes/svelte/network-loader/typedef.js").LoadingProgress, id?: string | Symbol) => void;
|
36
38
|
}, {}, "">;
|
@@ -25,6 +25,13 @@ export class PresenterState {
|
|
25
25
|
busy: boolean;
|
26
26
|
/** @type {string} */
|
27
27
|
currentSlideName: string;
|
28
|
+
/**
|
29
|
+
* Returns a simplified presenter reference with essential methods
|
30
|
+
* for slide components to use
|
31
|
+
*
|
32
|
+
* @returns {PresenterRef} A reference object with presenter methods
|
33
|
+
*/
|
34
|
+
getPresenterRef(): PresenterRef;
|
28
35
|
/**
|
29
36
|
* Mark the slide as loaded, which triggers transitions to begin
|
30
37
|
*/
|
@@ -80,4 +87,14 @@ export type LoadController = {
|
|
80
87
|
*/
|
81
88
|
cancel: () => void;
|
82
89
|
};
|
90
|
+
export type PresenterRef = {
|
91
|
+
/**
|
92
|
+
* - Navigate to a slide by name
|
93
|
+
*/
|
94
|
+
gotoSlide: (name: string) => void;
|
95
|
+
/**
|
96
|
+
* - Get the current slide name
|
97
|
+
*/
|
98
|
+
getCurrentSlideName: () => string;
|
99
|
+
};
|
83
100
|
import { HkPromise } from '../../classes/promise/index.js';
|
@@ -26,6 +26,12 @@ import { HkPromise } from '../../classes/promise/index.js';
|
|
26
26
|
* @property {() => void} cancel - Function to return to the previous slide
|
27
27
|
*/
|
28
28
|
|
29
|
+
/**
|
30
|
+
* @typedef {Object} PresenterRef
|
31
|
+
* @property {(name: string) => void} gotoSlide - Navigate to a slide by name
|
32
|
+
* @property {() => string} getCurrentSlideName - Get the current slide name
|
33
|
+
*/
|
34
|
+
|
29
35
|
/* -------------------------------------------------------------- Constants */
|
30
36
|
|
31
37
|
const Z_BACK = 0;
|
@@ -96,6 +102,19 @@ export class PresenterState {
|
|
96
102
|
this.#setupLoadingTransitions();
|
97
103
|
}
|
98
104
|
|
105
|
+
/**
|
106
|
+
* Returns a simplified presenter reference with essential methods
|
107
|
+
* for slide components to use
|
108
|
+
*
|
109
|
+
* @returns {PresenterRef} A reference object with presenter methods
|
110
|
+
*/
|
111
|
+
getPresenterRef() {
|
112
|
+
return {
|
113
|
+
gotoSlide: (name) => this.gotoSlide(name),
|
114
|
+
getCurrentSlideName: () => this.currentSlideName
|
115
|
+
};
|
116
|
+
}
|
117
|
+
|
99
118
|
/**
|
100
119
|
* Set up reactivity for stage transitions between the before/after states
|
101
120
|
* This handles the animation timing for both layers
|
@@ -358,27 +377,31 @@ export class PresenterState {
|
|
358
377
|
|
359
378
|
// Add controller function to slide props if it has a component
|
360
379
|
if (slide.data?.component) {
|
380
|
+
// Get a presenter reference to pass to the slide
|
381
|
+
const presenterRef = this.getPresenterRef();
|
382
|
+
|
361
383
|
// Create a copy of the slide to avoid mutating the original
|
362
|
-
const
|
384
|
+
const slideWithExtras = {
|
363
385
|
...slide,
|
364
386
|
data: {
|
365
387
|
...slide.data,
|
366
388
|
props: {
|
367
389
|
...(slide.data.props || {}),
|
368
|
-
getLoadingController: () => this.getLoadingController()
|
390
|
+
getLoadingController: () => this.getLoadingController(),
|
391
|
+
presenter: presenterRef // Add presenter reference to props
|
369
392
|
}
|
370
393
|
}
|
371
394
|
};
|
372
395
|
|
373
|
-
// Add next slide to next layer with controller included
|
374
|
-
this.#updateSlide(this.nextLayerLabel,
|
396
|
+
// Add next slide to next layer with controller and presenter included
|
397
|
+
this.#updateSlide(this.nextLayerLabel, slideWithExtras);
|
375
398
|
|
376
399
|
// If a timeout is configured, automatically finish loading after delay
|
377
400
|
if (this.loadingTimeout > 0) {
|
378
401
|
setTimeout(() => {
|
379
402
|
// Only auto-finish if the controller wasn't requested
|
380
403
|
if (!this.controllerRequested && this.isSlideLoading) {
|
381
|
-
// console.
|
404
|
+
// console.debug(
|
382
405
|
// `Slide '${slide.name}' didn't request loading controller, auto-finishing.`
|
383
406
|
// );
|
384
407
|
this.finishSlideLoading();
|