PWA: tryb offline

This commit is contained in:
2022-12-26 18:43:15 +01:00
parent f93c1fbfec
commit 2e721fb8bf
17 changed files with 2531 additions and 2366 deletions
+1
View File
@@ -1,6 +1,7 @@
.DS_Store .DS_Store
node_modules node_modules
/dev-dist /dev-dist
/dist
# local env files # local env files
.env.local .env.local
+6 -6
View File
@@ -6,7 +6,7 @@
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting", "deploy": "yarn build && firebase deploy --only hosting",
"preview": "vite preview" "preview": "yarn build && vite preview"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.12.1",
@@ -21,13 +21,13 @@
"vue-router": "^4.0.0-0" "vue-router": "^4.0.0-0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.1", "@types/node": "^18.11.17",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"axios": "^1.1.2", "axios": "^1.2.1",
"typescript": "^4.6.4", "typescript": "^4.9.4",
"vite": "^4.0.2", "vite": "^4.0.3",
"vite-plugin-pwa": "^0.14.0", "vite-plugin-pwa": "^0.14.0",
"vue-tsc": "^1.0.16" "vue-tsc": "^1.0.18"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
+27 -3
View File
@@ -29,7 +29,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, provide, ref, watch } from 'vue'; import { computed, defineComponent, KeepAlive, provide, ref, watch } from 'vue';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
@@ -44,6 +44,9 @@ import imageMixin from './mixins/imageMixin';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import axios from 'axios'; import axios from 'axios';
import UpdatePrompt from './components/App/UpdatePrompt.vue'; import UpdatePrompt from './components/App/UpdatePrompt.vue';
import { VERSION } from 'vue-i18n';
import { RouterView } from 'vue-router';
import useCustomSW from './mixins/useCustomSW';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -52,8 +55,8 @@ export default defineComponent({
SelectBox, SelectBox,
TrainModal, TrainModal,
AppHeader, AppHeader,
UpdatePrompt UpdatePrompt,
}, },
mixins: [imageMixin], mixins: [imageMixin],
@@ -61,6 +64,8 @@ export default defineComponent({
const store = useStore(); const store = useStore();
store.connectToAPI(); store.connectToAPI();
const { offlineReady } = useCustomSW();
const isFilterCardVisible = ref(false); const isFilterCardVisible = ref(false);
provide('isFilterCardVisible', isFilterCardVisible); provide('isFilterCardVisible', isFilterCardVisible);
@@ -85,6 +90,25 @@ export default defineComponent({
created() { created() {
this.loadLang(); this.loadLang();
this.store.isOffline = !window.navigator.onLine;
window.addEventListener('offline', () => {
this.store.isOffline = true;
this.store.apiData = {
stations: [],
dispatchers: [],
trains: [],
connectedSocketCount: 0,
};
this.store.setOnlineData();
});
window.addEventListener('online', () => {
this.store.isOffline = false;
});
}, },
async mounted() { async mounted() {
+13 -1
View File
@@ -161,7 +161,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { DataStatus } from '../../scripts/enums/DataStatus'; import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
@@ -172,6 +171,7 @@ export default defineComponent({
return { return {
tooltipActive: false, tooltipActive: false,
indicator: { indicator: {
offline: false,
status: DataStatus.Loading, status: DataStatus.Loading,
message: 'data-status.S3', message: 'data-status.S3',
}, },
@@ -193,6 +193,7 @@ export default defineComponent({
return { return {
dataStatus: store.dataStatuses, dataStatus: store.dataStatuses,
store,
}; };
}, },
@@ -206,6 +207,13 @@ export default defineComponent({
const trainsDataStatus = statuses.trains; const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers; const dispatcherDataStatus = statuses.dispatchers;
if (this.store.isOffline) {
this.setSignalStatus(DataStatus.Initialized);
this.indicator.status = DataStatus.Initialized;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == DataStatus.Error) { if (connectionStatus == DataStatus.Error) {
this.setSignalStatus(connectionStatus); this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus; this.indicator.status = connectionStatus;
@@ -252,6 +260,10 @@ export default defineComponent({
this.orangeLight = false; this.orangeLight = false;
this.redBottomLight = false; this.redBottomLight = false;
if (status == DataStatus.Initialized) {
this.redTopLight = true;
}
if (status == DataStatus.Loaded) { if (status == DataStatus.Loaded) {
this.greenLight = true; this.greenLight = true;
} }
+2 -9
View File
@@ -14,18 +14,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRegisterSW } from 'virtual:pwa-register/vue';
import { ref } from 'vue'; import { ref } from 'vue';
import useCustomSW from '../../mixins/useCustomSW';
const hidePrompt = ref(false); const hidePrompt = ref(false);
const { needRefresh, updateServiceWorker } = useCustomSW();
const { needRefresh, updateServiceWorker } = useRegisterSW({
immediate: true,
onNeedRefresh() {
console.log('Needs refresh!');
},
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="journal-stats"> <div class="journal-stats" v-show="!store.isOffline">
<div class="tabs"> <div class="tabs">
<button <button
v-for="tab in data.tabs" v-for="tab in data.tabs"
+8 -4
View File
@@ -2,18 +2,22 @@
<div class="train-table"> <div class="train-table">
<transition name="anim" mode="out-in"> <transition name="anim" mode="out-in">
<div :key="store.dataStatuses.trains"> <div :key="store.dataStatuses.trains">
<Loading v-if="trains.length == 0 && store.dataStatuses.trains == 0" /> <div class="table-info" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<div class="table-info no-trains" v-if="trains.length == 0 && store.dataStatuses.trains != 0"> <Loading v-else-if="trains.length == 0 && store.dataStatuses.trains == 0" />
<div class="table-info no-trains" v-else-if="trains.length == 0 && store.dataStatuses.trains != 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }}
</div> </div>
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0"> <div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
<b class="warning-timeout">?</b> <b class="warning-timeout">?</b>
{{ $t('trains.timeout') }} {{ $t('trains.timeout') }}
</div> </div>
<ul class="train-list"> <ul class="train-list" v-else>
<li <li
class="train-row" class="train-row"
v-for="train in currentTrains" v-for="train in currentTrains"
+3 -1
View File
@@ -11,7 +11,8 @@
"error": "An error occured while loading data!", "error": "An error occured while loading data!",
"no-result": "No results for current search!", "no-result": "No results for current search!",
"migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!", "migration-warning": "Stacjownik services will be unavailable 2/06/2022 between 1-3am (CEST time) due to the migration of API hostings!",
"migration-confirm": "Roger that!" "migration-confirm": "Roger that!",
"offline": "App is in the offline mode!"
}, },
"update": { "update": {
"title": "New Stacjownik version is available!", "title": "New Stacjownik version is available!",
@@ -20,6 +21,7 @@
"confirm-button": "Understood!" "confirm-button": "Understood!"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
"S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!", "S1a-connection": "<b>S1a signal</b> <br> Cannot connect with Stacjownik API service!",
"S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!", "S1a-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
"S2": "<b>S2 signal</b> <br> All data loaded successfully!", "S2": "<b>S2 signal</b> <br> All data loaded successfully!",
+3 -1
View File
@@ -11,7 +11,8 @@
"error": "Wystąpił problem z załadowaniem danych!", "error": "Wystąpił problem z załadowaniem danych!",
"no-result": "Brak wyników o podanych kryteriach!", "no-result": "Brak wyników o podanych kryteriach!",
"migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!", "migration-warning": "Usługi Stacjownika będą niedostępne w godzinach 1:00-3:00 2 czerwca 2022r. z powodu migracji hostingów API!",
"migration-confirm": "Przyjąłem!" "migration-confirm": "Przyjąłem!",
"offline": "Aplikacja w trybie offline!"
}, },
"update": { "update": {
@@ -22,6 +23,7 @@
}, },
"data-status": { "data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!", "S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
"S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!", "S1a-sceneries": "<b>Sygnał S1a</b> <br> Błąd podczas pobierania danych o sceneriach online!",
"S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!", "S2": "<b>Sygnał S2</b> <br> Pomyślnie załadowano dane!",
+13
View File
@@ -0,0 +1,13 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true,
});
return {
needRefresh,
updateServiceWorker,
offlineReady,
};
};
+5 -2
View File
@@ -3,6 +3,7 @@ import inputData from '../data/options.json';
import Filter from '../scripts/interfaces/Filter'; import Filter from '../scripts/interfaces/Filter';
import Station from '../scripts/interfaces/Station'; import Station from '../scripts/interfaces/Station';
import StorageManager from '../scripts/managers/storageManager'; import StorageManager from '../scripts/managers/storageManager';
import { useStore } from './store';
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => { const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
switch (sorter.index) { switch (sorter.index) {
@@ -58,7 +59,7 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}; };
const filterStations = (station: Station, filters: Filter) => { const filterStations = (station: Station, filters: Filter, isOffline = false) => {
const returnMode = false; const returnMode = false;
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic']) if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
@@ -236,6 +237,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
inputs: inputData, inputs: inputData,
filters: { ...filterInitStates }, filters: { ...filterInitStates },
sorterActive: { index: 0, dir: 1 }, sorterActive: { index: 0, dir: 1 },
store: useStore(),
}; };
}, },
@@ -249,7 +251,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
return station; return station;
}) })
.filter((station) => filterStations(station, this.filters)) .filter((station) => filterStations(station, this.filters, this.store.isOffline))
.sort((a, b) => sortStations(a, b, this.sorterActive)); .sort((a, b) => sortStations(a, b, this.sorterActive));
}, },
@@ -303,3 +305,4 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
}, },
}, },
}); });
+11 -4
View File
@@ -17,7 +17,6 @@ import {
} from '../scripts/utils/storeUtils'; } from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from './storeTypes'; import { APIData, StationJSONData, StoreState } from './storeTypes';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
@@ -35,6 +34,7 @@ export const useStore = defineStore('store', {
stationCount: 0, stationCount: 0,
webSocket: undefined, webSocket: undefined,
isOffline: false,
dispatcherStatsName: '', dispatcherStatsName: '',
dispatcherStatsData: undefined, dispatcherStatsData: undefined,
@@ -57,7 +57,6 @@ export const useStore = defineStore('store', {
blockScroll: false, blockScroll: false,
listenerLaunched: false, listenerLaunched: false,
} as StoreState), } as StoreState),
actions: { actions: {
@@ -225,6 +224,14 @@ export const useStore = defineStore('store', {
const onlineStationNames: string[] = []; const onlineStationNames: string[] = [];
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = []; const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
if (this.isOffline) {
this.stationList.forEach((station) => {
station.onlineInfo = undefined;
});
return;
}
this.apiData.stations?.forEach((stationAPIData) => { this.apiData.stations?.forEach((stationAPIData) => {
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return; if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
const station = this.stationList.find((s) => s.name === stationAPIData.stationName); const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
@@ -352,12 +359,11 @@ export const useStore = defineStore('store', {
transports: ['websocket', 'polling'], transports: ['websocket', 'polling'],
rememberUpgrade: true, rememberUpgrade: true,
reconnection: true, reconnection: true,
timeout: 10000, timeout: 2000,
}); });
socket.on('connect_error', (err) => { socket.on('connect_error', (err) => {
this.dataStatuses.connection = DataStatus.Error; this.dataStatuses.connection = DataStatus.Error;
this.webSocket = undefined;
}); });
socket.on('UPDATE', (data: APIData) => { socket.on('UPDATE', (data: APIData) => {
@@ -368,6 +374,7 @@ export const useStore = defineStore('store', {
socket.emit('FETCH_DATA', {}, (data: APIData) => { socket.emit('FETCH_DATA', {}, (data: APIData) => {
this.apiData = data; this.apiData = data;
this.dataStatuses.connection = DataStatus.Loaded;
this.setOnlineData(); this.setOnlineData();
}); });
+1
View File
@@ -23,6 +23,7 @@ export interface StoreState {
stationCount: number; stationCount: number;
webSocket?: Socket; webSocket?: Socket;
isOffline: boolean;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: DispatcherStatsAPIData; dispatcherStatsData?: DispatcherStatsAPIData;
+5 -1
View File
@@ -14,7 +14,11 @@
<div class="list_wrapper" @scroll="handleScroll"> <div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> --> <!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> --> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" /> <div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
+5 -1
View File
@@ -17,7 +17,11 @@
<div class="list_wrapper" @scroll="handleScroll"> <div class="list_wrapper" @scroll="handleScroll">
<!-- <transition name="warning" mode="out-in"> --> <!-- <transition name="warning" mode="out-in"> -->
<!-- <div :key="dataStatus"> --> <!-- <div :key="dataStatus"> -->
<Loading v-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" /> <div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Initialized || dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error"> <div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
+19 -16
View File
@@ -3,6 +3,9 @@ import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
server: {
port: 5001,
},
plugins: [ plugins: [
vue(), vue(),
VitePWA({ VitePWA({
@@ -11,28 +14,27 @@ export default defineConfig({
workbox: { workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,img}'], globPatterns: ['**/*.{js,css,html,png,svg,img}'],
runtimeCaching: [ runtimeCaching: [
// { {
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'), urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
// handler: 'NetworkFirst', handler: 'NetworkFirst',
// options: { options: {
// cacheName: 'sceneries-cache', cacheName: 'sceneries-cache',
// expiration: { expiration: {
// maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days },
// }, cacheableResponse: {
// cacheableResponse: { statuses: [0, 200],
// statuses: [0, 200], },
// }, },
// }, },
// },
{ {
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i, urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst', handler: 'CacheFirst',
options: { options: {
cacheName: 'images-cache', cacheName: 'images-cache',
expiration: { expiration: {
maxEntries: 300, maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7, maxAgeSeconds: 60 * 60 * 24 * 60,
}, },
cacheableResponse: { cacheableResponse: {
statuses: [0, 200, 404], statuses: [0, 200, 404],
@@ -48,3 +50,4 @@ export default defineConfig({
], ],
}); });
+2407 -2315
View File
File diff suppressed because it is too large Load Diff