fix(pwa): fluxo de instalação também no Firefox (sem beforeinstallprompt)
- Detecção por propriedade ('onbeforeinstallprompt' in window) — evento é
exclusivo de Chromium; Firefox/Safari nunca o disparam (nem 'appinstalled')
- Firefox: item 'Instalar Aplicativo…' sempre visível (fora de standalone) e
abre modal com instruções por plataforma — Android (menu ⋮ → Instalar),
Windows 143+ (fixar na barra de tarefas), Linux/macOS (sem instalação
nativa; app pleno em aba/offline)
- Escondido em standalone via display-mode (Firefox também o reporta)
- sw.js: bump CACHE para notas-shell-v2 (shell novo)
- README: matriz de compatibilidade por navegador
- Testado: Chromium (prompt real), Firefox-Win UA (modal taskbar),
Firefox-Android UA (modal menu), Chromium sem BIP (item escondido)
This commit is contained in:
1 parent
18d33ed141
commit
650229bdb4
3 files changed
+70
-19
No files matched your search
@@ -244,6 +244,21 @@ do navegador e atalho de área de trabalho "Nova nota" (`/?new=1`).
|
|||||||
- **Offline UX**: shell + interface funcionam sem rede; ações que precisam do servidor mostram
|
- **Offline UX**: shell + interface funcionam sem rede; ações que precisam do servidor mostram
|
||||||
toast claro ("Sem conexão — o app abre offline; salvar precisa do servidor").
|
toast claro ("Sem conexão — o app abre offline; salvar precisa do servidor").
|
||||||
|
|
||||||
|
### Compatibilidade de instalação por navegador
|
||||||
|
|
||||||
|
| Navegador | Service Worker / offline | Instalação | Botão "Instalar Aplicativo…" |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Chrome/Edge/Brave/Vivaldi | ✅ | `beforeinstallprompt` (prompt nativo) | ✅ dispara o prompt |
|
||||||
|
| Firefox Android | ✅ | menu **⋮ → Instalar** (tela inicial) | ✅ abre instruções |
|
||||||
|
| Firefox Windows 143+ | ✅ | **fixar na barra de tarefas** pela barra de endereço | ✅ abre instruções |
|
||||||
|
| Firefox Linux/macOS | ✅ (app funciona pleno em aba) | sem fluxo nativo (só extensão de terceiros) | ✅ abre instruções honestas |
|
||||||
|
| Safari (macOS/iOS) | ✅ | manual: Compartilhar → *Adicionar ao Dock/Tela de Início* | escondido (sem evento) |
|
||||||
|
|
||||||
|
O código usa a checagem de propriedade `'onbeforeinstallprompt' in window` (o evento **nunca**
|
||||||
|
dispara em Firefox/Safari — MDN), então no Firefox o item do menu sempre aparece e mostra o
|
||||||
|
passo a passo da plataforma; em standalone (app instalado) o item se esconde sozinho via
|
||||||
|
`display-mode: standalone`, que o Firefox também reporta.
|
||||||
|
|
||||||
### Servindo os arquivos (backend Go atual)
|
### Servindo os arquivos (backend Go atual)
|
||||||
|
|
||||||
Já implementado em `internal/api/server.go` (`staticHandler`):
|
Já implementado em `internal/api/server.go` (`staticHandler`):
|
||||||
|
|||||||
+54
-18
@@ -1793,13 +1793,17 @@
|
|||||||
'toggle-wrap': () => { prefs.wrap = !prefs.wrap; persistPrefs(); applyPrefs(); },
|
'toggle-wrap': () => { prefs.wrap = !prefs.wrap; persistPrefs(); applyPrefs(); },
|
||||||
'prefs': openPrefs,
|
'prefs': openPrefs,
|
||||||
'install': async () => {
|
'install': async () => {
|
||||||
if (!deferredPrompt) { toast('Use o ícone de instalação da barra de endereço.'); return; }
|
if (deferredPrompt) {
|
||||||
const p = deferredPrompt;
|
const p = deferredPrompt;
|
||||||
deferredPrompt = null;
|
deferredPrompt = null;
|
||||||
refreshInstallUI();
|
refreshInstallUI();
|
||||||
p.prompt();
|
p.prompt();
|
||||||
const { outcome } = await p.userChoice;
|
const { outcome } = await p.userChoice;
|
||||||
if (outcome === 'accepted') toast('Instalando o Notas como aplicativo…');
|
if (outcome === 'accepted') toast('Instalando o Notas como aplicativo…');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isFirefox) { firefoxInstallHelp(); return; }
|
||||||
|
toast('Use o ícone de instalação da barra de endereço.');
|
||||||
},
|
},
|
||||||
'toggle-edit': () => setMode('edit'),
|
'toggle-edit': () => setMode('edit'),
|
||||||
'toggle-wysiwyg': () => setMode('wysiwyg'),
|
'toggle-wysiwyg': () => setMode('wysiwyg'),
|
||||||
@@ -1954,23 +1958,55 @@
|
|||||||
window.matchMedia('(display-mode: window-controls-overlay)').matches ||
|
window.matchMedia('(display-mode: window-controls-overlay)').matches ||
|
||||||
window.navigator.standalone === true;
|
window.navigator.standalone === true;
|
||||||
|
|
||||||
|
// beforeinstallprompt só existe em Chromium (Firefox/Safari nunca disparam o
|
||||||
|
// evento nem 'appinstalled') — checagem de propriedade, não de evento.
|
||||||
|
const canBIP = 'onbeforeinstallprompt' in window;
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
const isFirefox = /Firefox\//.test(ua);
|
||||||
|
const isAndroid = /Android/.test(ua);
|
||||||
|
const isWindows = /Windows/.test(ua);
|
||||||
|
|
||||||
let deferredPrompt = null;
|
let deferredPrompt = null;
|
||||||
const miInstall = document.querySelector('.menu-dropdown [data-action="install"]');
|
const miInstall = document.querySelector('.menu-dropdown [data-action="install"]');
|
||||||
|
|
||||||
function refreshInstallUI() {
|
function refreshInstallUI() {
|
||||||
if (miInstall) miInstall.classList.toggle('hidden', isStandalone() || !deferredPrompt);
|
if (!miInstall) return;
|
||||||
|
const show = !isStandalone() && (canBIP ? !!deferredPrompt : isFirefox);
|
||||||
|
miInstall.classList.toggle('hidden', !show);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', (e) => {
|
function firefoxInstallHelp() {
|
||||||
e.preventDefault(); // prompt próprio via menu Ajuda
|
const steps = isAndroid
|
||||||
deferredPrompt = e;
|
? '<p>No <b>Firefox para Android</b>:</p><ol><li>Toque no menu <b>⋮</b> da barra de endereço;</li>' +
|
||||||
refreshInstallUI();
|
'<li>Toque em <b>Instalar</b> → <em>Adicionar automaticamente</em>;</li>' +
|
||||||
});
|
'<li>O Notas entra na tela inicial e abre em modo aplicativo (standalone).</li></ol>'
|
||||||
window.addEventListener('appinstalled', () => {
|
: isWindows
|
||||||
deferredPrompt = null;
|
? '<p>No <b>Firefox para Windows</b> (143+):</p><ol><li>Clique no ícone da barra de endereço ' +
|
||||||
refreshInstallUI();
|
'(<em>Adicionar aba à barra de tarefas</em>);</li>' +
|
||||||
toast('Notas instalado ✔ procure pelo menu do sistema');
|
'<li>O site fica fixado na barra de tarefas e abre em janela simplificada de app.</li></ol>'
|
||||||
});
|
: '<p>O <b>Firefox para Linux/macOS não oferece instalação nativa de PWAs</b> (a Mozilla ' +
|
||||||
|
'prevê isso via extensão de terceiros).</p>';
|
||||||
|
modal({
|
||||||
|
title: 'Instalar o Notas no Firefox',
|
||||||
|
bodyHTML: steps +
|
||||||
|
'<p>Em qualquer caso o app <b>funciona offline</b> (instalado ou em aba): a interface é ' +
|
||||||
|
'carregada pelo Service Worker mesmo sem servidor. As alterações só sincronizam com o ' +
|
||||||
|
'servidor acessível.</p>',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canBIP) {
|
||||||
|
window.addEventListener('beforeinstallprompt', (e) => {
|
||||||
|
e.preventDefault(); // prompt próprio via menu Ajuda
|
||||||
|
deferredPrompt = e;
|
||||||
|
refreshInstallUI();
|
||||||
|
});
|
||||||
|
window.addEventListener('appinstalled', () => {
|
||||||
|
deferredPrompt = null;
|
||||||
|
refreshInstallUI();
|
||||||
|
toast('Notas instalado ✔ procure pelo menu do sistema');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ('serviceWorker' in navigator && location.protocol.startsWith('http')) {
|
if ('serviceWorker' in navigator && location.protocol.startsWith('http')) {
|
||||||
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(() => { /* sem SW: app continua */ });
|
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch(() => { /* sem SW: app continua */ });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* Service Worker do Notas — caches versionados.
|
/* Service Worker do Notas — caches versionados.
|
||||||
* BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos).
|
* BUMP de CACHE em todo deploy que mude shell/assets (o activate limpa os antigos).
|
||||||
*/
|
*/
|
||||||
const CACHE = 'notas-shell-v1';
|
const CACHE = 'notas-shell-v2';
|
||||||
const SHELL = [
|
const SHELL = [
|
||||||
'/',
|
'/',
|
||||||
'/css/app.css',
|
'/css/app.css',
|
||||||
|
|||||||
Reference in new issue
Block a user