mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 13:28:11 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 619ce97b52 | |||
| 47d35f335f | |||
| 8fda8fa0df | |||
| 71d697eda5 | |||
| f2b1fc5369 | |||
| 4a9b142e16 | |||
| 08d8bf3c57 | |||
| 0ee90357aa | |||
| c8964dc20f | |||
| 6a62276d95 | |||
| b8550eed9a | |||
| 27b23ccc95 | |||
| b49517aded | |||
| ed2b8be4dc | |||
| 54c1dbbf15 | |||
| 0ac7ba51e5 | |||
| bdf85cd8ec | |||
| 63b268d9b9 | |||
| d73c8ef112 | |||
| 3d1c66b420 | |||
| b3f7108979 | |||
| feabfd29e0 | |||
| f17fedc976 | |||
| c83c75e014 | |||
| e57143f517 |
@@ -1,20 +0,0 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
channelId: live
|
||||
projectId: stacjownik-td2
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
|
||||
Generated
+457
-1761
File diff suppressed because it is too large
Load Diff
+13
-16
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.25.1",
|
||||
"version": "1.26.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:prod": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
|
||||
"preview": "yarn build && vite preview",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
@@ -19,25 +21,20 @@
|
||||
"showdown": "^2.1.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/node": "^20.6.2",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@vite-pwa/assets-generator": "^0.2.4",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"axios": "^1.5.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"axios": "^1.7.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vue-tsc": "^1.8.11"
|
||||
"vue-tsc": "^2.0.28"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
+6
-15
@@ -81,9 +81,7 @@ export default defineComponent({
|
||||
isUpdateCardOpen: false,
|
||||
|
||||
currentLang: 'pl',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
|
||||
nextUpdateTime: 0
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
||||
}),
|
||||
|
||||
created() {
|
||||
@@ -96,22 +94,13 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
|
||||
this.loadLang();
|
||||
this.setupOfflineHandling();
|
||||
this.checkAppVersion();
|
||||
|
||||
this.apiStore.setupAPIData();
|
||||
window.requestAnimationFrame(this.update);
|
||||
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
},
|
||||
|
||||
update(t: number) {
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.apiStore.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
window.requestAnimationFrame(this.update);
|
||||
},
|
||||
|
||||
async checkAppVersion() {
|
||||
@@ -131,7 +120,8 @@ export default defineComponent({
|
||||
};
|
||||
|
||||
this.isUpdateCardOpen =
|
||||
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
|
||||
import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||
} catch (error) {
|
||||
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
||||
}
|
||||
@@ -157,6 +147,7 @@ export default defineComponent({
|
||||
|
||||
handleOnlineMode() {
|
||||
this.store.isOffline = false;
|
||||
this.apiStore.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
this.apiStore.connectToAPI();
|
||||
},
|
||||
|
||||
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
|
||||
.status-badge {
|
||||
border-radius: 1em;
|
||||
font-weight: 500;
|
||||
text-wrap: nowrap;
|
||||
|
||||
padding: 0.2em 0.55em;
|
||||
padding: 0.2rem 0.55rem;
|
||||
|
||||
background-color: $online;
|
||||
|
||||
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
|
||||
|
||||
&.no-limit {
|
||||
background-color: $no-limit;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.not-signed,
|
||||
&.unavailable {
|
||||
background-color: $unav;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.afk {
|
||||
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
|
||||
background-color: $no-space;
|
||||
border: 1px solid white;
|
||||
color: white;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.unknown,
|
||||
|
||||
@@ -11,15 +11,11 @@
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<img
|
||||
<VehicleThumbnail
|
||||
v-for="(thumbnailImage, imageIndex) in images"
|
||||
:data-mouseover="vehicleName"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
:data-tooltip-content="vehicleName"
|
||||
:src="`https://static.spythere.eu/thumbnails/${thumbnailImage}.png`"
|
||||
@error="onImageError($event, imagesFallbacks[imageIndex])"
|
||||
@click.stop="() => {}"
|
||||
height="60"
|
||||
:vehicle-name="vehicleName"
|
||||
:img-name="thumbnailImage"
|
||||
:fallback-name="imagesFallbacks[imageIndex]"
|
||||
/>
|
||||
</span>
|
||||
</li>
|
||||
@@ -30,8 +26,11 @@
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import VehicleThumbnail from './VehicleThumbnail.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { VehicleThumbnail },
|
||||
|
||||
props: {
|
||||
trainStockList: {
|
||||
type: Array as PropType<string[]>,
|
||||
@@ -143,7 +142,7 @@ export default defineComponent({
|
||||
else {
|
||||
let fallbackVehicleImage = 'unknown_cargo';
|
||||
|
||||
if (/^(EP|EU)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||
if (/^(EP|EU|ET|201E)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
|
||||
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
|
||||
fallbackVehicleImage = 'unknown_passenger';
|
||||
@@ -160,7 +159,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
onImageError(event: Event, fallbackImage: string) {
|
||||
onImageError(event: Event, fallbackImage: string) {
|
||||
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="vehicle-thumbnail">
|
||||
<img
|
||||
ref="imgRef"
|
||||
:src="`https://static.spythere.eu/thumbnails/v2/${imgName}.png`"
|
||||
height="60"
|
||||
loading="lazy"
|
||||
:data-mouseover="vehicleName"
|
||||
:data-tooltip-content="vehicleName"
|
||||
:data-load-status="imgStatus"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
@error="onImageError"
|
||||
@load="onImageLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, Ref, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
vehicleName: { type: String, required: true },
|
||||
imgName: { type: String, required: true },
|
||||
fallbackName: { type: String, required: true },
|
||||
placeholderName: String
|
||||
});
|
||||
|
||||
const imgRef = ref(null) as Ref<HTMLElement | null>;
|
||||
|
||||
const imgStatus = ref('loading');
|
||||
|
||||
function onImageError(event: Event) {
|
||||
console.log('error');
|
||||
|
||||
(event.target as HTMLImageElement).src = `/images/${props.fallbackName}.png`;
|
||||
imgStatus.value = 'error';
|
||||
}
|
||||
|
||||
function onImageLoad() {
|
||||
if (imgStatus.value != 'error') {
|
||||
imgStatus.value = 'loaded';
|
||||
}
|
||||
|
||||
imgRef.value!.style.opacity = '1';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vehicle-thumbnail {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&[data-load-status='loading'] {
|
||||
min-height: 60px;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<li class="dispatcher-history-entry">
|
||||
<div class="entry-info">
|
||||
<span>
|
||||
<span>
|
||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||
<b>{{ entry.stationName }}</b>
|
||||
</router-link>
|
||||
|
||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||
</span>
|
||||
•
|
||||
<b
|
||||
v-if="entry.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
<b style="margin-left: 5px">
|
||||
<span
|
||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
<router-link
|
||||
class="text--donator"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</b>
|
||||
|
||||
<div>
|
||||
<span v-if="entry.timestampTo">
|
||||
<b>{{ $d(entry.timestampFrom) }}</b>
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
-
|
||||
<b
|
||||
v-if="
|
||||
new Date(entry.timestampFrom).getDate() != new Date(entry.timestampTo).getDate()
|
||||
"
|
||||
>
|
||||
{{ $d(entry.timestampTo) }}
|
||||
</b>
|
||||
{{ timestampToString(entry.timestampTo) }} ({{
|
||||
calculateDuration(entry.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
:to="`/scenery?station=${entry.stationName}`"
|
||||
class="dispatcher-online"
|
||||
v-else
|
||||
>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>
|
||||
{{
|
||||
new Date().getDate() != new Date(entry.timestampFrom).getDate()
|
||||
? $d(entry.timestampFrom)
|
||||
: ''
|
||||
}}
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(entry.currentDuration) }})
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span class="entry-info-right">
|
||||
<div>
|
||||
<span>
|
||||
{{ $t('scenery.dispatcher-rate') }}
|
||||
<b class="text--primary"> {{ entry.dispatcherRate }}</b>
|
||||
</span>
|
||||
<button class="btn btn--option" @click="toggleExtraInfo">
|
||||
{{ $t('scenery.dispatcher-status-changes') }}
|
||||
<b class="text--primary">{{ entry.statusHistory.length }}</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<b class="region-badge" :aria-describedby="entry.region">
|
||||
REGION: {{ regions.find((r) => r.id == entry.region)?.name }}
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="entry-extra" v-if="showExtraInfo">
|
||||
<ul class="status-list">
|
||||
<li v-for="statusItem in entry.statusHistory">
|
||||
<b style="margin-right: 0.5em">{{
|
||||
timestampToString(parseInt(statusItem.split('@')[0]))
|
||||
}}</b>
|
||||
|
||||
<StationStatusBadge
|
||||
:dispatcher-status="Number(statusItem.split('@')[1])"
|
||||
:is-online="true"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { API } from '../../../typings/api';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
entry: {
|
||||
type: Object as PropType<API.DispatcherHistory.Data>,
|
||||
required: true
|
||||
},
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
components: { StationStatusBadge },
|
||||
mixins: [dateMixin, styleMixin],
|
||||
emits: ['toggleShowExtraInfo'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
regions,
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleShowExtraInfo', this.entry.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
|
||||
.region-badge {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
.dispatcher-history-entry {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.75em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.entry-info-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.entry-extra {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.status-list > li {
|
||||
background-color: #313131;
|
||||
padding: 0.2rem 0 0.2rem 0.5em;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.entry-info {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,90 +15,16 @@
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<table class="dispatchers-table">
|
||||
<thead>
|
||||
<th>{{ $t('journal.history-name') }}</th>
|
||||
<th>{{ $t('journal.history-hash') }}</th>
|
||||
<th>{{ $t('journal.history-dispatcher') }}</th>
|
||||
<th>{{ $t('journal.history-level') }}</th>
|
||||
<th>{{ $t('journal.history-rate') }}</th>
|
||||
<th>{{ $t('journal.history-region') }}</th>
|
||||
<th>{{ $t('journal.history-date') }}</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<transition-group name="list-anim">
|
||||
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
|
||||
>
|
||||
<b>{{ historyItem.stationName }}</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>#{{ historyItem.stationHash }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||
>
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.dispatcher-message')"
|
||||
>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
|
||||
<b v-else>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<b
|
||||
v-if="historyItem.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="
|
||||
calculateExpStyle(
|
||||
historyItem.dispatcherLevel,
|
||||
historyItem.dispatcherIsSupporter
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
</td>
|
||||
<td class="text--primary">
|
||||
<b>{{ historyItem.dispatcherRate }}</b>
|
||||
</td>
|
||||
<td>
|
||||
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
||||
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
||||
}}</b>
|
||||
</td>
|
||||
<td style="min-width: 200px" class="time">
|
||||
<span v-if="historyItem.timestampTo" class="text--offline">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||
calculateDuration(historyItem.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
<span class="dispatcher-online" v-else>
|
||||
<b class="text--online">
|
||||
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
||||
$t('journal.online-since')
|
||||
}}</router-link>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</transition-group>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul v-else class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<JournalDispatcherEntry
|
||||
v-for="entry in dispatcherHistory"
|
||||
:key="entry.id"
|
||||
:entry="entry"
|
||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
|
||||
<AddDataButton
|
||||
:list="dispatcherHistory"
|
||||
@@ -106,7 +32,7 @@
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
@@ -121,20 +47,15 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
import { API } from '../../../typings/api';
|
||||
import { Status } from '../../../typings/common';
|
||||
import Loading from '../../Global/Loading.vue';
|
||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Loading, AddDataButton },
|
||||
|
||||
mixins: [dateMixin, styleMixin],
|
||||
components: { Loading, AddDataButton, JournalDispatcherEntry },
|
||||
|
||||
props: {
|
||||
dispatcherHistory: {
|
||||
@@ -159,99 +80,30 @@ export default defineComponent({
|
||||
return {
|
||||
Status,
|
||||
store: useMainStore(),
|
||||
apiStore: useApiStore(),
|
||||
regions
|
||||
|
||||
extraInfoIndexes: [] as number[]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedDispatcherHistory() {
|
||||
return this.dispatcherHistory.reduce(
|
||||
(acc, historyItem, i) => {
|
||||
if (this.isAnotherDay(i - 1, i))
|
||||
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||
acc.push(historyItem);
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as (API.DispatcherHistory.Data | string)[]
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToScenery(name: string, isOnline: boolean) {
|
||||
if (!isOnline) return;
|
||||
toggleExtraInfo(id: number) {
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||
|
||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||
},
|
||||
|
||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||
if (currIndex == 0) return true;
|
||||
|
||||
return (
|
||||
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||
);
|
||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
else this.extraInfoIndexes.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/animations.scss';
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/JournalSection.scss';
|
||||
|
||||
table.dispatchers-table {
|
||||
--_bg-table: #111;
|
||||
--_bg-head: #101010;
|
||||
--_bg-row: #2f2f2f;
|
||||
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--_bg-head);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--_bg-row);
|
||||
border-bottom: 2px solid black;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
&--online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
&--offline {
|
||||
color: #ddd;
|
||||
}
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
<strong
|
||||
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.driver-message')"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.driver-message')"
|
||||
>
|
||||
{{ timetable.driverName }}
|
||||
</strong>
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<li
|
||||
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.id"
|
||||
@click="showExtraInfo.value = !showExtraInfo.value"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<!-- General -->
|
||||
<TimetableGeneral :timetable="timetable" />
|
||||
<!-- Route -->
|
||||
<span class="item-route">
|
||||
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
||||
</span>
|
||||
|
||||
<hr />
|
||||
<!-- Stops -->
|
||||
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
<!-- Status -->
|
||||
<TimetableStatus :timetable="timetable" />
|
||||
|
||||
<button class="btn--action btn--show">
|
||||
{{ $t('journal.stock-info') }}
|
||||
<img
|
||||
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
|
||||
alt="Arrow icon"
|
||||
/>
|
||||
</button>
|
||||
<!-- Extra -->
|
||||
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent, ref } from 'vue';
|
||||
|
||||
import TimetableGeneral from './TimetableGeneral.vue';
|
||||
import TimetableStops from './TimetableStops.vue';
|
||||
import TimetableStatus from './TimetableStatus.vue';
|
||||
import TimetableExtra from './TimetableExtra.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
export default defineComponent({
|
||||
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
|
||||
|
||||
props: {
|
||||
timetableHistory: {
|
||||
type: Array as PropType<API.TimetableHistory.Response>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
return this.timetableHistory.map((timetable) => ({
|
||||
timetable,
|
||||
showExtraInfo: ref(false)
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/variables';
|
||||
@import '../../../styles/responsive';
|
||||
@import '../../../styles/JournalSection';
|
||||
|
||||
.btn--show {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
padding: 0.2em 0.45em;
|
||||
|
||||
img {
|
||||
height: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.journal_item-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-route {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn--show {
|
||||
margin: 1em auto 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,41 @@
|
||||
<template>
|
||||
<div class="stop-list" v-if="showExtraInfo == true">
|
||||
<span
|
||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||
)"
|
||||
class="stop-list-item"
|
||||
:key="stop.stopName"
|
||||
:data-confirmed="stop.confirmed"
|
||||
>
|
||||
<span v-if="i > 0">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
<div class="timetable-stops">
|
||||
<div class="stop-list">
|
||||
<span
|
||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||
)"
|
||||
class="stop-list-item"
|
||||
:key="stop.stopName"
|
||||
:data-confirmed="stop.confirmed"
|
||||
>
|
||||
<span v-if="i > 0">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
|
||||
<span
|
||||
v-for="(pathData, i) in timetablePathDetails"
|
||||
:data-visited="pathData.isVisited"
|
||||
:data-next-visited="
|
||||
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||
"
|
||||
>
|
||||
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} → </span>
|
||||
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||
<span class="path-departure" v-if="pathData.departure">
|
||||
→ {{ pathData.departure }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +60,24 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||
|
||||
return this.timetable.path.split(';').map((pathEl, i) => {
|
||||
const [arrival, name, departure] = pathEl.split(',');
|
||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||
|
||||
return {
|
||||
arrival,
|
||||
sceneryName,
|
||||
sceneryHash,
|
||||
departure,
|
||||
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops() {
|
||||
const timetable = this.timetable;
|
||||
|
||||
@@ -94,13 +130,14 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stop-list {
|
||||
.timetable-stops {
|
||||
word-wrap: break-word;
|
||||
gap: 0.25em;
|
||||
font-size: 0.95em;
|
||||
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
.stop-list {
|
||||
&-item[data-confirmed='true'] {
|
||||
color: lightgreen;
|
||||
|
||||
@@ -109,4 +146,19 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-details {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.path-details > span[data-visited='true'] {
|
||||
.path-arrival,
|
||||
.path-scenery {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-next-visited='true'] .path-departure {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="historyItem in historyList" :key="historyItem.id">
|
||||
<span>
|
||||
<span class="text--grayed" style="margin-right: 10px">
|
||||
@@ -165,14 +165,14 @@ export default defineComponent({
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -195,7 +195,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
@@ -238,11 +238,12 @@ export default defineComponent({
|
||||
sceneryTimetables(): SceneryTimetableRow[] {
|
||||
if (!this.onlineScenery) return [];
|
||||
|
||||
const sceneryName = this.$route.query['station']?.toString() ?? '';
|
||||
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
|
||||
|
||||
return this.onlineScenery.scheduledTrains
|
||||
.filter(
|
||||
(ct) =>
|
||||
ct.timetablePathElement.stationName == sceneryName &&
|
||||
ct.train.region == this.mainStore.region.id &&
|
||||
this.chosenCheckpoint &&
|
||||
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
|
||||
@@ -251,75 +252,18 @@ export default defineComponent({
|
||||
const trainStopStatus = getTrainStopStatus(
|
||||
ct.checkpointStop,
|
||||
ct.train.currentStationName,
|
||||
sceneryName.replace(/_/g, ' ')
|
||||
sceneryName
|
||||
);
|
||||
|
||||
const trainStopIndex =
|
||||
ct.train.timetableData?.followingStops.findIndex(
|
||||
(stop) => stop.stopName == ct.checkpointStop.stopName
|
||||
) ?? -1;
|
||||
|
||||
let prevStationName = '',
|
||||
nextStationName = '';
|
||||
|
||||
let departureLine: string | null = null;
|
||||
let arrivingLine: string | null = null;
|
||||
|
||||
let prevDepartureLine: string | null = null,
|
||||
nextArrivalLine: string | null = null;
|
||||
|
||||
if (trainStopIndex > -1 && ct.train.timetableData?.followingStops !== undefined) {
|
||||
for (let i = trainStopIndex; i >= 0; i--) {
|
||||
const stop = ct.train.timetableData.followingStops[i];
|
||||
|
||||
if (
|
||||
/strong|podg\.|pe\./g.test(stop.stopName) &&
|
||||
!prevStationName &&
|
||||
i <= trainStopIndex - 1
|
||||
)
|
||||
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
|
||||
|
||||
if (
|
||||
stop.arrivalLine != null &&
|
||||
!arrivingLine &&
|
||||
!/-|_|it|sbl/gi.test(stop.arrivalLine)
|
||||
) {
|
||||
arrivingLine = stop.arrivalLine;
|
||||
prevDepartureLine =
|
||||
ct.train.timetableData.followingStops[i - 1]?.departureLine || null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = trainStopIndex; i < ct.train.timetableData.followingStops.length; i++) {
|
||||
const stop = ct.train.timetableData.followingStops[i];
|
||||
|
||||
if (
|
||||
/strong|podg\.|pe\./g.test(stop.stopName) &&
|
||||
!nextStationName &&
|
||||
i > trainStopIndex
|
||||
)
|
||||
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
|
||||
|
||||
if (
|
||||
stop.departureLine &&
|
||||
!departureLine &&
|
||||
!/-|_|it|sbl/gi.test(stop.departureLine)
|
||||
) {
|
||||
departureLine = stop.departureLine;
|
||||
nextArrivalLine = ct.train.timetableData.followingStops[i + 1]?.arrivalLine || null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
checkpointStop: ct.checkpointStop,
|
||||
train: ct.train,
|
||||
prevDepartureLine,
|
||||
nextArrivalLine,
|
||||
departureLine,
|
||||
arrivingLine,
|
||||
prevStationName,
|
||||
nextStationName,
|
||||
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
|
||||
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
|
||||
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
|
||||
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
|
||||
prevStationName: ct.previousSceneryElement?.stationName ?? null,
|
||||
nextStationName: ct.nextSceneryElement?.stationName ?? null,
|
||||
status: trainStopStatus
|
||||
};
|
||||
})
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="history-list">
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
|
||||
<span>
|
||||
<div>
|
||||
@@ -219,14 +219,14 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.history-list > div {
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -235,7 +235,7 @@ export default defineComponent({
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.history-list > div > button > img {
|
||||
.journal-list > div > button > img {
|
||||
width: 2em;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
watch: {
|
||||
'tooltipStore.type'(prev, val) {
|
||||
vehicleName(prev, val) {
|
||||
if (prev != val) this.imageState = 'loading';
|
||||
}
|
||||
},
|
||||
@@ -81,18 +81,6 @@ export default defineComponent({
|
||||
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||
);
|
||||
}
|
||||
|
||||
// vehicleProps() {
|
||||
// const vehicleDataArray = this.apiStore.vehiclesData?.vehicleList.find(
|
||||
// ([name]) => name === this.vehicleName
|
||||
// );
|
||||
|
||||
// if (!vehicleDataArray) return null;
|
||||
|
||||
// return (
|
||||
// this.apiStore.vehiclesData!.vehicleProps.find((v) => v.type == vehicleDataArray[1]) ?? null
|
||||
// );
|
||||
// }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -8,15 +8,9 @@
|
||||
<span
|
||||
v-if="stop.position != 'begin'"
|
||||
class="date arrival"
|
||||
:data-status="
|
||||
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
|
||||
? 'delayed'
|
||||
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
|
||||
? 'preponed'
|
||||
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.arrivalDelay > 0"
|
||||
:data-status-preponed="stop.arrivalDelay < 0"
|
||||
:data-status="stop.status"
|
||||
>
|
||||
p.
|
||||
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
|
||||
@@ -31,10 +25,7 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
stop.duration ||
|
||||
(stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
|
||||
"
|
||||
v-if="stop.duration"
|
||||
class="date stop"
|
||||
:data-stop-types="stop.type.replace(', ', '-')"
|
||||
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
|
||||
@@ -53,20 +44,12 @@
|
||||
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
|
||||
"
|
||||
class="date departure"
|
||||
:data-status="
|
||||
stop.departureDelay > 0 && stop.status == 'confirmed'
|
||||
? 'delayed'
|
||||
: stop.departureDelay < 0 && stop.status == 'confirmed'
|
||||
? 'preponed'
|
||||
: stop.departureDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.departureDelay > 0"
|
||||
:data-status-preponed="stop.departureDelay < 0"
|
||||
:data-status-confirmed="stop.status == 'confirmed'"
|
||||
>
|
||||
o.
|
||||
<span
|
||||
v-if="stop.departureDelay != 0 && (stop.status == 'confirmed' || stop.status == 'stopped')"
|
||||
>
|
||||
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
|
||||
<s>{{ timestampToString(stop.departureScheduled) }}</s>
|
||||
{{ timestampToString(stop.departureReal) }}
|
||||
|
||||
@@ -105,6 +88,10 @@ $stopExchangeClr: #db8e29;
|
||||
$stopDefaultClr: #252525;
|
||||
$stopNameClr: #303030;
|
||||
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stop-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -157,27 +144,44 @@ $stopNameClr: #303030;
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrival,
|
||||
.departure {
|
||||
&[data-status='delayed'] {
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
.stop .arrival {
|
||||
&[data-status='confirmed'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='preponed'] {
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
&[data-status='confirmed'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
&[data-status='stopped'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='stopped'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stop .departure[data-status-confirmed='true'] {
|
||||
&[data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +108,19 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
currentDelay(stops: TrainStop[]) {
|
||||
const delay =
|
||||
stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)
|
||||
)?.departureDelay || 0;
|
||||
const lastConfirmedStop = stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) ||
|
||||
(i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
|
||||
(stops[i + 1] == undefined && stop.confirmed)
|
||||
);
|
||||
|
||||
if (delay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
|
||||
else if (delay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`;
|
||||
const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
|
||||
|
||||
if (lastDelay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
|
||||
else if (lastDelay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
|
||||
else return this.$t('trains.on-time');
|
||||
},
|
||||
|
||||
|
||||
+22
-11
@@ -18,7 +18,7 @@ export const useApiStore = defineStore('apiStore', {
|
||||
donatorsData: [] as API.Donators.Response,
|
||||
sceneryData: [] as StationJSONData[],
|
||||
|
||||
lastFetchData: new Date(),
|
||||
nextUpdateTime: 0,
|
||||
|
||||
client: undefined as AxiosInstance | undefined,
|
||||
|
||||
@@ -52,20 +52,32 @@ export const useApiStore = defineStore('apiStore', {
|
||||
this.fetchDonatorsData();
|
||||
this.fetchStationsGeneralInfo();
|
||||
this.fetchVehiclesInfo();
|
||||
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
updateTick(t: number) {
|
||||
if (this.dataStatuses.connection == Status.Data.Offline) return;
|
||||
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
|
||||
import('../../tests/data/getActiveData.json').then((data) => {
|
||||
console.warn('activeData: mocking mode');
|
||||
this.activeData = data.default as API.ActiveData.Response;
|
||||
this.lastFetchData = new Date();
|
||||
// if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
|
||||
// import('../../tests/data/getActiveData.json').then((data) => {
|
||||
// console.warn('activeData: mocking mode');
|
||||
// this.activeData = data.default as API.ActiveData.Response;
|
||||
|
||||
this.dataStatuses.connection = Status.Data.Loaded;
|
||||
});
|
||||
// this.dataStatuses.connection = Status.Data.Loaded;
|
||||
// });
|
||||
|
||||
return;
|
||||
}
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
@@ -73,7 +85,6 @@ export const useApiStore = defineStore('apiStore', {
|
||||
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
|
||||
|
||||
this.activeData = response.data;
|
||||
this.lastFetchData = new Date();
|
||||
this.dataStatuses.connection = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
this.dataStatuses.connection = Status.Data.Error;
|
||||
|
||||
+47
-17
@@ -96,7 +96,17 @@ export const useMainStore = defineStore('mainStore', {
|
||||
followingStops: timetable.stopList,
|
||||
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
|
||||
sceneries: timetable.sceneries,
|
||||
sceneryNames: sceneryNames.reverse()
|
||||
sceneryNames: sceneryNames.reverse(),
|
||||
timetablePath: timetable.path.split(';').map((pathElementString) => {
|
||||
const [arrival, station, departure] = pathElementString.split(',');
|
||||
|
||||
return {
|
||||
arrivalRouteExt: arrival,
|
||||
departureRouteExt: departure,
|
||||
stationName: station.split(' ').slice(0, -1).join(' '),
|
||||
stationHash: station.split(' ').slice(-1).join(' ')
|
||||
};
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
} as Train;
|
||||
@@ -110,21 +120,39 @@ export const useMainStore = defineStore('mainStore', {
|
||||
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
|
||||
|
||||
// Checkpoints trains map
|
||||
timetable?.stopList.forEach((stop, i) => {
|
||||
if (/strong|podg\.|pe\./.test(stop.stopName)) {
|
||||
const checkpointTrain: CheckpointTrain = {
|
||||
train: trainObj,
|
||||
checkpointStop: stop
|
||||
};
|
||||
if (trainObj.timetableData) {
|
||||
let currentSceneryIndex = 0;
|
||||
const timetablePath = trainObj.timetableData.timetablePath;
|
||||
|
||||
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
|
||||
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
|
||||
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
|
||||
checkpointTrain
|
||||
]);
|
||||
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
|
||||
}
|
||||
});
|
||||
trainObj.timetableData.followingStops.forEach((stop, i) => {
|
||||
if (/strong|podg|pe/.test(stop.stopName)) {
|
||||
const checkpointTrain: CheckpointTrain = {
|
||||
train: trainObj,
|
||||
checkpointStop: stop,
|
||||
|
||||
previousSceneryElement:
|
||||
currentSceneryIndex > 0 ? timetablePath[currentSceneryIndex - 1] : null,
|
||||
|
||||
nextSceneryElement:
|
||||
currentSceneryIndex < timetablePath.length - 1
|
||||
? timetablePath[currentSceneryIndex + 1]
|
||||
: null,
|
||||
|
||||
timetablePathElement: timetablePath[currentSceneryIndex]
|
||||
};
|
||||
|
||||
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
|
||||
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
|
||||
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
|
||||
checkpointTrain
|
||||
]);
|
||||
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
|
||||
}
|
||||
|
||||
if (timetablePath[currentSceneryIndex].departureRouteExt == stop.departureLine)
|
||||
currentSceneryIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
return trainObj;
|
||||
});
|
||||
@@ -252,8 +280,10 @@ export const useMainStore = defineStore('mainStore', {
|
||||
|
||||
if (!scheduledTrains) return;
|
||||
|
||||
scheduledTrains.forEach(({ train, checkpointStop }) => {
|
||||
scenery.scheduledTrains.push({ train, checkpointStop });
|
||||
scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
|
||||
if (scenery.name != timetablePathElement.stationName) return;
|
||||
|
||||
scenery.scheduledTrains.push({ train, checkpointStop, timetablePathElement, ...v });
|
||||
|
||||
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
|
||||
|
||||
|
||||
+5
-5
@@ -24,7 +24,7 @@ export namespace API {
|
||||
export type Response = Data[];
|
||||
|
||||
export interface Data {
|
||||
id: string;
|
||||
id: number;
|
||||
currentDuration: number;
|
||||
dispatcherId: number;
|
||||
dispatcherName: string;
|
||||
@@ -195,6 +195,8 @@ export namespace API {
|
||||
TWR: boolean;
|
||||
SKR: boolean;
|
||||
sceneries: string[];
|
||||
|
||||
path: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,16 +251,14 @@ export namespace API {
|
||||
hashesString?: string;
|
||||
currentSceneryName?: string;
|
||||
currentSceneryHash?: string;
|
||||
|
||||
routeSceneries?: string;
|
||||
|
||||
checkpointArrivals?: string[];
|
||||
checkpointDepartures?: string[];
|
||||
|
||||
checkpointArrivalsScheduled?: string[];
|
||||
checkpointDeparturesScheduled?: string[];
|
||||
|
||||
checkpointStopTypes?: string[];
|
||||
visitedSceneries?: string[];
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type Response = Data[];
|
||||
|
||||
@@ -39,6 +39,13 @@ export interface RegionCounters {
|
||||
timetablesCount: number;
|
||||
}
|
||||
|
||||
export interface TimetablePathElement {
|
||||
arrivalRouteExt?: string;
|
||||
departureRouteExt?: string;
|
||||
stationName: string;
|
||||
stationHash: string;
|
||||
}
|
||||
|
||||
export interface Train {
|
||||
id: string;
|
||||
modalId: string;
|
||||
@@ -73,6 +80,7 @@ export interface Train {
|
||||
routeDistance: number;
|
||||
sceneries: string[];
|
||||
sceneryNames: string[];
|
||||
timetablePath: TimetablePathElement[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,6 +196,9 @@ export interface TrainStop {
|
||||
export interface CheckpointTrain {
|
||||
checkpointStop: TrainStop;
|
||||
train: Train;
|
||||
timetablePathElement: TimetablePathElement;
|
||||
previousSceneryElement: TimetablePathElement | null;
|
||||
nextSceneryElement: TimetablePathElement | null;
|
||||
}
|
||||
|
||||
// Vehicles Data
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user