PWA: install banner copy

This commit is contained in:
2026-02-08 17:53:06 +01:00
parent 736ed0e5e8
commit 4b2d98fe05
2 changed files with 136 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue'; import { computed, onMounted, onUnmounted, ref } from 'vue';
import { usePuzzleStore } from './stores/puzzle'; import { usePuzzleStore } from './stores/puzzle';
import { useI18n } from './composables/useI18n'; import { useI18n } from './composables/useI18n';
import GameBoard from './components/GameBoard.vue'; import GameBoard from './components/GameBoard.vue';
@@ -16,11 +16,77 @@ const store = usePuzzleStore();
const { t, locale, setLocale } = useI18n(); const { t, locale, setLocale } = useI18n();
const showCustomModal = ref(false); const showCustomModal = ref(false);
const showGuide = ref(false); const showGuide = ref(false);
const deferredPrompt = ref(null);
const canInstall = ref(false);
const installDismissed = ref(false);
const isCoarsePointer = ref(false);
const isStandalone = ref(false);
let displayModeMedia = null;
const installLabel = computed(() => {
return isCoarsePointer.value ? t('pwa.installMobile') : t('pwa.installDesktop');
});
const updateStandalone = () => {
isStandalone.value = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
if (isStandalone.value) {
canInstall.value = false;
installDismissed.value = true;
}
};
const handleBeforeInstallPrompt = (e) => {
e.preventDefault();
deferredPrompt.value = e;
if (!isStandalone.value) {
canInstall.value = true;
}
};
const handleAppInstalled = () => {
deferredPrompt.value = null;
canInstall.value = false;
installDismissed.value = true;
};
const handleInstall = async () => {
if (!deferredPrompt.value) return;
deferredPrompt.value.prompt();
const choice = await deferredPrompt.value.userChoice;
deferredPrompt.value = null;
canInstall.value = false;
if (!choice || choice.outcome !== 'accepted') {
installDismissed.value = true;
}
};
onMounted(() => { onMounted(() => {
if (!store.loadState()) { if (!store.loadState()) {
store.initGame(); // Inicjalizacja domyślnej gry jeśli brak zapisu store.initGame(); // Inicjalizacja domyślnej gry jeśli brak zapisu
} }
if (typeof window !== 'undefined') {
isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches;
updateStandalone();
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.addEventListener('appinstalled', handleAppInstalled);
displayModeMedia = window.matchMedia('(display-mode: standalone)');
if (displayModeMedia?.addEventListener) {
displayModeMedia.addEventListener('change', updateStandalone);
} else if (displayModeMedia?.addListener) {
displayModeMedia.addListener(updateStandalone);
}
}
});
onUnmounted(() => {
if (typeof window === 'undefined') return;
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
window.removeEventListener('appinstalled', handleAppInstalled);
if (displayModeMedia?.removeEventListener) {
displayModeMedia.removeEventListener('change', updateStandalone);
} else if (displayModeMedia?.removeListener) {
displayModeMedia.removeListener(updateStandalone);
}
}); });
</script> </script>
@@ -37,6 +103,16 @@ onMounted(() => {
<div class="underline"></div> <div class="underline"></div>
</header> </header>
<div v-if="canInstall && !installDismissed" class="install-banner">
<div class="install-text">{{ t('pwa.installTitle') }}</div>
<div class="install-actions">
<button class="btn-neon secondary install-btn" @click="handleInstall">
{{ installLabel }}
</button>
<button class="install-close" @click="installDismissed = true">×</button>
</div>
</div>
<div class="game-layout"> <div class="game-layout">
<!-- Level Selection --> <!-- Level Selection -->
<LevelSelector <LevelSelector
@@ -143,6 +219,57 @@ h1 {
align-items: center; align-items: center;
} }
.install-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 16px;
border-radius: 16px;
background: rgba(0, 0, 0, 0.35);
border: 1px solid rgba(0, 242, 254, 0.35);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25);
width: min(680px, 92vw);
margin: -10px 0 20px;
}
.install-text {
color: rgba(255, 255, 255, 0.9);
font-size: 0.95rem;
letter-spacing: 0.5px;
}
.install-actions {
display: flex;
align-items: center;
gap: 10px;
}
.install-btn {
padding: 8px 16px;
font-size: 0.85rem;
}
.install-close {
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
width: 32px;
height: 32px;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
line-height: 1;
transition: all 0.2s ease;
}
.install-close:hover {
border-color: #fff;
}
/* Remove old glass panel style from game-layout since we split it */ /* Remove old glass panel style from game-layout since we split it */
.board-section { .board-section {
display: flex; display: flex;

View File

@@ -45,7 +45,10 @@ const messages = {
'win.shareX': 'X', 'win.shareX': 'X',
'win.shareFacebook': 'Facebook', 'win.shareFacebook': 'Facebook',
'win.shareWhatsapp': 'WhatsApp', 'win.shareWhatsapp': 'WhatsApp',
'win.shareDownload': 'Pobierz zrzut' 'win.shareDownload': 'Pobierz zrzut',
'pwa.installTitle': 'Zainstaluj aplikację i graj offline',
'pwa.installMobile': 'Dodaj do ekranu głównego',
'pwa.installDesktop': 'Zainstaluj na komputerze'
}, },
en: { en: {
'app.title': 'Nonograms', 'app.title': 'Nonograms',
@@ -84,7 +87,10 @@ const messages = {
'win.shareX': 'X', 'win.shareX': 'X',
'win.shareFacebook': 'Facebook', 'win.shareFacebook': 'Facebook',
'win.shareWhatsapp': 'WhatsApp', 'win.shareWhatsapp': 'WhatsApp',
'win.shareDownload': 'Download screenshot' 'win.shareDownload': 'Download screenshot',
'pwa.installTitle': 'Install the app and play offline',
'pwa.installMobile': 'Add to home screen',
'pwa.installDesktop': 'Install on desktop'
} }
}; };