Compare commits

...

24 Commits

Author SHA1 Message Date
Spythere d9a7ba122c Merge pull request #91 from Spythere/development
v1.24.3
2024-05-26 01:44:45 +02:00
Spythere bf8d4a9ef4 chore: global font sizing; chore: train modal dvh 2024-05-25 18:06:01 +02:00
Spythere 6ea1e91d1d hotfix: card positioning 2024-05-25 17:57:25 +02:00
Spythere 813b557455 chore: improved card positioning 2024-05-25 17:55:18 +02:00
Spythere 834b14da69 fix: card dvh 2024-05-25 17:26:27 +02:00
Spythere c809b2146d chore: locale update 2024-05-25 17:12:19 +02:00
Spythere 33b98ca313 chore: added text color for active filters info 2024-05-25 17:11:28 +02:00
Spythere bcb9c63cb0 chore: reactive hiding body scroll on modal 2024-05-25 17:05:41 +02:00
Spythere 17d77a80d8 bump: 1.24.3 2024-05-25 16:02:40 +02:00
Spythere 65b159f8fd fix: scenery timetable duplicates; fix: not opening train modal for queries 2024-05-25 16:02:20 +02:00
Spythere 063d5283e4 Merge pull request #90 from Spythere/development
v1.24.2
2024-05-24 13:56:39 +02:00
Spythere 29de1b3c4b chore: scenery view layout 2024-05-24 13:52:42 +02:00
Spythere f0c02bf12e chore: pwa adjustments 2024-05-24 13:43:29 +02:00
Spythere 8aa23468b3 chore: changed station stats median to avg 2024-05-23 15:53:18 +02:00
Spythere 4c1fcf710b refactor: global modals to cards 2024-05-23 15:01:30 +02:00
Spythere a529d6e9eb chore: changed no stations message 2024-05-23 14:08:42 +02:00
Spythere 9fc602e08f chore: filters improvements 2024-05-22 15:41:33 +02:00
Spythere 56e40bd84b bump: version (1.24.2) 2024-05-21 16:17:41 +02:00
Spythere a5b5df7452 refactor: restructured station filters 2024-05-21 16:17:23 +02:00
Spythere 1a8da02ced chore: checkpoints detection fix 2024-05-19 23:42:06 +02:00
Spythere 7e75fa2516 chore: checkpoints hotfix 2024-05-19 23:12:07 +02:00
Spythere 3ed2c09184 chore: checkpoints filtering 2024-05-19 23:05:57 +02:00
Spythere 6901c3d2b4 chore: hotfix 2024-05-19 22:30:21 +02:00
Spythere 8417754403 refactor: optimization of train schedules 2024-05-19 19:50:01 +02:00
39 changed files with 1230 additions and 1231 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.24.1", "version": "1.24.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+9 -9
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal <UpdateCard
:update-modal-open="isUpdateModalOpen" :is-update-card-open="isUpdateCardOpen"
@toggle-modal="() => (isUpdateModalOpen = false)" @toggle-card="() => (isUpdateCardOpen = false)"
/> />
<Tooltip /> <Tooltip />
@@ -27,7 +27,7 @@
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | {{ new Date().getUTCFullYear() }} |
<button class="btn--text" @click="() => (isUpdateModalOpen = true)"> <button class="btn--text" @click="() => (isUpdateCardOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }} v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button> </button>
@@ -56,7 +56,7 @@ import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue'; import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue'; import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateModal from './components/App/UpdateModal.vue'; import UpdateCard from './components/App/UpdateCard.vue';
import StorageManager from './managers/storageManager'; import StorageManager from './managers/storageManager';
@@ -68,7 +68,7 @@ export default defineComponent({
StatusIndicator, StatusIndicator,
AppHeader, AppHeader,
TrainModal, TrainModal,
UpdateModal, UpdateCard,
Tooltip Tooltip
}, },
@@ -78,7 +78,7 @@ export default defineComponent({
apiStore: useApiStore(), apiStore: useApiStore(),
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
isUpdateModalOpen: false, isUpdateCardOpen: false,
currentLang: 'pl', currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
@@ -130,7 +130,7 @@ export default defineComponent({
releaseURL: releaseData.html_url releaseURL: releaseData.html_url
}; };
this.isUpdateModalOpen = this.isUpdateCardOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test'; storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) { } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
@@ -210,7 +210,7 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.65rem + 0.8vw); font-size: calc(0.65rem + 0.85vw);
} }
@include screenLandscape() { @include screenLandscape() {
@@ -1,12 +1,12 @@
<template> <template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)"> <Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="modal-content"> <div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1> <h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div> <div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div> <div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)"> <button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
{{ $t('update.confirm') }} {{ $t('update.confirm') }}
</button> </button>
@@ -16,7 +16,7 @@
<span v-html="$t('update.info-2')"></span> <span v-html="$t('update.info-2')"></span>
</p> </p>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { Converter } from 'showdown'; import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue'; import Card from '../Global/Card.vue';
const converter = new Converter(); const converter = new Converter();
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
updateModalOpen: { isUpdateCardOpen: {
type: Boolean, type: Boolean,
required: true required: true
} }
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
data() { data() {
return { return {
@@ -49,7 +49,7 @@ export default defineComponent({
}, },
watch: { watch: {
updateModalOpen(val: boolean) { isUpdateCardOpen(val: boolean) {
this.$nextTick(() => { this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus(); if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
}); });
@@ -60,37 +60,38 @@ export default defineComponent({
htmlChangelog() { htmlChangelog() {
if (this.mainStore.appUpdate == null) return ''; if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog); return converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
} }
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) { ::v-deep(h1) {
text-align: center; text-align: center;
color: $accentCol;
} }
::v-deep(h2) { ::v-deep(h2) {
padding: 0.25em 0; padding: 0.25em 0;
border-bottom: 1px solid #aaa;
} }
::v-deep(ul) { ::v-deep(ul) {
list-style: inside; list-style: initial;
padding: 0.5em; padding: 1em;
line-height: 1.5em; line-height: 1.5em;
} }
.modal-content { .content {
display: grid; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
gap: 0.5em; gap: 0.5em;
@@ -98,6 +99,7 @@ export default defineComponent({
min-height: 700px; min-height: 700px;
overflow: auto; overflow: auto;
text-align: justify; text-align: justify;
max-width: 700px;
} }
.no-features { .no-features {
@@ -1,11 +1,10 @@
<template> <template>
<transition name="modal-anim" tag="div"> <transition name="modal-anim" tag="div">
<div class="modal" v-if="isOpen"> <div class="card" v-if="isOpen">
<div class="modal-background" @click="toggleModal(false)"></div> <div class="card-background" @click="toggleCard(false)"></div>
<div class="modal-wrapper" ref="wrapper" tabindex="0"> <div class="card-body" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div> </div>
</transition> </transition>
</template> </template>
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
export default defineComponent({ export default defineComponent({
emits: ['toggleModal'], emits: ['toggleCard'],
props: { props: {
isOpen: Boolean isOpen: Boolean
@@ -36,8 +35,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
@@ -46,17 +45,21 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal { .card {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100vh; height: 100%;
z-index: 200; z-index: 200;
display: flex;
justify-content: center;
align-items: center;
} }
.modal-background { .card-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -68,21 +71,23 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.modal-wrapper { .card-body {
position: absolute; position: relative;
top: 50%;
left: 50%; margin: 1em;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
& > :slotted(div) { background-color: #1a1a1a;
background-color: #1a1a1a; box-shadow: 0 0 15px 10px #0e0e0e;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw; overflow: auto;
max-width: 850px; }
@include smallScreen {
.card {
align-items: flex-start;
} }
} }
</style> </style>
@@ -1,12 +1,7 @@
<template> <template>
<AnimatedModal <Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
class="donation-modal" <div class="body">
:isOpen="isModalOpen" <div class="content">
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1> <h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0"> <div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span> <span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,9 +56,9 @@
</i> </i>
</div> </div>
<div class="modal_actions"> <div class="actions">
<a <a
class="modal-action a-button btn--image coffee" class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere" href="https://buycoffee.to/spythere"
target="_blank" target="_blank"
ref="action" ref="action"
@@ -73,7 +68,7 @@
</a> </a>
<a <a
class="modal-action a-button btn--image paypal" class="action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW" href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank" target="_blank"
> >
@@ -81,30 +76,30 @@
{{ $t('donations.action-paypal') }} {{ $t('donations.action-paypal') }}
</a> </a>
<button class="modal-action btn--image exit" @click="toggleModal(false)"> <button class="action btn--image exit" @click="toggleCard(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" /> <img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }} {{ $t('donations.action-exit') }}
</button> </button>
</div> </div>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
isModalOpen: Boolean isCardOpen: Boolean
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
watch: { watch: {
isModalOpen(val: boolean) { isCardOpen(val: boolean) {
this.running = val; this.running = val;
this.lastUpdate = Date.now(); this.lastUpdate = Date.now();
@@ -138,8 +133,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
}, },
runUpdate() { runUpdate() {
@@ -157,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal_content { .body {
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
gap: 1em; gap: 1em;
font-size: 1.1em; font-size: 1.1em;
& > div { max-width: 820px;
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
} }
.modal_main { .content {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 1em;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
} }
.modal_actions { img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
.actions {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em; gap: 0.5em;
padding: 1em;
form button { form button {
width: 100%; width: 100%;
} }
} }
.modal_actions > .modal-action { .actions > .action {
&.paypal { &.paypal {
$btnColor: #254069; $btnColor: #254069;
@@ -103,7 +103,7 @@ export default defineComponent({
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) { showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return; if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target); this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
} }
} }
}); });
+1 -1
View File
@@ -72,7 +72,7 @@
<div class="info-lists"> <div class="info-lists">
<!-- user list --> <!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" /> <SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list --> <!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" /> <SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -13,13 +13,13 @@
</li> </li>
<li <li
v-for="train in onlineScenery?.stationTrains" v-for="{ train, status } in stationTrains"
class="badge user" class="badge user"
:class="train.stopStatus"
:key="train.trainId"
tabindex="0" tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)" :key="train.id"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)" :data-status="status"
@click.prevent="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
@@ -32,7 +32,9 @@
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery } from '../../../typings/common'; import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
import { getTrainStopStatus } from '../utils';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin, modalTrainMixin], mixins: [routerMixin, modalTrainMixin],
@@ -41,6 +43,38 @@ export default defineComponent({
onlineScenery: { onlineScenery: {
type: Object as PropType<ActiveScenery>, type: Object as PropType<ActiveScenery>,
required: false required: false
},
station: {
type: Object as PropType<Station>
}
},
data() {
return {
mainStore: useMainStore()
};
},
computed: {
stationTrains() {
if (!this.onlineScenery) return;
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
return this.onlineScenery.stationTrains.map((train) => {
const stop = train.timetableData?.followingStops.find(
(stop) => stop.stopNameRAW.toLowerCase() == name.toLowerCase()
);
const status = stop
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
: 'no-timetable';
return {
train,
status
};
});
} }
} }
}); });
@@ -74,31 +108,31 @@ ul {
-webkit-transition: background-color 200ms; -webkit-transition: background-color 200ms;
} }
&.no-timetable .user_train { &[data-status='no-timetable'] .user_train {
background-color: $no-timetable; background-color: $no-timetable;
} }
&.departed > &_train { &[data-status='departed'] > &_train {
background-color: $departed; background-color: $departed;
} }
&.stopped > &_train { &[data-status='stopped'] > &_train {
background-color: $stopped; background-color: $stopped;
} }
&.online > &_train { &[data-status='online'] > &_train {
background-color: $online; background-color: $online;
} }
&.terminated > &_train { &[data-status='terminated'] > &_train {
background-color: $terminated; background-color: $terminated;
} }
&.disconnected > &_train { &[data-status='disconnected'] > &_train {
background-color: $disconnected; background-color: $disconnected;
} }
&.offline { &[data-status='offline'] {
background: firebrick; background: firebrick;
pointer-events: none; pointer-events: none;
} }
+129 -60
View File
@@ -39,8 +39,8 @@
<div class="timetable-list"> <div class="timetable-list">
<transition-group name="list-anim"> <transition-group name="list-anim">
<div <div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em" style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading" key="list-loading"
> >
<Loading /> <Loading />
@@ -48,7 +48,7 @@
<span <span
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery" v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline" key="list-offline"
> >
{{ $t('scenery.offline') }} {{ $t('scenery.offline') }}
@@ -56,7 +56,7 @@
<div <div
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0" v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables" key="list-no-timetables"
> >
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
@@ -65,59 +65,56 @@
<div <div
class="timetable-item" class="timetable-item"
v-else v-else
v-for="scheduledTrain in computedScheduledTrains" v-for="(row, i) in sceneryTimetables"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp" :key="row.train.id"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
<span class="info-number"> <span class="info-number">
<strong>{{ scheduledTrain.category }}</strong> <strong>{{ row.train.timetableData!.category }}</strong>
{{ scheduledTrain.trainNo }} {{ row.train.trainNo }}
<span <span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
v-if="scheduledTrain.stopInfo.comments"
:title="scheduledTrain.stopInfo.comments"
>
<img src="/images/icon-warning.svg" /> <img src="/images/icon-warning.svg" />
</span> </span>
</span> </span>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<span> <span>
{{ scheduledTrain.driverName }} {{ row.train.driverName }}
</span> </span>
<div class="info-route"> <div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong> <strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div> </div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" /> <ScheduledTrainStatus :sceneryTimetableRow="row" />
</span> </span>
</span> </span>
<span class="timetable-schedule"> <span class="timetable-schedule">
<span class="schedule-arrival"> <span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere"> <span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
{{ $t('timetables.begins') }} {{ $t('timetables.begins') }}
</span> </span>
<span class="arrival-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0"> <div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }} {{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' ({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }}) }}{{ row.checkpointStop.arrivalDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -125,41 +122,39 @@
<span class="schedule-stop"> <span class="schedule-stop">
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.arrivingLine }} {{ row.arrivingLine }}
</span> </span>
<span class="stop-time"> <span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }} {{ row.checkpointStop.stopTime || '' }}
{{ {{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
</span> </span>
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.departureLine }} {{ row.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere"> <span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
{{ $t('timetables.terminates') }} {{ $t('timetables.terminates') }}
</span> </span>
<span class="departure-time" v-else> <span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp) timestampToString(row.checkpointStop.departureTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ row.checkpointStop.departureDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -183,6 +178,8 @@ import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common'; import { ActiveScenery, Station } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -204,10 +201,6 @@ export default defineComponent({
listOpen: false listOpen: false
}), }),
mounted() {
this.loadSelectedOption();
},
activated() { activated() {
this.loadSelectedOption(); this.loadSelectedOption();
}, },
@@ -220,9 +213,7 @@ export default defineComponent({
const mainStore = useMainStore(); const mainStore = useMainStore();
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints[0] ?? props.station?.name ?? ''
? ''
: props.station?.generalInfo?.checkpoints[0] ?? null
); );
return { return {
@@ -241,27 +232,105 @@ export default defineComponent({
return url; return url;
}, },
computedScheduledTrains() { sceneryTimetables(): SceneryTimetableRow[] {
if (!this.station) return []; if (!this.station) return [];
if (!this.onlineScenery) return [];
return ( return this.onlineScenery.scheduledTrains
this.onlineScenery?.scheduledTrains .filter(
?.filter( (ct) =>
(train) => ct.train.region == this.mainStore.region.id &&
train.checkpointName.toLocaleLowerCase() == this.chosenCheckpoint &&
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() && ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
train.region == this.mainStore.region.id )
) .map((ct) => {
.sort((a, b) => { const trainStopStatus = getTrainStopStatus(
if (a.stopStatusID > b.stopStatusID) return 1; ct.checkpointStop,
if (a.stopStatusID < b.stopStatusID) return -1; ct.train.currentStationName,
this.station!.name
);
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1; const trainStopIndex =
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1; ct.train.timetableData?.followingStops.findIndex(
(stop) => stop.stopName == ct.checkpointStop.stopName
) ?? -1;
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -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,
status: trainStopStatus
};
})
.sort((a, b) => {
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
return -1;
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
return 1;
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
});
} }
}, },
@@ -1,7 +1,7 @@
<template> <template>
<div class="general-status"> <div class="general-status">
<span <span
:class="computedScheduledTrain.stopStatus" :class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription" :title="computedScheduledTrain.stopStatusDescription"
> >
{{ computedScheduledTrain.stopStatusIndicator }} {{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../typings/common'; import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
export default defineComponent({ export default defineComponent({
props: { props: {
scheduledTrain: { sceneryTimetableRow: {
type: Object as PropType<ScheduledTrain>, type: Object as PropType<SceneryTimetableRow>,
required: true required: true
} }
}, },
computed: { computed: {
computedScheduledTrain(): ScheduledTrainComp { computedScheduledTrain() {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.scheduledTrain; this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}` ? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '', let stopStatusDescription = '',
stopStatusIndicator = ''; stopStatusIndicator = '';
switch (stopStatus) { switch (status) {
case StopStatus.ARRIVING: case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`; stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', { stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}` ? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`; : `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine }) ? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: ''; : '';
break; break;
@@ -85,7 +81,7 @@ export default defineComponent({
break; break;
} }
return { return {
...this.scheduledTrain, ...this.sceneryTimetableRow,
stopStatusDescription, stopStatusDescription,
stopStatusIndicator stopStatusIndicator
}; };
+13
View File
@@ -0,0 +1,13 @@
import { StopStatus, Train, TrainStop } from '../../typings/common';
export interface SceneryTimetableRow {
checkpointStop: TrainStop;
train: Train;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
departureLine: string | null;
arrivingLine: string | null;
prevStationName: string | null;
nextStationName: string | null;
status: StopStatus;
}
+42
View File
@@ -0,0 +1,42 @@
import { StopStatus, TrainStop } from '../../typings/common';
export const stopStatusPriority = [
StopStatus.ONLINE,
StopStatus.STOPPED,
StopStatus.DEPARTED,
StopStatus.ARRIVING,
StopStatus.DEPARTED_AWAY,
StopStatus.TERMINATED
];
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
if (stopInfo.terminatesHere && stopInfo.confirmed) {
return StopStatus.TERMINATED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
return StopStatus.DEPARTED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
return StopStatus.DEPARTED_AWAY;
}
if (currentStationName == sceneryName && !stopInfo.stopped) {
return StopStatus.ONLINE;
}
if (currentStationName == sceneryName && stopInfo.stopped) {
return StopStatus.STOPPED;
}
if (currentStationName != sceneryName) {
return StopStatus.ARRIVING;
}
return StopStatus.ONLINE;
}
+9 -16
View File
@@ -15,7 +15,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'], emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: { watch: {
'option.value'() { 'option.value'() {
this.filterStore.changeFilterValue(this.option.name, !this.option.value); // this.filterStore.changeFilterValue(this.option.name, !this.option.value);
} }
}, },
@@ -56,17 +49,17 @@ export default defineComponent({
handleDbClick(e: Event) { handleDbClick(e: Event) {
e.preventDefault(); e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id; // this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true; // this.option.value = true;
this.$emit('update:optionValue', true); this.$emit('update:optionValue', true);
this.filterStore.inputs.options // this.filterStore.inputs.options
.filter((option) => { // .filter((option) => {
return option.section == this.option.section && option.id != this.option.id; // return option.section == this.option.section && option.id != this.option.id;
}) // })
.forEach((option) => { // .forEach((option) => {
option.value = !this.option.value; // option.value = !this.option.value;
}); // });
} }
} }
}); });
+179 -112
View File
@@ -4,7 +4,7 @@
<button class="card-button btn--filled btn--image" @click="toggleCard"> <button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" /> <img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
<p>[F] {{ $t('options.filters') }}</p> <p>[F] {{ $t('options.filters') }}</p>
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span> <span class="active-indicator" v-if="changedFilters.length != 0"></span>
</button> </button>
<label for="scenery-search"> <label for="scenery-search">
@@ -33,29 +33,45 @@
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
<div class="changed-filters" :data-active="changedFilters.length > 0">
<template v-if="changedFilters.length > 0">
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
</template>
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
</div>
<section class="card_options"> <section class="card_options">
<div <div
class="option-section" class="option-section"
v-for="section in filterStore.inputs.optionSections" v-for="(sectionFilters, sectionKey) in filtersSections"
:key="section" :key="sectionKey"
> >
<h3 class="text--primary"> <h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }} <span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button> <button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3> </h3>
<hr /> <hr />
<div class="section-inputs"> <div class="section-filters">
<FilterOption <label
v-for="(option, i) in filterStore.inputs.options.filter( v-for="filterKey in sectionFilters"
(o) => o.section == section @click="() => (filters[filterKey] = !filters[filterKey])"
)" @dblclick="setSingleSectionFilter(sectionKey, filterKey)"
v-model:optionValue="option.value" :for="filterKey"
:option="option" >
:key="i" <input
/> :checked="filters[filterKey]"
v-model="filters[filterKey]"
type="checkbox"
:class="sectionKey"
:name="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div> </div>
</div> </div>
</section> </section>
@@ -68,7 +84,7 @@
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
: minimumHours < 8 : minimumHours < 7
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
@@ -76,21 +92,21 @@
</span> </span>
</section> </section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search"> <section class="card_authors-search">
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3> <h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
<datalist id="authors" name="authors">
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
</datalist>
<form action="javascript:void(0);" @submit="handleAuthorsInput"> <form action="javascript:void(0);" @submit="handleAuthorsInput">
<input <input
type="text" type="text"
id="author" id="author"
list="authors" list="authors"
name="authors" name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')" :placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
/> />
@@ -100,19 +116,18 @@
</section> </section>
<section class="card_sliders"> <section class="card_sliders">
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i"> <div class="slider" v-for="(slider, i) in initSliders" :key="i">
<input <input
class="slider-input" class="slider-input"
type="range" type="range"
:name="slider.name" :name="slider.id"
:id="slider.id" :id="slider.id"
:min="slider.minRange" :min="slider.minRange"
:max="slider.maxRange" :max="slider.maxRange"
:step="slider.step" :step="slider.step"
v-model="slider.value" v-model="filters[slider.id]"
@change="handleInput"
/> />
<span class="slider-value">{{ slider.value }}</span> <span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content"> <div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }} {{ $t(`filters.sliders.${slider.id}`) }}
</div> </div>
@@ -133,9 +148,9 @@
<button <button
class="btn--action" class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters" @click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
> >
[R] {{ $t('filters.reset') }} [R] {{ $t('filters.reset') }}
</button> </button>
@@ -151,22 +166,36 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager'; import StorageManager from '../../managers/storageManager';
import {
filtersSections,
initSliders,
initFilters,
getChangedFilters
} from '../../managers/stationFilterManager';
import { StationFilterSection } from '../../managers/stationFilterManager';
import { computed } from 'vue';
import { watch } from 'vue';
const STORAGE_KEY = 'options_saved';
export default defineComponent({ export default defineComponent({
components: { FilterOption }, components: { FilterOption },
mixins: [keyMixin, routerMixin], mixins: [keyMixin, routerMixin],
data: () => ({ data: () => ({
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '', filtersSections,
initSliders,
minimumHours: 0, minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
@@ -180,22 +209,33 @@ export default defineComponent({
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useMainStore(); const store = useMainStore();
const filterStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const changedFilters = computed(() => getChangedFilters(filters));
// Save filters to persistent storage
watch(filters, (value) => {
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
Object.keys(value).forEach((filterKey) => {
StorageManager.setValue(filterKey, filters[filterKey]);
});
});
return { return {
isVisible, isVisible,
store, store,
filterStore filters,
changedFilters
}; };
}, },
mounted() { mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY); this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) { if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours'); this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
} }
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
@@ -214,7 +254,7 @@ export default defineComponent({
return true; return true;
}, },
authors() { authorsHint() {
return this.store.stationList return this.store.stationList
.reduce((acc, station) => { .reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => { station.generalInfo?.authors?.forEach((author) => {
@@ -258,61 +298,63 @@ export default defineComponent({
this.scrollTop = (e.target as HTMLElement).scrollTop; this.scrollTop = (e.target as HTMLElement).scrollTop;
}, },
handleInput(e: Event) {
const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue(target.name, target.value);
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
},
handleAuthorsInput() { handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue); this.filters['authors'] = this.authors;
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
},
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue(name, value);
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
}, },
subHour() { subHour() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1; this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
addHour() { addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1; this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
saveFilters() { saveFilters() {
this.saveOptions = !this.saveOptions; this.saveOptions = !this.saveOptions;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(STORAGE_KEY);
return; return;
} }
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) => Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setBooleanValue(option.name, !option.value) StorageManager.setValue(filterKey, this.filters[filterKey]);
); });
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
}, },
resetFilters() { resetFilters() {
this.authorsInputValue = ''; // Reset local model values
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.authors = '';
this.filterStore.resetFilters();
// Reset global filters
Object.keys(this.filters).forEach((filterKey) => {
this.filters[filterKey] = (initFilters as any)[filterKey];
});
},
areSectionFiltersDefault(sectionKey: StationFilterSection) {
return filtersSections[sectionKey].every((filterKey) => {
return this.filters[filterKey] == initFilters[filterKey];
});
},
resetSectionFilters(sectionKey: StationFilterSection) {
filtersSections[sectionKey].forEach((filterKey) => {
this.filters[filterKey] = initFilters[filterKey];
});
},
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
filtersSections[sectionKey].forEach((filterKey) => {
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
});
}, },
closeCard() { closeCard() {
@@ -330,6 +372,7 @@ export default defineComponent({
@import '../../styles/responsive'; @import '../../styles/responsive';
@import '../../styles/card'; @import '../../styles/card';
@import '../../styles/animations'; @import '../../styles/animations';
@import '../../styles/variables';
h3.section-header { h3.section-header {
text-align: center; text-align: center;
@@ -346,6 +389,15 @@ h3.section-header {
padding: 0.5em; padding: 0.5em;
} }
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls { .card_controls {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
@@ -374,28 +426,6 @@ h3.section-header {
text-align: center; text-align: center;
} }
.card_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
.card_timestamp { .card_timestamp {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -441,6 +471,52 @@ h3.section-header {
} }
} }
.section-filters {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.section-filters > label {
position: relative;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
span {
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
font-weight: bold;
background-color: forestgreen;
}
span:hover {
background-color: #22aa22;
}
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: #444;
&:hover {
background-color: #555;
}
}
&:focus-visible + span {
outline: 1px solid $accentCol;
}
}
}
.card_actions { .card_actions {
padding: 0.5em; padding: 0.5em;
@@ -475,35 +551,18 @@ h3.section-header {
} }
} }
.section-inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider { .slider {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25em;
margin-bottom: 1em; margin-bottom: 1em;
&-value { &-value {
color: $accentCol; color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em; padding: 0.1em 0.2em;
} }
&-content {
flex-grow: 2;
}
&-input { &-input {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
@@ -512,7 +571,6 @@ h3.section-header {
outline: none; outline: none;
min-width: 25%; min-width: 25%;
max-width: 120px;
&:focus-visible ~ * { &:focus-visible ~ * {
color: gold; color: gold;
@@ -587,5 +645,14 @@ h3.section-header {
.card_controls > button.card-button > p { .card_controls > button.card-button > p {
display: none; display: none;
} }
.slider {
flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
}
} }
</style> </style>
+12 -20
View File
@@ -21,8 +21,8 @@
<div> <div>
&bull; &bull;
{{ $t('station-stats.med-timetable-count') }} {{ $t('station-stats.avg-timetable-count') }}
<b>{{ medTimetableCount }}</b> <b>{{ avgTimetableCount.toFixed(2) }}</b>
</div> </div>
<div> <div>
@@ -89,27 +89,19 @@ export default defineComponent({
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0; return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
}, },
medTimetableCount() { avgTimetableCount() {
const scheduledTrainsArr = this.mainStore.activeSceneryList const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
.reduce<number[]>((acc, sc) => { return sc.region == this.mainStore.region.id;
if (sc.region != this.mainStore.region.id) return acc; });
acc.push(sc.scheduledTrainCount.all); const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
return acc; if (regionSceneries.length == 0) return 0;
}, [])
.sort((a, b) => Math.sign(a - b));
if (scheduledTrainsArr.length == 0) return 0; return timetableCountSum / regionSceneries.length;
if (scheduledTrainsArr.length % 2 == 0) {
let v1 = scheduledTrainsArr[scheduledTrainsArr.length / 2];
let v2 = scheduledTrainsArr[scheduledTrainsArr.length / 2 - 1];
return (v1 + v2) / 2;
}
return scheduledTrainsArr[~~(scheduledTrainsArr.length / 2)];
}, },
trackCount() { trackCount() {
+51 -40
View File
@@ -1,10 +1,10 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<Loading <Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0" v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/> />
<div class="table_wrapper" v-else-if="displayedStations.length > 0"> <div class="table_wrapper" v-else-if="filteredStationList.length > 0">
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -20,8 +20,8 @@
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.headerName == headerName" v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`" :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
@@ -43,8 +43,8 @@
<img <img
class="sort-icon" class="sort-icon"
v-if="sorterActive.headerName == headerName" v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`" :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon" alt="sort icon"
/> />
</span> </span>
@@ -54,7 +54,7 @@
<tbody> <tbody>
<tr <tr
v-for="station in displayedStations" v-for="station in filteredStationList"
:class="{ 'last-selected': lastSelectedStationName == station.name }" :class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name" :key="station.name"
@click.left="setScenery(station.name)" @click.left="setScenery(station.name)"
@@ -122,7 +122,7 @@
<span v-if="station.onlineInfo?.dispatcherName"> <span v-if="station.onlineInfo?.dispatcherName">
<b <b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationModal" @click.stop="openDonationCard"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')" :data-tooltip-content="$t('donations.dispatcher-message')"
> >
@@ -302,65 +302,73 @@
</div> </div>
<div class="no-stations" v-else> <div class="no-stations" v-else>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b <div>
>) {{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
>)
</div>
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
⚠ {{ $t('sceneries.active-filters') }}
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, inject, computed } from 'vue';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { getChangedFilters } from '../../managers/stationFilterManager';
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils';
export default defineComponent({ export default defineComponent({
emits: ['toggleDonationModal'], emits: ['toggleDonationCard'],
components: { Loading, StationStatusBadge }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin], mixins: [styleMixin, dateMixin],
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
headIds, headIds,
lastSelectedStationName: '' lastSelectedStationName: '',
getChangedFilters
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
}
},
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
const tooltipStore = useTooltipStore(); const tooltipStore = useTooltipStore();
const stationFiltersStore = useStationFiltersStore(); const filters = inject('StationsView_filters') as Record<string, any>;
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
const filteredStationList = computed(() =>
mainStore.allStationInfo
.filter((station) => filterStations(station, filters))
.sort((a, b) => sortStations(a, b, activeSorter))
);
return { return {
Status: Status.Data, Status: Status.Data,
stationFiltersStore,
mainStore, mainStore,
apiStore, apiStore,
tooltipStore tooltipStore,
filters,
filteredStationList,
activeSorter
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.displayedStations.find((station) => station.name === name); const station = this.filteredStationList.find((station) => station.name === name);
if (!station) return; if (!station) return;
@@ -376,8 +384,8 @@ export default defineComponent({
}); });
}, },
openDonationModal(e: Event) { openDonationCard(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide(); this.tooltipStore.hide();
}, },
@@ -388,10 +396,14 @@ export default defineComponent({
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(headerName: HeadIdsTypes) { changeSorter(headerName: HeadIdsType) {
if (headerName == 'general') return; if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName); if (headerName == this.activeSorter.headerName)
this.activeSorter.dir = -1 * this.activeSorter.dir;
else this.activeSorter.dir = 1;
this.activeSorter.headerName = headerName;
} }
} }
}); });
@@ -413,11 +425,10 @@ $rowCol: #424242;
.no-stations { .no-stations {
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.25em;
padding: 1em; padding: 1em;
background: #1a1a1a; background: #1a1a1a;
line-height: 1.5em;
} }
table { table {
+25 -52
View File
@@ -5,56 +5,29 @@ export interface FilterOption {
defaultValue: boolean; defaultValue: boolean;
} }
export interface Filter { export const headIds = [
[key: string]: boolean | number | string; 'station',
default: boolean; 'min-lvl',
notDefault: boolean; 'status',
real: boolean; 'dispatcher',
fictional: boolean; 'dispatcher-lvl',
SPK: boolean; 'routes-single',
SCS: boolean; 'routes-double',
SPE: boolean; 'general'
SUP: boolean; ] as const;
noSUP: boolean;
ASDEK: boolean; export const headIconsIds = [
noASDEK: boolean; 'user',
ręczne: boolean; 'like',
'ręczne+SPK': boolean; 'spawn',
'ręczne+SCS': boolean; 'timetableAll',
mechaniczne: boolean; 'timetableUnconfirmed',
'mechaniczne+SPK': boolean; 'timetableConfirmed'
'mechaniczne+SCS': boolean; ] as const;
SBL: boolean;
PBL: boolean; export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
współczesna: boolean;
kształtowa: boolean; export interface ActiveSorter {
historyczna: boolean; headerName: HeadIdsType;
mieszana: boolean; dir: number;
minLevel: number;
maxLevel: number;
minOneWayCatenary: number;
minOneWay: number;
minTwoWayCatenary: number;
minTwoWay: number;
minVmax: number;
maxVmax: number;
'no-1track': boolean;
'no-2track': boolean;
'include-selected': boolean;
free: boolean;
occupied: boolean;
nonPublic: boolean;
unavailable: boolean;
abandoned: boolean;
endingStatus: boolean;
afkStatus: boolean;
noSpaceStatus: boolean;
unavailableStatus: boolean;
unsignedStatus: boolean;
authors: string;
onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
junction: boolean;
nonJunction: boolean;
} }
+275
View File
@@ -0,0 +1,275 @@
import { ActiveSorter } from '../../components/StationsView/typings';
import { ActiveScenery, StationGeneralInfo, Status } from '../../typings/common';
import { Station } from '../../typings/common';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
const filtersAssociations: Record<string, string> = {
mechaniczne: 'mechanical',
ręczne: 'manual',
'mechaniczne+SPK': 'SPK-M',
'ręczne+SPK': 'SPK-R',
'mechaniczne+SCS': 'SCS-M',
'ręczne+SCS': 'SCS-R',
współczesna: 'modern',
historyczna: 'historical',
kształtowa: 'semaphores',
mieszana: 'mixed'
};
function filterStatusSection(
filters: Record<string, any>,
{ dispatcherStatus, dispatcherTimestamp }: ActiveScenery
) {
return (
(filters['endingStatus'] && dispatcherStatus == Status.ActiveDispatcher.ENDING) ||
(filters['unavailableStatus'] &&
(dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE ||
dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN)) ||
(filters['afkStatus'] && dispatcherStatus == Status.ActiveDispatcher.AFK) ||
(filters['noSpaceStatus'] && dispatcherStatus == Status.ActiveDispatcher.NO_SPACE) ||
(filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE) ||
(filters['onlineFromHours'] > 0 &&
(dispatcherTimestamp ?? 0) <= Date.now() + filters['onlineFromHours'] * 3600000)
);
}
function filterTimetablesSection(filters: Record<string, any>, station: Station) {
return (
(filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0)) ||
(filters['withActiveTimetables'] &&
station.onlineInfo &&
(station.onlineInfo.scheduledTrainCount.all != 0 ||
station.onlineInfo.dispatcherStatus == Status.ActiveDispatcher.FREE))
);
}
function filterAccessibilitySection(filters: Record<string, any>, station: Station) {
if (
filters['nonPublic'] &&
(!station.generalInfo || station.generalInfo.availability == 'nonPublic')
)
return true;
if (!station.generalInfo) return false;
const { availability } = station.generalInfo;
return (
(filters['unavailable'] && availability == 'unavailable' && !station.onlineInfo) ||
(filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) ||
(filters['default'] && availability == 'default') ||
(filters['notDefault'] &&
availability != 'default' &&
availability != 'abandoned' &&
availability != 'unavailable')
);
}
function filterRealitySection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (filters['real'] && generalInfo.lines) || (filters['fictional'] && !generalInfo.lines);
}
function filterProgramsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
(filters['SUP'] && generalInfo.SUP) ||
(filters['noSUP'] && !generalInfo.SUP) ||
(filters['ASDEK'] && generalInfo.ASDEK) ||
(filters['noASDEK'] && !generalInfo.ASDEK)
);
}
function filterControlsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.controlType] == true ||
filters[filtersAssociations[generalInfo.controlType]] == true
);
}
function filterSignalsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.signalType] == true ||
filters[filtersAssociations[generalInfo.signalType]] == true ||
(filters['SBL'] && generalInfo.routes.sblNames.length > 0) ||
(filters['PBL'] && generalInfo.routes.sblNames.length == 0)
);
}
function filterStationType(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const singleTracks = generalInfo.routes.single.filter((r) => !r.isInternal);
const doubleTracks = generalInfo.routes.double.filter((r) => !r.isInternal);
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
return (filters['junction'] && isJunction) || (filters['nonJunction'] && !isJunction);
}
function filterSliderValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const { availability, reqLevel, routes } = generalInfo;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
return (
filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0) ||
filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0) ||
filters['minVmax'] > routes.maxRouteSpeed ||
filters['maxVmax'] < routes.minRouteSpeed ||
(filters['no-1track'] && routes.single.length != 0) ||
(filters['no-2track'] && routes.double.length != 0) ||
filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length ||
filters['minOneWay'] > routes.singleOtherNames.length ||
filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length ||
filters['minTwoWay'] > routes.doubleOtherNames.length
);
}
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters['authors'].length > 3 &&
!generalInfo.authors
?.map((a) => a.toLocaleLowerCase())
.includes(filters['authors'].toLocaleLowerCase())
);
}
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Record<string, any>) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
// Scenery Timetables section
if (filterTimetablesSection(filters, station)) return false;
// Scenery Accessibility section
if (filterAccessibilitySection(filters, station)) return false;
// Scenery Status section
if (station.onlineInfo && filterStatusSection(filters, station.onlineInfo)) return false;
if (station.generalInfo) {
// Scenery Reality section
if (filterRealitySection(filters, station.generalInfo)) return false;
// Scenery Additional Programs section
if (filterProgramsSection(filters, station.generalInfo)) return false;
// Scenery Controls section
if (filterControlsSection(filters, station.generalInfo)) return false;
// Scenery Signalling section(s)
if (filterSignalsSection(filters, station.generalInfo)) return false;
// Scenery Station Type section
if (filterStationType(filters, station.generalInfo)) return false;
// Scenery sliders
if (filterSliderValues(filters, station.generalInfo)) return false;
// Scenery Authors section
if (filterInputValues(filters, station.generalInfo)) return false;
}
return true;
};
+2 -2
View File
@@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common'; import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -23,7 +23,7 @@ export default defineComponent({
trains() { trains() {
if (this.tooltipStore.content == '') return []; if (this.tooltipStore.content == '') return [];
const parsedTrains = JSON.parse(this.tooltipStore.content) as StationTrain[]; const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo); return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
} }
} }
+12 -23
View File
@@ -21,7 +21,7 @@ export default defineComponent({
computed: { computed: {
chosenTrain() { chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId); return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
} }
}, },
@@ -29,8 +29,15 @@ export default defineComponent({
chosenTrain(train: Train | undefined) { chosenTrain(train: Train | undefined) {
this.$nextTick(() => { this.$nextTick(() => {
if (train) { if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement; const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus(); contentEl.focus();
} else {
(this.store.modalLastClickedTarget as any)?.focus();
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 90);
} }
}); });
} }
@@ -40,20 +47,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.train-modal { .train-modal {
position: fixed; position: fixed;
@@ -61,12 +54,14 @@ export default defineComponent({
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%;
color: white; color: white;
z-index: 200; z-index: 200;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start;
text-align: left; text-align: left;
} }
@@ -87,10 +82,10 @@ export default defineComponent({
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
margin-top: 1em;
width: 95vw; width: 95vw;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; box-shadow: 0 0 15px 10px #0e0e0e;
@@ -105,10 +100,4 @@ export default defineComponent({
} }
} }
} }
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style> </style>
+6 -17
View File
@@ -8,17 +8,18 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" /> <Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0"> <div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
>)
</div> </div>
<transition-group name="list-anim" tag="ul"> <transition-group name="list-anim" tag="ul">
<li <li
class="train-row" class="train-row"
v-for="train in trains" v-for="train in trains"
:key="train.trainId" :key="train.id"
tabindex="0" tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)" @click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)" @keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<TrainInfo :train="train" :extended="false" /> <TrainInfo :train="train" :extended="false" />
</li> </li>
@@ -76,17 +77,6 @@ export default defineComponent({
return Status.Data.Loaded; return Status.Data.Loaded;
} }
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
} }
}); });
</script> </script>
@@ -108,8 +98,7 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
font-size: 1.25em;
font-size: 1.5em;
background: #1a1a1a; background: #1a1a1a;
} }
+20 -17
View File
@@ -112,8 +112,8 @@
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE", "donate": "DONATE",
"search-button": "Search", "search-button": "SEARCH",
"reset-button": "Reset", "reset-button": "RESET",
"sort-title": "SORT BY:", "sort-title": "SORT BY:",
"filter-title": "FILTER BY:", "filter-title": "FILTER BY:",
@@ -174,9 +174,9 @@
"sections": { "sections": {
"quick": "QUICK FILTERS", "quick": "QUICK FILTERS",
"station-type": "STATION TYPE", "stationType": "STATION TYPE",
"reality": "SCENERY REALITY", "reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY", "packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY", "access": "GENERAL AVAILABILITY",
"control": "CONTROLS", "control": "CONTROLS",
"signals": "SIGNALLING", "signals": "SIGNALLING",
@@ -187,6 +187,9 @@
"spawns": "OPEN SPAWNS" "spawns": "OPEN SPAWNS"
}, },
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE", "all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE", "all-free": "CURRENTLY FREE",
@@ -197,11 +200,11 @@
"title": "STATION FILTERS", "title": "STATION FILTERS",
"default": "IN-GAME", "default": "IN-GAME",
"not-default": "ADDITIONAL", "notDefault": "ADDITIONAL",
"real": "REAL", "real": "REAL",
"fictional": "FICTIONAL", "fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED", "unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC", "nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED", "abandoned": "ABANDONED",
"SPK": "SPK", "SPK": "SPK",
@@ -211,7 +214,6 @@
"SCS-R": "SCS + MANUAL", "SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.", "SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "MANUAL", "manual": "MANUAL",
"mechanical": "MECHANICAL", "mechanical": "MECHANICAL",
@@ -238,14 +240,14 @@
"nonJunction": "OTHER", "nonJunction": "OTHER",
"sliders": { "sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL", "minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL", "maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED", "minVmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED", "maxVmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES", "minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES", "minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES", "minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES" "minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
}, },
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):", "authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
@@ -298,12 +300,13 @@
"single-track-routes-other": "Not electrified single-track routes count: " "single-track-routes-other": "Not electrified single-track routes count: "
}, },
"no-stations": "No stations to show here!", "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..." "scenery-search": "Search for scenery...",
"active-filters": "Attention! You got active filters!"
}, },
"station-stats": { "station-stats": {
"u-factor": "U-factor", "u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)", "u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
"med-timetable-count": "Median of scenery timetables:", "avg-timetable-count": "Average count of scenery timetables:",
"single-track-count": "Single track routes:", "single-track-count": "Single track routes:",
"double-track-count": "Double track routes:", "double-track-count": "Double track routes:",
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)", "cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
+18 -14
View File
@@ -171,9 +171,9 @@
"sections": { "sections": {
"quick": "SZYBKIE FILTRY", "quick": "SZYBKIE FILTRY",
"station-type": "RODZAJ STACJI", "stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII", "reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE", "packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA", "control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI", "signals": "TYP SYGNALIZACJI",
@@ -184,6 +184,9 @@
"spawns": "OTWARTE SPAWNY" "spawns": "OTWARTE SPAWNY"
}, },
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE", "all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE", "all-free": "WSZYSTKIE WOLNE",
@@ -194,11 +197,11 @@
"title": "FILTRUJ STACJE", "title": "FILTRUJ STACJE",
"default": "DOMYŚLNA", "default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ", "notDefault": "POZA PACZKĄ",
"real": "REALNA", "real": "REALNA",
"fictional": "FIKCYJNA", "fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA", "unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA", "nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA", "abandoned": "WYCOFANA",
"SPK": "SPK", "SPK": "SPK",
@@ -234,14 +237,14 @@
"nonJunction": "INNE", "nonJunction": "INNE",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA", "minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA", "maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)", "minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)", "minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)" "minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
}, },
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):", "authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
@@ -291,12 +294,13 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: " "single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
}, },
"no-stations": "Brak stacji do wyświetlenia!", "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..." "scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
}, },
"station-stats": { "station-stats": {
"u-factor": "Współczynnik Ugla", "u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)", "u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
"med-timetable-count": "Mediana rozkładów jazdy na sceneriach:", "avg-timetable-count": "Średnia liczba rozkładów jazdy na sceneriach:",
"single-track-count": "Szlaki jednotorowe:", "single-track-count": "Szlaki jednotorowe:",
"double-track-count": "Szlaki dwutorowe:", "double-track-count": "Szlaki dwutorowe:",
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):", "cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
+119
View File
@@ -0,0 +1,119 @@
import StorageManager from './storageManager';
export const sections = [
'status',
'timetables',
'reality',
'packageAccess',
'stationType',
'access',
'control',
'blockades',
'signals',
'addons'
] as const;
export const initFilters = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
manual: false,
'SPK-R': false,
'SCS-R': false,
mechanical: false,
'SPK-M': false,
'SCS-M': false,
modern: false,
semaphores: false,
historical: false,
mixed: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
authors: ''
};
export const initSliders = [
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 }
];
export type StationFilter = keyof typeof initFilters;
export type StationFilterSection = (typeof sections)[number];
export const filtersSections: Record<StationFilterSection, StationFilter[]> = {
status: ['free', 'occupied', 'endingStatus', 'afkStatus', 'noSpaceStatus', 'unavailableStatus'],
timetables: ['withActiveTimetables', 'withoutActiveTimetables'],
reality: ['real', 'fictional'],
packageAccess: ['default', 'notDefault'],
stationType: ['junction', 'nonJunction'],
access: ['nonPublic', 'unavailable', 'abandoned'],
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
control: ['SPK', 'SCS', 'SPE', 'SPK-M', 'SCS-M', 'mechanical', 'SPK-R', 'SCS-R', 'manual'],
blockades: ['SBL', 'PBL'],
signals: ['modern', 'semaphores', 'mixed', 'historical']
};
export function setupFilters(currentFilters: Record<string, any>) {
if (!StorageManager.isRegistered('options_saved')) return;
Object.keys(currentFilters).forEach((filterKey) => {
const savedValue = StorageManager.getValue(filterKey);
if (savedValue != null) {
if (typeof currentFilters[filterKey] == 'boolean')
currentFilters[filterKey] = savedValue === 'true';
else if (typeof currentFilters[filterKey] == 'number')
currentFilters[filterKey] = Number(savedValue);
else currentFilters[filterKey] = savedValue.toString();
}
});
}
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
return (
Object.keys(currentFilters).filter(
(filterKey) =>
currentFilters[filterKey] !== initFilters[filterKey as keyof typeof initFilters]
) ?? []
);
}
+4
View File
@@ -34,6 +34,10 @@ export default class StorageManager {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean { static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false; return window.localStorage.getItem(key) === 'true' ? true : false;
} }
+8 -8
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore'; import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -11,20 +12,19 @@ export default defineComponent({
}, },
methods: { methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) { selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = train.modalId;
document.body.classList.add('no-scroll'); if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target; if (target) this.store.modalLastClickedTarget = target;
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide(); this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
} }
} }
}); });
-21
View File
@@ -1,21 +0,0 @@
export const headIds = [
'station',
'min-lvl',
'status',
'dispatcher',
'dispatcher-lvl',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
'timetableConfirmed'
] as const;
export type HeadIdsTypes = (typeof headIds)[number] | (typeof headIconsIds)[number];
-242
View File
@@ -1,242 +0,0 @@
import { Filter } from '../../components/StationsView/typings';
import { Status } from '../../typings/common';
import { HeadIdsTypes } from '../data/stationHeaderNames';
import { Station } from '../../typings/common';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
export const sortStations = (
a: Station,
b: Station,
sorter: { headerName: HeadIdsTypes; dir: number }
) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Filter) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
if (station.onlineInfo) {
const { dispatcherStatus } = station.onlineInfo;
const excludeEnding =
dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const excludeNotSigned =
(dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN ||
dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) &&
filters['unavailableStatus'];
const excludeAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const excludeNoSpace =
dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus'];
const excludeOccupied = filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE;
const excludeActiveTTs =
(dispatcherStatus == Status.ActiveDispatcher.FREE ||
station.onlineInfo.scheduledTrainCount.all != 0) &&
filters['withActiveTimetables'];
if (
excludeEnding ||
excludeAFK ||
excludeNoSpace ||
excludeNotSigned ||
excludeOccupied ||
excludeActiveTTs
)
return false;
if (
filters['onlineFromHours'] > 0 &&
dispatcherStatus <= Date.now() + filters['onlineFromHours'] * 3600000
)
return false;
}
const excludeNoActiveTTs =
filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0);
if (excludeNoActiveTTs) return false;
if (
(station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) &&
filters['nonPublic']
)
return false;
if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, ASDEK, authors } =
station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return false;
if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false;
if (availability == 'default' && filters['default']) return false;
if (
availability != 'default' &&
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable')
)
return false;
if (filters['real'] && lines) return false;
if (filters['fictional'] && !lines) return false;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false;
if (filters['minVmax'] > station.generalInfo.routes.maxRouteSpeed) return false;
if (filters['maxVmax'] < station.generalInfo.routes.minRouteSpeed) return false;
if (
filters['no-1track'] &&
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
)
return false;
if (
filters['no-2track'] &&
(routes.doubleElectrifiedNames.length != 0 || routes.doubleOtherNames.length != 0)
)
return false;
if (routes.singleElectrifiedNames.length < filters['minOneWayCatenary']) return false;
if (routes.singleOtherNames.length < filters['minOneWay']) return false;
if (routes.doubleElectrifiedNames.length < filters['minTwoWayCatenary']) return false;
if (routes.doubleOtherNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false;
if (filters[signalType]) return false;
if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false;
if (filters['ASDEK'] && ASDEK) return false;
if (filters['noASDEK'] && !ASDEK) return false;
if (filters['SBL'] && routes.sblNames.length > 0) return false;
if (filters['PBL'] && routes.sblNames.length == 0) return false;
if (
filters['authors'].length > 3 &&
!authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
return false;
const singleTracks = routes.single.filter((r) => !r.isInternal);
const doubleTracks = routes.double.filter((r) => !r.isInternal);
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
if (filters['junction'] && isJunction) return false;
if (filters['nonJunction'] && !isJunction) return false;
}
return true;
};
-2
View File
@@ -60,8 +60,6 @@ export const useApiStore = defineStore('apiStore', {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response.data;
+76 -35
View File
@@ -1,9 +1,9 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns } from './utils';
import { import {
ActiveScenery, ActiveScenery,
ScheduledTrain, CheckpointTrain,
Station, Station,
StationRoutes, StationRoutes,
Status, Status,
@@ -12,6 +12,9 @@ import {
import { useApiStore } from './apiStore'; import { useApiStore } from './apiStore';
import { MainStoreState } from './typings'; import { MainStoreState } from './typings';
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', { export const useMainStore = defineStore('mainStore', {
state: () => state: () =>
({ ({
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
trainList(): Train[] { trainList(): Train[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? []) return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online) .filter((train) => train.timetable || train.online)
.map((train) => { .map((train) => {
@@ -53,8 +59,9 @@ export const useMainStore = defineStore('mainStore', {
sceneryHash sceneryHash
) ?? []; ) ?? [];
return { const trainObj = {
trainId: train.driverName + train.trainNo.toString(), id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo, trainNo: train.trainNo,
mass: train.mass, mass: train.mass,
@@ -93,6 +100,33 @@ export const useMainStore = defineStore('mainStore', {
} }
: undefined : undefined
} as Train; } as Train;
// Sceneries trains map
if (sceneriesTrains.has(train.currentStationName)) {
sceneriesTrains.set(train.currentStationName, [
...sceneriesTrains.get(train.currentStationName)!,
trainObj
]);
} 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 (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
});
return trainObj;
}); });
}, },
@@ -131,13 +165,14 @@ export const useMainStore = defineStore('mainStore', {
dispatcherId: -1, dispatcherId: -1,
dispatcherExp: -1, dispatcherExp: -1,
dispatcherIsSupporter: false, dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE, dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1, dispatcherTimestamp: -1,
isOnline: false, isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -177,8 +212,9 @@ export const useMainStore = defineStore('mainStore', {
isOnline: scenery.isOnline == 1, isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [], stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -196,37 +232,39 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name); const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains( let checkpointsSet: Set<string> = new Set();
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
const stationTrains = getStationTrains( // Add checkpoints to active scenery data
this.trainList, checkpointsSet.add(scenery.name.toLowerCase());
scheduledTrains,
this.region.id,
scenery.name
);
// Remove checkpoint duplicates station?.generalInfo?.checkpoints.forEach((cpName) => {
const uniqueScheduledTrains = scheduledTrains.reduce( checkpointsSet.add(cpName.toLowerCase());
(uniqueList, sTrain) => });
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
scenery.scheduledTrains = scheduledTrains; const checkpoints = Array.from(checkpointsSet);
scenery.stationTrains = stationTrains;
scenery.scheduledTrainCount = { scenery.stationTrains =
all: uniqueScheduledTrains.length, sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length const uniqueTrainIds: string[] = [];
}; checkpoints.forEach((cp) => {
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop }) => {
scenery.scheduledTrains.push({ train, checkpointStop });
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
scenery.scheduledTrainCount.all += 1;
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
else scenery.scheduledTrainCount.unconfirmed++;
uniqueTrainIds.push(train.id);
});
});
} }
return allActiveSceneries; return allActiveSceneries;
@@ -281,7 +319,10 @@ export const useMainStore = defineStore('mainStore', {
...scenery, ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes, routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? [] checkpoints:
scenery.checkpoints && scenery.checkpoints.trim().length > 0
? scenery.checkpoints.split(';')
: []
} }
}; };
}); });
-146
View File
@@ -1,146 +0,0 @@
import { defineStore } from 'pinia';
import inputData from '../data/options.json';
import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/stationFilterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager';
import { Filter } from '../components/StationsView/typings';
const filterInitStates: Filter = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
ręczne: false,
'ręczne+SPK': false,
'ręczne+SCS': false,
mechaniczne: false,
'mechaniczne+SPK': false,
'mechaniczne+SCS': false,
współczesna: false,
kształtowa: false,
historyczna: false,
mieszana: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
ending: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
authors: '',
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
lastClickedFilterId: ''
};
},
getters: {
areFiltersAtDefault: (state) => {
return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
},
filteredStationList: (state) => {
const store = useMainStore();
return store.allStationInfo
.filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive));
}
},
actions: {
setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name);
this.filters[option.name] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(name: string, value: any) {
this.filters[name] = value;
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
},
resetFilters() {
this.filters = { ...filterInitStates };
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
resetSectionOptions(section: string) {
this.inputs.options
.filter((option) => option.section == section)
.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == this.sorterActive.headerName)
this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.headerName = headerName;
}
}
});
+1 -1
View File
@@ -1,5 +1,5 @@
import { API } from '../typings/api'; import { API } from '../typings/api';
import { Availability, StationRoutesInfo, Status } from '../typings/common'; import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export interface MainStoreState { export interface MainStoreState {
region: { id: string; value: string; name: string }; region: { id: string; value: string; name: string };
+1 -183
View File
@@ -1,13 +1,4 @@
import { import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
TrainStop,
StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
export function getStatusTimestamp(stationStatus: any): number { export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2; if (!stationStatus) return -2;
@@ -57,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
export function getTimestamp(date: string | null): number { export function getTimestamp(date: string | null): number {
return date ? new Date(date).getTime() : 0; return date ? new Date(date).getTime() : 0;
} }
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
let stopStatus = StopStatus.ARRIVING,
stopLabel = '',
stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = StopStatus.TERMINATED;
stopLabel = 'Skończył bieg';
stopStatusID = 5;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
stopStatus = StopStatus.DEPARTED;
stopLabel = 'Odprawiony';
stopStatusID = 2;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
stopStatus = StopStatus.DEPARTED_AWAY;
stopLabel = 'Odjechał';
stopStatusID = 4;
} else if (currentStationName == sceneryName && !stopInfo.stopped) {
stopStatus = StopStatus.ONLINE;
stopLabel = 'Na stacji';
stopStatusID = 0;
} else if (currentStationName == sceneryName && stopInfo.stopped) {
stopStatus = StopStatus.STOPPED;
stopLabel = 'Postój';
stopStatusID = 1;
} else if (currentStationName != sceneryName) {
stopStatus = StopStatus.ARRIVING;
stopLabel = 'W drodze';
stopStatusID = 3;
}
return { stopStatus, stopLabel, stopStatusID };
}
export function getCheckpointTrain(
train: Train,
trainStopIndex: number,
sceneryName: string
): ScheduledTrain {
const timetable = train.timetableData!;
const followingStops = timetable.followingStops;
const trainStop = followingStops[trainStopIndex];
const trainStopStatus = getTrainStopStatus(trainStop, train.currentStationName, sceneryName);
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex; i >= 0; i--) {
const stop = 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 = followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < followingStops.length; i++) {
const stop = 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 = followingStops[i + 1]?.arrivalLine || null;
}
}
return {
checkpointName: trainStop.stopNameRAW,
trainNo: train.trainNo,
trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nextStationName,
prevStationName,
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID,
region: train.region,
arrivingLine: arrivingLine,
departureLine: departureLine,
nextArrivalLine,
prevDepartureLine
};
}
export function getScheduledTrains(
trainList: Train[],
stationGeneralInfo: Station['generalInfo'],
stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] {
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
if (train.region != region) return acc;
const timetable = train.timetableData;
if (!timetable.sceneryNames.includes(stationName)) return acc;
const checkpoints = [stationName];
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
const checkpointScheduledTrains: ScheduledTrain[] = [];
for (let i = 0; i < timetable.followingStops.length; i++) {
if (
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
timetable.followingStops[i].stopNameRAW
)
) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
}
}
acc.push(...checkpointScheduledTrains);
return acc;
}, []) as ScheduledTrain[];
}
export function getStationTrains(
trainList: Train[],
scheduledTrainList: ScheduledTrain[],
region: string,
stationName: string
): StationTrain[] {
return trainList
.filter(
(train) =>
train?.region === region && train.online && train.currentStationName === stationName
)
.map((train) => ({
driverName: train.driverName,
driverId: train.driverId,
trainNo: train.trainNo,
trainId: train.trainId,
stopStatus:
scheduledTrainList.find((st) => st.trainNo === train.trainNo)?.stopStatus || 'no-timetable'
}));
}
-1
View File
@@ -161,7 +161,6 @@ export namespace API {
stopNameRAW: string; stopNameRAW: string;
stopType: string; stopType: string;
stopDistance: number; stopDistance: number;
pointId: string;
mainStop: boolean; mainStop: boolean;
+30 -74
View File
@@ -41,7 +41,7 @@ export interface RegionCounters {
export interface Train { export interface Train {
id: string; id: string;
trainId: string; modalId: string;
mass: number; mass: number;
length: number; length: number;
speed: number; speed: number;
@@ -79,35 +79,30 @@ export interface Train {
export interface Station { export interface Station {
name: string; name: string;
generalInfo?: { generalInfo?: StationGeneralInfo;
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
onlineInfo?: ActiveScenery; onlineInfo?: ActiveScenery;
} }
export interface StationGeneralInfo {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
}
export interface StationRoutes { export interface StationRoutes {
single: StationRoutesInfo[]; single: StationRoutesInfo[];
double: StationRoutesInfo[]; double: StationRoutesInfo[];
@@ -148,8 +143,8 @@ export interface ActiveScenery {
dispatcherStatus: Status.ActiveDispatcher | number; dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null; dispatcherTimestamp: number | null;
isOnline: boolean; isOnline: boolean;
stationTrains?: StationTrain[]; stationTrains: Train[];
scheduledTrains?: ScheduledTrain[]; scheduledTrains: CheckpointTrain[];
scheduledTrainCount: { scheduledTrainCount: {
all: number; all: number;
confirmed: number; confirmed: number;
@@ -164,49 +159,6 @@ export interface ScenerySpawn {
spawnType: ScenerySpawnType; spawnType: ScenerySpawnType;
} }
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export interface TrainStop { export interface TrainStop {
stopName: string; stopName: string;
stopNameRAW: string; stopNameRAW: string;
@@ -223,13 +175,17 @@ export interface TrainStop {
departureTimestamp: number; departureTimestamp: number;
departureRealTimestamp: number; departureRealTimestamp: number;
departureDelay: number; departureDelay: number;
pointId: number;
comments?: string; comments?: string;
beginsHere: boolean; beginsHere: boolean;
terminatesHere: boolean; terminatesHere: boolean;
confirmed: boolean; confirmed: number;
stopped: boolean; stopped: number;
stopTime: number | null; stopTime: number | null;
} }
export interface CheckpointTrain {
checkpointStop: TrainStop;
train: Train;
}
+3 -3
View File
@@ -200,7 +200,7 @@ button.back-btn {
display: inline-block; display: inline-block;
font-size: 1.5em; font-size: 1.25em;
button { button {
margin: 1em auto; margin: 1em auto;
@@ -233,7 +233,7 @@ button.back-btn {
background-color: #181818; background-color: #181818;
padding: 1em 0.5em; padding: 1em 0.5em;
height: 95vh; height: 100vh;
min-height: 750px; min-height: 750px;
max-height: 1000px; max-height: 1000px;
overflow: auto; overflow: auto;
@@ -246,7 +246,7 @@ button.back-btn {
background: #181818; background: #181818;
padding: 1em 0.5em; padding: 1em 0.5em;
height: 95vh; height: 100vh;
min-height: 750px; min-height: 750px;
max-height: 1000px; max-height: 1000px;
+27 -14
View File
@@ -11,16 +11,16 @@
<button <button
class="btn-donation btn--image" class="btn-donation btn--image"
ref="btn" ref="btn"
@click="isDonationModalOpen = true" @click="isDonationCardOpen = true"
@focus="isDonationModalOpen = false" @focus="isDonationCardOpen = false"
> >
<img src="/images/icon-dollar.svg" alt="dollar donation icon" /> <img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span> <span>{{ $t('donations.button-title') }}</span>
</button> </button>
</div> </div>
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" /> <DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggleDonationModal="toggleDonationModal" /> <StationTable @toggle-donation-card="toggleDonationCard" />
<StationStats /> <StationStats />
</div> </div>
</section> </section>
@@ -30,34 +30,47 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import DonationModal from '../components/Global/DonationModal.vue'; import DonationCard from '../components/Global/DonationCard.vue';
import StationStats from '../components/StationsView/StationStats.vue'; import StationStats from '../components/StationsView/StationStats.vue';
import { initFilters, setupFilters } from '../managers/stationFilterManager';
import { reactive } from 'vue';
import { provide } from 'vue';
import { ActiveSorter } from '../components/StationsView/typings';
import { onMounted } from 'vue';
const filterInitStates = { ...initFilters };
export default defineComponent({ export default defineComponent({
components: { components: {
StationTable, StationTable,
StationFilterCard, StationFilterCard,
StationStats, StationStats,
DonationModal DonationCard
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
isDonationModalOpen: false, isDonationCardOpen: false,
filterStore: useStationFiltersStore(), mainStore: useMainStore()
store: useMainStore()
}), }),
mounted() { setup() {
this.filterStore.setupFilters(); const filters = reactive(filterInitStates);
const activeSorter = reactive({ headerName: 'station', dir: 1 }) as ActiveSorter;
provide('StationsView_filters', filters);
provide('StationsView_activeSorter', activeSorter);
onMounted(() => {
setupFilters(filters);
});
}, },
methods: { methods: {
toggleDonationModal(value: boolean) { toggleDonationCard(value: boolean) {
this.isDonationModalOpen = value; this.isDonationCardOpen = value;
} }
} }
}); });
+1 -1
View File
@@ -109,7 +109,7 @@ export default defineComponent({
this.$nextTick(() => { this.$nextTick(() => {
if (this.trainId) { if (this.trainId) {
this.selectModalTrain(this.trainId); this.selectModalTrainById(this.trainId);
} }
}); });
} }
+8
View File
@@ -7,6 +7,10 @@ export default defineConfig({
port: 5001, port: 5001,
open: true open: true
}, },
preview: {
port: 4001,
open: true
},
publicDir: 'public', publicDir: 'public',
plugins: [ plugins: [
vue(), vue(),
@@ -36,6 +40,10 @@ export default defineConfig({
cacheName: 'spythere-static-cache', cacheName: 'spythere-static-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200] statuses: [0, 200]
},
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 8
} }
} }
} }