mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 05:18:11 +00:00
PWA: tryb offline
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/dev-dist
|
||||
/dist
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
|
||||
+6
-6
@@ -6,7 +6,7 @@
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"preview": "vite preview"
|
||||
"preview": "yarn build && vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.12.1",
|
||||
@@ -21,13 +21,13 @@
|
||||
"vue-router": "^4.0.0-0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.1",
|
||||
"@types/node": "^18.11.17",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"axios": "^1.1.2",
|
||||
"typescript": "^4.6.4",
|
||||
"vite": "^4.0.2",
|
||||
"axios": "^1.2.1",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-pwa": "^0.14.0",
|
||||
"vue-tsc": "^1.0.16"
|
||||
"vue-tsc": "^1.0.18"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
+27
-3
@@ -29,7 +29,7 @@
|
||||
</template>
|
||||
|
||||
<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';
|
||||
|
||||
@@ -44,6 +44,9 @@ import imageMixin from './mixins/imageMixin';
|
||||
import AppHeader from './components/App/AppHeader.vue';
|
||||
import axios from 'axios';
|
||||
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({
|
||||
components: {
|
||||
@@ -52,8 +55,8 @@ export default defineComponent({
|
||||
SelectBox,
|
||||
TrainModal,
|
||||
AppHeader,
|
||||
UpdatePrompt
|
||||
},
|
||||
UpdatePrompt,
|
||||
},
|
||||
|
||||
mixins: [imageMixin],
|
||||
|
||||
@@ -61,6 +64,8 @@ export default defineComponent({
|
||||
const store = useStore();
|
||||
store.connectToAPI();
|
||||
|
||||
const { offlineReady } = useCustomSW();
|
||||
|
||||
const isFilterCardVisible = ref(false);
|
||||
|
||||
provide('isFilterCardVisible', isFilterCardVisible);
|
||||
@@ -85,6 +90,25 @@ export default defineComponent({
|
||||
|
||||
created() {
|
||||
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() {
|
||||
|
||||
@@ -161,7 +161,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import { defineComponent } from 'vue';
|
||||
import { DataStatus } from '../../scripts/enums/DataStatus';
|
||||
import { useStore } from '../../store/store';
|
||||
@@ -172,6 +171,7 @@ export default defineComponent({
|
||||
return {
|
||||
tooltipActive: false,
|
||||
indicator: {
|
||||
offline: false,
|
||||
status: DataStatus.Loading,
|
||||
message: 'data-status.S3',
|
||||
},
|
||||
@@ -193,6 +193,7 @@ export default defineComponent({
|
||||
|
||||
return {
|
||||
dataStatus: store.dataStatuses,
|
||||
store,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -206,6 +207,13 @@ export default defineComponent({
|
||||
const trainsDataStatus = statuses.trains;
|
||||
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) {
|
||||
this.setSignalStatus(connectionStatus);
|
||||
this.indicator.status = connectionStatus;
|
||||
@@ -252,6 +260,10 @@ export default defineComponent({
|
||||
this.orangeLight = false;
|
||||
this.redBottomLight = false;
|
||||
|
||||
if (status == DataStatus.Initialized) {
|
||||
this.redTopLight = true;
|
||||
}
|
||||
|
||||
if (status == DataStatus.Loaded) {
|
||||
this.greenLight = true;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
import { ref } from 'vue';
|
||||
import useCustomSW from '../../mixins/useCustomSW';
|
||||
|
||||
const hidePrompt = ref(false);
|
||||
|
||||
const { needRefresh, updateServiceWorker } = useRegisterSW({
|
||||
immediate: true,
|
||||
|
||||
onNeedRefresh() {
|
||||
console.log('Needs refresh!');
|
||||
},
|
||||
});
|
||||
const { needRefresh, updateServiceWorker } = useCustomSW();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="journal-stats">
|
||||
<div class="journal-stats" v-show="!store.isOffline">
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="tab in data.tabs"
|
||||
|
||||
@@ -2,18 +2,22 @@
|
||||
<div class="train-table">
|
||||
<transition name="anim" mode="out-in">
|
||||
<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') }}
|
||||
</div>
|
||||
|
||||
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length != 0">
|
||||
<div class="timeouts-warning" v-if="trainNumbersWithTimeouts.length == 0">
|
||||
<b class="warning-timeout">?</b>
|
||||
{{ $t('trains.timeout') }}
|
||||
</div>
|
||||
|
||||
<ul class="train-list">
|
||||
|
||||
<ul class="train-list" v-else>
|
||||
<li
|
||||
class="train-row"
|
||||
v-for="train in currentTrains"
|
||||
|
||||
+3
-1
@@ -11,7 +11,8 @@
|
||||
"error": "An error occured while loading data!",
|
||||
"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-confirm": "Roger that!"
|
||||
"migration-confirm": "Roger that!",
|
||||
"offline": "App is in the offline mode!"
|
||||
},
|
||||
"update": {
|
||||
"title": "New Stacjownik version is available!",
|
||||
@@ -20,6 +21,7 @@
|
||||
"confirm-button": "Understood!"
|
||||
},
|
||||
"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-sceneries": "<b>S1a signal</b> <br> Cannot load online stations data!",
|
||||
"S2": "<b>S2 signal</b> <br> All data loaded successfully!",
|
||||
|
||||
+3
-1
@@ -11,7 +11,8 @@
|
||||
"error": "Wystąpił problem z załadowaniem danych!",
|
||||
"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-confirm": "Przyjąłem!"
|
||||
"migration-confirm": "Przyjąłem!",
|
||||
"offline": "Aplikacja w trybie offline!"
|
||||
},
|
||||
|
||||
"update": {
|
||||
@@ -22,6 +23,7 @@
|
||||
},
|
||||
|
||||
"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-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!",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
|
||||
export default () => {
|
||||
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
|
||||
immediate: true,
|
||||
});
|
||||
|
||||
return {
|
||||
needRefresh,
|
||||
updateServiceWorker,
|
||||
offlineReady,
|
||||
};
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import inputData from '../data/options.json';
|
||||
import Filter from '../scripts/interfaces/Filter';
|
||||
import Station from '../scripts/interfaces/Station';
|
||||
import StorageManager from '../scripts/managers/storageManager';
|
||||
import { useStore } from './store';
|
||||
|
||||
const sortStations = (a: Station, b: Station, sorter: { index: number; dir: number }) => {
|
||||
switch (sorter.index) {
|
||||
@@ -58,7 +59,7 @@ const sortStations = (a: Station, b: Station, sorter: { index: number; dir: numb
|
||||
return a.name.localeCompare(b.name);
|
||||
};
|
||||
|
||||
const filterStations = (station: Station, filters: Filter) => {
|
||||
const filterStations = (station: Station, filters: Filter, isOffline = false) => {
|
||||
const returnMode = false;
|
||||
|
||||
if ((station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && filters['nonPublic'])
|
||||
@@ -236,6 +237,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
inputs: inputData,
|
||||
filters: { ...filterInitStates },
|
||||
sorterActive: { index: 0, dir: 1 },
|
||||
store: useStore(),
|
||||
};
|
||||
},
|
||||
|
||||
@@ -249,7 +251,7 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
|
||||
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));
|
||||
},
|
||||
|
||||
@@ -303,3 +305,4 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+11
-4
@@ -17,7 +17,6 @@ import {
|
||||
} from '../scripts/utils/storeUtils';
|
||||
import { APIData, StationJSONData, StoreState } from './storeTypes';
|
||||
|
||||
|
||||
export const useStore = defineStore('store', {
|
||||
state: () =>
|
||||
({
|
||||
@@ -35,6 +34,7 @@ export const useStore = defineStore('store', {
|
||||
stationCount: 0,
|
||||
|
||||
webSocket: undefined,
|
||||
isOffline: false,
|
||||
|
||||
dispatcherStatsName: '',
|
||||
dispatcherStatsData: undefined,
|
||||
@@ -57,7 +57,6 @@ export const useStore = defineStore('store', {
|
||||
|
||||
blockScroll: false,
|
||||
listenerLaunched: false,
|
||||
|
||||
} as StoreState),
|
||||
|
||||
actions: {
|
||||
@@ -225,6 +224,14 @@ export const useStore = defineStore('store', {
|
||||
const onlineStationNames: string[] = [];
|
||||
const prevDispatcherStatuses: StoreState['lastDispatcherStatuses'] = [];
|
||||
|
||||
if (this.isOffline) {
|
||||
this.stationList.forEach((station) => {
|
||||
station.onlineInfo = undefined;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.apiData.stations?.forEach((stationAPIData) => {
|
||||
if (stationAPIData.region !== this.region.id || !stationAPIData.isOnline) return;
|
||||
const station = this.stationList.find((s) => s.name === stationAPIData.stationName);
|
||||
@@ -352,12 +359,11 @@ export const useStore = defineStore('store', {
|
||||
transports: ['websocket', 'polling'],
|
||||
rememberUpgrade: true,
|
||||
reconnection: true,
|
||||
timeout: 10000,
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
socket.on('connect_error', (err) => {
|
||||
this.dataStatuses.connection = DataStatus.Error;
|
||||
this.webSocket = undefined;
|
||||
});
|
||||
|
||||
socket.on('UPDATE', (data: APIData) => {
|
||||
@@ -368,6 +374,7 @@ export const useStore = defineStore('store', {
|
||||
|
||||
socket.emit('FETCH_DATA', {}, (data: APIData) => {
|
||||
this.apiData = data;
|
||||
this.dataStatuses.connection = DataStatus.Loaded;
|
||||
this.setOnlineData();
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface StoreState {
|
||||
stationCount: number;
|
||||
|
||||
webSocket?: Socket;
|
||||
isOffline: boolean;
|
||||
|
||||
dispatcherStatsName: string;
|
||||
dispatcherStatsData?: DispatcherStatsAPIData;
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
<!-- <transition name="warning" mode="out-in"> -->
|
||||
<!-- <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">
|
||||
{{ $t('app.error') }}
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
<!-- <transition name="warning" mode="out-in"> -->
|
||||
<!-- <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">
|
||||
{{ $t('app.error') }}
|
||||
|
||||
+19
-16
@@ -3,6 +3,9 @@ import vue from '@vitejs/plugin-vue';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
server: {
|
||||
port: 5001,
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
@@ -11,28 +14,27 @@ export default defineConfig({
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,png,svg,img}'],
|
||||
runtimeCaching: [
|
||||
// {
|
||||
// urlPattern: new RegExp('^https://stacjownik.eu-4.evennode.com/api/getSceneries'),
|
||||
// handler: 'NetworkFirst',
|
||||
// options: {
|
||||
// cacheName: 'sceneries-cache',
|
||||
// expiration: {
|
||||
// maxEntries: 200,
|
||||
// maxAgeSeconds: 60 * 60 * 24 * 60, // <== 60 days
|
||||
// },
|
||||
// cacheableResponse: {
|
||||
// statuses: [0, 200],
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
{
|
||||
urlPattern: new RegExp('^https://spythere.pl/api/getSceneries', 'i'),
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'sceneries-cache',
|
||||
expiration: {
|
||||
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'images-cache',
|
||||
expiration: {
|
||||
maxEntries: 300,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 7,
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 60,
|
||||
},
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200, 404],
|
||||
@@ -48,3 +50,4 @@ export default defineConfig({
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user