@mblinkov/whats-missing 20.0.47 → 20.0.49
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/CHANGELOG.md +477 -1
- package/dist/whats-missing.cjs.js +3 -3
- package/dist/whats-missing.es.js +265 -215
- package/package.json +1 -1
- package/src/Game.tsx +138 -14
package/package.json
CHANGED
package/src/Game.tsx
CHANGED
|
@@ -69,6 +69,27 @@ const levenshtein = (a: string, b: string) => {
|
|
|
69
69
|
return dp[a.length][b.length];
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
// ✅ Preload utilities
|
|
73
|
+
const preloadImage = (src: string): Promise<HTMLImageElement> => {
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
// ✅ Проверяем, не загружена ли уже картинка
|
|
76
|
+
const img = new Image();
|
|
77
|
+
img.onload = () => resolve(img);
|
|
78
|
+
img.onerror = () => resolve(img); // Continue even on error, но img все равно создан
|
|
79
|
+
img.src = src;
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const preloadImages = async (urls: string[], signal?: AbortSignal): Promise<Map<string, HTMLImageElement>> => {
|
|
84
|
+
const imageMap = new Map<string, HTMLImageElement>();
|
|
85
|
+
const promises = urls.map(async (url) => {
|
|
86
|
+
const img = await preloadImage(url);
|
|
87
|
+
imageMap.set(url, img);
|
|
88
|
+
});
|
|
89
|
+
await Promise.allSettled(promises);
|
|
90
|
+
return imageMap;
|
|
91
|
+
};
|
|
92
|
+
|
|
72
93
|
export default function Game({
|
|
73
94
|
gameCubeSize,
|
|
74
95
|
screenHeight,
|
|
@@ -92,6 +113,11 @@ export default function Game({
|
|
|
92
113
|
if (style) {
|
|
93
114
|
style.remove();
|
|
94
115
|
}
|
|
116
|
+
|
|
117
|
+
// ✅ Cancel any ongoing preloads
|
|
118
|
+
if (abortController.current) {
|
|
119
|
+
abortController.current.abort();
|
|
120
|
+
}
|
|
95
121
|
};
|
|
96
122
|
}, []);
|
|
97
123
|
|
|
@@ -102,7 +128,7 @@ export default function Game({
|
|
|
102
128
|
const [currentRound, setCurrentRound] = useState(1);
|
|
103
129
|
const [hiddenIndex, setHiddenIndex] = useState<number | null>(null);
|
|
104
130
|
const [phase, setPhase] = useState<"ready" | "memorize" | "guess">("ready");
|
|
105
|
-
const [readyTime, setReadyTime] = useState(
|
|
131
|
+
const [readyTime, setReadyTime] = useState(10);
|
|
106
132
|
const [memorizeTime, setMemorizeTime] = useState(10);
|
|
107
133
|
const [timeLeft, setTimeLeft] = useState(20);
|
|
108
134
|
const [score, setScore] = useState(0);
|
|
@@ -126,6 +152,12 @@ export default function Game({
|
|
|
126
152
|
const [isIPadProPortrait, setIsIPadProPortrait] = useState(false);
|
|
127
153
|
const [isIPadProLandscape, setIsIPadProLandscape] = useState(false);
|
|
128
154
|
const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
|
|
155
|
+
const [isWideScreen, setIsWideScreen] = useState(false);
|
|
156
|
+
|
|
157
|
+
// ✅ Система предзагрузки изображений
|
|
158
|
+
const preloadedUrls = useRef<Set<string>>(new Set());
|
|
159
|
+
const preloadedImages = useRef<Map<string, HTMLImageElement>>(new Map());
|
|
160
|
+
const abortController = useRef<AbortController | null>(null);
|
|
129
161
|
|
|
130
162
|
// ✅ адаптив под мобилки, планшеты и десктоп
|
|
131
163
|
useEffect(() => {
|
|
@@ -135,6 +167,7 @@ export default function Game({
|
|
|
135
167
|
const mobile = width < 768 || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape тоже считается мобильным
|
|
136
168
|
const isLandscape = (width > height && mobile) || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape
|
|
137
169
|
const isSmallHeight = height < 700; // Nest Hub, маленькие экраны
|
|
170
|
+
const isWideScreen = width / height > 1.8; // ✅ Широкие экраны
|
|
138
171
|
|
|
139
172
|
// Определяем iPhone до 14 Pro Max в landscape режиме
|
|
140
173
|
// iPhone 14 Pro Max: 926x428 (landscape)
|
|
@@ -201,7 +234,9 @@ export default function Game({
|
|
|
201
234
|
// ✅ Вычисляем горизонтальный layout ОДИН РАЗ
|
|
202
235
|
const isHorizontal =
|
|
203
236
|
(mobile && width > height) ||
|
|
237
|
+
mobile || // ✅ ВСЕ мобильные устройства (включая portrait)
|
|
204
238
|
height < 700 ||
|
|
239
|
+
isWideScreen || // ✅ Широкие экраны
|
|
205
240
|
(width === 1366 && height === 766) ||
|
|
206
241
|
(width === 1366 && height === 768) ||
|
|
207
242
|
(width === 1280 && height === 720) ||
|
|
@@ -231,6 +266,15 @@ export default function Game({
|
|
|
231
266
|
// Маленькие экраны: используем переданный gameCubeSize или полную высоту
|
|
232
267
|
setContainerSize(gameCubeSize && gameCubeSize >= 400 ? gameCubeSize : null);
|
|
233
268
|
setScale(1);
|
|
269
|
+
} else if (isWideScreen) {
|
|
270
|
+
// ✅ Широкие экраны: используем стандартный размер но уменьшаем масштаб картинок
|
|
271
|
+
const minSize = 400;
|
|
272
|
+
const maxSize = 1200;
|
|
273
|
+
const finalSize = gameCubeSize
|
|
274
|
+
? Math.max(minSize, Math.min(maxSize, gameCubeSize))
|
|
275
|
+
: Math.min(1000, Math.min(width, height) * 0.9);
|
|
276
|
+
setContainerSize(finalSize);
|
|
277
|
+
setScale(0.85); // Уменьшаем масштаб картинок на 15%
|
|
234
278
|
} else {
|
|
235
279
|
// Десктопы: используем переданный gameCubeSize с разумными ограничениями
|
|
236
280
|
const minSize = 400;
|
|
@@ -250,6 +294,33 @@ export default function Game({
|
|
|
250
294
|
const getRandomSix = (arr: ImageItem[]) =>
|
|
251
295
|
[...arr].sort(() => Math.random() - 0.5).slice(0, 6);
|
|
252
296
|
|
|
297
|
+
// ✅ Функция предзагрузки темы
|
|
298
|
+
const startThemePreload = (themeName: Theme) => {
|
|
299
|
+
// Cancel previous preload
|
|
300
|
+
if (abortController.current) {
|
|
301
|
+
abortController.current.abort();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Create new controller
|
|
305
|
+
abortController.current = new AbortController();
|
|
306
|
+
|
|
307
|
+
// Get theme images and start preloading
|
|
308
|
+
const themeImages = themes[themeName];
|
|
309
|
+
const urls = themeImages.map(item => item.src);
|
|
310
|
+
|
|
311
|
+
preloadImages(urls, abortController.current.signal).then((imageMap) => {
|
|
312
|
+
// Mark as preloaded
|
|
313
|
+
urls.forEach(url => {
|
|
314
|
+
preloadedUrls.current.add(url);
|
|
315
|
+
if (imageMap.has(url)) {
|
|
316
|
+
preloadedImages.current.set(url, imageMap.get(url)!);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}).catch(() => {
|
|
320
|
+
// Ignore abort errors
|
|
321
|
+
});
|
|
322
|
+
};
|
|
323
|
+
|
|
253
324
|
const startGame = () => {
|
|
254
325
|
if (!theme) return;
|
|
255
326
|
const selected = getRandomSix(themes[theme]);
|
|
@@ -260,6 +331,20 @@ export default function Game({
|
|
|
260
331
|
setScore(0);
|
|
261
332
|
setResultsTable([]);
|
|
262
333
|
setUsedHidden([]);
|
|
334
|
+
|
|
335
|
+
// ✅ Preload current round images
|
|
336
|
+
const urls = selected.map(item => item.src);
|
|
337
|
+
preloadImages(urls, abortController.current?.signal).then((imageMap) => {
|
|
338
|
+
urls.forEach(url => {
|
|
339
|
+
preloadedUrls.current.add(url);
|
|
340
|
+
if (imageMap.has(url)) {
|
|
341
|
+
preloadedImages.current.set(url, imageMap.get(url)!);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}).catch(() => {
|
|
345
|
+
// Ignore abort errors
|
|
346
|
+
});
|
|
347
|
+
|
|
263
348
|
startRound(selected, [], true);
|
|
264
349
|
};
|
|
265
350
|
|
|
@@ -280,9 +365,26 @@ export default function Game({
|
|
|
280
365
|
setInputValue("");
|
|
281
366
|
setTimeLeft(20);
|
|
282
367
|
setResult(null);
|
|
368
|
+
|
|
369
|
+
// ✅ Preload next round images in background
|
|
370
|
+
if (theme && !isFirst) {
|
|
371
|
+
const nextPool = getRandomSix(themes[theme].filter(item => !used.includes(item.name)));
|
|
372
|
+
const nextUrls = nextPool.map(item => item.src);
|
|
373
|
+
preloadImages(nextUrls, abortController.current?.signal).then((imageMap) => {
|
|
374
|
+
nextUrls.forEach(url => {
|
|
375
|
+
preloadedUrls.current.add(url);
|
|
376
|
+
if (imageMap.has(url)) {
|
|
377
|
+
preloadedImages.current.set(url, imageMap.get(url)!);
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}).catch(() => {
|
|
381
|
+
// Ignore abort errors
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
283
385
|
if (isFirst) {
|
|
284
386
|
setPhase("ready");
|
|
285
|
-
setReadyTime(
|
|
387
|
+
setReadyTime(10); // Увеличено до 10 секунд для VPN/медленных соединений
|
|
286
388
|
setMemorizeTime(10);
|
|
287
389
|
} else {
|
|
288
390
|
setPhase("memorize");
|
|
@@ -351,6 +453,11 @@ export default function Game({
|
|
|
351
453
|
currentRound < rounds ? (setCurrentRound((r) => r + 1), startRound()) : setFinished(true);
|
|
352
454
|
|
|
353
455
|
const exitGame = () => {
|
|
456
|
+
// ✅ Cancel any ongoing preloads
|
|
457
|
+
if (abortController.current) {
|
|
458
|
+
abortController.current.abort();
|
|
459
|
+
}
|
|
460
|
+
|
|
354
461
|
setStarted(false);
|
|
355
462
|
setFinished(false);
|
|
356
463
|
setTheme(null);
|
|
@@ -416,6 +523,7 @@ export default function Game({
|
|
|
416
523
|
boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.1)",
|
|
417
524
|
margin: isMobile ? "0 auto" : "unset",
|
|
418
525
|
position: "relative", // needed so absolute logo is inside the square
|
|
526
|
+
transform: `scale(${scale})`, // ✅ Применяем масштаб для широких экранов
|
|
419
527
|
}}
|
|
420
528
|
>
|
|
421
529
|
<div
|
|
@@ -442,7 +550,10 @@ export default function Game({
|
|
|
442
550
|
padding: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "8px 12px" : "12px 24px",
|
|
443
551
|
fontSize: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "12px" : "16px",
|
|
444
552
|
minWidth: (isMobile && window.innerWidth > window.innerHeight) || (isMobile && window.innerWidth <= 375 && window.innerHeight <= 667) || (window.innerWidth === 896 && window.innerHeight === 414) || (window.innerWidth === 844 && window.innerHeight === 390) || (window.innerWidth === 926 && window.innerHeight === 428) || (window.innerWidth === 932 && window.innerHeight === 430) ? "70px" : "auto"
|
|
445
|
-
}} onClick={() =>
|
|
553
|
+
}} onClick={() => {
|
|
554
|
+
setTheme(t as Theme);
|
|
555
|
+
startThemePreload(t as Theme);
|
|
556
|
+
}}>
|
|
446
557
|
{t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
|
|
447
558
|
</button>
|
|
448
559
|
))}
|
|
@@ -736,17 +847,30 @@ export default function Game({
|
|
|
736
847
|
: {}),
|
|
737
848
|
}}
|
|
738
849
|
>
|
|
739
|
-
{!isHidden && (
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
850
|
+
{!isHidden && (() => {
|
|
851
|
+
// ✅ Используем уже загруженный объект Image чтобы браузер взял из кеша
|
|
852
|
+
const preloadedImg = preloadedImages.current.get(file.src);
|
|
853
|
+
// Используем src из уже загруженного объекта или оригинальный URL
|
|
854
|
+
const imgSrc = preloadedImg?.complete && preloadedImg.src
|
|
855
|
+
? preloadedImg.src
|
|
856
|
+
: file.src;
|
|
857
|
+
|
|
858
|
+
return (
|
|
859
|
+
<img
|
|
860
|
+
key={`${file.src}-${preloadedImg?.complete ? 'loaded' : 'pending'}`}
|
|
861
|
+
src={imgSrc}
|
|
862
|
+
alt={file.name}
|
|
863
|
+
fetchPriority="high"
|
|
864
|
+
loading="eager"
|
|
865
|
+
decoding="async"
|
|
866
|
+
style={{
|
|
867
|
+
width: "100%",
|
|
868
|
+
height: "100%",
|
|
869
|
+
objectFit: "cover",
|
|
870
|
+
}}
|
|
871
|
+
/>
|
|
872
|
+
);
|
|
873
|
+
})()}
|
|
750
874
|
</div>
|
|
751
875
|
);
|
|
752
876
|
})}
|