Compare commits

...

64 Commits

Author SHA1 Message Date
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
Spythere de5c57181a Merge pull request #89 from Spythere/development
v1.24.1
2024-05-16 23:43:39 +02:00
Spythere d91d4cc6a8 fix: station stats spawn count regions 2024-05-16 23:42:35 +02:00
Spythere 9a5fd4d670 chore: version bump 2024-05-16 23:29:56 +02:00
Spythere 4202a55673 chore: updated pwa strategies 2024-05-16 21:36:16 +02:00
Spythere 5181e8f4af chore: fix journal refresh date visibility 2024-05-16 20:06:02 +02:00
Spythere e117f62fcb chore: added station filters (scenery types); pwa adjustments 2024-05-16 19:59:43 +02:00
Spythere e0036bf969 chore: filters & stats fixes 2024-05-15 18:40:42 +02:00
81 changed files with 9067 additions and 12787 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.24.0", "version": "1.25.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "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

+11 -10
View File
@@ -1,8 +1,8 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal <UpdateCard
:update-modal-open="isUpdateModalOpen" :is-update-card-open="isUpdateCardOpen"
@toggle-modal="() => (isUpdateModalOpen = false)" @toggle-card="() => (isUpdateCardOpen = false)"
/> />
<Tooltip /> <Tooltip />
@@ -27,7 +27,7 @@
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | {{ new Date().getUTCFullYear() }} |
<button class="btn--text" @click="() => (isUpdateModalOpen = true)"> <button class="btn--text" @click="() => (isUpdateCardOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }} v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button> </button>
@@ -56,7 +56,7 @@ import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue'; import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue'; import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateModal from './components/App/UpdateModal.vue'; import UpdateCard from './components/App/UpdateCard.vue';
import StorageManager from './managers/storageManager'; import StorageManager from './managers/storageManager';
@@ -68,7 +68,7 @@ export default defineComponent({
StatusIndicator, StatusIndicator,
AppHeader, AppHeader,
TrainModal, TrainModal,
UpdateModal, UpdateCard,
Tooltip Tooltip
}, },
@@ -78,7 +78,7 @@ export default defineComponent({
apiStore: useApiStore(), apiStore: useApiStore(),
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
isUpdateModalOpen: false, isUpdateCardOpen: false,
currentLang: 'pl', currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
@@ -130,7 +130,7 @@ export default defineComponent({
releaseURL: releaseData.html_url releaseURL: releaseData.html_url
}; };
this.isUpdateModalOpen = this.isUpdateCardOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test'; storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) { } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
@@ -180,7 +180,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString(); const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) { if (naviLanguage.startsWith('en')) {
this.changeLang('en'); this.changeLang('en');
return; return;
} }
@@ -210,7 +210,7 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.65rem + 0.8vw); font-size: calc(0.65rem + 0.85vw);
} }
@include screenLandscape() { @include screenLandscape() {
@@ -226,6 +226,7 @@ export default defineComponent({
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow: hidden;
position: relative;
} }
.app_main { .app_main {
@@ -1,12 +1,12 @@
<template> <template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)"> <Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="modal-content"> <div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1> <h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div> <div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div> <div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)"> <button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
{{ $t('update.confirm') }} {{ $t('update.confirm') }}
</button> </button>
@@ -16,7 +16,7 @@
<span v-html="$t('update.info-2')"></span> <span v-html="$t('update.info-2')"></span>
</p> </p>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { Converter } from 'showdown'; import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue'; import Card from '../Global/Card.vue';
const converter = new Converter(); const converter = new Converter();
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
updateModalOpen: { isUpdateCardOpen: {
type: Boolean, type: Boolean,
required: true required: true
} }
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
data() { data() {
return { return {
@@ -49,7 +49,7 @@ export default defineComponent({
}, },
watch: { watch: {
updateModalOpen(val: boolean) { isUpdateCardOpen(val: boolean) {
this.$nextTick(() => { this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus(); if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
}); });
@@ -60,37 +60,38 @@ export default defineComponent({
htmlChangelog() { htmlChangelog() {
if (this.mainStore.appUpdate == null) return ''; if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog); return converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
} }
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) { ::v-deep(h1) {
text-align: center; text-align: center;
color: $accentCol;
} }
::v-deep(h2) { ::v-deep(h2) {
padding: 0.25em 0; padding: 0.25em 0;
border-bottom: 1px solid #aaa;
} }
::v-deep(ul) { ::v-deep(ul) {
list-style: inside; list-style: initial;
padding: 0.5em; padding: 1em;
line-height: 1.5em; line-height: 1.5em;
} }
.modal-content { .content {
display: grid; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
gap: 0.5em; gap: 0.5em;
@@ -98,6 +99,7 @@ export default defineComponent({
min-height: 700px; min-height: 700px;
overflow: auto; overflow: auto;
text-align: justify; text-align: justify;
max-width: 700px;
} }
.no-features { .no-features {
@@ -1,11 +1,10 @@
<template> <template>
<transition name="modal-anim" tag="div"> <transition name="modal-anim" tag="div">
<div class="modal" v-if="isOpen"> <div class="card" v-if="isOpen">
<div class="modal-background" @click="toggleModal(false)"></div> <div class="card-background" @click="toggleCard(false)"></div>
<div class="modal-wrapper" ref="wrapper" tabindex="0"> <div class="card-body" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div> </div>
</transition> </transition>
</template> </template>
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
export default defineComponent({ export default defineComponent({
emits: ['toggleModal'], emits: ['toggleCard'],
props: { props: {
isOpen: Boolean isOpen: Boolean
@@ -36,8 +35,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
@@ -46,17 +45,21 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal { .card {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100vh; height: 100%;
z-index: 200; z-index: 200;
display: flex;
justify-content: center;
align-items: center;
} }
.modal-background { .card-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -68,21 +71,23 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.modal-wrapper { .card-body {
position: absolute; position: relative;
top: 50%;
left: 50%; margin: 1em;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
& > :slotted(div) { background-color: #1a1a1a;
background-color: #1a1a1a; box-shadow: 0 0 15px 10px #0e0e0e;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw; overflow: auto;
max-width: 850px; }
@include smallScreen {
.card {
align-items: flex-start;
} }
} }
</style> </style>
@@ -1,12 +1,7 @@
<template> <template>
<AnimatedModal <Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
class="donation-modal" <div class="body">
:isOpen="isModalOpen" <div class="content">
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1> <h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0"> <div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span> <span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,9 +56,9 @@
</i> </i>
</div> </div>
<div class="modal_actions"> <div class="actions">
<a <a
class="modal-action a-button btn--image coffee" class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere" href="https://buycoffee.to/spythere"
target="_blank" target="_blank"
ref="action" ref="action"
@@ -73,7 +68,7 @@
</a> </a>
<a <a
class="modal-action a-button btn--image paypal" class="action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW" href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank" target="_blank"
> >
@@ -81,30 +76,30 @@
{{ $t('donations.action-paypal') }} {{ $t('donations.action-paypal') }}
</a> </a>
<button class="modal-action btn--image exit" @click="toggleModal(false)"> <button class="action btn--image exit" @click="toggleCard(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" /> <img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }} {{ $t('donations.action-exit') }}
</button> </button>
</div> </div>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
isModalOpen: Boolean isCardOpen: Boolean
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
watch: { watch: {
isModalOpen(val: boolean) { isCardOpen(val: boolean) {
this.running = val; this.running = val;
this.lastUpdate = Date.now(); this.lastUpdate = Date.now();
@@ -138,8 +133,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
}, },
runUpdate() { runUpdate() {
@@ -157,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal_content { .body {
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
gap: 1em; gap: 1em;
font-size: 1.1em; font-size: 1.1em;
& > div { max-width: 820px;
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
} }
.modal_main { .content {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 1em;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
} }
.modal_actions { img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
.actions {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em; gap: 0.5em;
padding: 1em;
form button { form button {
width: 100%; width: 100%;
} }
} }
.modal_actions > .modal-action { .actions > .action {
&.paypal { &.paypal {
$btnColor: #254069; $btnColor: #254069;
+123 -89
View File
@@ -1,80 +1,26 @@
<template> <template>
<div class="stock-list"> <div class="stock-list">
<div v-if="tractionOnly"> <ul>
<p> <li
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }} v-for="({ vehicleName, vehicleCargo, images, imagesFallbacks }, i) in thumbnailNames"
{{ computedStockList[0].split(':')[1] }} :key="i"
</p> >
<div class="stock-text">
<img <p>{{ vehicleName.replace(/_/g, ' ') }}</p>
class="traction-only" <small v-if="vehicleCargo">({{ vehicleCargo }})</small>
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${ </div>
/^EN/.test(computedStockList[0]) ? 'rb' : ''
}.png`"
@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>
<span> <span>
<img <img
:data-mouseover="stockName" v-for="(thumbnailImage, imageIndex) in images"
:data-mouseover="vehicleName"
data-tooltip-type="VehiclePreviewTooltip" data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]" :data-tooltip-content="vehicleName"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${ :src="`https://static.spythere.eu/thumbnails/${thumbnailImage}.png`"
/^EN/.test(stockName) ? 'rb' : '' @error="onImageError($event, imagesFallbacks[imageIndex])"
}.png`"
@error="onImageError($event, stockName)"
@click.stop="() => {}" @click.stop="() => {}"
width="400"
height="60" 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="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@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="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@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="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@click.stop="() => {}"
/>
<!-- /// -->
</span> </span>
</li> </li>
</ul> </ul>
@@ -106,28 +52,116 @@ export default defineComponent({
computed: { computed: {
computedStockList() { computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList; 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: { methods: {
onImageError(event: Event, stockName: string) { onImageError(event: Event, fallbackImage: string) {
let fallbackName = ''; (event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
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`;
} }
} }
}); });
@@ -163,10 +197,10 @@ img.traction-only {
max-width: 100%; max-width: 100%;
} }
p { .stock-text {
text-align: center; text-align: center;
color: #aaa; color: #aaa;
font-size: 0.95em; font-size: 0.9em;
margin-bottom: 1em; margin-bottom: 0.25em;
} }
</style> </style>
@@ -301,6 +301,6 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/dropdown.scss'; @import '../../styles/dropdown';
@import '../../styles/dropdown_filters.scss'; @import '../../styles/dropdown_filters';
</style> </style>
@@ -103,7 +103,7 @@ export default defineComponent({
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) { showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return; if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target); this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
} }
} }
}); });
+3 -1
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train' | 'search-train'
| 'search-date' | 'search-date'
| 'search-dispatcher' | 'search-dispatcher'
| 'search-issuedFrom'; | 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = { export type TimetableSearchType = {
[key in TimetableSearchKey]: string; [key in TimetableSearchKey]: string;
@@ -1,31 +1,18 @@
<template> <template>
<section class="scenery-table-section"> <div class="scenery-dispatchers-history">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" /> <div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0"> <div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }} {{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div v-else class="history-list">
<thead> <div v-for="historyItem in historyList" :key="historyItem.id">
<th>{{ $t('scenery.dispatchers-history-hash') }}</th> <span>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th> <span class="text--grayed" style="margin-right: 10px">
<th>{{ $t('scenery.dispatchers-history-level') }}</th> #{{ historyItem.stationHash }}
<th>{{ $t('scenery.dispatchers-history-rate') }}</th> </span>
<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>
<b <b
v-if="historyItem.dispatcherLevel !== null" v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher" class="level-badge dispatcher"
@@ -35,37 +22,52 @@
> >
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b> </b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b> <div>
</td> <span>
<td class="text--primary"> {{ $t('scenery.dispatcher-rate') }}
<b>{{ historyItem.dispatcherRate }}</b> <b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</td> </span>
<td style="min-width: 300px"> |
<div v-if="historyItem.timestampTo"> <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> <b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }} {{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{ - {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration) calculateDuration(historyItem.currentDuration)
}}) }})
</div> </span>
<div class="dispatcher-online" v-else> <span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }} {{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }}) ({{ calculateDuration(historyItem.currentDuration) }})
</div> </span>
</td> </span>
</tr> </div>
</tbody> </div>
</table> </div>
</section>
<div class="bottom-info"> <div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory"> <button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }} {{ $t('scenery.bottom-info') }}
</button> </button>
</div>
</div> </div>
</template> </template>
@@ -149,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.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 { .level-badge {
margin: 0 auto; text-align: center;
display: inline-block;
line-height: 1.6em;
} }
.dispatcher-online { .dispatcher-online {
@@ -158,13 +195,10 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.history-list { .history-list > div {
font-size: 1.1em;
}
.list-item {
align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center;
text-align: center;
} }
} }
</style> </style>
../../store/storeTypes
+1 -6
View File
@@ -72,7 +72,7 @@
<div class="info-lists"> <div class="info-lists">
<!-- user list --> <!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" /> <SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list --> <!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" /> <SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center; align-items: center;
font-size: 1.2em; font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
} }
.info-lists { .info-lists {
@@ -13,13 +13,13 @@
</li> </li>
<li <li
v-for="train in onlineScenery?.stationTrains" v-for="{ train, status } in stationTrains"
class="badge user" class="badge user"
:class="train.stopStatus"
:key="train.trainId"
tabindex="0" tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)" :key="train.id"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)" :data-status="status"
@click.prevent="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
@@ -32,7 +32,9 @@
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery } from '../../../typings/common'; import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
import { getTrainStopStatus } from '../utils';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin, modalTrainMixin], mixins: [routerMixin, modalTrainMixin],
@@ -41,6 +43,40 @@ export default defineComponent({
onlineScenery: { onlineScenery: {
type: Object as PropType<ActiveScenery>, type: Object as PropType<ActiveScenery>,
required: false required: false
},
station: {
type: Object as PropType<Station>
}
},
data() {
return {
mainStore: useMainStore()
};
},
computed: {
stationTrains() {
if (!this.onlineScenery) return;
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
return this.onlineScenery.stationTrains.map((train) => {
const stop = train.timetableData?.followingStops.find(
(stop) =>
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
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; -webkit-transition: background-color 200ms;
} }
&.no-timetable .user_train { &[data-status='no-timetable'] .user_train {
background-color: $no-timetable; background-color: $no-timetable;
} }
&.departed > &_train { &[data-status='departed'] > &_train {
background-color: $departed; background-color: $departed;
} }
&.stopped > &_train { &[data-status='stopped'] > &_train {
background-color: $stopped; background-color: $stopped;
} }
&.online > &_train { &[data-status='online'] > &_train {
background-color: $online; background-color: $online;
} }
&.terminated > &_train { &[data-status='terminated'] > &_train {
background-color: $terminated; background-color: $terminated;
} }
&.disconnected > &_train { &[data-status='disconnected'] > &_train {
background-color: $disconnected; background-color: $disconnected;
} }
&.offline { &[data-status='offline'] {
background: firebrick; background: firebrick;
pointer-events: none; pointer-events: none;
} }
+134 -61
View File
@@ -39,8 +39,8 @@
<div class="timetable-list"> <div class="timetable-list">
<transition-group name="list-anim"> <transition-group name="list-anim">
<div <div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em" style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading" key="list-loading"
> >
<Loading /> <Loading />
@@ -48,7 +48,7 @@
<span <span
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery" v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline" key="list-offline"
> >
{{ $t('scenery.offline') }} {{ $t('scenery.offline') }}
@@ -56,7 +56,7 @@
<div <div
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0" v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables" key="list-no-timetables"
> >
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
@@ -65,59 +65,56 @@
<div <div
class="timetable-item" class="timetable-item"
v-else v-else
v-for="scheduledTrain in computedScheduledTrains" v-for="(row, i) in sceneryTimetables"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp" :key="row.train.id + i"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
<span class="info-number"> <span class="info-number">
<strong>{{ scheduledTrain.category }}</strong> <strong>{{ row.train.timetableData!.category }}</strong>
{{ scheduledTrain.trainNo }} {{ row.train.trainNo }}
<span <span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
v-if="scheduledTrain.stopInfo.comments"
:title="scheduledTrain.stopInfo.comments"
>
<img src="/images/icon-warning.svg" /> <img src="/images/icon-warning.svg" />
</span> </span>
</span> </span>
&nbsp;|&nbsp; &nbsp;|&nbsp;
<span> <span>
{{ scheduledTrain.driverName }} {{ row.train.driverName }}
</span> </span>
<div class="info-route"> <div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong> <strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div> </div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" /> <ScheduledTrainStatus :sceneryTimetableRow="row" />
</span> </span>
</span> </span>
<span class="timetable-schedule"> <span class="timetable-schedule">
<span class="schedule-arrival"> <span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere"> <span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
{{ $t('timetables.begins') }} {{ $t('timetables.begins') }}
</span> </span>
<span class="arrival-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0"> <div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }} {{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' ({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }}) }}{{ row.checkpointStop.arrivalDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -125,41 +122,39 @@
<span class="schedule-stop"> <span class="schedule-stop">
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.arrivingLine }} {{ row.arrivingLine }}
</span> </span>
<span class="stop-time"> <span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }} {{ row.checkpointStop.stopTime || '' }}
{{ {{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
</span> </span>
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.departureLine }} {{ row.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere"> <span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
{{ $t('timetables.terminates') }} {{ $t('timetables.terminates') }}
</span> </span>
<span class="departure-time" v-else> <span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp) timestampToString(row.checkpointStop.departureTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ row.checkpointStop.departureDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -183,6 +178,8 @@ import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common'; import { ActiveScenery, Station } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -204,10 +201,6 @@ export default defineComponent({
listOpen: false listOpen: false
}), }),
mounted() {
this.loadSelectedOption();
},
activated() { activated() {
this.loadSelectedOption(); this.loadSelectedOption();
}, },
@@ -220,9 +213,10 @@ export default defineComponent({
const mainStore = useMainStore(); const mainStore = useMainStore();
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints[0] ??
? '' props.station?.name ??
: props.station?.generalInfo?.checkpoints[0] ?? null route.query['station']?.toString() ??
''
); );
return { return {
@@ -241,27 +235,106 @@ export default defineComponent({
return url; return url;
}, },
computedScheduledTrains() { sceneryTimetables(): SceneryTimetableRow[] {
if (!this.station) return []; if (!this.onlineScenery) return [];
return ( const sceneryName = this.$route.query['station']?.toString() ?? '';
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;
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1; return this.onlineScenery.scheduledTrains
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1; .filter(
(ct) =>
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.replace(/_/g, ' ')
);
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1; const trainStopIndex =
}) || [] ct.train.timetableData?.followingStops.findIndex(
); (stop) => stop.stopName == ct.checkpointStop.stopName
) ?? -1;
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
if (trainStopIndex > -1 && ct.train.timetableData?.followingStops !== undefined) {
for (let i = trainStopIndex; i >= 0; i--) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!prevStationName &&
i <= trainStopIndex - 1
)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.arrivalLine != null &&
!arrivingLine &&
!/-|_|it|sbl/gi.test(stop.arrivalLine)
) {
arrivingLine = stop.arrivalLine;
prevDepartureLine =
ct.train.timetableData.followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < ct.train.timetableData.followingStops.length; i++) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!nextStationName &&
i > trainStopIndex
)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.departureLine &&
!departureLine &&
!/-|_|it|sbl/gi.test(stop.departureLine)
) {
departureLine = stop.departureLine;
nextArrivalLine = ct.train.timetableData.followingStops[i + 1]?.arrivalLine || null;
}
}
}
return {
checkpointStop: ct.checkpointStop,
train: ct.train,
prevDepartureLine,
nextArrivalLine,
departureLine,
arrivingLine,
prevStationName,
nextStationName,
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> <template>
<!-- WIP --> <div class="scenery-timetables-history">
<!-- <div class="top-filters"> <div class="history-modes">
<button class="btn btn--option">ROZPOCZYNA BIEG</button> <button
class="btn btn--option"
<button class="btn btn--option">PRZEZ</button> v-for="mode in historyModeList"
:key="mode"
<button class="btn btn--option">KOŃCZY BIEG</button> :class="{ checked: checkedHistoryMode == mode }"
</div> --> @click="checkHistoryMode(mode)"
>
<section class="scenery-table-section"> {{ $t(`scenery.timetable-${mode}`) }}
<Loading v-if="dataStatus != DataStatus.Loaded" /> </button>
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div class="history-wrapper">
<thead> <Loading v-if="dataStatus != DataStatus.Loaded" />
<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>
<tbody> <div v-else-if="historyList.length == 0" class="no-history">
<tr v-for="historyItem in historyList" :key="historyItem.id"> {{ $t('scenery.history-list-empty') }}
<td> </div>
<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>
<td> <div v-else class="history-list">
<router-link <div v-for="timetableHistory in historyList" :key="timetableHistory.id">
v-if="historyItem.authorName" <span>
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`" <div>
>{{ historyItem.authorName }} <span
</router-link> class="timetable-status-indicator"
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> :data-terminated="timetableHistory.terminated"
</td> :data-fulfilled="timetableHistory.fulfilled"
<td> >
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> &ofcir;
{{ localeTime(historyItem.beginDate, $i18n.locale) }} </span>
</td> #{{ timetableHistory.id }} |
</tr> <b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
</tbody> {{ timetableHistory.trainNo }}
</table> {{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</section> </div>
<div class="bottom-info"> <div class="text--grayed">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()"> <span>
{{ $t('scenery.bottom-info') }} {{ $t('scenery.timetable-issued-date') }}
</button> <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> </div>
</template> </template>
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { ActiveScenery, Station, Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; 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({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, routerMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -91,9 +124,14 @@ export default defineComponent({
data() { data() {
return { return {
historyList: [] as API.TimetableHistory.Response, historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(), apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
DataStatus: Status.Data DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
}; };
}, },
@@ -103,17 +141,22 @@ export default defineComponent({
methods: { methods: {
async fetchAPIData() { async fetchAPIData() {
if (!this.station && !this.onlineScenery) { const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
return; return;
} }
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try { try {
const response: API.TimetableHistory.Response = await ( const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: { params: requestFilters
issuedFrom: this.station?.name || this.onlineScenery?.name
}
}) })
).data; ).data;
@@ -125,11 +168,17 @@ export default defineComponent({
} }
}, },
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() { navigateToHistory() {
this.$router.push({ this.$router.push({
path: '/journal/timetables', path: '/journal/timetables',
query: { 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/responsive.scss';
@import '../../styles/sceneryViewTables.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; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
padding: 0.25em;
button { 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> </style>
@@ -1,7 +1,7 @@
<template> <template>
<div class="general-status"> <div class="general-status">
<span <span
:class="computedScheduledTrain.stopStatus" :class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription" :title="computedScheduledTrain.stopStatusDescription"
> >
{{ computedScheduledTrain.stopStatusIndicator }} {{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../typings/common'; import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
export default defineComponent({ export default defineComponent({
props: { props: {
scheduledTrain: { sceneryTimetableRow: {
type: Object as PropType<ScheduledTrain>, type: Object as PropType<SceneryTimetableRow>,
required: true required: true
} }
}, },
computed: { computed: {
computedScheduledTrain(): ScheduledTrainComp { computedScheduledTrain() {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.scheduledTrain; this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}` ? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '', let stopStatusDescription = '',
stopStatusIndicator = ''; stopStatusIndicator = '';
switch (stopStatus) { switch (status) {
case StopStatus.ARRIVING: case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`; stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', { stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}` ? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`; : `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine }) ? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: ''; : '';
break; break;
@@ -85,7 +81,7 @@ export default defineComponent({
break; break;
} }
return { return {
...this.scheduledTrain, ...this.sceneryTimetableRow,
stopStatusDescription, stopStatusDescription,
stopStatusIndicator stopStatusIndicator
}; };
+13
View File
@@ -0,0 +1,13 @@
import { StopStatus, Train, TrainStop } from '../../typings/common';
export interface SceneryTimetableRow {
checkpointStop: TrainStop;
train: Train;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
departureLine: string | null;
arrivingLine: string | null;
prevStationName: string | null;
nextStationName: string | null;
status: StopStatus;
}
+42
View File
@@ -0,0 +1,42 @@
import { StopStatus, TrainStop } from '../../typings/common';
export const stopStatusPriority = [
StopStatus.ONLINE,
StopStatus.STOPPED,
StopStatus.DEPARTED,
StopStatus.ARRIVING,
StopStatus.DEPARTED_AWAY,
StopStatus.TERMINATED
];
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
if (stopInfo.terminatesHere && stopInfo.confirmed) {
return StopStatus.TERMINATED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
return StopStatus.DEPARTED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
return StopStatus.DEPARTED_AWAY;
}
if (currentStationName == sceneryName && !stopInfo.stopped) {
return StopStatus.ONLINE;
}
if (currentStationName == sceneryName && stopInfo.stopped) {
return StopStatus.STOPPED;
}
if (currentStationName != sceneryName) {
return StopStatus.ARRIVING;
}
return StopStatus.ONLINE;
}
+9 -16
View File
@@ -15,7 +15,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'], emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: { watch: {
'option.value'() { 'option.value'() {
this.filterStore.changeFilterValue(this.option.name, !this.option.value); // this.filterStore.changeFilterValue(this.option.name, !this.option.value);
} }
}, },
@@ -56,17 +49,17 @@ export default defineComponent({
handleDbClick(e: Event) { handleDbClick(e: Event) {
e.preventDefault(); e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id; // this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true; // this.option.value = true;
this.$emit('update:optionValue', true); this.$emit('update:optionValue', true);
this.filterStore.inputs.options // this.filterStore.inputs.options
.filter((option) => { // .filter((option) => {
return option.section == this.option.section && option.id != this.option.id; // return option.section == this.option.section && option.id != this.option.id;
}) // })
.forEach((option) => { // .forEach((option) => {
option.value = !this.option.value; // option.value = !this.option.value;
}); // });
} }
} }
}); });
+190 -117
View File
@@ -1,10 +1,10 @@
<template> <template>
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard"> <section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_controls"> <div class="card_controls">
<button class="btn--filled btn--image" @click="toggleCard"> <button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" /> <img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
[F] {{ $t('options.filters') }} <p>[F] {{ $t('options.filters') }}</p>
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span> <span class="active-indicator" v-if="changedFilters.length != 0"></span>
</button> </button>
<label for="scenery-search"> <label for="scenery-search">
@@ -33,29 +33,45 @@
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
<div class="changed-filters" :data-active="changedFilters.length > 0">
<template v-if="changedFilters.length > 0">
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
</template>
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
</div>
<section class="card_options"> <section class="card_options">
<div <div
class="option-section" class="option-section"
v-for="section in filterStore.inputs.optionSections" v-for="(sectionFilters, sectionKey) in filtersSections"
:key="section" :key="sectionKey"
> >
<h3 class="text--primary"> <h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }} <span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button> <button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3> </h3>
<hr /> <hr />
<div class="section-inputs"> <div class="section-filters">
<FilterOption <label
v-for="(option, i) in filterStore.inputs.options.filter( v-for="filterKey in sectionFilters"
(o) => o.section == section @click="() => (filters[filterKey] = !filters[filterKey])"
)" @dblclick="setSingleSectionFilter(sectionKey, filterKey)"
v-model:optionValue="option.value" :for="filterKey"
:option="option" >
:key="i" <input
/> :checked="filters[filterKey]"
v-model="filters[filterKey]"
type="checkbox"
:class="sectionKey"
:name="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div> </div>
</div> </div>
</section> </section>
@@ -68,7 +84,7 @@
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
: minimumHours < 8 : minimumHours < 7
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
@@ -76,21 +92,21 @@
</span> </span>
</section> </section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search"> <section class="card_authors-search">
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3> <h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
<datalist id="authors" name="authors">
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
</datalist>
<form action="javascript:void(0);" @submit="handleAuthorsInput"> <form action="javascript:void(0);" @submit="handleAuthorsInput">
<input <input
type="text" type="text"
id="author" id="author"
list="authors" list="authors"
name="authors" name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')" :placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
/> />
@@ -100,19 +116,18 @@
</section> </section>
<section class="card_sliders"> <section class="card_sliders">
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i"> <div class="slider" v-for="(slider, i) in initSliders" :key="i">
<input <input
class="slider-input" class="slider-input"
type="range" type="range"
:name="slider.name" :name="slider.id"
:id="slider.id" :id="slider.id"
:min="slider.minRange" :min="slider.minRange"
:max="slider.maxRange" :max="slider.maxRange"
:step="slider.step" :step="slider.step"
v-model="slider.value" v-model="filters[slider.id]"
@change="handleInput"
/> />
<span class="slider-value">{{ slider.value }}</span> <span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content"> <div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }} {{ $t(`filters.sliders.${slider.id}`) }}
</div> </div>
@@ -133,9 +148,9 @@
<button <button
class="btn--action" class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters" @click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
> >
[R] {{ $t('filters.reset') }} [R] {{ $t('filters.reset') }}
</button> </button>
@@ -151,22 +166,36 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager'; import StorageManager from '../../managers/storageManager';
import {
filtersSections,
initSliders,
initFilters,
getChangedFilters
} from '../../managers/stationFilterManager';
import { StationFilterSection } from '../../managers/stationFilterManager';
import { computed } from 'vue';
import { watch } from 'vue';
const STORAGE_KEY = 'options_saved';
export default defineComponent({ export default defineComponent({
components: { FilterOption }, components: { FilterOption },
mixins: [keyMixin, routerMixin], mixins: [keyMixin, routerMixin],
data: () => ({ data: () => ({
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '', filtersSections,
initSliders,
minimumHours: 0, minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
@@ -180,22 +209,33 @@ export default defineComponent({
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useMainStore(); const store = useMainStore();
const filterStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const changedFilters = computed(() => getChangedFilters(filters));
// Save filters to persistent storage
watch(filters, (value) => {
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
Object.keys(value).forEach((filterKey) => {
StorageManager.setValue(filterKey, filters[filterKey]);
});
});
return { return {
isVisible, isVisible,
store, store,
filterStore filters,
changedFilters
}; };
}, },
mounted() { mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY); this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) { if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours'); this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
} }
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
@@ -214,7 +254,7 @@ export default defineComponent({
return true; return true;
}, },
authors() { authorsHint() {
return this.store.stationList return this.store.stationList
.reduce((acc, station) => { .reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => { station.generalInfo?.authors?.forEach((author) => {
@@ -258,61 +298,63 @@ export default defineComponent({
this.scrollTop = (e.target as HTMLElement).scrollTop; this.scrollTop = (e.target as HTMLElement).scrollTop;
}, },
handleInput(e: Event) {
const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue(target.name, target.value);
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
},
handleAuthorsInput() { handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue); this.filters['authors'] = this.authors;
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
},
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue(name, value);
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
}, },
subHour() { subHour() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1; this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
addHour() { addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1; this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
saveFilters() { saveFilters() {
this.saveOptions = !this.saveOptions; this.saveOptions = !this.saveOptions;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(STORAGE_KEY);
return; return;
} }
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) => Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setBooleanValue(option.name, !option.value) StorageManager.setValue(filterKey, this.filters[filterKey]);
); });
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
}, },
resetFilters() { resetFilters() {
this.authorsInputValue = ''; // Reset local model values
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.authors = '';
this.filterStore.resetFilters();
// Reset global filters
Object.keys(this.filters).forEach((filterKey) => {
this.filters[filterKey] = (initFilters as any)[filterKey];
});
},
areSectionFiltersDefault(sectionKey: StationFilterSection) {
return filtersSections[sectionKey].every((filterKey) => {
return this.filters[filterKey] == initFilters[filterKey];
});
},
resetSectionFilters(sectionKey: StationFilterSection) {
filtersSections[sectionKey].forEach((filterKey) => {
this.filters[filterKey] = initFilters[filterKey];
});
},
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
filtersSections[sectionKey].forEach((filterKey) => {
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
});
}, },
closeCard() { closeCard() {
@@ -327,9 +369,10 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive';
@import '../../styles/card.scss'; @import '../../styles/card';
@import '../../styles/animations.scss'; @import '../../styles/animations';
@import '../../styles/variables';
h3.section-header { h3.section-header {
text-align: center; text-align: center;
@@ -346,6 +389,15 @@ h3.section-header {
padding: 0.5em; padding: 0.5em;
} }
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls { .card_controls {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
@@ -374,28 +426,6 @@ h3.section-header {
text-align: center; text-align: center;
} }
.card_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
.card_timestamp { .card_timestamp {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -441,6 +471,52 @@ h3.section-header {
} }
} }
.section-filters {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.section-filters > label {
position: relative;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
span {
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
font-weight: bold;
background-color: forestgreen;
}
span:hover {
background-color: #22aa22;
}
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: #444;
&:hover {
background-color: #555;
}
}
&:focus-visible + span {
outline: 1px solid $accentCol;
}
}
}
.card_actions { .card_actions {
padding: 0.5em; padding: 0.5em;
@@ -475,35 +551,18 @@ h3.section-header {
} }
} }
.section-inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider { .slider {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25em;
margin-bottom: 1em; margin-bottom: 1em;
&-value { &-value {
color: $accentCol; color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em; padding: 0.1em 0.2em;
} }
&-content {
flex-grow: 2;
}
&-input { &-input {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
@@ -512,7 +571,6 @@ h3.section-header {
outline: none; outline: none;
min-width: 25%; min-width: 25%;
max-width: 120px;
&:focus-visible ~ * { &:focus-visible ~ * {
color: gold; color: gold;
@@ -582,4 +640,19 @@ h3.section-header {
} }
} }
} }
@include smallScreen {
.card_controls > button.card-button > p {
display: none;
}
.slider {
flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
}
}
</style> </style>
+105 -44
View File
@@ -1,29 +1,55 @@
<template> <template>
<div class="station-stats"> <div class="station-stats">
<div class="separator" /> <div class="separator" />
<div>
{{ $t('station-stats.u-factor') }} <div class="stats-row">
<a <div>
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011" <span
target="_blank" >{{ $t('station-stats.u-factor') }}
:data-tooltip="$t('station-stats.u-factor-tooltip')" <a
>(?)</a href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
>: target="_blank"
<b :style="calculateFactorStyle()"> :data-tooltip="$t('station-stats.u-factor-tooltip')"
{{ uFactor.toFixed(2) }} >(?)</a
</b> >:
| {{ $t('station-stats.avg-timetable-count') }} </span>
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</div> <b class="u-factor" :style="calculateFactorStyle()">
<div> {{ uFactor.toFixed(2) }}
{{ $t('station-stats.single-track-count') }} </b>
<b>{{ trackCount.oneWayElectric }}</b> {{ $t('station-stats.electrified') }} / </div>
<b>{{ trackCount.oneWayOther }}</b> {{ $t('station-stats.not-electrified') }} |
{{ $t('station-stats.double-track-count') }} <b>{{ trackCount.twoWayElectric }}</b> <div>
{{ $t('station-stats.electrified') }} / <b>{{ trackCount.twoWayOther }}</b> &bull;
{{ $t('station-stats.not-electrified') }} | {{ $t('station-stats.open-spawns') }} {{ $t('station-stats.avg-timetable-count') }}
<b>{{ spawnCount.passenger }}</b> - PAS / <b>{{ spawnCount.freight }}</b> - TOW / <b>{{ avgTimetableCount.toFixed(2) }}</b>
<b>{{ spawnCount.loco }}</b> - LUZ / <b>{{ spawnCount.all }}</b> - ALL </div>
<div>
&bull;
{{ $t('station-stats.single-track-count') }}
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} </b>)
</div>
<div>
&bull;
{{ $t('station-stats.double-track-count') }}
<b>{{ trackCount.twoWay }}</b>
(<b>{{ trackCount.twoWayElectric }} </b>)
</div>
<div>
&bull; {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
>{{ trackCount.crossTrackElectric }} </b
>)
</div>
<div>
&bull;
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
<b>{{ spawnCount.all }}</b> - ALL
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -64,17 +90,18 @@ export default defineComponent({
}, },
avgTimetableCount() { avgTimetableCount() {
const scheduledTrainsTotal = this.mainStore.activeSceneryList.reduce<number>((acc, sc) => { const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
if (sc.region != this.mainStore.region.id) return acc; return sc.region == this.mainStore.region.id;
});
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all; acc += sc.scheduledTrainCount.all;
return acc; return acc;
}, 0); }, 0);
return this.mainStore.activeSceneryList.length != 0 if (regionSceneries.length == 0) return 0;
? scheduledTrainsTotal / this.mainStore.activeSceneryList.length
: 0; return timetableCountSum / regionSceneries.length;
}, },
trackCount() { trackCount() {
@@ -87,23 +114,46 @@ export default defineComponent({
) )
.reduce( .reduce(
(acc, st) => { (acc, st) => {
[...st.generalInfo!.routes.single, ...st.generalInfo!.routes.double].forEach((r) => { const { routes } = st.generalInfo!;
if (
routes.single.filter((r) => !r.isInternal).length > 0 &&
routes.double.filter((r) => !r.isInternal).length > 0
) {
acc.crossTrack++;
if (
routes.single.some((r) => r.isElectric) &&
routes.double.some((r) => r.isElectric)
)
acc.crossTrackElectric++;
}
[...routes.single, ...routes.double].forEach((r) => {
if (r.isInternal) return; if (r.isInternal) return;
const keyName: keyof typeof acc = `${r.routeTracks == 2 ? 'twoWay' : 'oneWay'}${r.isElectric ? 'Electric' : 'Other'}`; acc[r.routeTracks == 2 ? 'twoWay' : 'oneWay'] += 1;
if (r.isElectric) acc[r.routeTracks == 2 ? 'twoWayElectric' : 'oneWayElectric'] += 1;
acc[keyName] += 1;
}); });
return acc; return acc;
}, },
{ oneWayElectric: 0, oneWayOther: 0, twoWayElectric: 0, twoWayOther: 0 } {
oneWay: 0,
oneWayElectric: 0,
twoWay: 0,
twoWayElectric: 0,
crossTrack: 0,
crossTrackElectric: 0
}
); );
}, },
spawnCount() { spawnCount() {
return this.mainStore.activeSceneryList.reduce( return this.mainStore.activeSceneryList.reduce(
(acc, scenery) => { (acc, scenery) => {
if (scenery.region != this.mainStore.region.id) return acc;
scenery.spawns.forEach((spawn) => { scenery.spawns.forEach((spawn) => {
if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1; if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1;
if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1; if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1;
@@ -133,19 +183,30 @@ export default defineComponent({
color: #ddd; color: #ddd;
} }
[data-factor-low='true'] { .stats-row {
color: #ddd; display: flex;
justify-content: center;
flex-wrap: wrap;
text-wrap: pretty;
gap: 0.25em;
margin-top: 0.25em;
} }
[data-factor-mediocre='true'] { .u-factor {
color: lightgreen; [data-factor-low='true'] {
} color: #ddd;
}
[data-factor-high='true'] { [data-factor-mediocre='true'] {
color: greenyellow; color: lightgreen;
} }
[data-factor-highest='true'] { [data-factor-high='true'] {
color: rgb(22, 245, 22); color: greenyellow;
}
[data-factor-highest='true'] {
color: rgb(22, 245, 22);
}
} }
</style> </style>
+311 -302
View File
@@ -1,366 +1,371 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<Loading <Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0" v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/> />
<div class="table_wrapper" v-else-if="displayedStations.length > 0"> <table v-else-if="filteredStationList.length > 0">
<table> <thead>
<thead> <tr>
<tr> <th
<th v-for="headerName in headIds"
v-for="headerName in headIds" :key="headerName"
:key="headerName" @click="changeSorter(headerName)"
@click="changeSorter(headerName)" class="header-text"
class="header-text" :class="headerName"
: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"
> >
<td class="station-name" :class="station.generalInfo?.availability"> <span class="header_wrapper">
<b v-if="station.generalInfo?.project" style="color: salmon">{{ <div v-html="$t(`sceneries.headers.${headerName}`)"></div>
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level"> <img
<span v-if="station.generalInfo"> class="sort-icon"
<span v-if="activeSorter.headerName == headerName"
v-if=" :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
station.generalInfo.reqLevel > -1 && alt="sort icon"
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"
/> />
</td> </span>
</th>
<td class="station-dispatcher-name"> <th
<span v-if="station.onlineInfo?.dispatcherName"> v-for="headerName in headIconsIds"
<b :key="headerName"
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" @click="changeSorter(headerName)"
@click.stop="openDonationModal" class="header-image"
data-tooltip-type="DonatorTooltip" :class="headerName"
:data-tooltip-content="$t('donations.dispatcher-message')" >
> <span class="header_wrapper">
<img src="/images/icon-diamond.svg" alt="" /> <img
{{ station.onlineInfo.dispatcherName }} :src="`/images/icon-${headerName}.svg`"
</b> :alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else> <img
{{ station.onlineInfo.dispatcherName }} class="sort-icon"
</div> v-if="activeSorter.headerName == headerName"
</span> :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
</td> 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 <span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1" v-if="
:style=" station.generalInfo.reqLevel > -1 &&
calculateExpStyle( station.generalInfo.availability != 'nonPublic' &&
station.onlineInfo.dispatcherExp, station.generalInfo.availability != 'unavailable'
station.onlineInfo.dispatcherIsSupporter
)
" "
:style="calculateExpStyle(station.generalInfo.reqLevel)"
> >
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }} {{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span> </span>
</td>
<td class="station-tracks"> <span v-else-if="station.generalInfo.availability == 'abandoned'">
<div v-if="station.generalInfo"> <img
<span src="/images/icon-abandoned.svg"
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0" alt="non-public"
class="track catenary" :title="$t('sceneries.info.abandoned')"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${ />
station.generalInfo.routes.singleElectrifiedNames.length </span>
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span <span v-else-if="station.generalInfo.availability == 'nonPublic'">
v-if="station.generalInfo.routes.singleOtherNames.length != 0" <img
class="track no-catenary" src="/images/icon-lock.svg"
:title="`${$t('sceneries.info.single-track-routes-other')}${ alt="non-public"
station.generalInfo.routes.singleOtherNames.length :title="$t('sceneries.info.non-public')"
}`" />
> </span>
{{ station.generalInfo.routes.singleOtherNames.length }}
</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> </div>
</td> </span>
</td>
<td class="station-tracks"> <td class="station-dispatcher-exp">
<div v-if="station.generalInfo"> <span
<span v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0" :style="
class="track catenary" calculateExpStyle(
:title="`${$t('sceneries.info.double-track-routes-catenary')}${ station.onlineInfo.dispatcherExp,
station.generalInfo.routes.doubleElectrifiedNames.length station.onlineInfo.dispatcherIsSupporter
}`" )
> "
{{ station.generalInfo.routes.doubleElectrifiedNames.length }} >
</span> {{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<span <td class="station-tracks">
v-if="station.generalInfo.routes.doubleOtherNames.length != 0" <div v-if="station.generalInfo">
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">
<span <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="scenery-icon icon-info" class="track catenary"
:class="station.generalInfo?.controlType.replace('+', '-')" :title="`${$t('sceneries.info.single-track-routes-catenary')}${
:title=" station.generalInfo.routes.singleElectrifiedNames.length
$t('sceneries.info.control-type') + }`"
$t(`controls.${station.generalInfo?.controlType}`)
"
> >
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }} {{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span> </span>
<img <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :title="`${$t('sceneries.info.single-track-routes-other')}${
:alt="station.generalInfo.signalType" station.generalInfo.routes.singleOtherNames.length
:title=" }`"
$t('sceneries.info.signals-type') + >
$t(`signals.${station.generalInfo.signalType}`) {{ station.generalInfo.routes.singleOtherNames.length }}
" </span>
/> </div>
</td>
<img <td class="station-tracks">
v-if="station.generalInfo?.SUP" <div v-if="station.generalInfo">
class="icon-info" <span
src="/images/icon-SUP.svg" v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
alt="SUP (RASP-UZK)" class="track catenary"
:title="$t('sceneries.info.SUP')" :title="`${$t('sceneries.info.double-track-routes-catenary')}${
/> station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<img <span
v-if="station.generalInfo?.ASDEK" v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
src="/images/icon-ASDEK.svg" :title="`${$t('sceneries.info.double-track-routes-other')}${
alt="dSAT ASDEK" station.generalInfo.routes.doubleOtherNames.length
:title="$t('sceneries.info.ASDEK')" }`"
/> >
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<img <td class="station-info">
v-if="!station.generalInfo" <span
class="icon-info" v-if="station.generalInfo?.signalType"
src="/images/icon-unknown.svg" class="scenery-icon icon-info"
alt="icon-unknown" :class="station.generalInfo?.controlType.replace('+', '-')"
:title="$t('sceneries.info.unknown')" :title="
/> $t('sceneries.info.control-type') +
</td> $t(`controls.${station.generalInfo?.controlType}`)
"
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
> >
<span class="text--primary">{{ {{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
station.onlineInfo?.stationTrains?.length ?? '-' </span>
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }"> <img
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span> v-if="station.generalInfo?.signalType"
</td> 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 <img
class="station-spawns" v-if="station.generalInfo?.SUP"
:class="{ inactive: !station.onlineInfo }" class="icon-info"
data-tooltip-type="SpawnsTooltip" src="/images/icon-SUP.svg"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])" alt="SUP (RASP-UZK)"
> :title="$t('sceneries.info.SUP')"
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span> />
</td>
<td <img
class="station-schedules all" v-if="station.generalInfo?.ASDEK"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" src="/images/icon-ASDEK.svg"
> alt="dSAT ASDEK"
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }} :title="$t('sceneries.info.ASDEK')"
</td> />
<td <img
class="station-schedules unconfirmed" v-if="!station.generalInfo"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" src="/images/icon-unknown.svg"
> alt="icon-unknown"
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }} :title="$t('sceneries.info.unknown')"
</td> />
</td>
<td <td
class="station-schedules confirmed" class="station-users"
style="width: 30px" :class="{ inactive: !station.onlineInfo }"
:class="{ inactive: !station.onlineInfo }" data-tooltip-type="UsersTooltip"
> :data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }} >
</td> <span class="text--primary">{{
</tr> station.onlineInfo?.stationTrains?.length ?? '-'
</tbody> }}</span>
</table> /
</div> <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> <div class="no-stations" v-else>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b <div>
>) {{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
>)
</div>
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
⚠ {{ $t('sceneries.active-filters') }}
</div>
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent, inject, computed } from 'vue';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { getChangedFilters } from '../../managers/stationFilterManager';
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils';
export default defineComponent({ export default defineComponent({
emits: ['toggleDonationModal'], emits: ['toggleDonationCard'],
components: { Loading, StationStatusBadge }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin], mixins: [styleMixin, dateMixin],
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
headIds, headIds,
lastSelectedStationName: '' lastSelectedStationName: '',
getChangedFilters
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
}
},
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
const tooltipStore = useTooltipStore(); const tooltipStore = useTooltipStore();
const stationFiltersStore = useStationFiltersStore(); const filters = inject('StationsView_filters') as Record<string, any>;
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
const filteredStationList = computed(() =>
mainStore.allStationInfo
.filter((station) => filterStations(station, filters))
.sort((a, b) => sortStations(a, b, activeSorter))
);
return { return {
Status: Status.Data, Status: Status.Data,
stationFiltersStore,
mainStore, mainStore,
apiStore, apiStore,
tooltipStore tooltipStore,
filters,
filteredStationList,
activeSorter
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.displayedStations.find((station) => station.name === name); const station = this.filteredStationList.find((station) => station.name === name);
if (!station) return; if (!station) return;
@@ -376,8 +381,8 @@ export default defineComponent({
}); });
}, },
openDonationModal(e: Event) { openDonationCard(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide(); this.tooltipStore.hide();
}, },
@@ -388,10 +393,14 @@ export default defineComponent({
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(headerName: HeadIdsTypes) { changeSorter(headerName: HeadIdsType) {
if (headerName == 'general') return; if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName); if (headerName == this.activeSorter.headerName)
this.activeSorter.dir = -1 * this.activeSorter.dir;
else this.activeSorter.dir = 1;
this.activeSorter.headerName = headerName;
} }
} }
}); });
@@ -406,18 +415,18 @@ $rowCol: #424242;
.station_table { .station_table {
height: 80vh; height: 80vh;
min-height: 550px; max-height: 2000px;
min-height: 700px;
overflow: auto; overflow: auto;
font-weight: 500; font-weight: 500;
} }
.no-stations { .no-stations {
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.25em;
padding: 1em; padding: 1em;
background: #1a1a1a; background: #1a1a1a;
line-height: 1.5em;
} }
table { table {
+25 -50
View File
@@ -5,54 +5,29 @@ export interface FilterOption {
defaultValue: boolean; defaultValue: boolean;
} }
export interface Filter { export const headIds = [
[key: string]: boolean | number | string; 'station',
default: boolean; 'min-lvl',
notDefault: boolean; 'status',
real: boolean; 'dispatcher',
fictional: boolean; 'dispatcher-lvl',
SPK: boolean; 'routes-single',
SCS: boolean; 'routes-double',
SPE: boolean; 'general'
SUP: boolean; ] as const;
noSUP: boolean;
ASDEK: boolean; export const headIconsIds = [
noASDEK: boolean; 'user',
ręczne: boolean; 'like',
'ręczne+SPK': boolean; 'spawn',
'ręczne+SCS': boolean; 'timetableAll',
mechaniczne: boolean; 'timetableUnconfirmed',
'mechaniczne+SPK': boolean; 'timetableConfirmed'
'mechaniczne+SCS': boolean; ] as const;
SBL: boolean;
PBL: boolean; export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
współczesna: boolean;
kształtowa: boolean; export interface ActiveSorter {
historyczna: boolean; headerName: HeadIdsType;
mieszana: boolean; dir: number;
minLevel: number;
maxLevel: number;
minOneWayCatenary: number;
minOneWay: number;
minTwoWayCatenary: number;
minTwoWay: number;
minVmax: number;
maxVmax: number;
'no-1track': boolean;
'no-2track': boolean;
'include-selected': boolean;
free: boolean;
occupied: boolean;
nonPublic: boolean;
unavailable: boolean;
abandoned: boolean;
endingStatus: boolean;
afkStatus: boolean;
noSpaceStatus: boolean;
unavailableStatus: boolean;
unsignedStatus: boolean;
authors: string;
onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
} }
+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> <template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview"> <div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" /> <component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div> </div>
</template> </template>
@@ -35,10 +35,7 @@ export default defineComponent({
let translateX = '0', let translateX = '0',
translateY = '30px'; translateY = '30px';
if (clientWidth < 500) { if (val[0] <= boxWidth / 2) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0'; previewEl.style.left = '0';
translateX = '0px'; translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) { } else if (val[0] >= clientWidth - boxWidth / 2) {
+2 -2
View File
@@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common'; import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -23,7 +23,7 @@ export default defineComponent({
trains() { trains() {
if (this.tooltipStore.content == '') return []; if (this.tooltipStore.content == '') return [];
const parsedTrains = JSON.parse(this.tooltipStore.content) as StationTrain[]; const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo); return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
} }
} }
@@ -13,13 +13,20 @@
width="300" width="300"
height="176" height="176"
class="rounded-md w-full h-auto" 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 v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name"> <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>
</div> </div>
</template> </template>
@@ -27,11 +34,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading' imageState: 'loading'
}; };
}, },
@@ -56,6 +65,34 @@ export default defineComponent({
(e.target as HTMLElement).style.display = 'none'; (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> </script>
@@ -85,10 +122,13 @@ img {
.vehicle-name { .vehicle-name {
text-align: center; text-align: center;
margin-top: 0.5em; margin-top: 0.5em;
color: #ccc;
text-wrap: wrap; text-wrap: wrap;
} }
.vehicle-props {
color: #ccc;
}
.error-placeholder { .error-placeholder {
height: 176px; height: 176px;
} }
+33 -21
View File
@@ -1,5 +1,8 @@
<template> <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 class="name" v-html="stop.nameHtml"></span>
<span <span
@@ -9,12 +12,13 @@
stop.arrivalDelay > 0 && stop.status != 'unconfirmed' stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
? 'delayed' ? 'delayed'
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed' : stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed' ? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed' : stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time' ? 'on-time'
: '' : ''
" "
> >
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'"> <span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s> <s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }} {{ timestampToString(stop.arrivalReal) }}
@@ -29,17 +33,17 @@
<span <span
v-if=" v-if="
stop.duration || stop.duration ||
(stop.status == 'stopped' && (stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
" "
class="date stop" class="date stop"
:data-stop-types="stop.type.replace(', ', '-')" :data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status=" :data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
stop.departureDelay - stop.arrivalDelay > 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 }} {{ stop.type == '' ? 'pt' : stop.type }}
</span> </span>
@@ -53,13 +57,16 @@
stop.departureDelay > 0 && stop.status == 'confirmed' stop.departureDelay > 0 && stop.status == 'confirmed'
? 'delayed' ? 'delayed'
: stop.departureDelay < 0 && stop.status == 'confirmed' : stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed' ? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed' : stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time' ? '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> <s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }} {{ timestampToString(stop.departureReal) }}
@@ -96,14 +103,14 @@ $delayedClr: salmon;
$dateClr: #525151; $dateClr: #525151;
$stopExchangeClr: #db8e29; $stopExchangeClr: #db8e29;
$stopDefaultClr: #252525; $stopDefaultClr: #252525;
$stopNameClr: #22a8d1; $stopNameClr: #303030;
.stop-label { .stop-label {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
&[data-sbl='true'] { &[data-minor='true'] {
.date { .date {
display: none; display: none;
} }
@@ -117,6 +124,7 @@ $stopNameClr: #22a8d1;
.name { .name {
background: $stopNameClr; background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
display: flex; display: flex;
@@ -130,6 +138,10 @@ $stopNameClr: #22a8d1;
.date { .date {
background: $dateClr; background: $dateClr;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
} }
.stop { .stop {
@@ -150,7 +162,7 @@ $stopNameClr: #22a8d1;
.departure { .departure {
&[data-status='delayed'] { &[data-status='delayed'] {
s { s {
color: #999; color: #ccc;
} }
span { span {
@@ -160,7 +172,7 @@ $stopNameClr: #22a8d1;
&[data-status='preponed'] { &[data-status='preponed'] {
s { s {
color: #999; color: #ccc;
} }
span { span {
+24
View File
@@ -131,6 +131,18 @@
<div> <div>
<img src="/images/icon-speed.svg" alt="speed icon" /> <img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h {{ 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>
</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: { methods: {
navigateToJournal() { navigateToJournal() {
this.$router.push({ this.$router.push({
+12 -23
View File
@@ -21,7 +21,7 @@ export default defineComponent({
computed: { computed: {
chosenTrain() { chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId); return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
} }
}, },
@@ -29,8 +29,15 @@ export default defineComponent({
chosenTrain(train: Train | undefined) { chosenTrain(train: Train | undefined) {
this.$nextTick(() => { this.$nextTick(() => {
if (train) { if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement; const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus(); contentEl.focus();
} else {
(this.store.modalLastClickedTarget as any)?.focus();
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 90);
} }
}); });
} }
@@ -40,20 +47,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.train-modal { .train-modal {
position: fixed; position: fixed;
@@ -61,12 +54,14 @@ export default defineComponent({
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%;
color: white; color: white;
z-index: 200; z-index: 200;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start;
text-align: left; text-align: left;
} }
@@ -87,10 +82,10 @@ export default defineComponent({
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
margin-top: 1em;
width: 95vw; width: 95vw;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; box-shadow: 0 0 15px 10px #0e0e0e;
@@ -105,10 +100,4 @@ export default defineComponent({
} }
} }
} }
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style> </style>
@@ -81,7 +81,6 @@
</div> </div>
<div class="filter-actions"> <div class="filter-actions">
<div></div>
<button class="btn--action" @click="resetAllFilters"> <button class="btn--action" @click="resetAllFilters">
{{ $t('options.filter-reset') }} {{ $t('options.filter-reset') }}
</button> </button>
@@ -223,9 +222,6 @@ export default defineComponent({
.filter-actions { .filter-actions {
display: flex; display: flex;
gap: 0.5em;
width: 100%;
margin-top: 1em; margin-top: 1em;
> * { > * {
+85 -42
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type" :data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive" :data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed" :data-last-confirmed="stop.isLastConfirmed"
x
> >
<span class="stop_info"> <span class="stop_info">
<span class="distance"> <span class="distance">
@@ -48,26 +47,46 @@
<span <span
v-if=" v-if="
stop.departureLine && stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine && scheduleStops[i + 1] != undefined &&
!/sbl/gi.test(stop.departureLine) !/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
" "
> >
{{ stop.departureLine }} <div class="scenery-route">
</span> <span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)"> | {{ stop.departureLineInfo.routeSpeed }}
<div>{{ stop.departureLine }}</div> <span v-if="stop.departureLineInfo.isElectric"></span>
<div <img
class="scenery-change-name" v-else
v-if=" src="/images/icon-we4a.png"
i < scheduleStops.length - 1 && :title="$t('trains.we4a-tooltip')"
stop.sceneryName != scheduleStops[i + 1].sceneryName width="12"
" />
> </span>
{{ scheduleStops[i + 1].sceneryName }}
</div> </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> </div>
</span> </span>
</div> </div>
@@ -85,7 +104,7 @@ import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common'; import { StationRoutesInfo, Train } from '../../typings/common';
export interface TrainScheduleStop { export interface TrainScheduleStop {
nameHtml: string; nameHtml: string;
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
isSBL: boolean; isSBL: boolean;
sceneryName: string | null; sceneryName: string | null;
sceneryHash: string;
distance: number; distance: number;
arrivalLine: string | null; arrivalLine: string | null;
departureLine: string | null; departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null; comments: string | null;
} }
@@ -146,13 +169,25 @@ export default defineComponent({
return ( return (
this.train.timetableData?.followingStops.map((stop, i, arr) => { this.train.timetableData?.followingStops.map((stop, i, arr) => {
if ( const isExternal =
i > 0 && i > 0 &&
stop.arrivalLine && stop.arrivalLine != null &&
stop.arrivalLine != arr[i - 1].departureLine && (stop.arrivalLine != arr[i - 1].departureLine ||
!/sbl/gi.test(stop.arrivalLine) (stop.arrivalLine == arr[i - 1].departureLine &&
) !/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
currentSceneryIndex++;
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 { return {
nameHtml: stop.stopName, nameHtml: stop.stopName,
@@ -174,14 +209,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine, arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine, departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType, type: stop.stopType,
distance: stop.stopDistance, distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i), isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere, isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName), isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route', position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '', sceneryName,
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed' 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-route {
.scenery-change-name { img {
position: relative; vertical-align: middle;
margin: 0.25em 0; }
}
&::before { .scenery-change-name {
content: ''; position: relative;
position: absolute; margin: 0.25em 0;
height: 2px;
width: 30px;
background-color: #aaa;
top: 50%; &::before {
right: calc(100% + 5px); content: '';
transform: translate(0, -50%); position: absolute;
} height: 2px;
width: 30px;
background-color: #aaa;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
} }
} }
</style> </style>
+6 -17
View File
@@ -8,17 +8,18 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" /> <Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0"> <div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
>)
</div> </div>
<transition-group name="list-anim" tag="ul"> <transition-group name="list-anim" tag="ul">
<li <li
class="train-row" class="train-row"
v-for="train in trains" v-for="train in trains"
:key="train.trainId" :key="train.id"
tabindex="0" tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)" @click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)" @keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<TrainInfo :train="train" :extended="false" /> <TrainInfo :train="train" :extended="false" />
</li> </li>
@@ -76,17 +77,6 @@ export default defineComponent({
return Status.Data.Loaded; return Status.Data.Loaded;
} }
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
} }
}); });
</script> </script>
@@ -108,8 +98,7 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
font-size: 1.25em;
font-size: 1.5em;
background: #1a1a1a; background: #1a1a1a;
} }
File diff suppressed because it is too large Load Diff
-350
View File
@@ -1,354 +1,4 @@
{ {
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"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": "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": [ "regions": [
{ {
"id": "eu", "id": "eu",
+40 -34
View File
@@ -112,8 +112,8 @@
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE", "donate": "DONATE",
"search-button": "Search", "search-button": "SEARCH",
"reset-button": "Reset", "reset-button": "RESET",
"sort-title": "SORT BY:", "sort-title": "SORT BY:",
"filter-title": "FILTER BY:", "filter-title": "FILTER BY:",
@@ -125,7 +125,9 @@
"search-dispatcher": "Dispatcher name", "search-dispatcher": "Dispatcher name",
"search-station": "Scenery name", "search-station": "Scenery name",
"search-author": "Timetable author 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-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)", "search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)", "search-date": "Date (UTC+2 / CEST)",
@@ -174,8 +176,9 @@
"sections": { "sections": {
"quick": "QUICK FILTERS", "quick": "QUICK FILTERS",
"stationType": "STATION TYPE",
"reality": "SCENERY REALITY", "reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY", "packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY", "access": "GENERAL AVAILABILITY",
"control": "CONTROLS", "control": "CONTROLS",
"signals": "SIGNALLING", "signals": "SIGNALLING",
@@ -186,6 +189,9 @@
"spawns": "OPEN SPAWNS" "spawns": "OPEN SPAWNS"
}, },
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE", "all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE", "all-free": "CURRENTLY FREE",
@@ -196,11 +202,11 @@
"title": "STATION FILTERS", "title": "STATION FILTERS",
"default": "IN-GAME", "default": "IN-GAME",
"not-default": "ADDITIONAL", "notDefault": "ADDITIONAL",
"real": "REAL", "real": "REAL",
"fictional": "FICTIONAL", "fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED", "unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC", "nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED", "abandoned": "ABANDONED",
"SPK": "SPK", "SPK": "SPK",
@@ -210,7 +216,6 @@
"SCS-R": "SCS + MANUAL", "SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.", "SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "MANUAL", "manual": "MANUAL",
"mechanical": "MECHANICAL", "mechanical": "MECHANICAL",
@@ -233,15 +238,18 @@
"withActiveTimetables": "ACTIVE", "withActiveTimetables": "ACTIVE",
"withoutActiveTimetables": "NO ACTIVE", "withoutActiveTimetables": "NO ACTIVE",
"junction": "JUNCTIONS",
"nonJunction": "OTHER",
"sliders": { "sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL", "minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL", "maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED", "minVmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED", "maxVmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES", "minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES", "minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES", "minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES" "minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
}, },
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):", "authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
@@ -294,16 +302,16 @@
"single-track-routes-other": "Not electrified single-track routes count: " "single-track-routes-other": "Not electrified single-track routes count: "
}, },
"no-stations": "No stations to show here!", "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..." "scenery-search": "Search for scenery...",
"active-filters": "Attention! You got active filters!"
}, },
"station-stats": { "station-stats": {
"u-factor": "U-factor", "u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)", "u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
"avg-timetable-count": "Average timetable count for one dispatcher:", "avg-timetable-count": "Average count of scenery timetables:",
"single-track-count": "Available single track routes:", "single-track-count": "Single track routes:",
"double-track-count": "Available double track routes:", "double-track-count": "Double track routes:",
"electrified": "(electrified)", "cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
"not-electrified": "(not electr.)",
"open-spawns": "Open spawns:" "open-spawns": "Open spawns:"
}, },
"trains": { "trains": {
@@ -323,6 +331,9 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "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: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -485,21 +496,16 @@
"option-timetables-history": "Timetables history PL1", "option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1", "option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by", "timetable-via": "ALL TIMETABLES",
"timetable-author-unknown": "Author unknown", "timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID", "timetable-issued-date": "Issued",
"timetables-history-number": "Number", "timetable-issued-by": " by:",
"timetables-history-route": "Route", "timetable-issued-for": " for driver:",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Rate:",
"dispatchers-history-dispatcher": "Dispatcher", "dispatcher-status-changes": "Status changes:",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No recorded scenery history!",
+39 -32
View File
@@ -108,8 +108,8 @@
"filters": "FILTRY", "filters": "FILTRY",
"donate": "WESPRZYJ", "donate": "WESPRZYJ",
"search-button": "Szukaj", "search-button": "SZUKAJ",
"reset-button": "Zresetuj", "reset-button": "ZRESETUJ",
"sort-title": "SORTUJ WG:", "sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:", "filter-title": "FILTRUJ WG:",
@@ -122,6 +122,8 @@
"search-station": "Nazwa scenerii", "search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy", "search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa", "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-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)", "search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)", "search-date": "Data (UTC+2 / CEST)",
@@ -171,8 +173,9 @@
"sections": { "sections": {
"quick": "SZYBKIE FILTRY", "quick": "SZYBKIE FILTRY",
"stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII", "reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE", "packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA", "control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI", "signals": "TYP SYGNALIZACJI",
@@ -183,6 +186,9 @@
"spawns": "OTWARTE SPAWNY" "spawns": "OTWARTE SPAWNY"
}, },
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE", "all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE", "all-free": "WSZYSTKIE WOLNE",
@@ -193,11 +199,11 @@
"title": "FILTRUJ STACJE", "title": "FILTRUJ STACJE",
"default": "DOMYŚLNA", "default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ", "notDefault": "POZA PACZKĄ",
"real": "REALNA", "real": "REALNA",
"fictional": "FIKCYJNA", "fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA", "unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA", "nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA", "abandoned": "WYCOFANA",
"SPK": "SPK", "SPK": "SPK",
@@ -229,15 +235,18 @@
"withActiveTimetables": "AKTYWNE", "withActiveTimetables": "AKTYWNE",
"withoutActiveTimetables": "BEZ AKTYWNYCH", "withoutActiveTimetables": "BEZ AKTYWNYCH",
"junction": "WĘZŁOWE",
"nonJunction": "INNE",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA", "minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA", "maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)", "minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)", "minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)" "minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
}, },
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):", "authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
@@ -287,16 +296,16 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: " "single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
}, },
"no-stations": "Brak stacji do wyświetlenia!", "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..." "scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
}, },
"station-stats": { "station-stats": {
"u-factor": "Współczynnik Ugla", "u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)", "u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
"avg-timetable-count": "Średnia liczba rozkładów jazdy na dyżurnego:", "avg-timetable-count": "Średnia liczba rozkładów jazdy na sceneriach:",
"single-track-count": "Dostępne szlaki jednotorowe:", "single-track-count": "Szlaki jednotorowe:",
"double-track-count": "Dostępne szlaki dwutorowe:", "double-track-count": "Szlaki dwutorowe:",
"electrified": "(zelektr.)", "cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
"not-electrified": "(niezelektr.)",
"open-spawns": "Otwarte spawny:" "open-spawns": "Otwarte spawny:"
}, },
"trains": { "trains": {
@@ -308,6 +317,9 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "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: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -467,21 +479,16 @@
"option-timetables-history": "Historia rozkładów PL1", "option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1", "option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez", "timetable-via": "WSZYSTKIE RJ",
"timetable-author-unknown": "Autor nieznany", "timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID", "timetable-issued-date": "Wystawiony",
"timetables-history-number": "Numer", "timetable-issued-by": " przez:",
"timetables-history-route": "Trasa", "timetable-issued-for": " dla maszynisty:",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Ocena:",
"dispatchers-history-dispatcher": "Dyżurny", "dispatcher-status-changes": "Zmiany statusów:",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!", "history-list-empty": "Brak historii dla tej scenerii!",
+7 -2
View File
@@ -5,10 +5,15 @@ import router from './router';
import i18n from './i18n'; import i18n from './i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import useCustomSW from './mixins/useCustomSW'; import { registerSW } from 'virtual:pwa-register';
// Service worker // Service worker
useCustomSW(); registerSW({
immediate: true,
onNeedRefresh() {
console.log('Needs refresh!');
}
});
const clickOutsideDirective: Directive = { const clickOutsideDirective: Directive = {
mounted(el, binding) { mounted(el, binding) {
+119
View File
@@ -0,0 +1,119 @@
import StorageManager from './storageManager';
export const sections = [
'status',
'timetables',
'reality',
'packageAccess',
'stationType',
'access',
'control',
'blockades',
'signals',
'addons'
] as const;
export const initFilters = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
manual: false,
'SPK-R': false,
'SCS-R': false,
mechanical: false,
'SPK-M': false,
'SCS-M': false,
modern: false,
semaphores: false,
historical: false,
mixed: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
authors: ''
};
export const initSliders = [
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 }
];
export type StationFilter = keyof typeof initFilters;
export type StationFilterSection = (typeof sections)[number];
export const filtersSections: Record<StationFilterSection, StationFilter[]> = {
status: ['free', 'occupied', 'endingStatus', 'afkStatus', 'noSpaceStatus', 'unavailableStatus'],
timetables: ['withActiveTimetables', 'withoutActiveTimetables'],
reality: ['real', 'fictional'],
packageAccess: ['default', 'notDefault'],
stationType: ['junction', 'nonJunction'],
access: ['nonPublic', 'unavailable', 'abandoned'],
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
control: ['SPK', 'SCS', 'SPE', 'SPK-M', 'SCS-M', 'mechanical', 'SPK-R', 'SCS-R', 'manual'],
blockades: ['SBL', 'PBL'],
signals: ['modern', 'semaphores', 'mixed', 'historical']
};
export function setupFilters(currentFilters: Record<string, any>) {
if (!StorageManager.isRegistered('options_saved')) return;
Object.keys(currentFilters).forEach((filterKey) => {
const savedValue = StorageManager.getValue(filterKey);
if (savedValue != null) {
if (typeof currentFilters[filterKey] == 'boolean')
currentFilters[filterKey] = savedValue === 'true';
else if (typeof currentFilters[filterKey] == 'number')
currentFilters[filterKey] = Number(savedValue);
else currentFilters[filterKey] = savedValue.toString();
}
});
}
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
return (
Object.keys(currentFilters).filter(
(filterKey) =>
currentFilters[filterKey] !== initFilters[filterKey as keyof typeof initFilters]
) ?? []
);
}
+4
View File
@@ -34,6 +34,10 @@ export default class StorageManager {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
} }
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean { static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false; return window.localStorage.getItem(key) === 'true' ? true : false;
} }
+8 -8
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore'; import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -11,20 +12,19 @@ export default defineComponent({
}, },
methods: { methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) { selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = train.modalId;
document.body.classList.add('no-scroll'); if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target; if (target) this.store.modalLastClickedTarget = target;
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide(); this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
} }
} }
}); });
-13
View File
@@ -1,13 +0,0 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true
});
return {
needRefresh,
updateServiceWorker,
offlineReady
};
};
+1 -1
View File
@@ -58,7 +58,7 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined) 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; 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];
-235
View File
@@ -1,235 +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;
}
return true;
};
+46 -16
View File
@@ -4,20 +4,17 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings'; import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', { export const useApiStore = defineStore('apiStore', {
state: () => ({ state: () => ({
dataStatuses: { dataStatuses: {
connection: Status.Data.Loading, connection: Status.Data.Loading,
sceneries: Status.Data.Loading sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
}, },
activeData: undefined as API.ActiveData.Response | undefined, activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response, donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[], sceneryData: [] as StationJSONData[],
@@ -54,14 +51,25 @@ export const useApiStore = defineStore('apiStore', {
// Static data // Static data
this.fetchDonatorsData(); this.fetchDonatorsData();
this.fetchStationsGeneralInfo(); this.fetchStationsGeneralInfo();
this.fetchVehiclesInfo();
}, },
async fetchActiveData() { 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; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response.data;
@@ -84,17 +92,39 @@ export const useApiStore = defineStore('apiStore', {
}, },
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = ( try {
await this.client!.get<StationJSONData[]>('api/getSceneries') const sceneryData: StationJSONData[] = (
).data; 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; 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; async fetchVehiclesInfo() {
this.sceneryData = sceneryData; // 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);
}
} }
} }
}); });
+76 -35
View File
@@ -1,9 +1,9 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns } from './utils';
import { import {
ActiveScenery, ActiveScenery,
ScheduledTrain, CheckpointTrain,
Station, Station,
StationRoutes, StationRoutes,
Status, Status,
@@ -12,6 +12,9 @@ import {
import { useApiStore } from './apiStore'; import { useApiStore } from './apiStore';
import { MainStoreState } from './typings'; import { MainStoreState } from './typings';
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', { export const useMainStore = defineStore('mainStore', {
state: () => state: () =>
({ ({
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
trainList(): Train[] { trainList(): Train[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? []) return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online) .filter((train) => train.timetable || train.online)
.map((train) => { .map((train) => {
@@ -53,8 +59,9 @@ export const useMainStore = defineStore('mainStore', {
sceneryHash sceneryHash
) ?? []; ) ?? [];
return { const trainObj = {
trainId: train.driverName + train.trainNo.toString(), id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo, trainNo: train.trainNo,
mass: train.mass, mass: train.mass,
@@ -93,6 +100,33 @@ export const useMainStore = defineStore('mainStore', {
} }
: undefined : undefined
} as Train; } as Train;
// Sceneries trains map
if (sceneriesTrains.has(train.currentStationName)) {
sceneriesTrains.set(train.currentStationName, [
...sceneriesTrains.get(train.currentStationName)!,
trainObj
]);
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
// Checkpoints trains map
timetable?.stopList.forEach((stop, i) => {
if (/strong|podg\.|pe\./.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = {
train: trainObj,
checkpointStop: stop
};
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
});
return trainObj;
}); });
}, },
@@ -131,13 +165,14 @@ export const useMainStore = defineStore('mainStore', {
dispatcherId: -1, dispatcherId: -1,
dispatcherExp: -1, dispatcherExp: -1,
dispatcherIsSupporter: false, dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE, dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1, dispatcherTimestamp: -1,
isOnline: false, isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -177,8 +212,9 @@ export const useMainStore = defineStore('mainStore', {
isOnline: scenery.isOnline == 1, isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [], stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -196,37 +232,39 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name); const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains( let checkpointsSet: Set<string> = new Set();
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
const stationTrains = getStationTrains( // Add checkpoints to active scenery data
this.trainList, checkpointsSet.add(scenery.name.toLowerCase());
scheduledTrains,
this.region.id,
scenery.name
);
// Remove checkpoint duplicates station?.generalInfo?.checkpoints.forEach((cpName) => {
const uniqueScheduledTrains = scheduledTrains.reduce( checkpointsSet.add(cpName.toLowerCase());
(uniqueList, sTrain) => });
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
scenery.scheduledTrains = scheduledTrains; const checkpoints = Array.from(checkpointsSet);
scenery.stationTrains = stationTrains;
scenery.scheduledTrainCount = { scenery.stationTrains =
all: uniqueScheduledTrains.length, sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length const uniqueTrainIds: string[] = [];
}; checkpoints.forEach((cp) => {
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop }) => {
scenery.scheduledTrains.push({ train, checkpointStop });
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
scenery.scheduledTrainCount.all += 1;
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
else scenery.scheduledTrainCount.unconfirmed++;
uniqueTrainIds.push(train.id);
});
});
} }
return allActiveSceneries; return allActiveSceneries;
@@ -281,7 +319,10 @@ export const useMainStore = defineStore('mainStore', {
...scenery, ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes, routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? [] checkpoints:
scenery.checkpoints && scenery.checkpoints.trim().length > 0
? scenery.checkpoints.split(';')
: []
} }
}; };
}); });
-142
View File
@@ -1,142 +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,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
'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,
maxVmax: 200,
minVmax: 0,
authors: '',
onlineFromHours: 0
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
lastClickedFilterId: ''
};
},
getters: {
areFiltersAtDefault: (state) => {
return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
},
filteredStationList: (state) => {
const store = useMainStore();
return store.allStationInfo
.filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive));
}
},
actions: {
setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name);
this.filters[option.name] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(name: string, value: any) {
this.filters[name] = value;
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
},
resetFilters() {
this.filters = { ...filterInitStates };
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
resetSectionOptions(section: string) {
this.inputs.options
.filter((option) => option.section == section)
.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == this.sorterActive.headerName)
this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.headerName = headerName;
}
}
});
+1 -1
View File
@@ -1,5 +1,5 @@
import { API } from '../typings/api'; import { API } from '../typings/api';
import { Availability, StationRoutesInfo, Status } from '../typings/common'; import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export interface MainStoreState { export interface MainStoreState {
region: { id: string; value: string; name: string }; region: { id: string; value: string; name: string };
+1 -189
View File
@@ -1,19 +1,4 @@
import { import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
TrainStop,
StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
export function getLocoURL(locoType: string): string {
return `https://rj.td2.info.pl/dist/img/thumbnails/${
locoType.includes('EN') ? locoType + 'rb' : locoType
}.png`;
}
export function getStatusTimestamp(stationStatus: any): number { export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2; if (!stationStatus) return -2;
@@ -63,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
export function getTimestamp(date: string | null): number { export function getTimestamp(date: string | null): number {
return date ? new Date(date).getTime() : 0; return date ? new Date(date).getTime() : 0;
} }
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
let stopStatus = StopStatus.ARRIVING,
stopLabel = '',
stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = StopStatus.TERMINATED;
stopLabel = 'Skończył bieg';
stopStatusID = 5;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
stopStatus = StopStatus.DEPARTED;
stopLabel = 'Odprawiony';
stopStatusID = 2;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
stopStatus = StopStatus.DEPARTED_AWAY;
stopLabel = 'Odjechał';
stopStatusID = 4;
} else if (currentStationName == sceneryName && !stopInfo.stopped) {
stopStatus = StopStatus.ONLINE;
stopLabel = 'Na stacji';
stopStatusID = 0;
} else if (currentStationName == sceneryName && stopInfo.stopped) {
stopStatus = StopStatus.STOPPED;
stopLabel = 'Postój';
stopStatusID = 1;
} else if (currentStationName != sceneryName) {
stopStatus = StopStatus.ARRIVING;
stopLabel = 'W drodze';
stopStatusID = 3;
}
return { stopStatus, stopLabel, stopStatusID };
}
export function getCheckpointTrain(
train: Train,
trainStopIndex: number,
sceneryName: string
): ScheduledTrain {
const timetable = train.timetableData!;
const followingStops = timetable.followingStops;
const trainStop = followingStops[trainStopIndex];
const trainStopStatus = getTrainStopStatus(trainStop, train.currentStationName, sceneryName);
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex; i >= 0; i--) {
const stop = followingStops[i];
if (/strong|podg\.|pe\./g.test(stop.stopName) && !prevStationName && i <= trainStopIndex - 1)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (stop.arrivalLine != null && !arrivingLine && !/-|_|it|sbl/gi.test(stop.arrivalLine)) {
arrivingLine = stop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < followingStops.length; i++) {
const stop = followingStops[i];
if (/strong|podg\.|pe\./g.test(stop.stopName) && !nextStationName && i > trainStopIndex)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (stop.departureLine && !departureLine && !/-|_|it|sbl/gi.test(stop.departureLine)) {
departureLine = stop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
}
}
return {
checkpointName: trainStop.stopNameRAW,
trainNo: train.trainNo,
trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nextStationName,
prevStationName,
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID,
region: train.region,
arrivingLine: arrivingLine,
departureLine: departureLine,
nextArrivalLine,
prevDepartureLine
};
}
export function getScheduledTrains(
trainList: Train[],
stationGeneralInfo: Station['generalInfo'],
stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] {
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
if (train.region != region) return acc;
const timetable = train.timetableData;
if (!timetable.sceneryNames.includes(stationName)) return acc;
const checkpoints = [stationName];
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
const checkpointScheduledTrains: ScheduledTrain[] = [];
for (let i = 0; i < timetable.followingStops.length; i++) {
if (
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
timetable.followingStops[i].stopNameRAW
)
) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
}
}
acc.push(...checkpointScheduledTrains);
return acc;
}, []) as ScheduledTrain[];
}
export function getStationTrains(
trainList: Train[],
scheduledTrainList: ScheduledTrain[],
region: string,
stationName: string
): StationTrain[] {
return trainList
.filter(
(train) =>
train?.region === region && train.online && train.currentStationName === stationName
)
.map((train) => ({
driverName: train.driverName,
driverId: train.driverId,
trainNo: train.trainNo,
trainId: train.trainId,
stopStatus:
scheduledTrainList.find((st) => st.trainNo === train.trainNo)?.stopStatus || 'no-timetable'
}));
}
+1 -1
View File
@@ -4,7 +4,7 @@
.list_wrapper { .list_wrapper {
overflow-y: auto; overflow-y: auto;
height: 90vh; height: 90vh;
min-height: 550px; min-height: 650px;
margin-top: 0.5em; margin-top: 0.5em;
position: relative; position: relative;
+31 -21
View File
@@ -121,8 +121,6 @@ input {
height: 7px; height: 7px;
background-color: lightgreen; background-color: lightgreen;
border-radius: 50%; border-radius: 50%;
margin-left: 10px;
} }
a { a {
@@ -230,6 +228,10 @@ a.a-button {
background-color: #3c3c3c; background-color: #3c3c3c;
} }
&:hover {
background-color: #555;
}
} }
&.btn--image { &.btn--image {
@@ -285,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 { @include smallScreen {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.5em; width: 0.5em;
@@ -298,24 +321,11 @@ a.a-button {
background-color: #777; background-color: #777;
} }
} }
}
// Basic tooltip [data-tooltip]:hover::after,
[data-tooltip]:hover::after, [data-tooltip]:focus::after {
[data-tooltip]:focus::after { transform: translate(-50%, 2em);
position: absolute; left: 50%;
transform: translate(10px, -50%); width: 100%;
}
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;
} }
+3 -7
View File
@@ -1,11 +1,7 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table { table.scenery-history-table {
width: 100%; width: 100%;
table-layout: fixed;
min-width: 900px;
border-collapse: collapse; border-collapse: collapse;
thead { thead {
@@ -25,7 +21,7 @@ table.scenery-history-table {
td { td {
padding: 0.75em; padding: 0.75em;
border-bottom: solid 5px #111; border-bottom: solid 5px #181818;
} }
} }
+7 -2
View File
@@ -1,4 +1,4 @@
import { Status } from './common'; import { Status, VehicleData } from './common';
export enum APIDataStatus { export enum APIDataStatus {
OK = 'OK', OK = 'OK',
@@ -19,6 +19,7 @@ export namespace API {
apiStatuses?: APIStatuses; apiStatuses?: APIStatuses;
} }
} }
export namespace DispatcherHistory { export namespace DispatcherHistory {
export type Response = Data[]; export type Response = Data[];
@@ -38,6 +39,7 @@ export namespace API {
stationName: string; stationName: string;
timestampFrom: number; timestampFrom: number;
timestampTo?: number; timestampTo?: number;
statusHistory: string[];
} }
} }
@@ -161,7 +163,6 @@ export namespace API {
stopNameRAW: string; stopNameRAW: string;
stopType: string; stopType: string;
stopDistance: number; stopDistance: number;
pointId: string;
mainStop: boolean; mainStop: boolean;
@@ -317,6 +318,10 @@ export namespace API {
export namespace Donators { export namespace Donators {
export type Response = string[]; export type Response = string[];
} }
export namespace Vehicles {
export type Response = VehicleData[];
}
} }
export namespace GithubAPI { export namespace GithubAPI {
+60 -74
View File
@@ -41,7 +41,7 @@ export interface RegionCounters {
export interface Train { export interface Train {
id: string; id: string;
trainId: string; modalId: string;
mass: number; mass: number;
length: number; length: number;
speed: number; speed: number;
@@ -79,35 +79,30 @@ export interface Train {
export interface Station { export interface Station {
name: string; name: string;
generalInfo?: { generalInfo?: StationGeneralInfo;
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
onlineInfo?: ActiveScenery; onlineInfo?: ActiveScenery;
} }
export interface StationGeneralInfo {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
}
export interface StationRoutes { export interface StationRoutes {
single: StationRoutesInfo[]; single: StationRoutesInfo[];
double: StationRoutesInfo[]; double: StationRoutesInfo[];
@@ -148,8 +143,8 @@ export interface ActiveScenery {
dispatcherStatus: Status.ActiveDispatcher | number; dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null; dispatcherTimestamp: number | null;
isOnline: boolean; isOnline: boolean;
stationTrains?: StationTrain[]; stationTrains: Train[];
scheduledTrains?: ScheduledTrain[]; scheduledTrains: CheckpointTrain[];
scheduledTrainCount: { scheduledTrainCount: {
all: number; all: number;
confirmed: number; confirmed: number;
@@ -164,49 +159,6 @@ export interface ScenerySpawn {
spawnType: ScenerySpawnType; spawnType: ScenerySpawnType;
} }
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export interface TrainStop { export interface TrainStop {
stopName: string; stopName: string;
stopNameRAW: string; stopNameRAW: string;
@@ -223,13 +175,47 @@ export interface TrainStop {
departureTimestamp: number; departureTimestamp: number;
departureRealTimestamp: number; departureRealTimestamp: number;
departureDelay: number; departureDelay: number;
pointId: number;
comments?: string; comments?: string;
beginsHere: boolean; beginsHere: boolean;
terminatesHere: boolean; terminatesHere: boolean;
confirmed: boolean; confirmed: number;
stopped: boolean; stopped: number;
stopTime: number | null; stopTime: number | null;
} }
export interface CheckpointTrain {
checkpointStop: TrainStop;
train: Train;
}
// 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;
}
+3 -2
View File
@@ -17,8 +17,9 @@
<JournalStats :statsButtons="statsButtons" /> <JournalStats :statsButtons="statsButtons" />
</div> </div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt"> <div class="journal_refreshed-date">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} {{ $t('journal.data-refreshed-at') }}:
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
</div> </div>
<div class="list_wrapper" @scroll="handleScroll"> <div class="list_wrapper" @scroll="handleScroll">
+24 -7
View File
@@ -17,8 +17,9 @@
<JournalStats :statsButtons="statsButtons" /> <JournalStats :statsButtons="statsButtons" />
</div> </div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt"> <div class="journal_refreshed-date">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }} {{ $t('journal.data-refreshed-at') }}:
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
</div> </div>
<div class="list_wrapper" @scroll="handleScroll"> <div class="list_wrapper" @scroll="handleScroll">
@@ -125,6 +126,8 @@ interface TimetablesQueryParams {
dateTo?: string; dateTo?: string;
issuedFrom?: string; issuedFrom?: string;
terminatingAt?: string;
via?: string;
countFrom?: number; countFrom?: number;
countLimit?: number; countLimit?: number;
@@ -205,6 +208,8 @@ export default defineComponent({
'search-driver': '', 'search-driver': '',
'search-dispatcher': '', 'search-dispatcher': '',
'search-issuedFrom': '', 'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-date': '' 'search-date': ''
} as Journal.TimetableSearchType); } as Journal.TimetableSearchType);
@@ -298,11 +303,17 @@ export default defineComponent({
}, },
setOptions(options: { [key: string]: string }) { setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? ''; Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues['search-driver'] = options['search-driver'] ?? ''; this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? ''; });
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? ''; // 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 = this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId'; (options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
@@ -346,6 +357,8 @@ export default defineComponent({
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined; const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateFrom = this.searchersValues['search-date'].trim() || undefined; const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].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; let dateTo: string | undefined = undefined;
@@ -417,6 +430,10 @@ export default defineComponent({
queryParams['authorName'] = authorName; queryParams['authorName'] = authorName;
queryParams['dateFrom'] = dateFrom; queryParams['dateFrom'] = dateFrom;
queryParams['dateTo'] = dateTo; queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom;
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['issuedFrom'] = issuedFrom; queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] = queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined; this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
+16 -30
View File
@@ -22,8 +22,8 @@
v-for="(viewMode, i) in viewModes" v-for="(viewMode, i) in viewModes"
:key="i" :key="i"
class="btn btn--option" class="btn btn--option"
:class="{ checked: currentMode == viewMode.component }"
@click="setViewMode(viewMode.component)" @click="setViewMode(viewMode.component)"
:data-checked="currentMode == viewMode.component"
> >
{{ $t(viewMode.id) }} {{ $t(viewMode.id) }}
</button> </button>
@@ -121,10 +121,6 @@ export default defineComponent({
Status: Status.Data Status: Status.Data
}), }),
// activated() {
// this.loadSelectedCheckpoint();
// },
setup() { setup() {
const route = useRoute(); const route = useRoute();
@@ -200,7 +196,7 @@ button.back-btn {
display: inline-block; display: inline-block;
font-size: 1.5em; font-size: 1.25em;
button { button {
margin: 1em auto; margin: 1em auto;
@@ -215,11 +211,10 @@ button.back-btn {
position: relative; position: relative;
width: 100%;
max-width: var(--max-container-width); max-width: var(--max-container-width);
min-height: 100vh; width: 100%;
margin: 1rem 0; padding: 1rem 0;
text-align: center; text-align: center;
&[data-timetable-only='true'] { &[data-timetable-only='true'] {
@@ -228,30 +223,27 @@ button.back-btn {
} }
} }
.scenery-left { .scenery-left,
.scenery-right {
position: relative; position: relative;
overflow: auto;
background-color: #181818; background-color: #181818;
padding: 1em 0.5em; padding: 1em 0.5em;
height: 95vh; height: calc(100vh - 0.5em);
min-height: 750px; min-height: 800px;
max-height: 1000px; max-height: 2000px;
overflow: auto; }
.scenery-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.scenery-right { .scenery-right {
background: #181818;
padding: 1em 0.5em;
height: 95vh;
min-height: 750px;
max-height: 1000px;
display: grid; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr;
gap: 1em; gap: 1em;
} }
@@ -261,18 +253,12 @@ button.back-btn {
.info-actions { .info-actions {
display: flex; display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
gap: 0.75em; gap: 0.75em;
.btn { button {
padding: 0.5em; padding: 0.5em;
box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] {
color: var(--clr-primary);
}
} }
} }
+28 -14
View File
@@ -11,16 +11,16 @@
<button <button
class="btn-donation btn--image" class="btn-donation btn--image"
ref="btn" ref="btn"
@click="isDonationModalOpen = true" @click="isDonationCardOpen = true"
@focus="isDonationModalOpen = false" @focus="isDonationCardOpen = false"
> >
<img src="/images/icon-dollar.svg" alt="dollar donation icon" /> <img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span> <span>{{ $t('donations.button-title') }}</span>
</button> </button>
</div> </div>
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" /> <DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggleDonationModal="toggleDonationModal" /> <StationTable @toggle-donation-card="toggleDonationCard" />
<StationStats /> <StationStats />
</div> </div>
</section> </section>
@@ -30,34 +30,47 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue'; import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import DonationModal from '../components/Global/DonationModal.vue'; import DonationCard from '../components/Global/DonationCard.vue';
import StationStats from '../components/StationsView/StationStats.vue'; import StationStats from '../components/StationsView/StationStats.vue';
import { initFilters, setupFilters } from '../managers/stationFilterManager';
import { reactive } from 'vue';
import { provide } from 'vue';
import { ActiveSorter } from '../components/StationsView/typings';
import { onMounted } from 'vue';
const filterInitStates = { ...initFilters };
export default defineComponent({ export default defineComponent({
components: { components: {
StationTable, StationTable,
StationFilterCard, StationFilterCard,
StationStats, StationStats,
DonationModal DonationCard
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
isDonationModalOpen: false, isDonationCardOpen: false,
filterStore: useStationFiltersStore(), mainStore: useMainStore()
store: useMainStore()
}), }),
mounted() { setup() {
this.filterStore.setupFilters(); const filters = reactive(filterInitStates);
const activeSorter = reactive({ headerName: 'station', dir: 1 }) as ActiveSorter;
provide('StationsView_filters', filters);
provide('StationsView_activeSorter', activeSorter);
onMounted(() => {
setupFilters(filters);
});
}, },
methods: { methods: {
toggleDonationModal(value: boolean) { toggleDonationCard(value: boolean) {
this.isDonationModalOpen = value; this.isDonationCardOpen = value;
} }
} }
}); });
@@ -84,6 +97,7 @@ export default defineComponent({
.stations-options { .stations-options {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
+1 -1
View File
@@ -109,7 +109,7 @@ export default defineComponent({
this.$nextTick(() => { this.$nextTick(() => {
if (this.trainId) { if (this.trainId) {
this.selectModalTrain(this.trainId); this.selectModalTrainById(this.trainId);
} }
}); });
} }
+4 -2
View File
@@ -6,8 +6,10 @@ declare module '*.vue' {
export default component; export default component;
} }
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_APP_API_DEV: string; readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
readonly VITE_APP_WS_DEV: string; 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 { 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
+8 -23
View File
@@ -7,6 +7,10 @@ export default defineConfig({
port: 5001, port: 5001,
open: true open: true
}, },
preview: {
port: 4001,
open: true
},
publicDir: 'public', publicDir: 'public',
plugins: [ plugins: [
vue(), vue(),
@@ -20,35 +24,16 @@ export default defineConfig({
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'), urlPattern:
handler: 'CacheFirst', /^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
handler: 'StaleWhileRevalidate',
options: { options: {
cacheName: 'spythere-sceneries-cache', cacheName: 'stacjownik-api-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200] statuses: [0, 200]
} }
} }
}, },
{
urlPattern: new RegExp('^https://rj.td2.info.pl/dist/img/thumbnails/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'swdr-images-cache',
cacheableResponse: {
statuses: [0, 200, 404]
}
}
},
{
urlPattern: new RegExp('^https://static.spythere.eu/images/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'spythere-images-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
}
] ]
}, },
devOptions: { devOptions: {