@mblinkov/whats-missing 20.0.47 → 20.0.48

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mblinkov/whats-missing",
3
- "version": "20.0.47",
3
+ "version": "20.0.48",
4
4
  "main": "dist/whats-missing.umd.js",
5
5
  "module": "dist/whats-missing.es.js",
6
6
  "types": "dist/index.d.ts",
package/src/Game.tsx CHANGED
@@ -92,6 +92,11 @@ export default function Game({
92
92
  if (style) {
93
93
  style.remove();
94
94
  }
95
+
96
+ // ✅ Cancel any ongoing preloads
97
+ if (abortController.current) {
98
+ abortController.current.abort();
99
+ }
95
100
  };
96
101
  }, []);
97
102
 
@@ -126,6 +131,10 @@ export default function Game({
126
131
  const [isIPadProPortrait, setIsIPadProPortrait] = useState(false);
127
132
  const [isIPadProLandscape, setIsIPadProLandscape] = useState(false);
128
133
  const [isHorizontalLayout, setIsHorizontalLayout] = useState(false);
134
+
135
+ // ✅ Система предзагрузки изображений
136
+ const preloadedUrls = useRef<Set<string>>(new Set());
137
+ const abortController = useRef<AbortController | null>(null);
129
138
 
130
139
  // ✅ адаптив под мобилки, планшеты и десктоп
131
140
  useEffect(() => {
@@ -135,6 +144,7 @@ export default function Game({
135
144
  const mobile = width < 768 || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape тоже считается мобильным
136
145
  const isLandscape = (width > height && mobile) || (width === 926 && height === 428) || (width === 932 && height === 430); // iPhone 14 Pro Max в landscape
137
146
  const isSmallHeight = height < 700; // Nest Hub, маленькие экраны
147
+ const isWideScreen = width / height > 1.8; // ✅ Широкие экраны
138
148
 
139
149
  // Определяем iPhone до 14 Pro Max в landscape режиме
140
150
  // iPhone 14 Pro Max: 926x428 (landscape)
@@ -201,7 +211,9 @@ export default function Game({
201
211
  // ✅ Вычисляем горизонтальный layout ОДИН РАЗ
202
212
  const isHorizontal =
203
213
  (mobile && width > height) ||
214
+ mobile || // ✅ ВСЕ мобильные устройства (включая portrait)
204
215
  height < 700 ||
216
+ isWideScreen || // ✅ Широкие экраны
205
217
  (width === 1366 && height === 766) ||
206
218
  (width === 1366 && height === 768) ||
207
219
  (width === 1280 && height === 720) ||
@@ -231,6 +243,15 @@ export default function Game({
231
243
  // Маленькие экраны: используем переданный gameCubeSize или полную высоту
232
244
  setContainerSize(gameCubeSize && gameCubeSize >= 400 ? gameCubeSize : null);
233
245
  setScale(1);
246
+ } else if (isWideScreen) {
247
+ // ✅ Широкие экраны: используем стандартный размер но уменьшаем масштаб картинок
248
+ const minSize = 400;
249
+ const maxSize = 1200;
250
+ const finalSize = gameCubeSize
251
+ ? Math.max(minSize, Math.min(maxSize, gameCubeSize))
252
+ : Math.min(1000, Math.min(width, height) * 0.9);
253
+ setContainerSize(finalSize);
254
+ setScale(0.85); // Уменьшаем масштаб картинок на 15%
234
255
  } else {
235
256
  // Десктопы: используем переданный gameCubeSize с разумными ограничениями
236
257
  const minSize = 400;
@@ -250,6 +271,43 @@ export default function Game({
250
271
  const getRandomSix = (arr: ImageItem[]) =>
251
272
  [...arr].sort(() => Math.random() - 0.5).slice(0, 6);
252
273
 
274
+ // ✅ Утилиты предзагрузки
275
+ const preloadImage = (src: string): Promise<void> => {
276
+ return new Promise((resolve) => {
277
+ const img = new Image();
278
+ img.onload = () => resolve();
279
+ img.onerror = () => resolve(); // Continue even on error
280
+ img.src = src;
281
+ });
282
+ };
283
+
284
+ const preloadImages = async (urls: string[], signal?: AbortSignal): Promise<void> => {
285
+ const promises = urls.map(url => preloadImage(url));
286
+ await Promise.allSettled(promises);
287
+ };
288
+
289
+ // ✅ Функция предзагрузки темы
290
+ const startThemePreload = (themeName: Theme) => {
291
+ // Cancel previous preload
292
+ if (abortController.current) {
293
+ abortController.current.abort();
294
+ }
295
+
296
+ // Create new controller
297
+ abortController.current = new AbortController();
298
+
299
+ // Get theme images and start preloading
300
+ const themeImages = themes[themeName];
301
+ const urls = themeImages.map(item => item.src);
302
+
303
+ preloadImages(urls, abortController.current.signal).then(() => {
304
+ // Mark as preloaded
305
+ urls.forEach(url => preloadedUrls.current.add(url));
306
+ }).catch(() => {
307
+ // Ignore abort errors
308
+ });
309
+ };
310
+
253
311
  const startGame = () => {
254
312
  if (!theme) return;
255
313
  const selected = getRandomSix(themes[theme]);
@@ -260,6 +318,15 @@ export default function Game({
260
318
  setScore(0);
261
319
  setResultsTable([]);
262
320
  setUsedHidden([]);
321
+
322
+ // ✅ Preload current round images
323
+ const urls = selected.map(item => item.src);
324
+ preloadImages(urls, abortController.current?.signal).then(() => {
325
+ urls.forEach(url => preloadedUrls.current.add(url));
326
+ }).catch(() => {
327
+ // Ignore abort errors
328
+ });
329
+
263
330
  startRound(selected, [], true);
264
331
  };
265
332
 
@@ -280,9 +347,21 @@ export default function Game({
280
347
  setInputValue("");
281
348
  setTimeLeft(20);
282
349
  setResult(null);
350
+
351
+ // ✅ Preload next round images in background
352
+ if (theme && !isFirst) {
353
+ const nextPool = getRandomSix(themes[theme].filter(item => !used.includes(item.name)));
354
+ const nextUrls = nextPool.map(item => item.src);
355
+ preloadImages(nextUrls, abortController.current?.signal).then(() => {
356
+ nextUrls.forEach(url => preloadedUrls.current.add(url));
357
+ }).catch(() => {
358
+ // Ignore abort errors
359
+ });
360
+ }
361
+
283
362
  if (isFirst) {
284
363
  setPhase("ready");
285
- setReadyTime(3);
364
+ setReadyTime(5); // Увеличено с 3 до 5
286
365
  setMemorizeTime(10);
287
366
  } else {
288
367
  setPhase("memorize");
@@ -416,6 +495,7 @@ export default function Game({
416
495
  boxShadow: isMobile ? "none" : "0 0 40px rgba(0,0,0,0.1)",
417
496
  margin: isMobile ? "0 auto" : "unset",
418
497
  position: "relative", // needed so absolute logo is inside the square
498
+ transform: `scale(${scale})`, // ✅ Применяем масштаб для широких экранов
419
499
  }}
420
500
  >
421
501
  <div
@@ -442,7 +522,10 @@ export default function Game({
442
522
  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
523
  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
524
  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={() => setTheme(t as Theme)}>
525
+ }} onClick={() => {
526
+ setTheme(t as Theme);
527
+ startThemePreload(t as Theme);
528
+ }}>
446
529
  {t === "animals" ? "🐶 Animals" : t === "food" ? "🍎 Food" : "🧸 Toys"}
447
530
  </button>
448
531
  ))}