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
This commit is contained in:
1 parent
c652183a24
commit
ee5fb17c6b
3 files changed
+49
-4
No files matched your search
@@ -498,6 +498,10 @@ export interface Translations {
|
|||||||
aboutAppName: string;
|
aboutAppName: string;
|
||||||
aboutLicense: string;
|
aboutLicense: string;
|
||||||
installPwa: string;
|
installPwa: string;
|
||||||
|
installPwaAlreadyInstalled: string;
|
||||||
|
installPwaHintFirefox: string;
|
||||||
|
installPwaHintIos: string;
|
||||||
|
installPwaHintGeneric: string;
|
||||||
offlineOfferTitle: string;
|
offlineOfferTitle: string;
|
||||||
offlineOfferText: string;
|
offlineOfferText: string;
|
||||||
offlineOfferPreloadAll: string;
|
offlineOfferPreloadAll: string;
|
||||||
@@ -1063,6 +1067,10 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
aboutAppName: 'Ponomar',
|
aboutAppName: 'Ponomar',
|
||||||
aboutLicense: 'Based on the Ponomar Java desktop application by Aleksandr Andreev and Yuri Shardt. Licensed under GPL v3.',
|
aboutLicense: 'Based on the Ponomar Java desktop application by Aleksandr Andreev and Yuri Shardt. Licensed under GPL v3.',
|
||||||
installPwa: 'Install Ponomar',
|
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',
|
offlineOfferTitle: 'Offline Data',
|
||||||
offlineOfferText: 'If you plan to use the application without internet access, you should preload data for offline usage.',
|
offlineOfferText: 'If you plan to use the application without internet access, you should preload data for offline usage.',
|
||||||
offlineOfferPreloadAll: 'Pre-load everything',
|
offlineOfferPreloadAll: 'Pre-load everything',
|
||||||
@@ -1626,6 +1634,10 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
aboutAppName: 'Пономарь',
|
aboutAppName: 'Пономарь',
|
||||||
aboutLicense: 'Основано на Java-приложении Понома́рь от Александра Андреева и Юрия Шардт. Лицензия GPL v3.',
|
aboutLicense: 'Основано на Java-приложении Понома́рь от Александра Андреева и Юрия Шардт. Лицензия GPL v3.',
|
||||||
installPwa: 'Установить Пономарь',
|
installPwa: 'Установить Пономарь',
|
||||||
|
installPwaAlreadyInstalled: 'Приложение уже установлено.',
|
||||||
|
installPwaHintFirefox: 'Используйте значок установки в адресной строке или меню браузера → «Установить».',
|
||||||
|
installPwaHintIos: 'Нажмите «Поделиться» и выберите «На экран “Домой”».',
|
||||||
|
installPwaHintGeneric: 'Откройте меню браузера и выберите «Установить приложение» / «Добавить на главный экран».',
|
||||||
offlineOfferTitle: 'Офлайн-данные',
|
offlineOfferTitle: 'Офлайн-данные',
|
||||||
offlineOfferText: 'Если вы планируете использовать приложение без интернета, предзагрузите данные для офлайн-использования.',
|
offlineOfferText: 'Если вы планируете использовать приложение без интернета, предзагрузите данные для офлайн-использования.',
|
||||||
offlineOfferPreloadAll: 'Загрузить всё',
|
offlineOfferPreloadAll: 'Загрузить всё',
|
||||||
@@ -2189,6 +2201,10 @@ const translations: Record<LanguageCode, Translations> = {
|
|||||||
aboutAppName: 'Пономарь',
|
aboutAppName: 'Пономарь',
|
||||||
aboutLicense: 'Основано на Java-приложенiи Понома́рь отъ Александра Андреева и Юрия Шардт. Лицензия GPL v3.',
|
aboutLicense: 'Основано на Java-приложенiи Понома́рь отъ Александра Андреева и Юрия Шардт. Лицензия GPL v3.',
|
||||||
installPwa: 'Оустановити Понома́рь',
|
installPwa: 'Оустановити Понома́рь',
|
||||||
|
installPwaAlreadyInstalled: 'Приложе́нiе ужѐ оустановлено́.',
|
||||||
|
installPwaHintFirefox: 'Оустановле́нiе: зна́чокъ въ адре́сной строкѐ и҆лѝ меню̀ браꙋ́зера → «Оустанови́ти».',
|
||||||
|
installPwaHintIos: 'Нажми́те «Подели́тися» и҆ избери́те «На экра́нъ “Домо́въ”».',
|
||||||
|
installPwaHintGeneric: 'Откро́йте меню̀ браꙋ́зера и҆ избери́те «Оустанови́ти приложе́нiе».',
|
||||||
offlineOfferTitle: 'Офлайн-данныѧ',
|
offlineOfferTitle: 'Офлайн-данныѧ',
|
||||||
offlineOfferText: 'А҆́ще плани́рꙋете и҆спо́льзовати приложе́нїе без̾ и҆нтернета, предзагризите да́нныѧ длѧ офлайн-потребле́нїѧ.',
|
offlineOfferText: 'А҆́ще плани́рꙋете и҆спо́льзовати приложе́нїе без̾ и҆нтернета, предзагризите да́нныѧ длѧ офлайн-потребле́нїѧ.',
|
||||||
offlineOfferPreloadAll: 'Загризи́ти всѧ̑',
|
offlineOfferPreloadAll: 'Загризи́ти всѧ̑',
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface PreloadProgress {
|
|||||||
file: string;
|
file: string;
|
||||||
done: boolean;
|
done: boolean;
|
||||||
failed: number;
|
failed: number;
|
||||||
|
failedFiles?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheStats {
|
export interface CacheStats {
|
||||||
@@ -157,11 +158,15 @@ export class OfflineManager {
|
|||||||
if (await DataCache.get(cacheKey) !== null) continue;
|
if (await DataCache.get(cacheKey) !== null) continue;
|
||||||
|
|
||||||
let success = false;
|
let success = false;
|
||||||
|
let notFound = false;
|
||||||
for (let attempt = 0; attempt < 3; attempt++) {
|
for (let attempt = 0; attempt < 3; attempt++) {
|
||||||
if (attempt > 0) await new Promise(r => setTimeout(r, 1000));
|
if (attempt > 0) await new Promise(r => setTimeout(r, 1000));
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(file);
|
const resp = await fetch(file);
|
||||||
if (resp.status === 404) break;
|
if (resp.status === 404) {
|
||||||
|
notFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (!resp.ok) continue;
|
if (!resp.ok) continue;
|
||||||
const ct = resp.headers.get('content-type');
|
const ct = resp.headers.get('content-type');
|
||||||
if (ct && ct.includes('text/html')) break;
|
if (ct && ct.includes('text/html')) break;
|
||||||
@@ -179,7 +184,11 @@ export class OfflineManager {
|
|||||||
// Retry on network error
|
// 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;
|
progress.done = true;
|
||||||
|
|||||||
+22
-2
@@ -255,6 +255,7 @@ export class SettingsView {
|
|||||||
<button id="install-pwa" class="w-full bg-navy text-parchment rounded p-3 font-bold hover:bg-navy-light transition-colors ${!('serviceWorker' in navigator) || window.matchMedia('(display-mode: standalone)').matches ? 'hidden' : ''}">
|
<button id="install-pwa" class="w-full bg-navy text-parchment rounded p-3 font-bold hover:bg-navy-light transition-colors ${!('serviceWorker' in navigator) || window.matchMedia('(display-mode: standalone)').matches ? 'hidden' : ''}">
|
||||||
${t.settings.installPwa}
|
${t.settings.installPwa}
|
||||||
</button>
|
</button>
|
||||||
|
<p id="install-hint" class="hidden text-sm text-navy-light mt-2"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Offline Content -->
|
<!-- Offline Content -->
|
||||||
@@ -403,12 +404,29 @@ export class SettingsView {
|
|||||||
});
|
});
|
||||||
document.getElementById('install-pwa')?.addEventListener('click', async () => {
|
document.getElementById('install-pwa')?.addEventListener('click', async () => {
|
||||||
const prompt = (window as any).__deferredPrompt;
|
const prompt = (window as any).__deferredPrompt;
|
||||||
|
const hintEl = document.getElementById('install-hint');
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
prompt.prompt();
|
prompt.prompt();
|
||||||
await prompt.userChoice;
|
await prompt.userChoice;
|
||||||
(window as any).__deferredPrompt = null;
|
(window as any).__deferredPrompt = null;
|
||||||
document.getElementById('install-pwa')?.remove();
|
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
|
// Location
|
||||||
@@ -535,7 +553,8 @@ export class SettingsView {
|
|||||||
if (progressBar) progressBar.style.width = '100%';
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
if (prog.failed > 0) {
|
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 {
|
} else {
|
||||||
progressText.textContent = t.settings.offlineDone;
|
progressText.textContent = t.settings.offlineDone;
|
||||||
}
|
}
|
||||||
@@ -658,8 +677,9 @@ export class SettingsView {
|
|||||||
}).then((prog) => {
|
}).then((prog) => {
|
||||||
if (progressBar) progressBar.style.width = '100%';
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
if (progressText) {
|
if (progressText) {
|
||||||
|
const failList = prog.failedFiles?.length ? `: ${prog.failedFiles.join(', ')}` : '';
|
||||||
progressText.textContent = prog.failed > 0
|
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;
|
: t.settings.offlineDone;
|
||||||
}
|
}
|
||||||
OfflineManager.getStats().then(stats => {
|
OfflineManager.getStats().then(stats => {
|
||||||
|
|||||||
Reference in new issue
Block a user