PWA: install banner copy
This commit is contained in:
129
src/App.vue
129
src/App.vue
@@ -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;
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user