From ee5fb17c6b8085661c61edd04c657119d70d307a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 22 Aug 2026 21:51:38 +0300 Subject: [PATCH] feat: install hints for non-Chromium browsers; preload 404 hardening - Install button without beforeinstallprompt now shows inline per-platform instructions (Firefox/iOS/generic) instead of silent no-op - Preload: 404s no longer counted as failures nor retried - PreloadProgress.failedFiles (up to 10) surfaced in completion message --- src/core/i18n.ts | 16 ++++++++++++++++ src/core/offline-manager.ts | 13 +++++++++++-- src/ui/settings-view.ts | 24 ++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/core/i18n.ts b/src/core/i18n.ts index ba5da48..734ff58 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -498,6 +498,10 @@ export interface Translations { aboutAppName: string; aboutLicense: string; installPwa: string; + installPwaAlreadyInstalled: string; + installPwaHintFirefox: string; + installPwaHintIos: string; + installPwaHintGeneric: string; offlineOfferTitle: string; offlineOfferText: string; offlineOfferPreloadAll: string; @@ -1063,6 +1067,10 @@ const translations: Record = { aboutAppName: 'Ponomar', aboutLicense: 'Based on the Ponomar Java desktop application by Aleksandr Andreev and Yuri Shardt. Licensed under GPL v3.', installPwa: 'Install Ponomar', + installPwaAlreadyInstalled: 'App is already installed.', + installPwaHintFirefox: 'Use the install icon in the address bar, or open the menu and choose "Install".', + installPwaHintIos: 'Tap the Share button and choose "Add to Home Screen".', + installPwaHintGeneric: 'Use your browser menu and choose "Install app" / "Add to Home screen".', offlineOfferTitle: 'Offline Data', offlineOfferText: 'If you plan to use the application without internet access, you should preload data for offline usage.', offlineOfferPreloadAll: 'Pre-load everything', @@ -1626,6 +1634,10 @@ const translations: Record = { aboutAppName: 'Пономарь', aboutLicense: 'Основано на Java-приложении Понома́рь от Александра Андреева и Юрия Шардт. Лицензия GPL v3.', installPwa: 'Установить Пономарь', + installPwaAlreadyInstalled: 'Приложение уже установлено.', + installPwaHintFirefox: 'Используйте значок установки в адресной строке или меню браузера → «Установить».', + installPwaHintIos: 'Нажмите «Поделиться» и выберите «На экран “Домой”».', + installPwaHintGeneric: 'Откройте меню браузера и выберите «Установить приложение» / «Добавить на главный экран».', offlineOfferTitle: 'Офлайн-данные', offlineOfferText: 'Если вы планируете использовать приложение без интернета, предзагрузите данные для офлайн-использования.', offlineOfferPreloadAll: 'Загрузить всё', @@ -2189,6 +2201,10 @@ const translations: Record = { aboutAppName: 'Пономарь', aboutLicense: 'Основано на Java-приложенiи Понома́рь отъ Александра Андреева и Юрия Шардт. Лицензия GPL v3.', installPwa: 'Оустановити Понома́рь', + installPwaAlreadyInstalled: 'Приложе́нiе ужѐ оустановлено́.', + installPwaHintFirefox: 'Оустановле́нiе: зна́чокъ въ адре́сной строкѐ и҆лѝ меню̀ браꙋ́зера → «Оустанови́ти».', + installPwaHintIos: 'Нажми́те «Подели́тися» и҆ избери́те «На экра́нъ “Домо́въ”».', + installPwaHintGeneric: 'Откро́йте меню̀ браꙋ́зера и҆ избери́те «Оустанови́ти приложе́нiе».', offlineOfferTitle: 'Офлайн-данныѧ', offlineOfferText: 'А҆́ще плани́рꙋете и҆спо́льзовати приложе́нїе без̾ и҆нтернета, предзагризите да́нныѧ длѧ офлайн-потребле́нїѧ.', offlineOfferPreloadAll: 'Загризи́ти всѧ̑', diff --git a/src/core/offline-manager.ts b/src/core/offline-manager.ts index 62591bf..ee20534 100644 --- a/src/core/offline-manager.ts +++ b/src/core/offline-manager.ts @@ -28,6 +28,7 @@ export interface PreloadProgress { file: string; done: boolean; failed: number; + failedFiles?: string[]; } export interface CacheStats { @@ -157,11 +158,15 @@ export class OfflineManager { if (await DataCache.get(cacheKey) !== null) continue; let success = false; + let notFound = false; for (let attempt = 0; attempt < 3; attempt++) { if (attempt > 0) await new Promise(r => setTimeout(r, 1000)); try { const resp = await fetch(file); - if (resp.status === 404) break; + if (resp.status === 404) { + notFound = true; + break; + } if (!resp.ok) continue; const ct = resp.headers.get('content-type'); if (ct && ct.includes('text/html')) break; @@ -179,7 +184,11 @@ export class OfflineManager { // Retry on network error } } - if (!success) progress.failed++; + if (!success && !notFound) { + progress.failed++; + if (!progress.failedFiles) progress.failedFiles = []; + if (progress.failedFiles.length < 10) progress.failedFiles.push(file); + } } progress.done = true; diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index 6b27d59..109d368 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -255,6 +255,7 @@ export class SettingsView { + @@ -403,12 +404,29 @@ export class SettingsView { }); document.getElementById('install-pwa')?.addEventListener('click', async () => { const prompt = (window as any).__deferredPrompt; + const hintEl = document.getElementById('install-hint'); if (prompt) { prompt.prompt(); await prompt.userChoice; (window as any).__deferredPrompt = null; document.getElementById('install-pwa')?.remove(); + return; } + if (!hintEl) return; + const ua = navigator.userAgent; + const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone === true; + let hint: string; + if (isStandalone) { + hint = t.settings.installPwaAlreadyInstalled; + } else if (/firefox/i.test(ua)) { + hint = t.settings.installPwaHintFirefox; + } else if (/iphone|ipad|ipod/i.test(ua)) { + hint = t.settings.installPwaHintIos; + } else { + hint = t.settings.installPwaHintGeneric; + } + hintEl.textContent = hint; + hintEl.classList.remove('hidden'); }); // Location @@ -535,7 +553,8 @@ export class SettingsView { if (progressBar) progressBar.style.width = '100%'; if (progressText) { if (prog.failed > 0) { - progressText.textContent = `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}`; + const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : ''; + progressText.textContent = `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}`; } else { progressText.textContent = t.settings.offlineDone; } @@ -658,8 +677,9 @@ export class SettingsView { }).then((prog) => { if (progressBar) progressBar.style.width = '100%'; if (progressText) { + const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : ''; progressText.textContent = prog.failed > 0 - ? `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}` + ? `${t.settings.offlineDone} ${prog.failed} ${t.settings.offlineFailed}${failList}` : t.settings.offlineDone; } OfflineManager.getStats().then(stats => {