mirror of
https://github.com/Spythere/srjp-td2.git
synced 2026-05-03 05:28:12 +00:00
feat: offline mode; PWA
This commit is contained in:
+52
-13
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="text-white min-h-screen bg-zinc-950">
|
||||
<!-- PWA update prompt -->
|
||||
<transition name="slide-anim">
|
||||
<UpdatePrompt v-if="needRefresh" @onUpdateClick="updateApp()" />
|
||||
</transition>
|
||||
|
||||
<!-- Content -->
|
||||
<Navbar />
|
||||
<MainContainer />
|
||||
</div>
|
||||
@@ -8,10 +14,14 @@
|
||||
<script lang="ts" setup>
|
||||
import Navbar from './components/App/Navbar.vue';
|
||||
import MainContainer from './components/App/MainContainer.vue';
|
||||
import UpdatePrompt from './components/App/UpdatePrompt.vue';
|
||||
|
||||
import { onMounted } from 'vue';
|
||||
import { useApiStore } from './stores/api.store';
|
||||
import { useGlobalStore } from './stores/global.store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
import { DataStatus } from './types/api.types';
|
||||
|
||||
const originalDocumentTitle = document.title;
|
||||
|
||||
@@ -19,28 +29,24 @@ const apiStore = useApiStore();
|
||||
const globalStore = useGlobalStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
|
||||
|
||||
onMounted(async () => {
|
||||
setupLocale();
|
||||
setupDarkMode();
|
||||
setupOfflineMode();
|
||||
loadStorageTimetables();
|
||||
setupAfterPrintClose();
|
||||
|
||||
await apiStore.setupAPIData();
|
||||
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
if (query.has('id')) {
|
||||
const id = query.get('id')!;
|
||||
|
||||
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
|
||||
|
||||
if (queryTrain) {
|
||||
globalStore.selectedTrainId = id;
|
||||
globalStore.selectedActiveTrain = queryTrain;
|
||||
}
|
||||
}
|
||||
handleQueries();
|
||||
});
|
||||
|
||||
function updateApp() {
|
||||
updateServiceWorker(true);
|
||||
needRefresh.value = false;
|
||||
}
|
||||
|
||||
function loadStorageTimetables() {
|
||||
if (!window.localStorage.getItem('savedTimetables')) return;
|
||||
|
||||
@@ -73,4 +79,37 @@ function setupLocale() {
|
||||
i18n.locale.value = window.localStorage.getItem('locale')!;
|
||||
}
|
||||
}
|
||||
|
||||
function setupOfflineMode() {
|
||||
apiStore.connectionMode = !navigator.onLine ? 'offline' : 'online';
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
apiStore.connectionMode = 'offline';
|
||||
|
||||
apiStore.journalTimetablesData = null;
|
||||
apiStore.activeData = null;
|
||||
});
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
apiStore.connectionMode = 'online';
|
||||
apiStore.journalDataStatus = DataStatus.SUCCESS;
|
||||
|
||||
apiStore.setupAPIData();
|
||||
});
|
||||
}
|
||||
|
||||
function handleQueries() {
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
|
||||
if (query.has('id')) {
|
||||
const id = query.get('id')!;
|
||||
|
||||
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
|
||||
|
||||
if (queryTrain) {
|
||||
globalStore.selectedTrainId = id;
|
||||
globalStore.selectedActiveTrain = queryTrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="fixed z-50 bottom-0 right-0">
|
||||
<button @click="onUpdateClick" class="p-3 m-3 bg-cyan-600 rounded-md text-xl" ref="updateBtnEl">
|
||||
<div>{{ $t('update-prompt.line1') }}</div>
|
||||
<u>{{ $t('update-prompt.line2') }}</u>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const emit = defineEmits(['onUpdateClick']);
|
||||
const updateBtnEl = ref<HTMLElement | null>(null);
|
||||
|
||||
function onUpdateClick() {
|
||||
emit('onUpdateClick');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateBtnEl.value?.focus();
|
||||
});
|
||||
</script>
|
||||
@@ -99,9 +99,14 @@
|
||||
<input
|
||||
type="text"
|
||||
v-else-if="globalStore.viewMode == 'journal'"
|
||||
v-model="globalStore.journalTimetableSearch"
|
||||
@change="fetchJournalTimetables"
|
||||
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
|
||||
v-model="globalStore.journalTimetableSearch"
|
||||
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
|
||||
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
|
||||
}`"
|
||||
:disabled="
|
||||
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
|
||||
"
|
||||
:placeholder="$t('journal-search-placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
|
||||
<div class="overflow-auto text-center font-bold text-zinc-400 p-1 min-h-full" v-else>
|
||||
<div v-if="globalStore.viewMode == 'active'">
|
||||
<div>{{ $t('train-select-info') }}</div>
|
||||
<div v-if="apiStore.connectionMode == 'online'">{{ $t('train-select-info') }}</div>
|
||||
<div v-else class="bg-red-500 text-white p-2">{{ $t('data-offline-mode') }}</div>
|
||||
</div>
|
||||
|
||||
<LocalStorageView v-else-if="globalStore.viewMode == 'storage'" />
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
{{ $t('journal-preview-title') }}
|
||||
</h2>
|
||||
|
||||
<div v-if="apiStore.journalDataStatus == DataStatus.LOADING" class="bg-zinc-900 p-2">
|
||||
<div v-if="apiStore.connectionMode == 'offline'" class="bg-red-500 p-2">
|
||||
{{ $t('data-offline-mode') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="apiStore.journalDataStatus == DataStatus.LOADING" class="bg-zinc-900 p-2">
|
||||
{{ $t('data-loading-text') }}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"train-select-placeholder": "Choose active train from the list",
|
||||
"train-select-info": "Choose active train to generate SRJP timetable",
|
||||
"train-search-placeholder": "Enter TT details (number, route, user)",
|
||||
|
||||
"update-prompt": {
|
||||
"line1": "New version of SRJP is available!",
|
||||
"line2": "Click here to update the app!"
|
||||
},
|
||||
|
||||
"data-offline-mode": "You're currently using the offline mode of the SRJP app - server data is unavailable!",
|
||||
|
||||
"headers": {
|
||||
"line_no": "Line\nno.",
|
||||
"line_km": "Km",
|
||||
@@ -17,6 +25,7 @@
|
||||
"vmax": "Vmax",
|
||||
"relation": "Route"
|
||||
},
|
||||
|
||||
"storage-empty-header": "ARCHIVED TIMETABLES SEARCH MODE",
|
||||
"storage-empty-info": "Timetables will be shown here after their archiving.",
|
||||
"storage-preview-title": "ARCHIVED TIMETABLES",
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
"train-select-placeholder": "Wybierz pociąg z listy",
|
||||
"train-select-info": "Wybierz aktywny pociąg, aby wygenerować SRJP",
|
||||
"train-search-placeholder": "Wpisz szczegóły RJ (nr, relacja, gracz)",
|
||||
|
||||
"update-prompt": {
|
||||
"line1": "Nowa wersja SRJP jest dostępna!",
|
||||
"line2": "Kliknij, aby zaktualizować aplikację!"
|
||||
},
|
||||
|
||||
"data-offline-mode": "Korzystasz z trybu offline aplikacji SRJP - dane serwerowe są niedostępne!",
|
||||
|
||||
"headers": {
|
||||
"line_no": "Nr\nlinii",
|
||||
"line_km": "Km",
|
||||
@@ -17,6 +25,7 @@
|
||||
"vmax": "Vmax",
|
||||
"relation": "Relacja"
|
||||
},
|
||||
|
||||
"storage-empty-header": "TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY",
|
||||
"storage-empty-info": "Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.",
|
||||
"storage-preview-title": "ZAPISANE ROZKŁADY JAZDY",
|
||||
|
||||
+25
-19
@@ -15,6 +15,8 @@ import type {
|
||||
} from '../types/common.types';
|
||||
import { useGlobalStore } from './global.store';
|
||||
|
||||
let activeDataInterval = -1;
|
||||
|
||||
export const useApiStore = defineStore('api', {
|
||||
state() {
|
||||
return {
|
||||
@@ -28,37 +30,41 @@ export const useApiStore = defineStore('api', {
|
||||
isActiveDataOutdated: false,
|
||||
|
||||
activeDataStatus: DataStatus.LOADING,
|
||||
journalDataStatus: DataStatus.SUCCESS
|
||||
journalDataStatus: DataStatus.SUCCESS,
|
||||
|
||||
connectionMode: 'online' as 'online' | 'offline'
|
||||
};
|
||||
},
|
||||
|
||||
actions: {
|
||||
async setupAPIData() {
|
||||
if (this.client != null) return;
|
||||
if (this.client == null) {
|
||||
let baseURL = 'https://stacjownik.spythere.eu';
|
||||
|
||||
let baseURL = 'https://stacjownik.spythere.eu';
|
||||
switch (import.meta.env.VITE_API_MODE) {
|
||||
case 'development':
|
||||
baseURL = 'http://localhost:3001';
|
||||
break;
|
||||
case 'mocking':
|
||||
baseURL = 'http://localhost:3123';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
switch (import.meta.env.VITE_API_MODE) {
|
||||
case 'development':
|
||||
baseURL = 'http://localhost:3001';
|
||||
break;
|
||||
case 'mocking':
|
||||
baseURL = 'http://localhost:3123';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
this.client = axios.create({
|
||||
baseURL
|
||||
});
|
||||
}
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL
|
||||
});
|
||||
clearInterval(activeDataInterval);
|
||||
|
||||
activeDataInterval = setInterval(() => {
|
||||
this.fetchActiveData();
|
||||
}, 25000);
|
||||
|
||||
this.fetchSceneriesData();
|
||||
await this.fetchActiveData();
|
||||
|
||||
setInterval(() => {
|
||||
this.fetchActiveData();
|
||||
}, 25000);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
|
||||
+12
-1
@@ -32,7 +32,6 @@ body {
|
||||
::-webkit-scrollbar-corner {
|
||||
background: theme('colors.stone.900');
|
||||
border-radius: 0 0 theme('borderRadius.md') 0;
|
||||
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
@@ -86,3 +85,15 @@ body {
|
||||
color-scheme: light;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.slide-anim-enter-active,
|
||||
.slide-anim-leave-active {
|
||||
transition: all 250ms ease-in-out;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.slide-anim-enter-from,
|
||||
.slide-anim-leave-to {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user