Compare commits

...

67 Commits

Author SHA1 Message Date
Spythere 0ac7ba51e5 Merge pull request #100 from Spythere/development
v1.25.2
2024-07-12 16:13:06 +02:00
Spythere bdf85cd8ec bump: 1.25.2 2024-07-12 16:01:45 +02:00
Spythere 63b268d9b9 feat: added journal timetable path 2024-07-12 15:59:08 +02:00
Spythere d73c8ef112 fix: update modal won't open on first visit 2024-07-12 15:11:17 +02:00
Spythere 3d1c66b420 fix: cache control 2024-07-12 14:50:01 +02:00
Spythere b3f7108979 fix: detecting podg in timetables 2024-07-12 13:58:43 +02:00
Spythere feabfd29e0 Merge pull request #99 from Spythere/development
fix: recognizing timetables for sceneries with the same stop names
2024-07-09 20:33:16 +02:00
Spythere f17fedc976 fix: recognizing timetables for sceneries with the same stop names; optimization 2024-07-09 19:15:04 +02:00
Spythere c83c75e014 Merge pull request #98 from Spythere/development
hotfix: thumbnails v2 src
2024-07-08 22:12:41 +02:00
Spythere e57143f517 hotfix: thumbnails v2 src 2024-07-08 22:12:05 +02:00
Spythere fb45a783ee Merge pull request #97 from Spythere/development
v1.25.1
2024-07-08 21:40:50 +02:00
Spythere 71476e9552 bump: v1.25.1 2024-07-08 21:38:05 +02:00
Spythere 922a338143 hotfix: stock naming 2024-07-08 21:37:51 +02:00
Spythere 231d36e877 chore: adjusted for new vehicle thumbnails 2024-07-08 21:35:22 +02:00
Spythere 27d6ac9f14 Merge pull request #96 from Spythere/development
hotfix: scenery timetable train statuses
2024-06-11 20:56:25 +02:00
Spythere a6029da2cc hotfix: scenery timetable train statuses 2024-06-11 20:55:07 +02:00
Spythere a3f3790205 Merge pull request #95 from Spythere/development
hotfix: timetables for unknown sceneries
2024-06-10 20:19:20 +02:00
Spythere ebfb24f729 hotfix: timetables for unknown sceneries 2024-06-10 20:18:09 +02:00
Spythere e521736618 Merge pull request #94 from Spythere/development
hotfix: changed pwa strategy
2024-06-10 00:37:21 +02:00
Spythere fc7662e431 chore: changed pwa strategy 2024-06-10 00:36:30 +02:00
Spythere a459fdf178 Merge pull request #93 from Spythere/development
v1.25.0
2024-06-09 23:40:54 +02:00
Spythere 4e7fba89ee chore: improved stop label information 2024-06-09 00:58:45 +02:00
Spythere 6084e5876d chore: changed default history mode 2024-06-08 21:38:05 +02:00
Spythere 44f548c7b7 chore: scenery history locales 2024-06-08 21:37:28 +02:00
Spythere 59a5fbe5ac chore: adjusted to new version of API vehicles data 2024-06-08 20:53:22 +02:00
Spythere c252213ed9 hotfix 2024-06-07 18:31:20 +02:00
Spythere fb56378f18 chore: redesigned scenery history tables 2024-06-07 16:44:09 +02:00
Spythere e9635eae06 chore: redesigned train schedule list 2024-06-06 17:11:52 +02:00
Spythere 1fc98a8f99 chore: added test data mocks 2024-06-06 14:41:54 +02:00
Spythere c9de1a48ce chore: scenery timetables history translation; layout fixes 2024-06-06 14:19:17 +02:00
Spythere fee9774f88 chore: layout fixes 2024-06-06 14:12:21 +02:00
Spythere 7c974e8d0e bump: 1.25.0 2024-06-06 14:04:07 +02:00
Spythere c84fbbcf42 chore: added scenery timetables history modes 2024-06-05 20:03:05 +02:00
Spythere 45af649505 chore: changes in scenery view layout 2024-06-05 16:01:17 +02:00
Spythere 6c1e00d002 chore: layout & design fixes 2024-06-04 15:57:17 +02:00
Spythere 69ff85cfb1 chore: added route electrification indicators in train schedule 2024-06-03 22:26:58 +02:00
Spythere bdc2ca784c chore: missing translations 2024-06-03 21:37:33 +02:00
Spythere dbd73d448d chore: added active train's rolling stock vmax 2024-06-03 20:09:15 +02:00
Spythere 26b1ec246d chore: added extra data to vehicles tooltip 2024-06-03 18:10:45 +02:00
Spythere 8190dfa2cb chore: fetching & caching vehicles data information 2024-06-03 01:31:31 +02:00
Spythere 44df685606 Merge pull request #92 from Spythere/development
v1.24.4
2024-05-30 14:38:04 +02:00
Spythere 785a42b849 hotfix: detecting user timetable status at checkpoints 2024-05-30 14:29:09 +02:00
Spythere ccfcca8728 hotfix: scenery timetable duplicating 2024-05-30 14:24:18 +02:00
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
78 changed files with 9000 additions and 12765 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.24.1",
"version": "1.25.2",
"private": true,
"scripts": {
"dev": "vite",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+13 -11
View File
@@ -1,8 +1,8 @@
<template>
<div class="app_container">
<UpdateModal
:update-modal-open="isUpdateModalOpen"
@toggle-modal="() => (isUpdateModalOpen = false)"
<UpdateCard
:is-update-card-open="isUpdateCardOpen"
@toggle-card="() => (isUpdateCardOpen = false)"
/>
<Tooltip />
@@ -27,7 +27,7 @@
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<button class="btn--text" @click="() => (isUpdateModalOpen = true)">
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
@@ -56,7 +56,7 @@ import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.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';
@@ -68,7 +68,7 @@ export default defineComponent({
StatusIndicator,
AppHeader,
TrainModal,
UpdateModal,
UpdateCard,
Tooltip
},
@@ -78,7 +78,7 @@ export default defineComponent({
apiStore: useApiStore(),
tooltipStore: useTooltipStore(),
isUpdateModalOpen: false,
isUpdateCardOpen: false,
currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
@@ -130,8 +130,9 @@ export default defineComponent({
releaseURL: releaseData.html_url
};
this.isUpdateModalOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
this.isUpdateCardOpen =
(storageVersion != '' && storageVersion != version) ||
import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
@@ -180,7 +181,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) {
if (naviLanguage.startsWith('en')) {
this.changeLang('en');
return;
}
@@ -210,7 +211,7 @@ export default defineComponent({
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
font-size: calc(0.65rem + 0.85vw);
}
@include screenLandscape() {
@@ -226,6 +227,7 @@ export default defineComponent({
min-height: 100vh;
overflow: hidden;
position: relative;
}
.app_main {
@@ -1,12 +1,12 @@
<template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
<div class="modal-content">
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></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') }}
</button>
@@ -16,7 +16,7 @@
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</AnimatedModal>
</Card>
</template>
<script lang="ts">
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue';
import Card from '../Global/Card.vue';
const converter = new Converter();
export default defineComponent({
components: { AnimatedModal },
components: { Card },
props: {
updateModalOpen: {
isUpdateCardOpen: {
type: Boolean,
required: true
}
},
emits: ['toggleModal'],
emits: ['toggleCard'],
data() {
return {
@@ -49,7 +49,7 @@ export default defineComponent({
},
watch: {
updateModalOpen(val: boolean) {
isUpdateCardOpen(val: boolean) {
this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
});
@@ -60,37 +60,38 @@ export default defineComponent({
htmlChangelog() {
if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
return converter.makeHtml(this.mainStore.appUpdate.changelog);
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) {
text-align: center;
color: $accentCol;
}
::v-deep(h2) {
padding: 0.25em 0;
border-bottom: 1px solid #aaa;
}
::v-deep(ul) {
list-style: inside;
padding: 0.5em;
list-style: initial;
padding: 1em;
line-height: 1.5em;
}
.modal-content {
.content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
@@ -98,6 +99,7 @@ export default defineComponent({
min-height: 700px;
overflow: auto;
text-align: justify;
max-width: 700px;
}
.no-features {
@@ -1,11 +1,10 @@
<template>
<transition name="modal-anim" tag="div">
<div class="modal" v-if="isOpen">
<div class="modal-background" @click="toggleModal(false)"></div>
<div class="modal-wrapper" ref="wrapper" tabindex="0">
<div class="card" v-if="isOpen">
<div class="card-background" @click="toggleCard(false)"></div>
<div class="card-body" tabindex="0">
<slot></slot>
</div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div>
</transition>
</template>
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
emits: ['toggleModal'],
emits: ['toggleCard'],
props: {
isOpen: Boolean
@@ -36,8 +35,8 @@ export default defineComponent({
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
@@ -46,17 +45,21 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal {
.card {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
height: 100%;
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
}
.modal-background {
.card-background {
position: absolute;
top: 0;
left: 0;
@@ -68,21 +71,23 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55);
}
.modal-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
.card-body {
position: relative;
margin: 1em;
max-height: 95vh;
max-height: 95dvh;
& > :slotted(div) {
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw;
max-width: 850px;
overflow: auto;
}
@include smallScreen {
.card {
align-items: flex-start;
}
}
</style>
@@ -1,12 +1,7 @@
<template>
<AnimatedModal
class="donation-modal"
:isOpen="isModalOpen"
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
<div class="body">
<div class="content">
<h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,9 +56,9 @@
</i>
</div>
<div class="modal_actions">
<div class="actions">
<a
class="modal-action a-button btn--image coffee"
class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
ref="action"
@@ -73,7 +68,7 @@
</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"
target="_blank"
>
@@ -81,30 +76,30 @@
{{ $t('donations.action-paypal') }}
</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" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({
components: { AnimatedModal },
components: { Card },
props: {
isModalOpen: Boolean
isCardOpen: Boolean
},
emits: ['toggleModal'],
emits: ['toggleCard'],
watch: {
isModalOpen(val: boolean) {
isCardOpen(val: boolean) {
this.running = val;
this.lastUpdate = Date.now();
@@ -138,8 +133,8 @@ export default defineComponent({
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
},
runUpdate() {
@@ -157,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal_content {
.body {
display: grid;
grid-template-rows: 1fr auto;
gap: 1em;
font-size: 1.1em;
& > div {
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
max-width: 820px;
}
.modal_main {
.content {
overflow: auto;
overflow-x: hidden;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
padding: 1em;
}
.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;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em;
padding: 1em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
.actions > .action {
&.paypal {
$btnColor: #254069;
+123 -96
View File
@@ -1,83 +1,26 @@
<template>
<div class="stock-list">
<div v-if="tractionOnly">
<p>
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ computedStockList[0].split(':')[1] }}
</p>
<img
class="traction-only"
:src="
getVehicleThumbnailURL(
computedStockList[0].split(':')[0],
/^EN/.test(computedStockList[0]) ? 'rb' : ''
)
"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
/>
</div>
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
</p>
<ul>
<li
v-for="({ vehicleName, vehicleCargo, images, imagesFallbacks }, i) in thumbnailNames"
:key="i"
>
<div class="stock-text">
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
</div>
<span>
<img
:data-mouseover="stockName"
v-for="(thumbnailImage, imageIndex) in images"
:data-mouseover="vehicleName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
:src="
getVehicleThumbnailURL(stockName.split(':')[0], /^EN/.test(stockName) ? 'rb' : '')
"
@error="onImageError($event, stockName)"
:data-tooltip-content="vehicleName"
:src="`https://static.spythere.eu/thumbnails/v2/${thumbnailImage}.png`"
@error="onImageError($event, imagesFallbacks[imageIndex])"
@click.stop="() => {}"
width="400"
height="60"
/>
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 'ra')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@click.stop="() => {}"
/>
<!-- /// -->
</span>
</li>
</ul>
@@ -109,32 +52,116 @@ export default defineComponent({
computed: {
computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
},
thumbnailNames() {
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
.filter((v) => v.length != 0)
.map((vehicleString) => {
const [vehicleName, vehicleCargo] = vehicleString.split(':');
const vehicleThumbnailData = {
images: [] as string[],
imagesFallbacks: [] as string[],
vehicleName,
vehicleCargo
};
// Generowanie członów EN57
if (vehicleName.startsWith('EN57')) {
vehicleThumbnailData['images'] = [
vehicleName + 'ra',
vehicleName + 's',
vehicleName + 'rb'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb'
];
}
// Generowanie członów EN71
else if (vehicleName.startsWith('EN71')) {
vehicleThumbnailData['images'] = [
vehicleName + 'ra',
vehicleName + 'sa',
vehicleName + 'sb',
vehicleName + 'rb'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-sa',
'unknown_ezt-sb',
'unknown_ezt-rb'
];
}
// Generowanie pojazdów i członów 2EN57
else if (vehicleString.startsWith('2EN57')) {
const [firstVehicleNumber, secondVehicleNumber] = vehicleString
.replace('2EN57-', '')
.split('+');
vehicleThumbnailData['images'] = [
`EN57-${firstVehicleNumber}ra`,
`EN57-${firstVehicleNumber}s`,
`EN57-${firstVehicleNumber}rb`,
`EN57-${secondVehicleNumber}ra`,
`EN57-${secondVehicleNumber}s`,
`EN57-${secondVehicleNumber}rb`
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb',
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb'
];
}
// Generowanie członów Gor77
else if (vehicleString.startsWith('Gor77')) {
vehicleThumbnailData['images'] = [
vehicleName + '-A',
vehicleName + '-B',
vehicleName + '-C',
vehicleName + '-D'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_Gor77-A',
'unknown_Gor77-B',
'unknown_Gor77-C',
'unknown_Gor77-D'
];
}
// Generowanie członów ET41
else if (vehicleString.startsWith('ET41')) {
vehicleThumbnailData['images'] = [vehicleName + '-A', vehicleName + '-B'];
vehicleThumbnailData['imagesFallbacks'] = ['unknown_ET41-A', 'unknown_ET41-B'];
}
// Generowanie pozostałych pojazdów
else {
let fallbackVehicleImage = 'unknown_cargo';
if (/^(EP|EU)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
fallbackVehicleImage = 'unknown_passenger';
vehicleThumbnailData['images'] = [vehicleName];
vehicleThumbnailData['imagesFallbacks'] = [fallbackVehicleImage];
}
if (this.tractionOnly) vehicleThumbnailData['images'].length = 1;
return vehicleThumbnailData;
});
}
},
methods: {
getVehicleThumbnailURL(locoType: string, suffix?: string) {
return `https://static.spythere.eu/thumbnails/${locoType}${suffix}.png`;
},
onImageError(event: Event, stockName: string) {
let fallbackName = '';
const isLoco = /.-\d{3}/.test(stockName);
if (isLoco) {
if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
else fallbackName = 'loco-s';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
fallbackName += 'car-';
fallbackName += isCarPassenger ? 'passenger' : 'cargo';
}
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
onImageError(event: Event, fallbackImage: string) {
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
}
}
});
@@ -170,10 +197,10 @@ img.traction-only {
max-width: 100%;
}
p {
.stock-text {
text-align: center;
color: #aaa;
font-size: 0.95em;
margin-bottom: 1em;
font-size: 0.9em;
margin-bottom: 0.25em;
}
</style>
@@ -103,7 +103,7 @@ export default defineComponent({
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
}
}
});
@@ -1,23 +1,41 @@
<template>
<div class="stop-list" v-if="showExtraInfo == true">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
<div class="timetable-stops">
<div class="stop-list">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
<span
v-for="(pathData, i) in timetablePathDetails"
:data-visited="pathData.isVisited"
:data-next-visited="
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
"
>
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} &RightArrow; </span>
<b class="path-scenery">{{ pathData.sceneryName }}</b>
<span class="path-departure" v-if="pathData.departure">
&RightArrow; {{ pathData.departure }}&nbsp;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
</div>
</template>
@@ -42,6 +60,24 @@ export default defineComponent({
},
computed: {
timetablePathDetails() {
if (!this.timetable.path || this.timetable.path == '') return null;
return this.timetable.path.split(';').map((pathEl, i) => {
const [arrival, name, departure] = pathEl.split(',');
const sceneryName = name.split(' ').slice(0, -1).join(' ');
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
return {
arrival,
sceneryName,
sceneryHash,
departure,
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
};
});
},
timetableStops() {
const timetable = this.timetable;
@@ -94,13 +130,14 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.stop-list {
.timetable-stops {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
}
.stop-list {
&-item[data-confirmed='true'] {
color: lightgreen;
@@ -109,4 +146,19 @@ export default defineComponent({
}
}
}
.path-details {
margin-top: 0.5em;
}
.path-details > span[data-visited='true'] {
.path-arrival,
.path-scenery {
color: lightgreen;
}
&[data-next-visited='true'] .path-departure {
color: lightgreen;
}
}
</style>
+3 -1
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
| 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = {
[key in TimetableSearchKey]: string;
@@ -1,31 +1,18 @@
<template>
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="scenery-dispatchers-history">
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
</div>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<div v-else class="history-list">
<div v-for="historyItem in historyList" :key="historyItem.id">
<span>
<span class="text--grayed" style="margin-right: 10px">
#{{ historyItem.stationHash }}
</span>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
@@ -35,37 +22,52 @@
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<div>
<span>
{{ $t('scenery.dispatcher-rate') }}
<b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</span>
|
<span>
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
</span>
</div>
</span>
<span>
<span v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</div>
</span>
<div class="dispatcher-online" v-else>
<span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
</div>
</td>
</tr>
</tbody>
</table>
</section>
</span>
</span>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
@@ -149,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.scenery-dispatchers-history {
height: 100%;
overflow: auto;
display: grid;
gap: 0.5em;
grid-template-rows: auto 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.history-list > div {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.75em;
}
.level-badge {
margin: 0 auto;
text-align: center;
display: inline-block;
line-height: 1.6em;
}
.dispatcher-online {
@@ -158,13 +195,10 @@ export default defineComponent({
}
@include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item {
align-items: center;
.history-list > div {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
</style>
../../store/storeTypes
+1 -6
View File
@@ -72,7 +72,7 @@
<div class="info-lists">
<!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" />
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center;
font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
}
.info-lists {
@@ -13,13 +13,13 @@
</li>
<li
v-for="train in onlineScenery?.stationTrains"
v-for="{ train, status } in stationTrains"
class="badge user"
:class="train.stopStatus"
:key="train.trainId"
tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
:key="train.id"
: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_name">{{ train.driverName }}</span>
@@ -32,7 +32,9 @@
import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
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({
mixins: [routerMixin, modalTrainMixin],
@@ -41,6 +43,40 @@ export default defineComponent({
onlineScenery: {
type: Object as PropType<ActiveScenery>,
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() ||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
);
const status = stop
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
: 'no-timetable';
return {
train,
status
};
});
}
}
});
@@ -74,31 +110,31 @@ ul {
-webkit-transition: background-color 200ms;
}
&.no-timetable .user_train {
&[data-status='no-timetable'] .user_train {
background-color: $no-timetable;
}
&.departed > &_train {
&[data-status='departed'] > &_train {
background-color: $departed;
}
&.stopped > &_train {
&[data-status='stopped'] > &_train {
background-color: $stopped;
}
&.online > &_train {
&[data-status='online'] > &_train {
background-color: $online;
}
&.terminated > &_train {
&[data-status='terminated'] > &_train {
background-color: $terminated;
}
&.disconnected > &_train {
&[data-status='disconnected'] > &_train {
background-color: $disconnected;
}
&.offline {
&[data-status='offline'] {
background: firebrick;
pointer-events: none;
}
+78 -61
View File
@@ -39,8 +39,8 @@
<div class="timetable-list">
<transition-group name="list-anim">
<div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading"
>
<Loading />
@@ -48,7 +48,7 @@
<span
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery"
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline"
>
{{ $t('scenery.offline') }}
@@ -56,7 +56,7 @@
<div
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0"
v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables"
>
{{ $t('scenery.no-timetables') }}
@@ -65,59 +65,56 @@
<div
class="timetable-item"
v-else
v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
v-for="(row, i) in sceneryTimetables"
:key="row.train.id + i"
tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
>
<span class="timetable-general">
<span class="general-info">
<span class="info-number">
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<strong>{{ row.train.timetableData!.category }}</strong>
{{ row.train.trainNo }}
<span
v-if="scheduledTrain.stopInfo.comments"
:title="scheduledTrain.stopInfo.comments"
>
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
<img src="/images/icon-warning.svg" />
</span>
</span>
&nbsp;|&nbsp;
<span>
{{ scheduledTrain.driverName }}
{{ row.train.driverName }}
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<ScheduledTrainStatus :sceneryTimetableRow="row" />
</span>
</span>
<span class="timetable-schedule">
<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') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
<div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.arrivalDelay }})
</span>
</div>
</span>
@@ -125,41 +122,39 @@
<span class="schedule-stop">
<span class="stop-connection">
{{ scheduledTrain.arrivingLine }}
{{ row.arrivingLine }}
</span>
<span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
{{ row.checkpointStop.stopTime || '' }}
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
</span>
<span class="stop-connection">
{{ scheduledTrain.departureLine }}
{{ row.departureLine }}
</span>
</span>
<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') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
<div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
timestampToString(row.checkpointStop.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.departureDelay }})
</span>
</div>
</span>
@@ -183,6 +178,8 @@ import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({
name: 'SceneryTimetable',
@@ -204,10 +201,6 @@ export default defineComponent({
listOpen: false
}),
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
@@ -220,9 +213,10 @@ export default defineComponent({
const mainStore = useMainStore();
const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0
? ''
: props.station?.generalInfo?.checkpoints[0] ?? null
props.station?.generalInfo?.checkpoints[0] ??
props.station?.name ??
route.query['station']?.toString() ??
''
);
return {
@@ -241,27 +235,50 @@ export default defineComponent({
return url;
},
computedScheduledTrains() {
if (!this.station) return [];
sceneryTimetables(): SceneryTimetableRow[] {
if (!this.onlineScenery) return [];
return (
this.onlineScenery?.scheduledTrains
?.filter(
(train) =>
train.checkpointName.toLocaleLowerCase() ==
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
train.region == this.mainStore.region.id
)
.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
if (a.stopStatusID < b.stopStatusID) return -1;
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1;
return this.onlineScenery.scheduledTrains
.filter(
(ct) =>
ct.timetablePathElement.stationName == sceneryName &&
ct.train.region == this.mainStore.region.id &&
this.chosenCheckpoint &&
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
)
.map((ct) => {
const trainStopStatus = getTrainStopStatus(
ct.checkpointStop,
ct.train.currentStationName,
sceneryName
);
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1;
}) || []
);
return {
checkpointStop: ct.checkpointStop,
train: ct.train,
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
prevStationName: ct.previousSceneryElement?.stationName ?? null,
nextStationName: ct.nextSceneryElement?.stationName ?? null,
status: trainStopStatus
};
})
.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,69 +1,97 @@
<template>
<!-- WIP -->
<!-- <div class="top-filters">
<button class="btn btn--option">ROZPOCZYNA BIEG</button>
<button class="btn btn--option">PRZEZ</button>
<button class="btn btn--option">KOŃCZY BIEG</button>
</div> -->
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
<div class="scenery-timetables-history">
<div class="history-modes">
<button
class="btn btn--option"
v-for="mode in historyModeList"
:key="mode"
:class="{ checked: checkedHistoryMode == mode }"
@click="checkHistoryMode(mode)"
>
{{ $t(`scenery.timetable-${mode}`) }}
</button>
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.timetables-history-id') }}</th>
<th>{{ $t('scenery.timetables-history-number') }}</th>
<th>{{ $t('scenery.timetables-history-route') }}</th>
<th>{{ $t('scenery.timetables-history-driver') }}</th>
<th>{{ $t('scenery.timetables-history-author') }}</th>
<th>{{ $t('scenery.timetables-history-date') }}</th>
</thead>
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
#{{ historyItem.id }}
</router-link>
</td>
<td>
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
{{ historyItem.trainNo }}
</td>
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<td>
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
{{ historyItem.driverName }}
</router-link>
</td>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<td>
<router-link
v-if="historyItem.authorName"
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
>{{ historyItem.authorName }}
</router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</td>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</td>
</tr>
</tbody>
</table>
</section>
<div v-else class="history-list">
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
<span>
<div>
<span
class="timetable-status-indicator"
:data-terminated="timetableHistory.terminated"
:data-fulfilled="timetableHistory.fulfilled"
>
&ofcir;
</span>
#{{ timetableHistory.id }} |
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
{{ timetableHistory.trainNo }}
{{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
<div class="text--grayed">
<span>
{{ $t('scenery.timetable-issued-date') }}
<b>
{{
localeDateTime(
timetableHistory.createdAt > timetableHistory.beginDate
? timetableHistory.beginDate
: timetableHistory.createdAt,
$i18n.locale
)
}}
</b></span
>
<span v-if="timetableHistory.authorName">
{{ $t('scenery.timetable-issued-by') }}
<b>
<router-link
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
>
{{ timetableHistory.authorName }}
</router-link>
</b>
</span>
<span>
{{ $t('scenery.timetable-issued-for') }}
<b>
<router-link
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
>
{{ timetableHistory.driverName }}
</router-link>
</b>
</span>
</div>
</span>
<button
@click="
navigateTo(`/journal/timetables`, {
'search-train': `#${timetableHistory.id}`
})
"
>
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
</button>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api';
import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import routerMixin from '../../mixins/routerMixin';
import { useMainStore } from '../../store/mainStore';
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
type HistoryMode = (typeof historyModeList)[number];
export default defineComponent({
name: 'SceneryTimetablesHistory',
mixins: [dateMixin],
mixins: [dateMixin, routerMixin],
props: {
station: {
type: Object as PropType<Station>
@@ -91,9 +124,14 @@ export default defineComponent({
data() {
return {
historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading,
DataStatus: Status.Data
DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
};
},
@@ -103,17 +141,22 @@ export default defineComponent({
methods: {
async fetchAPIData() {
if (!this.station && !this.onlineScenery) {
const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded;
return;
}
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try {
const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', {
params: {
issuedFrom: this.station?.name || this.onlineScenery?.name
}
params: requestFilters
})
).data;
@@ -125,11 +168,17 @@ export default defineComponent({
}
},
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
}
});
}
@@ -142,13 +191,66 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.top-filters {
.scenery-timetables-history {
height: 100%;
overflow: auto;
display: grid;
gap: 1em;
grid-template-rows: auto 1fr 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-modes {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.25em;
button {
padding: 0.5em;
padding: 0.35em;
min-width: 120px;
}
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.history-list > div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.5em;
}
.history-list > div > button > img {
width: 2em;
transform: rotate(180deg);
}
.timetable-status-indicator {
&[data-fulfilled='true'] {
color: lightgreen;
}
&[data-terminated='false'] {
color: lightblue;
}
&[data-terminated='true'][data-fulfilled='false'] {
color: lightcoral;
}
}
</style>
@@ -1,7 +1,7 @@
<template>
<div class="general-status">
<span
:class="computedScheduledTrain.stopStatus"
:class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription"
>
{{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../typings/common';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
sceneryTimetableRow: {
type: Object as PropType<SceneryTimetableRow>,
required: true
}
},
computed: {
computedScheduledTrain(): ScheduledTrainComp {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } =
this.scheduledTrain;
computedScheduledTrain() {
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '',
stopStatusIndicator = '';
switch (stopStatus) {
switch (status) {
case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: '';
break;
@@ -85,7 +81,7 @@ export default defineComponent({
break;
}
return {
...this.scheduledTrain,
...this.sceneryTimetableRow,
stopStatusDescription,
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">
import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption {
id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: {
'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) {
e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id;
// this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true;
this.$emit('update:optionValue', true);
this.filterStore.inputs.options
.filter((option) => {
return option.section == this.option.section && option.id != this.option.id;
})
.forEach((option) => {
option.value = !this.option.value;
});
// this.filterStore.inputs.options
// .filter((option) => {
// return option.section == this.option.section && option.id != this.option.id;
// })
// .forEach((option) => {
// option.value = !this.option.value;
// });
}
}
});
+179 -112
View File
@@ -4,7 +4,7 @@
<button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
<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>
<label for="scenery-search">
@@ -33,29 +33,45 @@
<div class="card_title flex">{{ $t('filters.title') }}</div>
<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">
<div
class="option-section"
v-for="section in filterStore.inputs.optionSections"
:key="section"
v-for="(sectionFilters, sectionKey) in filtersSections"
:key="sectionKey"
>
<h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3>
<hr />
<div class="section-inputs">
<FilterOption
v-for="(option, i) in filterStore.inputs.options.filter(
(o) => o.section == section
)"
v-model:optionValue="option.value"
:option="option"
:key="i"
/>
<div class="section-filters">
<label
v-for="filterKey in sectionFilters"
@click="() => (filters[filterKey] = !filters[filterKey])"
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
:for="filterKey"
>
<input
:checked="filters[filterKey]"
v-model="filters[filterKey]"
type="checkbox"
:class="sectionKey"
:name="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div>
</div>
</section>
@@ -68,7 +84,7 @@
<span>{{
minimumHours == 0
? $t('filters.now')
: minimumHours < 8
: minimumHours < 7
? minimumHours + $t('filters.hour')
: $t('filters.no-limit')
}}</span>
@@ -76,21 +92,21 @@
</span>
</section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search">
<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">
<input
type="text"
id="author"
list="authors"
name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/>
@@ -100,19 +116,18 @@
</section>
<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
class="slider-input"
type="range"
:name="slider.name"
:name="slider.id"
:id="slider.id"
:min="slider.minRange"
:max="slider.maxRange"
:step="slider.step"
v-model="slider.value"
@change="handleInput"
v-model="filters[slider.id]"
/>
<span class="slider-value">{{ slider.value }}</span>
<span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }}
</div>
@@ -133,9 +148,9 @@
<button
class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
>
[R] {{ $t('filters.reset') }}
</button>
@@ -151,22 +166,36 @@
import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue';
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({
components: { FilterOption },
mixins: [keyMixin, routerMixin],
data: () => ({
saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '',
filtersSections,
initSliders,
minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' },
@@ -180,22 +209,33 @@ export default defineComponent({
setup() {
const isVisible = inject('isFilterCardVisible');
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 {
isVisible,
store,
filterStore
filters,
changedFilters
};
},
mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
}
this.currentRegion = this.store.region;
@@ -214,7 +254,7 @@ export default defineComponent({
return true;
},
authors() {
authorsHint() {
return this.store.stationList
.reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => {
@@ -258,61 +298,63 @@ export default defineComponent({
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() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue);
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);
this.filters['authors'] = this.authors;
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
},
subHour() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
saveFilters() {
this.saveOptions = !this.saveOptions;
if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY);
StorageManager.unregisterStorage(STORAGE_KEY);
return;
}
StorageManager.registerStorage(this.STORAGE_KEY);
StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) =>
StorageManager.setBooleanValue(option.name, !option.value)
);
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setValue(filterKey, this.filters[filterKey]);
});
},
resetFilters() {
this.authorsInputValue = '';
// Reset local model values
this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.authors = '';
// 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() {
@@ -330,6 +372,7 @@ export default defineComponent({
@import '../../styles/responsive';
@import '../../styles/card';
@import '../../styles/animations';
@import '../../styles/variables';
h3.section-header {
text-align: center;
@@ -346,6 +389,15 @@ h3.section-header {
padding: 0.5em;
}
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls {
display: flex;
gap: 0.5em;
@@ -374,28 +426,6 @@ h3.section-header {
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 {
display: flex;
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 {
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 {
display: flex;
align-items: center;
gap: 0.25em;
margin-bottom: 1em;
&-value {
color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em;
}
&-content {
flex-grow: 2;
}
&-input {
-webkit-appearance: none;
appearance: none;
@@ -512,7 +571,6 @@ h3.section-header {
outline: none;
min-width: 25%;
max-width: 120px;
&:focus-visible ~ * {
color: gold;
@@ -587,5 +645,14 @@ h3.section-header {
.card_controls > button.card-button > p {
display: none;
}
.slider {
flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
}
}
</style>
+12 -20
View File
@@ -21,8 +21,8 @@
<div>
&bull;
{{ $t('station-stats.med-timetable-count') }}
<b>{{ medTimetableCount }}</b>
{{ $t('station-stats.avg-timetable-count') }}
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</div>
<div>
@@ -89,27 +89,19 @@ export default defineComponent({
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
},
medTimetableCount() {
const scheduledTrainsArr = this.mainStore.activeSceneryList
.reduce<number[]>((acc, sc) => {
if (sc.region != this.mainStore.region.id) return acc;
avgTimetableCount() {
const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
return sc.region == this.mainStore.region.id;
});
acc.push(sc.scheduledTrainCount.all);
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
return acc;
}, [])
.sort((a, b) => Math.sign(a - b));
if (regionSceneries.length == 0) return 0;
if (scheduledTrainsArr.length == 0) return 0;
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)];
return timetableCountSum / regionSceneries.length;
},
trackCount() {
+310 -301
View File
@@ -1,366 +1,371 @@
<template>
<section class="station_table">
<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">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="station in displayedStations"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
<table v-else-if="filteredStationList.length > 0">
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</td>
</span>
</th>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationModal"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<td class="station-dispatcher-exp">
<tbody>
<tr
v-for="station in filteredStationList"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</td>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationCard"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</td>
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<td class="station-dispatcher-exp">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<td class="station-info">
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/>
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
</div>
</td>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
"
/>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
</div>
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
<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>
</section>
</template>
<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 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 { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
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({
emits: ['toggleDonationModal'],
emits: ['toggleDonationCard'],
components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin],
data: () => ({
headIconsIds,
headIds,
lastSelectedStationName: ''
lastSelectedStationName: '',
getChangedFilters
}),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
}
},
setup() {
const mainStore = useMainStore();
const apiStore = useApiStore();
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 {
Status: Status.Data,
stationFiltersStore,
mainStore,
apiStore,
tooltipStore
tooltipStore,
filters,
filteredStationList,
activeSorter
};
},
methods: {
setScenery(name: string) {
const station = this.displayedStations.find((station) => station.name === name);
const station = this.filteredStationList.find((station) => station.name === name);
if (!station) return;
@@ -376,8 +381,8 @@ export default defineComponent({
});
},
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
openDonationCard(e: Event) {
this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide();
},
@@ -388,10 +393,14 @@ export default defineComponent({
window.open(url, '_blank');
},
changeSorter(headerName: HeadIdsTypes) {
changeSorter(headerName: HeadIdsType) {
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;
}
}
});
@@ -406,6 +415,7 @@ $rowCol: #424242;
.station_table {
height: 80vh;
max-height: 2000px;
min-height: 700px;
overflow: auto;
font-weight: 500;
@@ -413,11 +423,10 @@ $rowCol: #424242;
.no-stations {
text-align: center;
font-size: 1.5em;
font-size: 1.25em;
padding: 1em;
background: #1a1a1a;
line-height: 1.5em;
}
table {
+25 -52
View File
@@ -5,56 +5,29 @@ export interface FilterOption {
defaultValue: boolean;
}
export interface Filter {
[key: string]: boolean | number | string;
default: boolean;
notDefault: boolean;
real: boolean;
fictional: boolean;
SPK: boolean;
SCS: boolean;
SPE: boolean;
SUP: boolean;
noSUP: boolean;
ASDEK: boolean;
noASDEK: boolean;
ręczne: boolean;
'ręczne+SPK': boolean;
'ręczne+SCS': boolean;
mechaniczne: boolean;
'mechaniczne+SPK': boolean;
'mechaniczne+SCS': boolean;
SBL: boolean;
PBL: boolean;
współczesna: boolean;
kształtowa: boolean;
historyczna: boolean;
mieszana: boolean;
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;
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 HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
export interface ActiveSorter {
headerName: HeadIdsType;
dir: number;
}
+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 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview">
<div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div>
</template>
@@ -35,10 +35,7 @@ export default defineComponent({
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) {
+2 -2
View File
@@ -10,7 +10,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common';
import { Train } from '../../typings/common';
export default defineComponent({
data() {
@@ -23,7 +23,7 @@ export default defineComponent({
trains() {
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);
}
}
@@ -13,13 +13,20 @@
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
:src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
/>
<div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }}
{{ vehicleName.replace(/_/g, ' ') }}
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
</div>
<div class="vehicle-props" v-if="vehicleData">
{{ vehicleData.group.speed }}km/h &bull; {{ vehicleData.group.length }}m &bull;
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
</div>
</div>
</template>
@@ -27,11 +34,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading'
};
},
@@ -56,6 +65,34 @@ export default defineComponent({
(e.target as HTMLElement).style.display = 'none';
}
},
computed: {
vehicleName() {
return this.tooltipStore.content.split(':')[0];
},
vehicleData() {
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
},
vehicleCargo() {
return this.vehicleData?.group.cargoTypes?.find(
(c) => c.id == this.tooltipStore.content.split(':')[1]
);
}
// vehicleProps() {
// const vehicleDataArray = this.apiStore.vehiclesData?.vehicleList.find(
// ([name]) => name === this.vehicleName
// );
// if (!vehicleDataArray) return null;
// return (
// this.apiStore.vehiclesData!.vehicleProps.find((v) => v.type == vehicleDataArray[1]) ?? null
// );
// }
}
});
</script>
@@ -85,10 +122,13 @@ img {
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
.vehicle-props {
color: #ccc;
}
.error-placeholder {
height: 176px;
}
+33 -21
View File
@@ -1,5 +1,8 @@
<template>
<span class="stop-label" :data-sbl="stop.isSBL">
<span
class="stop-label"
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po.') && !stop.duration)"
>
<span class="name" v-html="stop.nameHtml"></span>
<span
@@ -9,12 +12,13 @@
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
? 'delayed'
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }}
@@ -29,17 +33,17 @@
<span
v-if="
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
(stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
"
class="date stop"
:data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status="
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
>
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }}
{{
stop.duration == 0 && stop.departureDelay > 0
? stop.departureDelay - stop.arrivalDelay
: stop.duration
}}
{{ stop.type == '' ? 'pt' : stop.type }}
</span>
@@ -53,13 +57,16 @@
stop.departureDelay > 0 && stop.status == 'confirmed'
? 'delayed'
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
o.
<span
v-if="stop.departureDelay != 0 && (stop.status == 'confirmed' || stop.status == 'stopped')"
>
<s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }}
@@ -96,14 +103,14 @@ $delayedClr: salmon;
$dateClr: #525151;
$stopExchangeClr: #db8e29;
$stopDefaultClr: #252525;
$stopNameClr: #22a8d1;
$stopNameClr: #303030;
.stop-label {
display: flex;
flex-wrap: wrap;
align-items: center;
&[data-sbl='true'] {
&[data-minor='true'] {
.date {
display: none;
}
@@ -117,6 +124,7 @@ $stopNameClr: #22a8d1;
.name {
background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em;
display: flex;
@@ -130,6 +138,10 @@ $stopNameClr: #22a8d1;
.date {
background: $dateClr;
padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
}
.stop {
@@ -150,7 +162,7 @@ $stopNameClr: #22a8d1;
.departure {
&[data-status='delayed'] {
s {
color: #999;
color: #ccc;
}
span {
@@ -160,7 +172,7 @@ $stopNameClr: #22a8d1;
&[data-status='preponed'] {
s {
color: #999;
color: #ccc;
}
span {
+24
View File
@@ -131,6 +131,18 @@
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h
<span v-if="stockSpeedLimit != Infinity">
&bull;
<em
class="text--grayed"
style="text-decoration: underline dotted"
tabindex="0"
:data-tooltip="$t('trains.vmax-tooltip')"
>
{{ stockSpeedLimit }} km/h
</em>
</span>
</div>
</div>
@@ -191,6 +203,18 @@ export default defineComponent({
};
},
computed: {
stockSpeedLimit() {
return this.train.stockList.reduce((acc, stockName) => {
const vehicleSpeed =
this.apiStore.vehiclesData?.find((v) => v.name == stockName.split(':')[0])?.group.speed ??
300;
return Math.min(vehicleSpeed, acc);
}, 300);
}
},
methods: {
navigateToJournal() {
this.$router.push({
+12 -23
View File
@@ -21,7 +21,7 @@ export default defineComponent({
computed: {
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) {
this.$nextTick(() => {
if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement;
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>
@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 {
position: fixed;
@@ -61,12 +54,14 @@ export default defineComponent({
left: 0;
width: 100%;
height: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
align-items: flex-start;
text-align: left;
}
@@ -87,10 +82,10 @@ export default defineComponent({
position: relative;
overflow-y: scroll;
margin-top: 1em;
width: 95vw;
max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
@@ -105,10 +100,4 @@ export default defineComponent({
}
}
}
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style>
+85 -42
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed"
x
>
<span class="stop_info">
<span class="distance">
@@ -48,26 +47,46 @@
<span
v-if="
stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine &&
!/sbl/gi.test(stop.departureLine)
scheduleStops[i + 1] != undefined &&
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
"
>
{{ stop.departureLine }}
</span>
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
<div>{{ stop.departureLine }}</div>
<div
class="scenery-change-name"
v-if="
i < scheduleStops.length - 1 &&
stop.sceneryName != scheduleStops[i + 1].sceneryName
"
>
{{ scheduleStops[i + 1].sceneryName }}
<div class="scenery-route">
<span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
| {{ stop.departureLineInfo.routeSpeed }}
<span v-if="stop.departureLineInfo.isElectric">⚡</span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span>
</div>
<div>
{{ scheduleStops[i + 1].arrivalLine }}
<div
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
class="scenery-change-name"
>
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
<span v-if="stop.departureLineInfo?.routeTracks == 1"> &UpDownArrow;</span>
<span v-else> &UpArrowDownArrow;</span>
</div>
<div class="scenery-route">
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
<span v-if="scheduleStops[i + 1].arrivalLineInfo!.isElectric">⚡</span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span>
</div>
</span>
</div>
@@ -85,7 +104,7 @@ import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common';
import { StationRoutesInfo, Train } from '../../typings/common';
export interface TrainScheduleStop {
nameHtml: string;
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
isSBL: boolean;
sceneryName: string | null;
sceneryHash: string;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null;
}
@@ -146,13 +169,25 @@ export default defineComponent({
return (
this.train.timetableData?.followingStops.map((stop, i, arr) => {
if (
const isExternal =
i > 0 &&
stop.arrivalLine &&
stop.arrivalLine != arr[i - 1].departureLine &&
!/sbl/gi.test(stop.arrivalLine)
)
currentSceneryIndex++;
stop.arrivalLine != null &&
(stop.arrivalLine != arr[i - 1].departureLine ||
(stop.arrivalLine == arr[i - 1].departureLine &&
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
if (isExternal) currentSceneryIndex++;
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.arrivalLine
);
const departureLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.departureLine
);
return {
nameHtml: stop.stopName,
@@ -174,14 +209,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType,
distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '',
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
sceneryName,
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
};
}) ?? []
@@ -483,22 +522,26 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
}
}
.bottom-line-info {
.scenery-change-name {
position: relative;
margin: 0.25em 0;
.scenery-route {
img {
vertical-align: middle;
}
}
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
.scenery-change-name {
position: relative;
margin: 0.25em 0;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
}
</style>
+6 -17
View File
@@ -8,17 +8,18 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<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>
<transition-group name="list-anim" tag="ul">
<li
class="train-row"
v-for="train in trains"
:key="train.trainId"
:key="train.id"
tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
@click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
>
<TrainInfo :train="train" :extended="false" />
</li>
@@ -76,17 +77,6 @@ export default defineComponent({
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>
@@ -108,8 +98,7 @@ export default defineComponent({
text-align: center;
padding: 1em 0;
font-size: 1.5em;
font-size: 1.25em;
background: #1a1a1a;
}
File diff suppressed because it is too large Load Diff
-365
View File
@@ -1,369 +1,4 @@
{
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"station-type",
"access",
"control",
"blockades",
"signals",
"addons"
],
"options": [
{
"id": "real",
"name": "real",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "fictional",
"name": "fictional",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "non-public",
"name": "nonPublic",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "unavailable",
"name": "unavailable",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "abandoned",
"name": "abandoned",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "junction",
"name": "junction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "nonJunction",
"name": "nonJunction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "SPK",
"name": "SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS",
"name": "SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPE",
"name": "SPE",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-M",
"name": "mechaniczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "mechanical",
"name": "mechaniczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SUP",
"name": "SUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "ASDEK",
"name": "ASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noASDEK",
"name": "noASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "SBL",
"name": "SBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "modern",
"name": "współczesna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "semaphores",
"name": "kształtowa",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "mixed",
"name": "mieszana",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "historical",
"name": "historyczna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "free",
"name": "free",
"section": "status",
"value": false,
"defaultValue": false
},
{
"id": "occupied",
"name": "occupied",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "endingStatus",
"name": "endingStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "afkStatus",
"name": "afkStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "noSpaceStatus",
"name": "noSpaceStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "unavailableStatus",
"name": "unavailableStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
}
],
"sliders": [
{
"id": "min-lvl",
"name": "minLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "max-lvl",
"name": "maxLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 20,
"defaultValue": 20
},
{
"id": "min-vmax",
"name": "minVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 0,
"defaultValue": 0
},
{
"id": "max-vmax",
"name": "maxVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 200,
"defaultValue": 200
},
{
"id": "routes-1t-cat",
"name": "minOneWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-1t-other",
"name": "minOneWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-cat",
"name": "minTwoWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-other",
"name": "minTwoWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
}
],
"modes": [
{
"id": "include-selected",
"name": "include-selected",
"section": "mode",
"value": true,
"defaultValue": true
},
{
"id": "save",
"name": "save",
"section": "mode",
"value": true,
"defaultValue": true
}
],
"regions": [
{
"id": "eu",
+34 -31
View File
@@ -112,8 +112,8 @@
"filters": "FILTERS",
"donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"search-button": "SEARCH",
"reset-button": "RESET",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
@@ -125,7 +125,9 @@
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-author": "Timetable author name",
"search-issuedFrom": "Origin scenery name",
"search-issuedFrom": "Issuing scenery name",
"search-via": "Via scenery name",
"search-terminatingAt": "Terminating scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
@@ -174,9 +176,9 @@
"sections": {
"quick": "QUICK FILTERS",
"station-type": "STATION TYPE",
"stationType": "STATION TYPE",
"reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY",
"packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY",
"control": "CONTROLS",
"signals": "SIGNALLING",
@@ -187,6 +189,9 @@
"spawns": "OPEN SPAWNS"
},
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
@@ -197,11 +202,11 @@
"title": "STATION FILTERS",
"default": "IN-GAME",
"not-default": "ADDITIONAL",
"notDefault": "ADDITIONAL",
"real": "REAL",
"fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC",
"nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED",
"SPK": "SPK",
@@ -211,7 +216,6 @@
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
@@ -238,14 +242,14 @@
"nonJunction": "OTHER",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
"minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"minVmax": "MIN. SCENERY ROUTE SPEED",
"maxVmax": "MAX. SCENERY ROUTE SPEED",
"minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
},
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
@@ -298,12 +302,13 @@
"single-track-routes-other": "Not electrified single-track routes count: "
},
"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": {
"u-factor": "U-factor",
"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:",
"double-track-count": "Double track routes:",
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
@@ -326,6 +331,9 @@
"current-signal": "at signal",
"current-track": "on track",
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
"we4a-tooltip": "Non-electrified track",
"delayed": "Delayed: ",
"preponed": "Ahead of schedule: ",
"on-time": "On time",
@@ -488,21 +496,16 @@
"option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown",
"timetable-via": "ALL TIMETABLES",
"timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID",
"timetables-history-number": "Number",
"timetables-history-route": "Route",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"timetable-issued-date": "Issued",
"timetable-issued-by": " by:",
"timetable-issued-for": " for driver:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"dispatcher-rate": "Rate:",
"dispatcher-status-changes": "Status changes:",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!",
+31 -27
View File
@@ -122,6 +122,8 @@
"search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa",
"search-via": "Przez scenerię",
"search-terminatingAt": "Sceneria końcowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
@@ -171,9 +173,9 @@
"sections": {
"quick": "SZYBKIE FILTRY",
"station-type": "RODZAJ STACJI",
"stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE",
"packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI",
@@ -184,6 +186,9 @@
"spawns": "OTWARTE SPAWNY"
},
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
@@ -194,11 +199,11 @@
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"notDefault": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA",
"nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA",
"SPK": "SPK",
@@ -234,14 +239,14 @@
"nonJunction": "INNE",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
"minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
},
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
@@ -291,12 +296,13 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
},
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
"scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
},
"station-stats": {
"u-factor": "Współczynnik Ugla",
"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:",
"double-track-count": "Szlaki dwutorowe:",
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
@@ -311,6 +317,9 @@
"current-signal": "przy semaforze",
"current-track": "na szlaku",
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
"we4a-tooltip": "Szlak niezelektryfikowany",
"delayed": "Opóźniony: ",
"preponed": "Przed czasem: ",
"on-time": "Planowo",
@@ -470,21 +479,16 @@
"option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany",
"timetable-via": "WSZYSTKIE RJ",
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID",
"timetables-history-number": "Numer",
"timetables-history-route": "Trasa",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"timetable-issued-date": "Wystawiony",
"timetable-issued-by": " przez:",
"timetable-issued-for": " dla maszynisty:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"dispatcher-rate": "Ocena:",
"dispatcher-status-changes": "Zmiany statusów:",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!",
+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);
}
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false;
}
+8 -8
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({
data() {
@@ -11,20 +12,19 @@ export default defineComponent({
},
methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = train.modalId;
if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target;
},
closeModal() {
this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
}
}
});
+1 -1
View File
@@ -58,7 +58,7 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` };
return { el: `.app_main`, top: -15 };
if (savedPosition) return savedPosition;
},
-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;
};
+52 -16
View File
@@ -4,20 +4,17 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', {
state: () => ({
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading
sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
},
activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[],
@@ -54,14 +51,31 @@ export const useApiStore = defineStore('apiStore', {
// Static data
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
// Ponowne pobieranie danych po ServiceWorkerze
setTimeout(() => {
this.fetchStationsGeneralInfo();
}, Math.floor(Math.random() * 500) + 1000);
this.fetchVehiclesInfo();
},
async fetchActiveData() {
if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
import('../../tests/data/getActiveData.json').then((data) => {
console.warn('activeData: mocking mode');
this.activeData = data.default as API.ActiveData.Response;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded;
});
return;
}
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data;
@@ -84,17 +98,39 @@ export const useApiStore = defineStore('apiStore', {
},
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
try {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
} catch (error) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o sceneriach:', error);
}
},
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
async fetchVehiclesInfo() {
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
// import('../../tests/data/vehicles.json').then((data) => {
// console.warn('vehicles.json: mocking mode');
// this.vehiclesData = data.default;
// this.dataStatuses.vehicles = Status.Data.Loaded;
// });
// return;
// }
try {
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
this.vehiclesData = response.data;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) {
this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
}
}
}
});
+107 -36
View File
@@ -1,9 +1,9 @@
import { defineStore } from 'pinia';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { parseSpawns } from './utils';
import {
ActiveScenery,
ScheduledTrain,
CheckpointTrain,
Station,
StationRoutes,
Status,
@@ -12,6 +12,9 @@ import {
import { useApiStore } from './apiStore';
import { MainStoreState } from './typings';
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', {
state: () =>
({
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
trainList(): Train[] {
const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online)
.map((train) => {
@@ -53,8 +59,9 @@ export const useMainStore = defineStore('mainStore', {
sceneryHash
) ?? [];
return {
trainId: train.driverName + train.trainNo.toString(),
const trainObj = {
id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo,
mass: train.mass,
@@ -89,10 +96,65 @@ export const useMainStore = defineStore('mainStore', {
followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
sceneries: timetable.sceneries,
sceneryNames: sceneryNames.reverse()
sceneryNames: sceneryNames.reverse(),
timetablePath: timetable.path.split(';').map((pathElementString) => {
const [arrival, station, departure] = pathElementString.split(',');
return {
arrivalRouteExt: arrival,
departureRouteExt: departure,
stationName: station.split(' ').slice(0, -1).join(' '),
stationHash: station.split(' ').slice(-1).join(' ')
};
})
}
: undefined
} as Train;
// 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
if (trainObj.timetableData) {
let currentSceneryIndex = 0;
const timetablePath = trainObj.timetableData.timetablePath;
trainObj.timetableData.followingStops.forEach((stop, i) => {
if (/strong|podg|pe/.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = {
train: trainObj,
checkpointStop: stop,
previousSceneryElement:
currentSceneryIndex > 0 ? timetablePath[currentSceneryIndex - 1] : null,
nextSceneryElement:
currentSceneryIndex < timetablePath.length - 1
? timetablePath[currentSceneryIndex + 1]
: null,
timetablePathElement: timetablePath[currentSceneryIndex]
};
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
if (timetablePath[currentSceneryIndex].departureRouteExt == stop.departureLine)
currentSceneryIndex++;
});
}
return trainObj;
});
},
@@ -131,13 +193,14 @@ export const useMainStore = defineStore('mainStore', {
dispatcherId: -1,
dispatcherExp: -1,
dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1,
isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -177,8 +240,9 @@ export const useMainStore = defineStore('mainStore', {
isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -196,37 +260,41 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains(
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
let checkpointsSet: Set<string> = new Set();
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery.name
);
// Add checkpoints to active scenery data
checkpointsSet.add(scenery.name.toLowerCase());
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
station?.generalInfo?.checkpoints.forEach((cpName) => {
checkpointsSet.add(cpName.toLowerCase());
});
scenery.scheduledTrains = scheduledTrains;
scenery.stationTrains = stationTrains;
const checkpoints = Array.from(checkpointsSet);
scenery.scheduledTrainCount = {
all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
};
scenery.stationTrains =
sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
const uniqueTrainIds: string[] = [];
checkpoints.forEach((cp) => {
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
if (scenery.name != timetablePathElement.stationName) return;
scenery.scheduledTrains.push({ train, checkpointStop, timetablePathElement, ...v });
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
scenery.scheduledTrainCount.all += 1;
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
else scenery.scheduledTrainCount.unconfirmed++;
uniqueTrainIds.push(train.id);
});
});
}
return allActiveSceneries;
@@ -281,7 +349,10 @@ export const useMainStore = defineStore('mainStore', {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
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 { Availability, StationRoutesInfo, Status } from '../typings/common';
import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export interface MainStoreState {
region: { id: string; value: string; name: string };
+1 -183
View File
@@ -1,13 +1,4 @@
import {
TrainStop,
StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2;
@@ -57,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
export function getTimestamp(date: string | null): number {
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 -1
View File
@@ -4,7 +4,7 @@
.list_wrapper {
overflow-y: auto;
height: 90vh;
min-height: 550px;
min-height: 650px;
margin-top: 0.5em;
position: relative;
+31 -19
View File
@@ -228,6 +228,10 @@ a.a-button {
background-color: #3c3c3c;
}
&:hover {
background-color: #555;
}
}
&.btn--image {
@@ -283,6 +287,27 @@ a.a-button {
}
}
// Basic tooltip
[data-tooltip] {
cursor: help;
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(0, -50%);
content: attr(data-tooltip);
color: white;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.5em;
max-width: 300px;
z-index: 100;
}
@include smallScreen {
::-webkit-scrollbar {
width: 0.5em;
@@ -296,24 +321,11 @@ a.a-button {
background-color: #777;
}
}
}
// Basic tooltip
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(10px, -50%);
content: attr(data-tooltip);
color: white;
background-color: #171717;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.25em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: help;
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
transform: translate(-50%, 2em);
left: 50%;
width: 100%;
}
}
+3 -7
View File
@@ -1,11 +1,7 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table {
width: 100%;
table-layout: fixed;
min-width: 900px;
border-collapse: collapse;
thead {
@@ -25,7 +21,7 @@ table.scenery-history-table {
td {
padding: 0.75em;
border-bottom: solid 5px #111;
border-bottom: solid 5px #181818;
}
}
+11 -6
View File
@@ -1,4 +1,4 @@
import { Status } from './common';
import { Status, VehicleData } from './common';
export enum APIDataStatus {
OK = 'OK',
@@ -19,6 +19,7 @@ export namespace API {
apiStatuses?: APIStatuses;
}
}
export namespace DispatcherHistory {
export type Response = Data[];
@@ -38,6 +39,7 @@ export namespace API {
stationName: string;
timestampFrom: number;
timestampTo?: number;
statusHistory: string[];
}
}
@@ -161,7 +163,6 @@ export namespace API {
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
@@ -194,6 +195,8 @@ export namespace API {
TWR: boolean;
SKR: boolean;
sceneries: string[];
path: string;
}
}
@@ -248,16 +251,14 @@ export namespace API {
hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
routeSceneries?: string;
checkpointArrivals?: string[];
checkpointDepartures?: string[];
checkpointArrivalsScheduled?: string[];
checkpointDeparturesScheduled?: string[];
checkpointStopTypes?: string[];
visitedSceneries?: string[];
path: string;
}
export type Response = Data[];
@@ -317,6 +318,10 @@ export namespace API {
export namespace Donators {
export type Response = string[];
}
export namespace Vehicles {
export type Response = VehicleData[];
}
}
export namespace GithubAPI {
+71 -74
View File
@@ -39,9 +39,16 @@ export interface RegionCounters {
timetablesCount: number;
}
export interface TimetablePathElement {
arrivalRouteExt?: string;
departureRouteExt?: string;
stationName: string;
stationHash: string;
}
export interface Train {
id: string;
trainId: string;
modalId: string;
mass: number;
length: number;
speed: number;
@@ -73,41 +80,37 @@ export interface Train {
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
timetablePath: TimetablePathElement[];
};
}
export interface Station {
name: string;
generalInfo?: {
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[];
};
generalInfo?: StationGeneralInfo;
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 {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
@@ -148,8 +151,8 @@ export interface ActiveScenery {
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
stationTrains: Train[];
scheduledTrains: CheckpointTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
@@ -164,49 +167,6 @@ export interface ScenerySpawn {
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 {
stopName: string;
stopNameRAW: string;
@@ -223,13 +183,50 @@ export interface TrainStop {
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
confirmed: number;
stopped: number;
stopTime: number | null;
}
export interface CheckpointTrain {
checkpointStop: TrainStop;
train: Train;
timetablePathElement: TimetablePathElement;
previousSceneryElement: TimetablePathElement | null;
nextSceneryElement: TimetablePathElement | null;
}
// Vehicles Data
export interface VehicleData {
id: number;
name: string;
type: string;
cabinName: string | null;
restrictions: Record<string, any> | null;
vehicleGroupsId: number;
group: VehiclesGroup;
}
export interface VehiclesGroup {
id: number;
name: string;
speed: number;
length: number;
weight: number;
cargoTypes: VehicleCargo[] | null;
locoProps: {
coldStart: boolean;
doubleManned: boolean;
} | null;
}
export interface VehicleCargo {
id: string;
weight: number;
}
+21 -5
View File
@@ -126,6 +126,8 @@ interface TimetablesQueryParams {
dateTo?: string;
issuedFrom?: string;
terminatingAt?: string;
via?: string;
countFrom?: number;
countLimit?: number;
@@ -206,6 +208,8 @@ export default defineComponent({
'search-driver': '',
'search-dispatcher': '',
'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-date': ''
} as Journal.TimetableSearchType);
@@ -299,11 +303,17 @@ export default defineComponent({
},
setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? '';
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? '';
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
});
// this.searchersValues['search-date'] = options['search-date'] ?? '';
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
// this.searchersValues['search-train'] = options['search-train'] ?? '';
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
// this.searchersValues['search-via'] = options['search-via'] ?? '';
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
@@ -347,6 +357,8 @@ export default defineComponent({
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
let dateTo: string | undefined = undefined;
@@ -418,6 +430,10 @@ export default defineComponent({
queryParams['authorName'] = authorName;
queryParams['dateFrom'] = dateFrom;
queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom;
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
+16 -30
View File
@@ -22,8 +22,8 @@
v-for="(viewMode, i) in viewModes"
:key="i"
class="btn btn--option"
:class="{ checked: currentMode == viewMode.component }"
@click="setViewMode(viewMode.component)"
:data-checked="currentMode == viewMode.component"
>
{{ $t(viewMode.id) }}
</button>
@@ -121,10 +121,6 @@ export default defineComponent({
Status: Status.Data
}),
// activated() {
// this.loadSelectedCheckpoint();
// },
setup() {
const route = useRoute();
@@ -200,7 +196,7 @@ button.back-btn {
display: inline-block;
font-size: 1.5em;
font-size: 1.25em;
button {
margin: 1em auto;
@@ -215,11 +211,10 @@ button.back-btn {
position: relative;
width: 100%;
max-width: var(--max-container-width);
min-height: 100vh;
width: 100%;
margin: 1rem 0;
padding: 1rem 0;
text-align: center;
&[data-timetable-only='true'] {
@@ -228,30 +223,27 @@ button.back-btn {
}
}
.scenery-left {
.scenery-left,
.scenery-right {
position: relative;
overflow: auto;
background-color: #181818;
padding: 1em 0.5em;
height: 95vh;
min-height: 750px;
max-height: 1000px;
overflow: auto;
height: calc(100vh - 0.5em);
min-height: 800px;
max-height: 2000px;
}
.scenery-left {
display: flex;
flex-direction: column;
}
.scenery-right {
background: #181818;
padding: 1em 0.5em;
height: 95vh;
min-height: 750px;
max-height: 1000px;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-rows: auto 1fr;
gap: 1em;
}
@@ -261,18 +253,12 @@ button.back-btn {
.info-actions {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 0.75em;
.btn {
button {
padding: 0.5em;
box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] {
color: var(--clr-primary);
}
}
}
+27 -14
View File
@@ -11,16 +11,16 @@
<button
class="btn-donation btn--image"
ref="btn"
@click="isDonationModalOpen = true"
@focus="isDonationModalOpen = false"
@click="isDonationCardOpen = true"
@focus="isDonationCardOpen = false"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
</div>
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
<StationTable @toggleDonationModal="toggleDonationModal" />
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggle-donation-card="toggleDonationCard" />
<StationStats />
</div>
</section>
@@ -30,34 +30,47 @@
import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
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 { 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({
components: {
StationTable,
StationFilterCard,
StationStats,
DonationModal
DonationCard
},
data: () => ({
filterCardOpen: false,
isDonationModalOpen: false,
isDonationCardOpen: false,
filterStore: useStationFiltersStore(),
store: useMainStore()
mainStore: useMainStore()
}),
mounted() {
this.filterStore.setupFilters();
setup() {
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: {
toggleDonationModal(value: boolean) {
this.isDonationModalOpen = value;
toggleDonationCard(value: boolean) {
this.isDonationCardOpen = value;
}
}
});
+1 -1
View File
@@ -109,7 +109,7 @@ export default defineComponent({
this.$nextTick(() => {
if (this.trainId) {
this.selectModalTrain(this.trainId);
this.selectModalTrainById(this.trainId);
}
});
}
+4 -2
View File
@@ -6,8 +6,10 @@ declare module '*.vue' {
export default component;
}
interface ImportMetaEnv {
readonly VITE_APP_API_DEV: string;
readonly VITE_APP_WS_DEV: string;
readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_VEHICLES_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_ACTIVE_DATA_MODE: 'production' | 'mocking' | 'development';
readonly VITE_UPDATE_TEST: 'test' | 'production';
}
interface ImportMeta {
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
["kowbojYT","matseb","peterminecraft333","MIlanSVK_SimRailCZ","kierownik_z_ulicy","luk31as","pppatryk123","Kryszakos","MilyPan","paweld","Isitkiwi","Krisoy007","zeswaq","robcioRK","Ugulele","Spanky","KapitanKoza","Kuba6396","BravuraLion","trichlor","jasieleczeq","trannelgamer","tommy001","Waffel","krytaqu","NadrazakHonza","zordem","Ludolog"]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -12
View File
@@ -7,6 +7,10 @@ export default defineConfig({
port: 5001,
open: true
},
preview: {
port: 4001,
open: true
},
publicDir: 'public',
plugins: [
vue(),
@@ -20,25 +24,16 @@ export default defineConfig({
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/getSceneries/i,
urlPattern:
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'spythere-sceneries-cache',
cacheName: 'stacjownik-api-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/static.spythere.eu\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'spythere-static-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
},
devOptions: {