Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 619ce97b52 | |||
| 47d35f335f | |||
| 8fda8fa0df | |||
| 71d697eda5 | |||
| f2b1fc5369 | |||
| 4a9b142e16 | |||
| 08d8bf3c57 | |||
| 0ee90357aa | |||
| c8964dc20f | |||
| 6a62276d95 | |||
| b8550eed9a | |||
| 27b23ccc95 | |||
| b49517aded | |||
| ed2b8be4dc | |||
| 54c1dbbf15 | |||
| 0ac7ba51e5 | |||
| bdf85cd8ec | |||
| 63b268d9b9 | |||
| d73c8ef112 | |||
| 3d1c66b420 | |||
| b3f7108979 | |||
| feabfd29e0 | |||
| f17fedc976 | |||
| c83c75e014 | |||
| e57143f517 | |||
| fb45a783ee | |||
| 71476e9552 | |||
| 922a338143 | |||
| 231d36e877 | |||
| 27d6ac9f14 | |||
| a6029da2cc | |||
| a3f3790205 | |||
| ebfb24f729 | |||
| e521736618 | |||
| fc7662e431 | |||
| a459fdf178 | |||
| 4e7fba89ee | |||
| 6084e5876d | |||
| 44f548c7b7 | |||
| 59a5fbe5ac | |||
| c252213ed9 | |||
| fb56378f18 | |||
| e9635eae06 | |||
| 1fc98a8f99 | |||
| c9de1a48ce | |||
| fee9774f88 | |||
| 7c974e8d0e | |||
| c84fbbcf42 | |||
| 45af649505 | |||
| 6c1e00d002 | |||
| 69ff85cfb1 | |||
| bdc2ca784c | |||
| dbd73d448d | |||
| 26b1ec246d | |||
| 8190dfa2cb | |||
| 44df685606 | |||
| 785a42b849 | |||
| ccfcca8728 | |||
| d9a7ba122c | |||
| bf8d4a9ef4 | |||
| 6ea1e91d1d | |||
| 813b557455 | |||
| 834b14da69 | |||
| c809b2146d | |||
| 33b98ca313 | |||
| bcb9c63cb0 | |||
| 17d77a80d8 | |||
| 65b159f8fd | |||
| 063d5283e4 | |||
| 29de1b3c4b | |||
| f0c02bf12e | |||
| 8aa23468b3 | |||
| 4c1fcf710b | |||
| a529d6e9eb | |||
| 9fc602e08f | |||
| 56e40bd84b | |||
| a5b5df7452 | |||
| 1a8da02ced | |||
| 7e75fa2516 | |||
| 3ed2c09184 | |||
| 6901c3d2b4 | |||
| 8417754403 | |||
| de5c57181a | |||
| d91d4cc6a8 | |||
| 9a5fd4d670 | |||
| 4202a55673 | |||
| 5181e8f4af | |||
| e117f62fcb | |||
| e0036bf969 |
@@ -1,20 +0,0 @@
|
||||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
|
||||
channelId: live
|
||||
projectId: stacjownik-td2
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: npm ci && npm run build
|
||||
- run: yarn && yarn build
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.24.0",
|
||||
"version": "1.26.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"deploy": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:prod": "yarn build && firebase deploy --only hosting",
|
||||
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
|
||||
"preview": "yarn build && vite preview",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
@@ -19,25 +21,20 @@
|
||||
"showdown": "^2.1.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/node": "^20.6.2",
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@vite-pwa/assets-generator": "^0.2.4",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"axios": "^1.5.0",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"@vitejs/plugin-vue": "^5.1.0",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"axios": "^1.7.2",
|
||||
"prettier": "^3.3.3",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vue-tsc": "^1.8.11"
|
||||
"vue-tsc": "^2.0.28"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="app_container">
|
||||
<UpdateModal
|
||||
:update-modal-open="isUpdateModalOpen"
|
||||
@toggle-modal="() => (isUpdateModalOpen = false)"
|
||||
<UpdateCard
|
||||
:is-update-card-open="isUpdateCardOpen"
|
||||
@toggle-card="() => (isUpdateCardOpen = false)"
|
||||
/>
|
||||
|
||||
<Tooltip />
|
||||
@@ -27,7 +27,7 @@
|
||||
©
|
||||
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
|
||||
{{ new Date().getUTCFullYear() }} |
|
||||
<button class="btn--text" @click="() => (isUpdateModalOpen = true)">
|
||||
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
|
||||
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||
</button>
|
||||
|
||||
@@ -56,7 +56,7 @@ import StatusIndicator from './components/App/StatusIndicator.vue';
|
||||
import AppHeader from './components/App/AppHeader.vue';
|
||||
import TrainModal from './components/TrainsView/TrainModal.vue';
|
||||
import Tooltip from './components/Tooltip/Tooltip.vue';
|
||||
import UpdateModal from './components/App/UpdateModal.vue';
|
||||
import UpdateCard from './components/App/UpdateCard.vue';
|
||||
|
||||
import StorageManager from './managers/storageManager';
|
||||
|
||||
@@ -68,7 +68,7 @@ export default defineComponent({
|
||||
StatusIndicator,
|
||||
AppHeader,
|
||||
TrainModal,
|
||||
UpdateModal,
|
||||
UpdateCard,
|
||||
Tooltip
|
||||
},
|
||||
|
||||
@@ -78,12 +78,10 @@ export default defineComponent({
|
||||
apiStore: useApiStore(),
|
||||
tooltipStore: useTooltipStore(),
|
||||
|
||||
isUpdateModalOpen: false,
|
||||
isUpdateCardOpen: false,
|
||||
|
||||
currentLang: 'pl',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
|
||||
nextUpdateTime: 0
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
||||
}),
|
||||
|
||||
created() {
|
||||
@@ -96,22 +94,13 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
init() {
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
|
||||
this.loadLang();
|
||||
this.setupOfflineHandling();
|
||||
this.checkAppVersion();
|
||||
|
||||
this.apiStore.setupAPIData();
|
||||
window.requestAnimationFrame(this.update);
|
||||
|
||||
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
|
||||
},
|
||||
|
||||
update(t: number) {
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.apiStore.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
window.requestAnimationFrame(this.update);
|
||||
},
|
||||
|
||||
async checkAppVersion() {
|
||||
@@ -130,8 +119,9 @@ export default defineComponent({
|
||||
releaseURL: releaseData.html_url
|
||||
};
|
||||
|
||||
this.isUpdateModalOpen =
|
||||
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||
this.isUpdateCardOpen =
|
||||
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
|
||||
import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||
} catch (error) {
|
||||
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
||||
}
|
||||
@@ -157,6 +147,7 @@ export default defineComponent({
|
||||
|
||||
handleOnlineMode() {
|
||||
this.store.isOffline = false;
|
||||
this.apiStore.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
this.apiStore.connectToAPI();
|
||||
},
|
||||
@@ -180,7 +171,7 @@ export default defineComponent({
|
||||
|
||||
const naviLanguage = window.navigator.language.toString();
|
||||
|
||||
if (naviLanguage.includes('en')) {
|
||||
if (naviLanguage.startsWith('en')) {
|
||||
this.changeLang('en');
|
||||
return;
|
||||
}
|
||||
@@ -210,7 +201,7 @@ export default defineComponent({
|
||||
overflow-x: hidden;
|
||||
|
||||
@include smallScreen() {
|
||||
font-size: calc(0.65rem + 0.8vw);
|
||||
font-size: calc(0.65rem + 0.85vw);
|
||||
}
|
||||
|
||||
@include screenLandscape() {
|
||||
@@ -226,6 +217,7 @@ export default defineComponent({
|
||||
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app_main {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
|
||||
<div class="modal-content">
|
||||
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
||||
<div class="content">
|
||||
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
||||
|
||||
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
||||
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
|
||||
|
||||
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)">
|
||||
<button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
|
||||
{{ $t('update.confirm') }}
|
||||
</button>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<span v-html="$t('update.info-2')"></span>
|
||||
</p>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
|
||||
import { version } from '../../../package.json';
|
||||
import { Converter } from 'showdown';
|
||||
|
||||
import AnimatedModal from '../Global/AnimatedModal.vue';
|
||||
import Card from '../Global/Card.vue';
|
||||
|
||||
const converter = new Converter();
|
||||
|
||||
export default defineComponent({
|
||||
components: { AnimatedModal },
|
||||
components: { Card },
|
||||
|
||||
props: {
|
||||
updateModalOpen: {
|
||||
isUpdateCardOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['toggleModal'],
|
||||
emits: ['toggleCard'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
@@ -49,7 +49,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateModalOpen(val: boolean) {
|
||||
isUpdateCardOpen(val: boolean) {
|
||||
this.$nextTick(() => {
|
||||
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
||||
});
|
||||
@@ -60,37 +60,38 @@ export default defineComponent({
|
||||
htmlChangelog() {
|
||||
if (this.mainStore.appUpdate == null) return '';
|
||||
|
||||
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
|
||||
console.log(x);
|
||||
|
||||
return x;
|
||||
return converter.makeHtml(this.mainStore.appUpdate.changelog);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleModal(value: boolean) {
|
||||
this.$emit('toggleModal', value);
|
||||
toggleCard(value: boolean) {
|
||||
this.$emit('toggleCard', value);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/variables';
|
||||
|
||||
::v-deep(h1) {
|
||||
text-align: center;
|
||||
color: $accentCol;
|
||||
}
|
||||
|
||||
::v-deep(h2) {
|
||||
padding: 0.25em 0;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
::v-deep(ul) {
|
||||
list-style: inside;
|
||||
padding: 0.5em;
|
||||
list-style: initial;
|
||||
padding: 1em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 0.5em;
|
||||
@@ -98,6 +99,7 @@ export default defineComponent({
|
||||
min-height: 700px;
|
||||
overflow: auto;
|
||||
text-align: justify;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.no-features {
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<transition name="modal-anim" tag="div">
|
||||
<div class="modal" v-if="isOpen">
|
||||
<div class="modal-background" @click="toggleModal(false)"></div>
|
||||
<div class="modal-wrapper" ref="wrapper" tabindex="0">
|
||||
<div class="card" v-if="isOpen">
|
||||
<div class="card-background" @click="toggleCard(false)"></div>
|
||||
<div class="card-body" tabindex="0">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['toggleModal'],
|
||||
emits: ['toggleCard'],
|
||||
|
||||
props: {
|
||||
isOpen: Boolean
|
||||
@@ -36,8 +35,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleModal(value: boolean) {
|
||||
this.$emit('toggleModal', value);
|
||||
toggleCard(value: boolean) {
|
||||
this.$emit('toggleCard', value);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -46,17 +45,21 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.modal {
|
||||
.card {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
z-index: 200;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-background {
|
||||
.card-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -68,21 +71,23 @@ export default defineComponent({
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 210;
|
||||
overflow: auto;
|
||||
.card-body {
|
||||
position: relative;
|
||||
|
||||
margin: 1em;
|
||||
|
||||
max-height: 95vh;
|
||||
max-height: 95dvh;
|
||||
|
||||
& > :slotted(div) {
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
|
||||
width: 95vw;
|
||||
max-width: 850px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.card {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<AnimatedModal
|
||||
class="donation-modal"
|
||||
:isOpen="isModalOpen"
|
||||
@toggleModal="toggleModal"
|
||||
@keydown.esc="toggleModal(false)"
|
||||
>
|
||||
<div class="modal_content">
|
||||
<div class="modal_main">
|
||||
<Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
|
||||
<div class="body">
|
||||
<div class="content">
|
||||
<h1 v-html="$t('donations.header')"></h1>
|
||||
<div class="donators-slider" v-if="donatorList.length != 0">
|
||||
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
|
||||
@@ -61,9 +56,9 @@
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<div class="modal_actions">
|
||||
<div class="actions">
|
||||
<a
|
||||
class="modal-action a-button btn--image coffee"
|
||||
class="action a-button btn--image coffee"
|
||||
href="https://buycoffee.to/spythere"
|
||||
target="_blank"
|
||||
ref="action"
|
||||
@@ -73,7 +68,7 @@
|
||||
</a>
|
||||
|
||||
<a
|
||||
class="modal-action a-button btn--image paypal"
|
||||
class="action a-button btn--image paypal"
|
||||
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
|
||||
target="_blank"
|
||||
>
|
||||
@@ -81,30 +76,30 @@
|
||||
{{ $t('donations.action-paypal') }}
|
||||
</a>
|
||||
|
||||
<button class="modal-action btn--image exit" @click="toggleModal(false)">
|
||||
<button class="action btn--image exit" @click="toggleCard(false)">
|
||||
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
|
||||
{{ $t('donations.action-exit') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import AnimatedModal from './AnimatedModal.vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import Card from './Card.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { AnimatedModal },
|
||||
components: { Card },
|
||||
props: {
|
||||
isModalOpen: Boolean
|
||||
isCardOpen: Boolean
|
||||
},
|
||||
|
||||
emits: ['toggleModal'],
|
||||
emits: ['toggleCard'],
|
||||
|
||||
watch: {
|
||||
isModalOpen(val: boolean) {
|
||||
isCardOpen(val: boolean) {
|
||||
this.running = val;
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
@@ -138,8 +133,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleModal(value: boolean) {
|
||||
this.$emit('toggleModal', value);
|
||||
toggleCard(value: boolean) {
|
||||
this.$emit('toggleCard', value);
|
||||
},
|
||||
|
||||
runUpdate() {
|
||||
@@ -157,53 +152,53 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.modal_content {
|
||||
.body {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 1em;
|
||||
|
||||
font-size: 1.1em;
|
||||
|
||||
& > div {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.95em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
a.discord {
|
||||
text-decoration: underline;
|
||||
}
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.modal_main {
|
||||
.content {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
img {
|
||||
max-height: 20px;
|
||||
margin-right: 5px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.modal_actions {
|
||||
img {
|
||||
max-height: 20px;
|
||||
margin-right: 5px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.95em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
a.discord {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 0.5em;
|
||||
padding: 1em;
|
||||
|
||||
form button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal_actions > .modal-action {
|
||||
.actions > .action {
|
||||
&.paypal {
|
||||
$btnColor: #254069;
|
||||
|
||||
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
|
||||
.status-badge {
|
||||
border-radius: 1em;
|
||||
font-weight: 500;
|
||||
text-wrap: nowrap;
|
||||
|
||||
padding: 0.2em 0.55em;
|
||||
padding: 0.2rem 0.55rem;
|
||||
|
||||
background-color: $online;
|
||||
|
||||
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
|
||||
|
||||
&.no-limit {
|
||||
background-color: $no-limit;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.not-signed,
|
||||
&.unavailable {
|
||||
background-color: $unav;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.afk {
|
||||
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
|
||||
background-color: $no-space;
|
||||
border: 1px solid white;
|
||||
color: white;
|
||||
font-size: 0.85em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
&.unknown,
|
||||
|
||||
@@ -1,80 +1,22 @@
|
||||
<template>
|
||||
<div class="stock-list">
|
||||
<div v-if="tractionOnly">
|
||||
<p>
|
||||
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }}
|
||||
{{ computedStockList[0].split(':')[1] }}
|
||||
</p>
|
||||
|
||||
<img
|
||||
class="traction-only"
|
||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
|
||||
/^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>
|
||||
<ul>
|
||||
<li
|
||||
v-for="({ vehicleName, vehicleCargo, images, imagesFallbacks }, i) in thumbnailNames"
|
||||
:key="i"
|
||||
>
|
||||
<div class="stock-text">
|
||||
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
|
||||
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
<img
|
||||
:data-mouseover="stockName"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
:data-tooltip-content="stockName.split(':')[0]"
|
||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
|
||||
/^EN/.test(stockName) ? 'rb' : ''
|
||||
}.png`"
|
||||
@error="onImageError($event, stockName)"
|
||||
@click.stop="() => {}"
|
||||
width="400"
|
||||
height="60"
|
||||
<VehicleThumbnail
|
||||
v-for="(thumbnailImage, imageIndex) in images"
|
||||
:vehicle-name="vehicleName"
|
||||
:img-name="thumbnailImage"
|
||||
:fallback-name="imagesFallbacks[imageIndex]"
|
||||
/>
|
||||
|
||||
<!-- /// 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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -84,8 +26,11 @@
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import VehicleThumbnail from './VehicleThumbnail.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { VehicleThumbnail },
|
||||
|
||||
props: {
|
||||
trainStockList: {
|
||||
type: Array as PropType<string[]>,
|
||||
@@ -106,28 +51,116 @@ export default defineComponent({
|
||||
computed: {
|
||||
computedStockList() {
|
||||
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
|
||||
},
|
||||
|
||||
thumbnailNames() {
|
||||
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
|
||||
.filter((v) => v.length != 0)
|
||||
.map((vehicleString) => {
|
||||
const [vehicleName, vehicleCargo] = vehicleString.split(':');
|
||||
|
||||
const vehicleThumbnailData = {
|
||||
images: [] as string[],
|
||||
imagesFallbacks: [] as string[],
|
||||
vehicleName,
|
||||
vehicleCargo
|
||||
};
|
||||
|
||||
// Generowanie członów EN57
|
||||
if (vehicleName.startsWith('EN57')) {
|
||||
vehicleThumbnailData['images'] = [
|
||||
vehicleName + 'ra',
|
||||
vehicleName + 's',
|
||||
vehicleName + 'rb'
|
||||
];
|
||||
vehicleThumbnailData['imagesFallbacks'] = [
|
||||
'unknown_ezt-ra',
|
||||
'unknown_ezt-s',
|
||||
'unknown_ezt-rb'
|
||||
];
|
||||
}
|
||||
// Generowanie członów EN71
|
||||
else if (vehicleName.startsWith('EN71')) {
|
||||
vehicleThumbnailData['images'] = [
|
||||
vehicleName + 'ra',
|
||||
vehicleName + 'sa',
|
||||
vehicleName + 'sb',
|
||||
vehicleName + 'rb'
|
||||
];
|
||||
vehicleThumbnailData['imagesFallbacks'] = [
|
||||
'unknown_ezt-ra',
|
||||
'unknown_ezt-sa',
|
||||
'unknown_ezt-sb',
|
||||
'unknown_ezt-rb'
|
||||
];
|
||||
}
|
||||
// Generowanie pojazdów i członów 2EN57
|
||||
else if (vehicleString.startsWith('2EN57')) {
|
||||
const [firstVehicleNumber, secondVehicleNumber] = vehicleString
|
||||
.replace('2EN57-', '')
|
||||
.split('+');
|
||||
|
||||
vehicleThumbnailData['images'] = [
|
||||
`EN57-${firstVehicleNumber}ra`,
|
||||
`EN57-${firstVehicleNumber}s`,
|
||||
`EN57-${firstVehicleNumber}rb`,
|
||||
`EN57-${secondVehicleNumber}ra`,
|
||||
`EN57-${secondVehicleNumber}s`,
|
||||
`EN57-${secondVehicleNumber}rb`
|
||||
];
|
||||
|
||||
vehicleThumbnailData['imagesFallbacks'] = [
|
||||
'unknown_ezt-ra',
|
||||
'unknown_ezt-s',
|
||||
'unknown_ezt-rb',
|
||||
'unknown_ezt-ra',
|
||||
'unknown_ezt-s',
|
||||
'unknown_ezt-rb'
|
||||
];
|
||||
}
|
||||
// Generowanie członów Gor77
|
||||
else if (vehicleString.startsWith('Gor77')) {
|
||||
vehicleThumbnailData['images'] = [
|
||||
vehicleName + '-A',
|
||||
vehicleName + '-B',
|
||||
vehicleName + '-C',
|
||||
vehicleName + '-D'
|
||||
];
|
||||
vehicleThumbnailData['imagesFallbacks'] = [
|
||||
'unknown_Gor77-A',
|
||||
'unknown_Gor77-B',
|
||||
'unknown_Gor77-C',
|
||||
'unknown_Gor77-D'
|
||||
];
|
||||
}
|
||||
// Generowanie członów ET41
|
||||
else if (vehicleString.startsWith('ET41')) {
|
||||
vehicleThumbnailData['images'] = [vehicleName + '-A', vehicleName + '-B'];
|
||||
vehicleThumbnailData['imagesFallbacks'] = ['unknown_ET41-A', 'unknown_ET41-B'];
|
||||
}
|
||||
// Generowanie pozostałych pojazdów
|
||||
else {
|
||||
let fallbackVehicleImage = 'unknown_cargo';
|
||||
|
||||
if (/^(EP|EU|ET|201E)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
|
||||
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
|
||||
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
|
||||
fallbackVehicleImage = 'unknown_passenger';
|
||||
|
||||
vehicleThumbnailData['images'] = [vehicleName];
|
||||
vehicleThumbnailData['imagesFallbacks'] = [fallbackVehicleImage];
|
||||
}
|
||||
|
||||
if (this.tractionOnly) vehicleThumbnailData['images'].length = 1;
|
||||
|
||||
return vehicleThumbnailData;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onImageError(event: Event, stockName: string) {
|
||||
let fallbackName = '';
|
||||
|
||||
const isLoco = /.-\d{3}/.test(stockName);
|
||||
|
||||
if (isLoco) {
|
||||
if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
|
||||
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
|
||||
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
|
||||
else fallbackName = 'loco-s';
|
||||
} else {
|
||||
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
|
||||
|
||||
fallbackName += 'car-';
|
||||
fallbackName += isCarPassenger ? 'passenger' : 'cargo';
|
||||
}
|
||||
|
||||
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
|
||||
onImageError(event: Event, fallbackImage: string) {
|
||||
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -163,10 +196,10 @@ img.traction-only {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
.stock-text {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
font-size: 0.95em;
|
||||
margin-bottom: 1em;
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="vehicle-thumbnail">
|
||||
<img
|
||||
ref="imgRef"
|
||||
:src="`https://static.spythere.eu/thumbnails/v2/${imgName}.png`"
|
||||
height="60"
|
||||
loading="lazy"
|
||||
:data-mouseover="vehicleName"
|
||||
:data-tooltip-content="vehicleName"
|
||||
:data-load-status="imgStatus"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
@error="onImageError"
|
||||
@load="onImageLoad"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, Ref, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
vehicleName: { type: String, required: true },
|
||||
imgName: { type: String, required: true },
|
||||
fallbackName: { type: String, required: true },
|
||||
placeholderName: String
|
||||
});
|
||||
|
||||
const imgRef = ref(null) as Ref<HTMLElement | null>;
|
||||
|
||||
const imgStatus = ref('loading');
|
||||
|
||||
function onImageError(event: Event) {
|
||||
console.log('error');
|
||||
|
||||
(event.target as HTMLImageElement).src = `/images/${props.fallbackName}.png`;
|
||||
imgStatus.value = 'error';
|
||||
}
|
||||
|
||||
function onImageLoad() {
|
||||
if (imgStatus.value != 'error') {
|
||||
imgStatus.value = 'loaded';
|
||||
}
|
||||
|
||||
imgRef.value!.style.opacity = '1';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vehicle-thumbnail {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
|
||||
&[data-load-status='loading'] {
|
||||
min-height: 60px;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<li class="dispatcher-history-entry">
|
||||
<div class="entry-info">
|
||||
<span>
|
||||
<span>
|
||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||
<b>{{ entry.stationName }}</b>
|
||||
</router-link>
|
||||
|
||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||
</span>
|
||||
•
|
||||
<b
|
||||
v-if="entry.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||
>
|
||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
<b style="margin-left: 5px">
|
||||
<span
|
||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
<router-link
|
||||
class="text--donator"
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
v-else
|
||||
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
|
||||
>
|
||||
{{ entry.dispatcherName }}
|
||||
</router-link>
|
||||
</b>
|
||||
|
||||
<div>
|
||||
<span v-if="entry.timestampTo">
|
||||
<b>{{ $d(entry.timestampFrom) }}</b>
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
-
|
||||
<b
|
||||
v-if="
|
||||
new Date(entry.timestampFrom).getDate() != new Date(entry.timestampTo).getDate()
|
||||
"
|
||||
>
|
||||
{{ $d(entry.timestampTo) }}
|
||||
</b>
|
||||
{{ timestampToString(entry.timestampTo) }} ({{
|
||||
calculateDuration(entry.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
|
||||
<router-link
|
||||
:to="`/scenery?station=${entry.stationName}`"
|
||||
class="dispatcher-online"
|
||||
v-else
|
||||
>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>
|
||||
{{
|
||||
new Date().getDate() != new Date(entry.timestampFrom).getDate()
|
||||
? $d(entry.timestampFrom)
|
||||
: ''
|
||||
}}
|
||||
{{ timestampToString(entry.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(entry.currentDuration) }})
|
||||
</router-link>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span class="entry-info-right">
|
||||
<div>
|
||||
<span>
|
||||
{{ $t('scenery.dispatcher-rate') }}
|
||||
<b class="text--primary"> {{ entry.dispatcherRate }}</b>
|
||||
</span>
|
||||
<button class="btn btn--option" @click="toggleExtraInfo">
|
||||
{{ $t('scenery.dispatcher-status-changes') }}
|
||||
<b class="text--primary">{{ entry.statusHistory.length }}</b>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<b class="region-badge" :aria-describedby="entry.region">
|
||||
REGION: {{ regions.find((r) => r.id == entry.region)?.name }}
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="entry-extra" v-if="showExtraInfo">
|
||||
<ul class="status-list">
|
||||
<li v-for="statusItem in entry.statusHistory">
|
||||
<b style="margin-right: 0.5em">{{
|
||||
timestampToString(parseInt(statusItem.split('@')[0]))
|
||||
}}</b>
|
||||
|
||||
<StationStatusBadge
|
||||
:dispatcher-status="Number(statusItem.split('@')[1])"
|
||||
:is-online="true"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { API } from '../../../typings/api';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
entry: {
|
||||
type: Object as PropType<API.DispatcherHistory.Data>,
|
||||
required: true
|
||||
},
|
||||
showExtraInfo: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
components: { StationStatusBadge },
|
||||
mixins: [dateMixin, styleMixin],
|
||||
emits: ['toggleShowExtraInfo'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
regions,
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleExtraInfo() {
|
||||
this.$emit('toggleShowExtraInfo', this.entry.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
|
||||
.region-badge {
|
||||
padding: 0 0.25em;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
.dispatcher-history-entry {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 1.75em;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.entry-info-right {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.entry-extra {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.status-list {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
gap: 0.5em;
|
||||
}
|
||||
|
||||
.status-list > li {
|
||||
background-color: #313131;
|
||||
padding: 0.2rem 0 0.2rem 0.5em;
|
||||
margin: 0.5em 0;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.entry-info {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,90 +15,16 @@
|
||||
{{ $t('app.no-result') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<table class="dispatchers-table">
|
||||
<thead>
|
||||
<th>{{ $t('journal.history-name') }}</th>
|
||||
<th>{{ $t('journal.history-hash') }}</th>
|
||||
<th>{{ $t('journal.history-dispatcher') }}</th>
|
||||
<th>{{ $t('journal.history-level') }}</th>
|
||||
<th>{{ $t('journal.history-rate') }}</th>
|
||||
<th>{{ $t('journal.history-region') }}</th>
|
||||
<th>{{ $t('journal.history-date') }}</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<transition-group name="list-anim">
|
||||
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
|
||||
>
|
||||
<b>{{ historyItem.stationName }}</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>#{{ historyItem.stationHash }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||
>
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.dispatcher-message')"
|
||||
>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
|
||||
<b v-else>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<b
|
||||
v-if="historyItem.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
:style="
|
||||
calculateExpStyle(
|
||||
historyItem.dispatcherLevel,
|
||||
historyItem.dispatcherIsSupporter
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
</td>
|
||||
<td class="text--primary">
|
||||
<b>{{ historyItem.dispatcherRate }}</b>
|
||||
</td>
|
||||
<td>
|
||||
<b class="region-badge" :aria-describedby="historyItem.region">{{
|
||||
regions.find((r) => r.id == historyItem.region)?.value || '???'
|
||||
}}</b>
|
||||
</td>
|
||||
<td style="min-width: 200px" class="time">
|
||||
<span v-if="historyItem.timestampTo" class="text--offline">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||
calculateDuration(historyItem.currentDuration)
|
||||
}})
|
||||
</span>
|
||||
<span class="dispatcher-online" v-else>
|
||||
<b class="text--online">
|
||||
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
|
||||
$t('journal.online-since')
|
||||
}}</router-link>
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</transition-group>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul v-else class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<JournalDispatcherEntry
|
||||
v-for="entry in dispatcherHistory"
|
||||
:key="entry.id"
|
||||
:entry="entry"
|
||||
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
|
||||
/>
|
||||
</transition-group>
|
||||
|
||||
<AddDataButton
|
||||
:list="dispatcherHistory"
|
||||
@@ -106,7 +32,7 @@
|
||||
:scrollNoMoreData="scrollNoMoreData"
|
||||
@addHistoryData="addHistoryData"
|
||||
/>
|
||||
</div>
|
||||
</ul>
|
||||
|
||||
<div class="journal_warning" v-if="scrollNoMoreData">
|
||||
{{ $t('journal.no-further-data') }}
|
||||
@@ -121,20 +47,15 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { regions } from '../../../data/options.json';
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
import { API } from '../../../typings/api';
|
||||
import { Status } from '../../../typings/common';
|
||||
import Loading from '../../Global/Loading.vue';
|
||||
import AddDataButton from '../../Global/AddDataButton.vue';
|
||||
import dateMixin from '../../../mixins/dateMixin';
|
||||
import styleMixin from '../../../mixins/styleMixin';
|
||||
import { useApiStore } from '../../../store/apiStore';
|
||||
import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: { Loading, AddDataButton },
|
||||
|
||||
mixins: [dateMixin, styleMixin],
|
||||
components: { Loading, AddDataButton, JournalDispatcherEntry },
|
||||
|
||||
props: {
|
||||
dispatcherHistory: {
|
||||
@@ -159,99 +80,30 @@ export default defineComponent({
|
||||
return {
|
||||
Status,
|
||||
store: useMainStore(),
|
||||
apiStore: useApiStore(),
|
||||
regions
|
||||
|
||||
extraInfoIndexes: [] as number[]
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedDispatcherHistory() {
|
||||
return this.dispatcherHistory.reduce(
|
||||
(acc, historyItem, i) => {
|
||||
if (this.isAnotherDay(i - 1, i))
|
||||
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
|
||||
acc.push(historyItem);
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as (API.DispatcherHistory.Data | string)[]
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToScenery(name: string, isOnline: boolean) {
|
||||
if (!isOnline) return;
|
||||
toggleExtraInfo(id: number) {
|
||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
||||
|
||||
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
|
||||
},
|
||||
|
||||
isAnotherDay(prevIndex: number, currIndex: number) {
|
||||
if (currIndex == 0) return true;
|
||||
|
||||
return (
|
||||
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
|
||||
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
|
||||
);
|
||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
||||
else this.extraInfoIndexes.push(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/animations.scss';
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/JournalSection.scss';
|
||||
|
||||
table.dispatchers-table {
|
||||
--_bg-table: #111;
|
||||
--_bg-head: #101010;
|
||||
--_bg-row: #2f2f2f;
|
||||
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--_bg-head);
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: var(--_bg-row);
|
||||
border-bottom: 2px solid black;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
&--online {
|
||||
color: springgreen;
|
||||
}
|
||||
|
||||
&--offline {
|
||||
color: #ddd;
|
||||
}
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -301,6 +301,6 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/dropdown.scss';
|
||||
@import '../../styles/dropdown_filters.scss';
|
||||
@import '../../styles/dropdown';
|
||||
@import '../../styles/dropdown_filters';
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
<strong
|
||||
v-if="apiStore.donatorsData.includes(timetable.driverName)"
|
||||
class="text--donator"
|
||||
:title="$t('donations.driver-message')"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.driver-message')"
|
||||
>
|
||||
{{ timetable.driverName }}
|
||||
</strong>
|
||||
@@ -103,7 +104,7 @@ export default defineComponent({
|
||||
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
|
||||
if (timetable?.terminated) return;
|
||||
|
||||
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
|
||||
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<template>
|
||||
<ul class="journal-list">
|
||||
<transition-group name="list-anim">
|
||||
<li
|
||||
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
|
||||
class="journal_item"
|
||||
:key="timetable.id"
|
||||
@click="showExtraInfo.value = !showExtraInfo.value"
|
||||
>
|
||||
<div class="journal_item-info">
|
||||
<!-- General -->
|
||||
<TimetableGeneral :timetable="timetable" />
|
||||
<!-- Route -->
|
||||
<span class="item-route">
|
||||
<b>{{ timetable.route.replace('|', ' - ') }}</b>
|
||||
</span>
|
||||
|
||||
<hr />
|
||||
<!-- Stops -->
|
||||
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
<!-- Status -->
|
||||
<TimetableStatus :timetable="timetable" />
|
||||
|
||||
<button class="btn--action btn--show">
|
||||
{{ $t('journal.stock-info') }}
|
||||
<img
|
||||
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
|
||||
alt="Arrow icon"
|
||||
/>
|
||||
</button>
|
||||
<!-- Extra -->
|
||||
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { PropType, defineComponent, ref } from 'vue';
|
||||
|
||||
import TimetableGeneral from './TimetableGeneral.vue';
|
||||
import TimetableStops from './TimetableStops.vue';
|
||||
import TimetableStatus from './TimetableStatus.vue';
|
||||
import TimetableExtra from './TimetableExtra.vue';
|
||||
import { API } from '../../../typings/api';
|
||||
|
||||
export default defineComponent({
|
||||
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
|
||||
|
||||
props: {
|
||||
timetableHistory: {
|
||||
type: Array as PropType<API.TimetableHistory.Response>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedTimetableHistory() {
|
||||
return this.timetableHistory.map((timetable) => ({
|
||||
timetable,
|
||||
showExtraInfo: ref(false)
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/variables';
|
||||
@import '../../../styles/responsive';
|
||||
@import '../../../styles/JournalSection';
|
||||
|
||||
.btn--show {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
padding: 0.2em 0.45em;
|
||||
|
||||
img {
|
||||
height: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.journal_item-info {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.item-route {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn--show {
|
||||
margin: 1em auto 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,41 @@
|
||||
<template>
|
||||
<div class="stop-list" v-if="showExtraInfo == true">
|
||||
<span
|
||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||
)"
|
||||
class="stop-list-item"
|
||||
:key="stop.stopName"
|
||||
:data-confirmed="stop.confirmed"
|
||||
>
|
||||
<span v-if="i > 0">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
<div class="timetable-stops">
|
||||
<div class="stop-list">
|
||||
<span
|
||||
v-for="(stop, i) in timetableStops.filter((_, i) =>
|
||||
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
|
||||
)"
|
||||
class="stop-list-item"
|
||||
:key="stop.stopName"
|
||||
:data-confirmed="stop.confirmed"
|
||||
>
|
||||
<span v-if="i > 0">
|
||||
>
|
||||
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
|
||||
... (+{{ timetableStops.length - 2 }}) >
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
|
||||
<span
|
||||
v-for="(pathData, i) in timetablePathDetails"
|
||||
:data-visited="pathData.isVisited"
|
||||
:data-next-visited="
|
||||
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
|
||||
"
|
||||
>
|
||||
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} → </span>
|
||||
<b class="path-scenery">{{ pathData.sceneryName }}</b>
|
||||
<span class="path-departure" v-if="pathData.departure">
|
||||
→ {{ pathData.departure }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="stop-name">{{ stop.stopName }}</span>
|
||||
<span v-html="stop.html"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +60,24 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
computed: {
|
||||
timetablePathDetails() {
|
||||
if (!this.timetable.path || this.timetable.path == '') return null;
|
||||
|
||||
return this.timetable.path.split(';').map((pathEl, i) => {
|
||||
const [arrival, name, departure] = pathEl.split(',');
|
||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
||||
|
||||
return {
|
||||
arrival,
|
||||
sceneryName,
|
||||
sceneryHash,
|
||||
departure,
|
||||
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
timetableStops() {
|
||||
const timetable = this.timetable;
|
||||
|
||||
@@ -94,13 +130,14 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stop-list {
|
||||
.timetable-stops {
|
||||
word-wrap: break-word;
|
||||
gap: 0.25em;
|
||||
font-size: 0.95em;
|
||||
|
||||
color: #adadad;
|
||||
}
|
||||
|
||||
.stop-list {
|
||||
&-item[data-confirmed='true'] {
|
||||
color: lightgreen;
|
||||
|
||||
@@ -109,4 +146,19 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.path-details {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.path-details > span[data-visited='true'] {
|
||||
.path-arrival,
|
||||
.path-scenery {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-next-visited='true'] .path-departure {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,7 +6,9 @@ export namespace Journal {
|
||||
| 'search-train'
|
||||
| 'search-date'
|
||||
| 'search-dispatcher'
|
||||
| 'search-issuedFrom';
|
||||
| 'search-issuedFrom'
|
||||
| 'search-terminatingAt'
|
||||
| 'search-via';
|
||||
|
||||
export type TimetableSearchType = {
|
||||
[key in TimetableSearchKey]: string;
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
<template>
|
||||
<section class="scenery-table-section">
|
||||
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
|
||||
<div class="scenery-dispatchers-history">
|
||||
<div class="history-wrapper">
|
||||
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
|
||||
|
||||
<div class="no-history" v-else-if="historyList.length == 0">
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
<div v-else-if="historyList.length == 0" class="no-history">
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<table class="scenery-history-table" v-else>
|
||||
<thead>
|
||||
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
|
||||
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
|
||||
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
|
||||
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
|
||||
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
||||
<td>#{{ historyItem.stationHash }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||
>
|
||||
<b>{{ historyItem.dispatcherName }}</b>
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="historyItem in historyList" :key="historyItem.id">
|
||||
<span>
|
||||
<span class="text--grayed" style="margin-right: 10px">
|
||||
#{{ historyItem.stationHash }}
|
||||
</span>
|
||||
<b
|
||||
v-if="historyItem.dispatcherLevel !== null"
|
||||
class="level-badge dispatcher"
|
||||
@@ -35,37 +22,52 @@
|
||||
>
|
||||
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
|
||||
</b>
|
||||
<b style="margin-left: 5px">
|
||||
<router-link
|
||||
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
|
||||
>
|
||||
{{ historyItem.dispatcherName }}
|
||||
</router-link>
|
||||
</b>
|
||||
|
||||
<b v-else>?</b>
|
||||
</td>
|
||||
<td class="text--primary">
|
||||
<b>{{ historyItem.dispatcherRate }}</b>
|
||||
</td>
|
||||
<td style="min-width: 300px">
|
||||
<div v-if="historyItem.timestampTo">
|
||||
<div>
|
||||
<span>
|
||||
{{ $t('scenery.dispatcher-rate') }}
|
||||
<b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
|
||||
</span>
|
||||
|
|
||||
<span>
|
||||
{{ $t('scenery.dispatcher-status-changes') }}
|
||||
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span v-if="historyItem.timestampTo">
|
||||
<b>{{ $d(historyItem.timestampFrom) }}</b>
|
||||
|
||||
{{ timestampToString(historyItem.timestampFrom) }}
|
||||
- {{ timestampToString(historyItem.timestampTo) }} ({{
|
||||
calculateDuration(historyItem.currentDuration)
|
||||
}})
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<div class="dispatcher-online" v-else>
|
||||
<span class="dispatcher-online" v-else>
|
||||
{{ $t('journal.online-since') }}
|
||||
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
|
||||
({{ calculateDuration(historyItem.currentDuration) }})
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -149,8 +151,43 @@ export default defineComponent({
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/sceneryViewTables.scss';
|
||||
|
||||
.scenery-dispatchers-history {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
display: grid;
|
||||
gap: 0.5em;
|
||||
grid-template-rows: auto 40px;
|
||||
}
|
||||
|
||||
.history-wrapper {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.5em;
|
||||
padding: 0.5em;
|
||||
background-color: #2b2b2b;
|
||||
line-height: 1.75em;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.dispatcher-online {
|
||||
@@ -158,13 +195,10 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.history-list {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.list-item {
|
||||
align-items: center;
|
||||
.journal-list > div {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
../../store/storeTypes
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
|
||||
<div class="info-lists">
|
||||
<!-- user list -->
|
||||
<SceneryInfoUserList :onlineScenery="onlineScenery" />
|
||||
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
|
||||
|
||||
<!-- spawn list -->
|
||||
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
|
||||
@@ -124,11 +124,6 @@ h3.section-header {
|
||||
align-items: center;
|
||||
|
||||
font-size: 1.2em;
|
||||
|
||||
img {
|
||||
width: 1.1em;
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.info-lists {
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="train in onlineScenery?.stationTrains"
|
||||
v-for="{ train, status } in stationTrains"
|
||||
class="badge user"
|
||||
:class="train.stopStatus"
|
||||
:key="train.trainId"
|
||||
tabindex="0"
|
||||
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
:key="train.id"
|
||||
:data-status="status"
|
||||
@click.prevent="selectModalTrain(train, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
||||
>
|
||||
<span class="user_train">{{ train.trainNo }}</span>
|
||||
<span class="user_name">{{ train.driverName }}</span>
|
||||
@@ -32,7 +32,9 @@
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import { ActiveScenery } from '../../../typings/common';
|
||||
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
|
||||
import { getTrainStopStatus } from '../utils';
|
||||
import { useMainStore } from '../../../store/mainStore';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [routerMixin, modalTrainMixin],
|
||||
@@ -41,6 +43,40 @@ export default defineComponent({
|
||||
onlineScenery: {
|
||||
type: Object as PropType<ActiveScenery>,
|
||||
required: false
|
||||
},
|
||||
station: {
|
||||
type: Object as PropType<Station>
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
mainStore: useMainStore()
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
stationTrains() {
|
||||
if (!this.onlineScenery) return;
|
||||
|
||||
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
|
||||
|
||||
return this.onlineScenery.stationTrains.map((train) => {
|
||||
const stop = train.timetableData?.followingStops.find(
|
||||
(stop) =>
|
||||
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
|
||||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
|
||||
);
|
||||
|
||||
const status = stop
|
||||
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
|
||||
: 'no-timetable';
|
||||
|
||||
return {
|
||||
train,
|
||||
status
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -74,31 +110,31 @@ ul {
|
||||
-webkit-transition: background-color 200ms;
|
||||
}
|
||||
|
||||
&.no-timetable .user_train {
|
||||
&[data-status='no-timetable'] .user_train {
|
||||
background-color: $no-timetable;
|
||||
}
|
||||
|
||||
&.departed > &_train {
|
||||
&[data-status='departed'] > &_train {
|
||||
background-color: $departed;
|
||||
}
|
||||
|
||||
&.stopped > &_train {
|
||||
&[data-status='stopped'] > &_train {
|
||||
background-color: $stopped;
|
||||
}
|
||||
|
||||
&.online > &_train {
|
||||
&[data-status='online'] > &_train {
|
||||
background-color: $online;
|
||||
}
|
||||
|
||||
&.terminated > &_train {
|
||||
&[data-status='terminated'] > &_train {
|
||||
background-color: $terminated;
|
||||
}
|
||||
|
||||
&.disconnected > &_train {
|
||||
&[data-status='disconnected'] > &_train {
|
||||
background-color: $disconnected;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
&[data-status='offline'] {
|
||||
background: firebrick;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<div class="timetable-list">
|
||||
<transition-group name="list-anim">
|
||||
<div
|
||||
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
|
||||
style="padding-bottom: 5em"
|
||||
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
|
||||
key="list-loading"
|
||||
>
|
||||
<Loading />
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<span
|
||||
class="timetable-item empty"
|
||||
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery"
|
||||
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
|
||||
key="list-offline"
|
||||
>
|
||||
{{ $t('scenery.offline') }}
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div
|
||||
class="timetable-item empty"
|
||||
v-else-if="computedScheduledTrains.length == 0"
|
||||
v-else-if="sceneryTimetables.length == 0"
|
||||
key="list-no-timetables"
|
||||
>
|
||||
{{ $t('scenery.no-timetables') }}
|
||||
@@ -65,59 +65,56 @@
|
||||
<div
|
||||
class="timetable-item"
|
||||
v-else
|
||||
v-for="scheduledTrain in computedScheduledTrains"
|
||||
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
|
||||
v-for="(row, i) in sceneryTimetables"
|
||||
:key="row.train.id + i"
|
||||
tabindex="0"
|
||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
|
||||
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
|
||||
>
|
||||
<span class="timetable-general">
|
||||
<span class="general-info">
|
||||
<span class="info-number">
|
||||
<strong>{{ scheduledTrain.category }}</strong>
|
||||
{{ scheduledTrain.trainNo }}
|
||||
<strong>{{ row.train.timetableData!.category }}</strong>
|
||||
{{ row.train.trainNo }}
|
||||
|
||||
<span
|
||||
v-if="scheduledTrain.stopInfo.comments"
|
||||
:title="scheduledTrain.stopInfo.comments"
|
||||
>
|
||||
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
|
||||
<img src="/images/icon-warning.svg" />
|
||||
</span>
|
||||
</span>
|
||||
|
|
||||
<span>
|
||||
{{ scheduledTrain.driverName }}
|
||||
{{ row.train.driverName }}
|
||||
</span>
|
||||
|
||||
<div class="info-route">
|
||||
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
|
||||
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
||||
</div>
|
||||
|
||||
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
|
||||
<ScheduledTrainStatus :sceneryTimetableRow="row" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="timetable-schedule">
|
||||
<span class="schedule-arrival">
|
||||
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
|
||||
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
|
||||
{{ $t('timetables.begins') }}
|
||||
</span>
|
||||
|
||||
<span class="arrival-time" v-else>
|
||||
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
|
||||
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
|
||||
<div v-if="row.checkpointStop.arrivalDelay == 0">
|
||||
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
|
||||
timestampToString(row.checkpointStop.arrivalTimestamp)
|
||||
}}</s>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
|
||||
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
|
||||
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
|
||||
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
|
||||
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
|
||||
}}{{ row.checkpointStop.arrivalDelay }})
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
@@ -125,41 +122,39 @@
|
||||
|
||||
<span class="schedule-stop">
|
||||
<span class="stop-connection">
|
||||
{{ scheduledTrain.arrivingLine }}
|
||||
{{ row.arrivingLine }}
|
||||
</span>
|
||||
|
||||
<span class="stop-time">
|
||||
{{ scheduledTrain.stopInfo.stopTime || '' }}
|
||||
{{
|
||||
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
|
||||
}}
|
||||
{{ row.checkpointStop.stopTime || '' }}
|
||||
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
|
||||
</span>
|
||||
|
||||
<span class="stop-connection">
|
||||
{{ scheduledTrain.departureLine }}
|
||||
{{ row.departureLine }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="schedule-departure">
|
||||
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
|
||||
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
|
||||
{{ $t('timetables.terminates') }}
|
||||
</span>
|
||||
|
||||
<span class="departure-time" v-else>
|
||||
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
|
||||
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
|
||||
<div v-if="row.checkpointStop.departureDelay == 0">
|
||||
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
|
||||
timestampToString(row.checkpointStop.departureTimestamp)
|
||||
}}</s>
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
|
||||
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
|
||||
}}{{ scheduledTrain.stopInfo.departureDelay }})
|
||||
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
|
||||
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
|
||||
}}{{ row.checkpointStop.departureDelay }})
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
@@ -183,6 +178,8 @@ import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { ActiveScenery, Station } from '../../typings/common';
|
||||
import { SceneryTimetableRow } from './typings';
|
||||
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryTimetable',
|
||||
@@ -204,10 +201,6 @@ export default defineComponent({
|
||||
listOpen: false
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
|
||||
activated() {
|
||||
this.loadSelectedOption();
|
||||
},
|
||||
@@ -220,9 +213,10 @@ export default defineComponent({
|
||||
const mainStore = useMainStore();
|
||||
|
||||
const chosenCheckpoint = ref(
|
||||
props.station?.generalInfo?.checkpoints?.length == 0
|
||||
? ''
|
||||
: props.station?.generalInfo?.checkpoints[0] ?? null
|
||||
props.station?.generalInfo?.checkpoints[0] ??
|
||||
props.station?.name ??
|
||||
route.query['station']?.toString() ??
|
||||
''
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -241,27 +235,50 @@ export default defineComponent({
|
||||
return url;
|
||||
},
|
||||
|
||||
computedScheduledTrains() {
|
||||
if (!this.station) return [];
|
||||
sceneryTimetables(): SceneryTimetableRow[] {
|
||||
if (!this.onlineScenery) return [];
|
||||
|
||||
return (
|
||||
this.onlineScenery?.scheduledTrains
|
||||
?.filter(
|
||||
(train) =>
|
||||
train.checkpointName.toLocaleLowerCase() ==
|
||||
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
|
||||
train.region == this.mainStore.region.id
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.stopStatusID > b.stopStatusID) return 1;
|
||||
if (a.stopStatusID < b.stopStatusID) return -1;
|
||||
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
|
||||
|
||||
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
|
||||
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1;
|
||||
return this.onlineScenery.scheduledTrains
|
||||
.filter(
|
||||
(ct) =>
|
||||
ct.timetablePathElement.stationName == sceneryName &&
|
||||
ct.train.region == this.mainStore.region.id &&
|
||||
this.chosenCheckpoint &&
|
||||
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
|
||||
)
|
||||
.map((ct) => {
|
||||
const trainStopStatus = getTrainStopStatus(
|
||||
ct.checkpointStop,
|
||||
ct.train.currentStationName,
|
||||
sceneryName
|
||||
);
|
||||
|
||||
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1;
|
||||
}) || []
|
||||
);
|
||||
return {
|
||||
checkpointStop: ct.checkpointStop,
|
||||
train: ct.train,
|
||||
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
|
||||
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
|
||||
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
|
||||
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
|
||||
prevStationName: ct.previousSceneryElement?.stationName ?? null,
|
||||
nextStationName: ct.nextSceneryElement?.stationName ?? null,
|
||||
status: trainStopStatus
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
|
||||
return -1;
|
||||
|
||||
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
|
||||
return 1;
|
||||
|
||||
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
|
||||
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
|
||||
|
||||
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,69 +1,97 @@
|
||||
<template>
|
||||
<!-- WIP -->
|
||||
<!-- <div class="top-filters">
|
||||
<button class="btn btn--option">ROZPOCZYNA BIEG</button>
|
||||
|
||||
<button class="btn btn--option">PRZEZ</button>
|
||||
|
||||
<button class="btn btn--option">KOŃCZY BIEG</button>
|
||||
</div> -->
|
||||
|
||||
<section class="scenery-table-section">
|
||||
<Loading v-if="dataStatus != DataStatus.Loaded" />
|
||||
|
||||
<div class="no-history" v-else-if="historyList.length == 0">
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
<div class="scenery-timetables-history">
|
||||
<div class="history-modes">
|
||||
<button
|
||||
class="btn btn--option"
|
||||
v-for="mode in historyModeList"
|
||||
:key="mode"
|
||||
:class="{ checked: checkedHistoryMode == mode }"
|
||||
@click="checkHistoryMode(mode)"
|
||||
>
|
||||
{{ $t(`scenery.timetable-${mode}`) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="scenery-history-table" v-else>
|
||||
<thead>
|
||||
<th>{{ $t('scenery.timetables-history-id') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-number') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-route') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-driver') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-author') }}</th>
|
||||
<th>{{ $t('scenery.timetables-history-date') }}</th>
|
||||
</thead>
|
||||
<div class="history-wrapper">
|
||||
<Loading v-if="dataStatus != DataStatus.Loaded" />
|
||||
|
||||
<tbody>
|
||||
<tr v-for="historyItem in historyList" :key="historyItem.id">
|
||||
<td>
|
||||
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
|
||||
#{{ historyItem.id }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
|
||||
{{ historyItem.trainNo }}
|
||||
</td>
|
||||
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
|
||||
<td>
|
||||
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
|
||||
{{ historyItem.driverName }}
|
||||
</router-link>
|
||||
</td>
|
||||
<div v-else-if="historyList.length == 0" class="no-history">
|
||||
{{ $t('scenery.history-list-empty') }}
|
||||
</div>
|
||||
|
||||
<td>
|
||||
<router-link
|
||||
v-if="historyItem.authorName"
|
||||
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
|
||||
>{{ historyItem.authorName }}
|
||||
</router-link>
|
||||
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
|
||||
</td>
|
||||
<td>
|
||||
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
|
||||
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<div v-else class="journal-list">
|
||||
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
|
||||
<span>
|
||||
<div>
|
||||
<span
|
||||
class="timetable-status-indicator"
|
||||
:data-terminated="timetableHistory.terminated"
|
||||
:data-fulfilled="timetableHistory.fulfilled"
|
||||
>
|
||||
⦿
|
||||
</span>
|
||||
#{{ timetableHistory.id }} |
|
||||
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
|
||||
{{ timetableHistory.trainNo }}
|
||||
{{ timetableHistory.route.replace('|', ' ⇒ ') }}
|
||||
</div>
|
||||
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
<div class="text--grayed">
|
||||
<span>
|
||||
{{ $t('scenery.timetable-issued-date') }}
|
||||
<b>
|
||||
{{
|
||||
localeDateTime(
|
||||
timetableHistory.createdAt > timetableHistory.beginDate
|
||||
? timetableHistory.beginDate
|
||||
: timetableHistory.createdAt,
|
||||
$i18n.locale
|
||||
)
|
||||
}}
|
||||
</b></span
|
||||
>
|
||||
<span v-if="timetableHistory.authorName">
|
||||
{{ $t('scenery.timetable-issued-by') }}
|
||||
<b>
|
||||
<router-link
|
||||
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
||||
>
|
||||
{{ timetableHistory.authorName }}
|
||||
</router-link>
|
||||
</b>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
{{ $t('scenery.timetable-issued-for') }}
|
||||
<b>
|
||||
<router-link
|
||||
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
||||
>
|
||||
{{ timetableHistory.driverName }}
|
||||
</router-link>
|
||||
</b>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<button
|
||||
@click="
|
||||
navigateTo(`/journal/timetables`, {
|
||||
'search-train': `#${timetableHistory.id}`
|
||||
})
|
||||
"
|
||||
>
|
||||
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom-info">
|
||||
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
|
||||
{{ $t('scenery.bottom-info') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
|
||||
import { API } from '../../typings/api';
|
||||
import { ActiveScenery, Station, Status } from '../../typings/common';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import routerMixin from '../../mixins/routerMixin';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
|
||||
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
|
||||
type HistoryMode = (typeof historyModeList)[number];
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryTimetablesHistory',
|
||||
mixins: [dateMixin],
|
||||
mixins: [dateMixin, routerMixin],
|
||||
props: {
|
||||
station: {
|
||||
type: Object as PropType<Station>
|
||||
@@ -91,9 +124,14 @@ export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
historyList: [] as API.TimetableHistory.Response,
|
||||
historyModeList,
|
||||
|
||||
apiStore: useApiStore(),
|
||||
mainStore: useMainStore(),
|
||||
dataStatus: Status.Data.Loading,
|
||||
DataStatus: Status.Data
|
||||
DataStatus: Status.Data,
|
||||
|
||||
checkedHistoryMode: 'via' as HistoryMode
|
||||
};
|
||||
},
|
||||
|
||||
@@ -103,17 +141,22 @@ export default defineComponent({
|
||||
|
||||
methods: {
|
||||
async fetchAPIData() {
|
||||
if (!this.station && !this.onlineScenery) {
|
||||
const stationName = this.$route.query['station'];
|
||||
|
||||
if (!stationName) {
|
||||
this.historyList = [];
|
||||
this.dataStatus = Status.Data.Loaded;
|
||||
return;
|
||||
}
|
||||
|
||||
const requestFilters: Record<string, any> = {};
|
||||
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
||||
requestFilters.countLimit = 30;
|
||||
|
||||
try {
|
||||
const response: API.TimetableHistory.Response = await (
|
||||
await this.apiStore.client!.get('api/getTimetables', {
|
||||
params: {
|
||||
issuedFrom: this.station?.name || this.onlineScenery?.name
|
||||
}
|
||||
params: requestFilters
|
||||
})
|
||||
).data;
|
||||
|
||||
@@ -125,11 +168,17 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
checkHistoryMode(mode: HistoryMode) {
|
||||
this.checkedHistoryMode = mode;
|
||||
this.dataStatus = Status.Data.Loading;
|
||||
this.fetchAPIData();
|
||||
},
|
||||
|
||||
navigateToHistory() {
|
||||
this.$router.push({
|
||||
path: '/journal/timetables',
|
||||
query: {
|
||||
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
|
||||
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -142,13 +191,66 @@ export default defineComponent({
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/sceneryViewTables.scss';
|
||||
|
||||
.top-filters {
|
||||
.scenery-timetables-history {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
grid-template-rows: auto 1fr 40px;
|
||||
}
|
||||
|
||||
.history-wrapper {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.history-modes {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
gap: 0.5em;
|
||||
padding: 0.25em;
|
||||
|
||||
button {
|
||||
padding: 0.5em;
|
||||
padding: 0.35em;
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.journal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.journal-list > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5em;
|
||||
background-color: #2b2b2b;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.journal-list > div > button > img {
|
||||
width: 2em;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.timetable-status-indicator {
|
||||
&[data-fulfilled='true'] {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
&[data-terminated='false'] {
|
||||
color: lightblue;
|
||||
}
|
||||
|
||||
&[data-terminated='true'][data-fulfilled='false'] {
|
||||
color: lightcoral;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="general-status">
|
||||
<span
|
||||
:class="computedScheduledTrain.stopStatus"
|
||||
:class="computedScheduledTrain.status"
|
||||
:title="computedScheduledTrain.stopStatusDescription"
|
||||
>
|
||||
{{ computedScheduledTrain.stopStatusIndicator }}
|
||||
@@ -11,25 +11,21 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { ScheduledTrain, StopStatus } from '../../typings/common';
|
||||
|
||||
interface ScheduledTrainComp extends ScheduledTrain {
|
||||
stopStatusIndicator: string;
|
||||
stopStatusDescription: string;
|
||||
}
|
||||
import { StopStatus } from '../../typings/common';
|
||||
import { SceneryTimetableRow } from './typings';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
scheduledTrain: {
|
||||
type: Object as PropType<ScheduledTrain>,
|
||||
sceneryTimetableRow: {
|
||||
type: Object as PropType<SceneryTimetableRow>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedScheduledTrain(): ScheduledTrainComp {
|
||||
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } =
|
||||
this.scheduledTrain;
|
||||
computedScheduledTrain() {
|
||||
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
|
||||
this.sceneryTimetableRow;
|
||||
|
||||
const prevDepartureIndicator = prevDepartureLine
|
||||
? `(${prevDepartureLine}) ${prevStationName}`
|
||||
@@ -41,7 +37,7 @@ export default defineComponent({
|
||||
let stopStatusDescription = '',
|
||||
stopStatusIndicator = '';
|
||||
|
||||
switch (stopStatus) {
|
||||
switch (status) {
|
||||
case StopStatus.ARRIVING:
|
||||
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
|
||||
stopStatusDescription = this.$t('timetables.desc-arriving', {
|
||||
@@ -56,7 +52,7 @@ export default defineComponent({
|
||||
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
|
||||
: `${this.$t('timetables.desc-end')}`;
|
||||
stopStatusDescription = nextArrivalLine
|
||||
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
|
||||
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
|
||||
: '';
|
||||
break;
|
||||
|
||||
@@ -85,7 +81,7 @@ export default defineComponent({
|
||||
break;
|
||||
}
|
||||
return {
|
||||
...this.scheduledTrain,
|
||||
...this.sceneryTimetableRow,
|
||||
stopStatusDescription,
|
||||
stopStatusIndicator
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||
|
||||
interface FilterOption {
|
||||
id: string;
|
||||
@@ -40,15 +39,9 @@ export default defineComponent({
|
||||
|
||||
emits: ['update:optionValue'],
|
||||
|
||||
setup() {
|
||||
return {
|
||||
filterStore: useStationFiltersStore()
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
'option.value'() {
|
||||
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
|
||||
// this.filterStore.changeFilterValue(this.option.name, !this.option.value);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -56,17 +49,17 @@ export default defineComponent({
|
||||
handleDbClick(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
this.filterStore.lastClickedFilterId = this.option.id;
|
||||
// this.filterStore.lastClickedFilterId = this.option.id;
|
||||
// this.option.value = true;
|
||||
this.$emit('update:optionValue', true);
|
||||
|
||||
this.filterStore.inputs.options
|
||||
.filter((option) => {
|
||||
return option.section == this.option.section && option.id != this.option.id;
|
||||
})
|
||||
.forEach((option) => {
|
||||
option.value = !this.option.value;
|
||||
});
|
||||
// this.filterStore.inputs.options
|
||||
// .filter((option) => {
|
||||
// return option.section == this.option.section && option.id != this.option.id;
|
||||
// })
|
||||
// .forEach((option) => {
|
||||
// option.value = !this.option.value;
|
||||
// });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
|
||||
<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" />
|
||||
[F] {{ $t('options.filters') }}
|
||||
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
|
||||
<p>[F] {{ $t('options.filters') }}</p>
|
||||
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
|
||||
</button>
|
||||
|
||||
<label for="scenery-search">
|
||||
@@ -33,29 +33,45 @@
|
||||
<div class="card_title flex">{{ $t('filters.title') }}</div>
|
||||
<p class="card_info" v-html="$t('filters.desc')"></p>
|
||||
|
||||
<div class="changed-filters" :data-active="changedFilters.length > 0">
|
||||
<template v-if="changedFilters.length > 0">
|
||||
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
|
||||
</template>
|
||||
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
||||
</div>
|
||||
|
||||
<section class="card_options">
|
||||
<div
|
||||
class="option-section"
|
||||
v-for="section in filterStore.inputs.optionSections"
|
||||
:key="section"
|
||||
v-for="(sectionFilters, sectionKey) in filtersSections"
|
||||
:key="sectionKey"
|
||||
>
|
||||
<h3 class="text--primary">
|
||||
{{ $t(`filters.sections.${section}`) }}
|
||||
|
||||
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
|
||||
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
||||
{{ $t(`filters.sections.${sectionKey}`) }}
|
||||
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
||||
</h3>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="section-inputs">
|
||||
<FilterOption
|
||||
v-for="(option, i) in filterStore.inputs.options.filter(
|
||||
(o) => o.section == section
|
||||
)"
|
||||
v-model:optionValue="option.value"
|
||||
:option="option"
|
||||
:key="i"
|
||||
/>
|
||||
<div class="section-filters">
|
||||
<label
|
||||
v-for="filterKey in sectionFilters"
|
||||
@click="() => (filters[filterKey] = !filters[filterKey])"
|
||||
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
|
||||
:for="filterKey"
|
||||
>
|
||||
<input
|
||||
:checked="filters[filterKey]"
|
||||
v-model="filters[filterKey]"
|
||||
type="checkbox"
|
||||
:class="sectionKey"
|
||||
:name="filterKey"
|
||||
/>
|
||||
<span>
|
||||
{{ $t(`filters.${filterKey}`) }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -68,7 +84,7 @@
|
||||
<span>{{
|
||||
minimumHours == 0
|
||||
? $t('filters.now')
|
||||
: minimumHours < 8
|
||||
: minimumHours < 7
|
||||
? minimumHours + $t('filters.hour')
|
||||
: $t('filters.no-limit')
|
||||
}}</span>
|
||||
@@ -76,21 +92,21 @@
|
||||
</span>
|
||||
</section>
|
||||
|
||||
<datalist id="authors">
|
||||
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
|
||||
</datalist>
|
||||
|
||||
<section class="card_authors-search">
|
||||
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
|
||||
|
||||
<datalist id="authors" name="authors">
|
||||
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
|
||||
</datalist>
|
||||
|
||||
<form action="javascript:void(0);" @submit="handleAuthorsInput">
|
||||
<input
|
||||
type="text"
|
||||
id="author"
|
||||
list="authors"
|
||||
name="authors"
|
||||
v-model="authors"
|
||||
:placeholder="$t('filters.authors-placeholder')"
|
||||
v-model="authorsInputValue"
|
||||
@focus="preventKeyDown = true"
|
||||
@blur="preventKeyDown = false"
|
||||
/>
|
||||
@@ -100,19 +116,18 @@
|
||||
</section>
|
||||
|
||||
<section class="card_sliders">
|
||||
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
|
||||
<div class="slider" v-for="(slider, i) in initSliders" :key="i">
|
||||
<input
|
||||
class="slider-input"
|
||||
type="range"
|
||||
:name="slider.name"
|
||||
:name="slider.id"
|
||||
:id="slider.id"
|
||||
:min="slider.minRange"
|
||||
:max="slider.maxRange"
|
||||
:step="slider.step"
|
||||
v-model="slider.value"
|
||||
@change="handleInput"
|
||||
v-model="filters[slider.id]"
|
||||
/>
|
||||
<span class="slider-value">{{ slider.value }}</span>
|
||||
<span class="slider-value">{{ filters[slider.id] }}</span>
|
||||
<div class="slider-content">
|
||||
{{ $t(`filters.sliders.${slider.id}`) }}
|
||||
</div>
|
||||
@@ -133,9 +148,9 @@
|
||||
|
||||
<button
|
||||
class="btn--action"
|
||||
:disabled="changedFilters.length == 0"
|
||||
:data-disabled="changedFilters.length == 0"
|
||||
@click="resetFilters"
|
||||
:disabled="filterStore.areFiltersAtDefault"
|
||||
:data-disabled="filterStore.areFiltersAtDefault"
|
||||
>
|
||||
[R] {{ $t('filters.reset') }}
|
||||
</button>
|
||||
@@ -151,22 +166,36 @@
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import keyMixin from '../../mixins/keyMixin';
|
||||
import routerMixin from '../../mixins/routerMixin';
|
||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
|
||||
import FilterOption from './FilterOption.vue';
|
||||
import StorageManager from '../../managers/storageManager';
|
||||
|
||||
import {
|
||||
filtersSections,
|
||||
initSliders,
|
||||
initFilters,
|
||||
getChangedFilters
|
||||
} from '../../managers/stationFilterManager';
|
||||
|
||||
import { StationFilterSection } from '../../managers/stationFilterManager';
|
||||
import { computed } from 'vue';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const STORAGE_KEY = 'options_saved';
|
||||
|
||||
export default defineComponent({
|
||||
components: { FilterOption },
|
||||
mixins: [keyMixin, routerMixin],
|
||||
|
||||
data: () => ({
|
||||
saveOptions: false,
|
||||
STORAGE_KEY: 'options_saved',
|
||||
|
||||
authorsInputValue: '',
|
||||
filtersSections,
|
||||
initSliders,
|
||||
|
||||
minimumHours: 0,
|
||||
authors: '',
|
||||
|
||||
currentRegion: { id: '', value: '' },
|
||||
|
||||
@@ -180,22 +209,33 @@ export default defineComponent({
|
||||
setup() {
|
||||
const isVisible = inject('isFilterCardVisible');
|
||||
const store = useMainStore();
|
||||
const filterStore = useStationFiltersStore();
|
||||
|
||||
const filters = inject('StationsView_filters') as Record<string, any>;
|
||||
|
||||
const changedFilters = computed(() => getChangedFilters(filters));
|
||||
|
||||
// Save filters to persistent storage
|
||||
watch(filters, (value) => {
|
||||
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
|
||||
|
||||
Object.keys(value).forEach((filterKey) => {
|
||||
StorageManager.setValue(filterKey, filters[filterKey]);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
isVisible,
|
||||
store,
|
||||
filterStore
|
||||
filters,
|
||||
changedFilters
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
|
||||
this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
|
||||
|
||||
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
|
||||
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
|
||||
|
||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
|
||||
}
|
||||
|
||||
this.currentRegion = this.store.region;
|
||||
@@ -214,7 +254,7 @@ export default defineComponent({
|
||||
return true;
|
||||
},
|
||||
|
||||
authors() {
|
||||
authorsHint() {
|
||||
return this.store.stationList
|
||||
.reduce((acc, station) => {
|
||||
station.generalInfo?.authors?.forEach((author) => {
|
||||
@@ -258,61 +298,63 @@ export default defineComponent({
|
||||
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
||||
},
|
||||
|
||||
handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
this.filterStore.changeFilterValue(target.name, target.value);
|
||||
|
||||
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
|
||||
},
|
||||
|
||||
handleAuthorsInput() {
|
||||
this.filterStore.changeFilterValue('authors', this.authorsInputValue);
|
||||
|
||||
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
|
||||
},
|
||||
|
||||
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
|
||||
this.filterStore.changeFilterValue(name, value);
|
||||
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
|
||||
this.filters['authors'] = this.authors;
|
||||
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
|
||||
},
|
||||
|
||||
subHour() {
|
||||
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1;
|
||||
|
||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
||||
this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
|
||||
this.filters['onlineFromHours'] = this.minimumHours;
|
||||
},
|
||||
|
||||
addHour() {
|
||||
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1;
|
||||
|
||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
||||
this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
|
||||
this.filters['onlineFromHours'] = this.minimumHours;
|
||||
},
|
||||
|
||||
saveFilters() {
|
||||
this.saveOptions = !this.saveOptions;
|
||||
|
||||
if (!this.saveOptions) {
|
||||
StorageManager.unregisterStorage(this.STORAGE_KEY);
|
||||
StorageManager.unregisterStorage(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
StorageManager.registerStorage(this.STORAGE_KEY);
|
||||
StorageManager.registerStorage(STORAGE_KEY);
|
||||
|
||||
this.filterStore.inputs.options.forEach((option) =>
|
||||
StorageManager.setBooleanValue(option.name, !option.value)
|
||||
);
|
||||
this.filterStore.inputs.sliders.forEach((slider) =>
|
||||
StorageManager.setNumericValue(slider.name, slider.value)
|
||||
);
|
||||
Object.keys(this.filters).forEach((filterKey) => {
|
||||
StorageManager.setValue(filterKey, this.filters[filterKey]);
|
||||
});
|
||||
},
|
||||
|
||||
resetFilters() {
|
||||
this.authorsInputValue = '';
|
||||
|
||||
// Reset local model values
|
||||
this.minimumHours = 0;
|
||||
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
|
||||
this.filterStore.resetFilters();
|
||||
this.authors = '';
|
||||
|
||||
// Reset global filters
|
||||
Object.keys(this.filters).forEach((filterKey) => {
|
||||
this.filters[filterKey] = (initFilters as any)[filterKey];
|
||||
});
|
||||
},
|
||||
|
||||
areSectionFiltersDefault(sectionKey: StationFilterSection) {
|
||||
return filtersSections[sectionKey].every((filterKey) => {
|
||||
return this.filters[filterKey] == initFilters[filterKey];
|
||||
});
|
||||
},
|
||||
|
||||
resetSectionFilters(sectionKey: StationFilterSection) {
|
||||
filtersSections[sectionKey].forEach((filterKey) => {
|
||||
this.filters[filterKey] = initFilters[filterKey];
|
||||
});
|
||||
},
|
||||
|
||||
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
|
||||
filtersSections[sectionKey].forEach((filterKey) => {
|
||||
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
|
||||
});
|
||||
},
|
||||
|
||||
closeCard() {
|
||||
@@ -327,9 +369,10 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/card.scss';
|
||||
@import '../../styles/animations.scss';
|
||||
@import '../../styles/responsive';
|
||||
@import '../../styles/card';
|
||||
@import '../../styles/animations';
|
||||
@import '../../styles/variables';
|
||||
|
||||
h3.section-header {
|
||||
text-align: center;
|
||||
@@ -346,6 +389,15 @@ h3.section-header {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.changed-filters {
|
||||
background-color: #111;
|
||||
padding: 0.5em;
|
||||
|
||||
&[data-active='true'] {
|
||||
color: lightgreen;
|
||||
}
|
||||
}
|
||||
|
||||
.card_controls {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
@@ -374,28 +426,6 @@ h3.section-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card_regions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
label > input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
label > span {
|
||||
padding: 0.25em 0.5em;
|
||||
margin: 0 0.25em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
background-color: gray;
|
||||
|
||||
&.checked {
|
||||
background-color: seagreen;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card_timestamp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -441,6 +471,52 @@ h3.section-header {
|
||||
}
|
||||
}
|
||||
|
||||
.section-filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.section-filters > label {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.25em;
|
||||
font-weight: bold;
|
||||
background-color: forestgreen;
|
||||
}
|
||||
|
||||
span:hover {
|
||||
background-color: #22aa22;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
|
||||
&:checked + span {
|
||||
background-color: #444;
|
||||
|
||||
&:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible + span {
|
||||
outline: 1px solid $accentCol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card_actions {
|
||||
padding: 0.5em;
|
||||
|
||||
@@ -475,35 +551,18 @@ h3.section-header {
|
||||
}
|
||||
}
|
||||
|
||||
.section-inputs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.quick-actions div {
|
||||
display: flex;
|
||||
margin: 1em 0;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
&-value {
|
||||
color: $accentCol;
|
||||
margin-right: 0.5em;
|
||||
padding: 0.1em 0.2em;
|
||||
}
|
||||
|
||||
&-content {
|
||||
flex-grow: 2;
|
||||
}
|
||||
|
||||
&-input {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
@@ -512,7 +571,6 @@ h3.section-header {
|
||||
outline: none;
|
||||
|
||||
min-width: 25%;
|
||||
max-width: 120px;
|
||||
|
||||
&:focus-visible ~ * {
|
||||
color: gold;
|
||||
@@ -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>
|
||||
|
||||
@@ -1,29 +1,55 @@
|
||||
<template>
|
||||
<div class="station-stats">
|
||||
<div class="separator" />
|
||||
<div>
|
||||
{{ $t('station-stats.u-factor') }}
|
||||
<a
|
||||
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
|
||||
target="_blank"
|
||||
:data-tooltip="$t('station-stats.u-factor-tooltip')"
|
||||
>(?)</a
|
||||
>:
|
||||
<b :style="calculateFactorStyle()">
|
||||
{{ uFactor.toFixed(2) }}
|
||||
</b>
|
||||
| {{ $t('station-stats.avg-timetable-count') }}
|
||||
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('station-stats.single-track-count') }}
|
||||
<b>{{ trackCount.oneWayElectric }}</b> {{ $t('station-stats.electrified') }} /
|
||||
<b>{{ trackCount.oneWayOther }}</b> {{ $t('station-stats.not-electrified') }} |
|
||||
{{ $t('station-stats.double-track-count') }} <b>{{ trackCount.twoWayElectric }}</b>
|
||||
{{ $t('station-stats.electrified') }} / <b>{{ trackCount.twoWayOther }}</b>
|
||||
{{ $t('station-stats.not-electrified') }} | {{ $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 class="stats-row">
|
||||
<div>
|
||||
<span
|
||||
>{{ $t('station-stats.u-factor') }}
|
||||
<a
|
||||
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
|
||||
target="_blank"
|
||||
:data-tooltip="$t('station-stats.u-factor-tooltip')"
|
||||
>(?)</a
|
||||
>:
|
||||
</span>
|
||||
|
||||
<b class="u-factor" :style="calculateFactorStyle()">
|
||||
{{ uFactor.toFixed(2) }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.avg-timetable-count') }}
|
||||
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.single-track-count') }}
|
||||
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} ⚡</b>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $t('station-stats.double-track-count') }}
|
||||
<b>{{ trackCount.twoWay }}</b>
|
||||
(<b>{{ trackCount.twoWayElectric }} ⚡</b>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
• {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
|
||||
>{{ trackCount.crossTrackElectric }} ⚡</b
|
||||
>)
|
||||
</div>
|
||||
|
||||
<div>
|
||||
•
|
||||
{{ $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>
|
||||
</template>
|
||||
@@ -64,17 +90,18 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
avgTimetableCount() {
|
||||
const scheduledTrainsTotal = this.mainStore.activeSceneryList.reduce<number>((acc, sc) => {
|
||||
if (sc.region != this.mainStore.region.id) return acc;
|
||||
const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
|
||||
return sc.region == this.mainStore.region.id;
|
||||
});
|
||||
|
||||
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
|
||||
acc += sc.scheduledTrainCount.all;
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return this.mainStore.activeSceneryList.length != 0
|
||||
? scheduledTrainsTotal / this.mainStore.activeSceneryList.length
|
||||
: 0;
|
||||
if (regionSceneries.length == 0) return 0;
|
||||
|
||||
return timetableCountSum / regionSceneries.length;
|
||||
},
|
||||
|
||||
trackCount() {
|
||||
@@ -87,23 +114,46 @@ export default defineComponent({
|
||||
)
|
||||
.reduce(
|
||||
(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;
|
||||
|
||||
const keyName: keyof typeof acc = `${r.routeTracks == 2 ? 'twoWay' : 'oneWay'}${r.isElectric ? 'Electric' : 'Other'}`;
|
||||
|
||||
acc[keyName] += 1;
|
||||
acc[r.routeTracks == 2 ? 'twoWay' : 'oneWay'] += 1;
|
||||
if (r.isElectric) acc[r.routeTracks == 2 ? 'twoWayElectric' : 'oneWayElectric'] += 1;
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ oneWayElectric: 0, oneWayOther: 0, twoWayElectric: 0, twoWayOther: 0 }
|
||||
{
|
||||
oneWay: 0,
|
||||
oneWayElectric: 0,
|
||||
twoWay: 0,
|
||||
twoWayElectric: 0,
|
||||
crossTrack: 0,
|
||||
crossTrackElectric: 0
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
spawnCount() {
|
||||
return this.mainStore.activeSceneryList.reduce(
|
||||
(acc, scenery) => {
|
||||
if (scenery.region != this.mainStore.region.id) return acc;
|
||||
|
||||
scenery.spawns.forEach((spawn) => {
|
||||
if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1;
|
||||
if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1;
|
||||
@@ -133,19 +183,30 @@ export default defineComponent({
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
[data-factor-low='true'] {
|
||||
color: #ddd;
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
text-wrap: pretty;
|
||||
gap: 0.25em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
[data-factor-mediocre='true'] {
|
||||
color: lightgreen;
|
||||
}
|
||||
.u-factor {
|
||||
[data-factor-low='true'] {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
[data-factor-high='true'] {
|
||||
color: greenyellow;
|
||||
}
|
||||
[data-factor-mediocre='true'] {
|
||||
color: lightgreen;
|
||||
}
|
||||
|
||||
[data-factor-highest='true'] {
|
||||
color: rgb(22, 245, 22);
|
||||
[data-factor-high='true'] {
|
||||
color: greenyellow;
|
||||
}
|
||||
|
||||
[data-factor-highest='true'] {
|
||||
color: rgb(22, 245, 22);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,366 +1,371 @@
|
||||
<template>
|
||||
<section class="station_table">
|
||||
<Loading
|
||||
v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0"
|
||||
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
|
||||
/>
|
||||
|
||||
<div class="table_wrapper" v-else-if="displayedStations.length > 0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="headerName in headIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-text"
|
||||
:class="headerName"
|
||||
>
|
||||
<span class="header_wrapper">
|
||||
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
||||
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="sorterActive.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<th
|
||||
v-for="headerName in headIconsIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-image"
|
||||
:class="headerName"
|
||||
>
|
||||
<span class="header_wrapper">
|
||||
<img
|
||||
:src="`/images/icon-${headerName}.svg`"
|
||||
:alt="headerName"
|
||||
:title="$t(`sceneries.headers.${headerName}`)"
|
||||
/>
|
||||
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="sorterActive.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="station in displayedStations"
|
||||
:class="{ 'last-selected': lastSelectedStationName == station.name }"
|
||||
:key="station.name"
|
||||
@click.left="setScenery(station.name)"
|
||||
@click.right="openForumSite($event, station.generalInfo?.url)"
|
||||
@keydown.enter="setScenery(station.name)"
|
||||
@keydown.space="openForumSite($event, station.generalInfo?.url)"
|
||||
tabindex="0"
|
||||
<table v-else-if="filteredStationList.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="headerName in headIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-text"
|
||||
:class="headerName"
|
||||
>
|
||||
<td class="station-name" :class="station.generalInfo?.availability">
|
||||
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
||||
station.generalInfo.project
|
||||
}}</b>
|
||||
{{ station.name }}
|
||||
</td>
|
||||
<span class="header_wrapper">
|
||||
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
||||
|
||||
<td class="station-level">
|
||||
<span v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="
|
||||
station.generalInfo.reqLevel > -1 &&
|
||||
station.generalInfo.availability != 'nonPublic' &&
|
||||
station.generalInfo.availability != 'unavailable'
|
||||
"
|
||||
:style="calculateExpStyle(station.generalInfo.reqLevel)"
|
||||
>
|
||||
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="station.generalInfo.availability == 'abandoned'">
|
||||
<img
|
||||
src="/images/icon-abandoned.svg"
|
||||
alt="non-public"
|
||||
:title="$t('sceneries.info.abandoned')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
|
||||
<img
|
||||
src="/images/icon-lock.svg"
|
||||
alt="non-public"
|
||||
:title="$t('sceneries.info.non-public')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<img
|
||||
src="/images/icon-unavailable.svg"
|
||||
alt="unavailable"
|
||||
:title="$t('sceneries.info.unavailable')"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span v-else> ? </span>
|
||||
</td>
|
||||
|
||||
<td class="station-status">
|
||||
<StationStatusBadge
|
||||
:isOnline="station.onlineInfo ? true : false"
|
||||
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="activeSorter.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</td>
|
||||
</span>
|
||||
</th>
|
||||
|
||||
<td class="station-dispatcher-name">
|
||||
<span v-if="station.onlineInfo?.dispatcherName">
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||
@click.stop="openDonationModal"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
<img src="/images/icon-diamond.svg" alt="" />
|
||||
{{ station.onlineInfo.dispatcherName }}
|
||||
</b>
|
||||
<th
|
||||
v-for="headerName in headIconsIds"
|
||||
:key="headerName"
|
||||
@click="changeSorter(headerName)"
|
||||
class="header-image"
|
||||
:class="headerName"
|
||||
>
|
||||
<span class="header_wrapper">
|
||||
<img
|
||||
:src="`/images/icon-${headerName}.svg`"
|
||||
:alt="headerName"
|
||||
:title="$t(`sceneries.headers.${headerName}`)"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
{{ station.onlineInfo.dispatcherName }}
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<img
|
||||
class="sort-icon"
|
||||
v-if="activeSorter.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<td class="station-dispatcher-exp">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="station in filteredStationList"
|
||||
:class="{ 'last-selected': lastSelectedStationName == station.name }"
|
||||
:key="station.name"
|
||||
@click.left="setScenery(station.name)"
|
||||
@click.right="openForumSite($event, station.generalInfo?.url)"
|
||||
@keydown.enter="setScenery(station.name)"
|
||||
@keydown.space="openForumSite($event, station.generalInfo?.url)"
|
||||
tabindex="0"
|
||||
>
|
||||
<td class="station-name" :class="station.generalInfo?.availability">
|
||||
<b v-if="station.generalInfo?.project" style="color: salmon">{{
|
||||
station.generalInfo.project
|
||||
}}</b>
|
||||
{{ station.name }}
|
||||
</td>
|
||||
|
||||
<td class="station-level">
|
||||
<span v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
||||
:style="
|
||||
calculateExpStyle(
|
||||
station.onlineInfo.dispatcherExp,
|
||||
station.onlineInfo.dispatcherIsSupporter
|
||||
)
|
||||
v-if="
|
||||
station.generalInfo.reqLevel > -1 &&
|
||||
station.generalInfo.availability != 'nonPublic' &&
|
||||
station.generalInfo.availability != 'unavailable'
|
||||
"
|
||||
:style="calculateExpStyle(station.generalInfo.reqLevel)"
|
||||
>
|
||||
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="station-tracks">
|
||||
<div v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
|
||||
class="track catenary"
|
||||
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
|
||||
station.generalInfo.routes.singleElectrifiedNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
|
||||
</span>
|
||||
<span v-else-if="station.generalInfo.availability == 'abandoned'">
|
||||
<img
|
||||
src="/images/icon-abandoned.svg"
|
||||
alt="non-public"
|
||||
:title="$t('sceneries.info.abandoned')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
|
||||
class="track no-catenary"
|
||||
:title="`${$t('sceneries.info.single-track-routes-other')}${
|
||||
station.generalInfo.routes.singleOtherNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.singleOtherNames.length }}
|
||||
</span>
|
||||
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
|
||||
<img
|
||||
src="/images/icon-lock.svg"
|
||||
alt="non-public"
|
||||
:title="$t('sceneries.info.non-public')"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<img
|
||||
src="/images/icon-unavailable.svg"
|
||||
alt="unavailable"
|
||||
:title="$t('sceneries.info.unavailable')"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span v-else> ? </span>
|
||||
</td>
|
||||
|
||||
<td class="station-status">
|
||||
<StationStatusBadge
|
||||
:isOnline="station.onlineInfo ? true : false"
|
||||
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="station-dispatcher-name">
|
||||
<span v-if="station.onlineInfo?.dispatcherName">
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
|
||||
@click.stop="openDonationCard"
|
||||
data-tooltip-type="DonatorTooltip"
|
||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||
>
|
||||
<img src="/images/icon-diamond.svg" alt="" />
|
||||
{{ station.onlineInfo.dispatcherName }}
|
||||
</b>
|
||||
|
||||
<div v-else>
|
||||
{{ station.onlineInfo.dispatcherName }}
|
||||
</div>
|
||||
</td>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="station-tracks">
|
||||
<div v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
|
||||
class="track catenary"
|
||||
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
|
||||
station.generalInfo.routes.doubleElectrifiedNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
|
||||
</span>
|
||||
<td class="station-dispatcher-exp">
|
||||
<span
|
||||
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
||||
:style="
|
||||
calculateExpStyle(
|
||||
station.onlineInfo.dispatcherExp,
|
||||
station.onlineInfo.dispatcherIsSupporter
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<span
|
||||
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
|
||||
class="track no-catenary"
|
||||
:title="`${$t('sceneries.info.double-track-routes-other')}${
|
||||
station.generalInfo.routes.doubleOtherNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.doubleOtherNames.length }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="station-info">
|
||||
<td class="station-tracks">
|
||||
<div v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="station.generalInfo?.signalType"
|
||||
class="scenery-icon icon-info"
|
||||
:class="station.generalInfo?.controlType.replace('+', '-')"
|
||||
:title="
|
||||
$t('sceneries.info.control-type') +
|
||||
$t(`controls.${station.generalInfo?.controlType}`)
|
||||
"
|
||||
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
|
||||
class="track catenary"
|
||||
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
|
||||
station.generalInfo.routes.singleElectrifiedNames.length
|
||||
}`"
|
||||
>
|
||||
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
|
||||
</span>
|
||||
|
||||
<img
|
||||
v-if="station.generalInfo?.signalType"
|
||||
class="icon-info"
|
||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||
:alt="station.generalInfo.signalType"
|
||||
:title="
|
||||
$t('sceneries.info.signals-type') +
|
||||
$t(`signals.${station.generalInfo.signalType}`)
|
||||
"
|
||||
/>
|
||||
<span
|
||||
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
|
||||
class="track no-catenary"
|
||||
:title="`${$t('sceneries.info.single-track-routes-other')}${
|
||||
station.generalInfo.routes.singleOtherNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.singleOtherNames.length }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<img
|
||||
v-if="station.generalInfo?.SUP"
|
||||
class="icon-info"
|
||||
src="/images/icon-SUP.svg"
|
||||
alt="SUP (RASP-UZK)"
|
||||
:title="$t('sceneries.info.SUP')"
|
||||
/>
|
||||
<td class="station-tracks">
|
||||
<div v-if="station.generalInfo">
|
||||
<span
|
||||
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
|
||||
class="track catenary"
|
||||
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
|
||||
station.generalInfo.routes.doubleElectrifiedNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
|
||||
</span>
|
||||
|
||||
<img
|
||||
v-if="station.generalInfo?.ASDEK"
|
||||
class="icon-info"
|
||||
src="/images/icon-ASDEK.svg"
|
||||
alt="dSAT ASDEK"
|
||||
:title="$t('sceneries.info.ASDEK')"
|
||||
/>
|
||||
<span
|
||||
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
|
||||
class="track no-catenary"
|
||||
:title="`${$t('sceneries.info.double-track-routes-other')}${
|
||||
station.generalInfo.routes.doubleOtherNames.length
|
||||
}`"
|
||||
>
|
||||
{{ station.generalInfo.routes.doubleOtherNames.length }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<img
|
||||
v-if="!station.generalInfo"
|
||||
class="icon-info"
|
||||
src="/images/icon-unknown.svg"
|
||||
alt="icon-unknown"
|
||||
:title="$t('sceneries.info.unknown')"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-users"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
data-tooltip-type="UsersTooltip"
|
||||
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
|
||||
<td class="station-info">
|
||||
<span
|
||||
v-if="station.generalInfo?.signalType"
|
||||
class="scenery-icon icon-info"
|
||||
:class="station.generalInfo?.controlType.replace('+', '-')"
|
||||
:title="
|
||||
$t('sceneries.info.control-type') +
|
||||
$t(`controls.${station.generalInfo?.controlType}`)
|
||||
"
|
||||
>
|
||||
<span class="text--primary">{{
|
||||
station.onlineInfo?.stationTrains?.length ?? '-'
|
||||
}}</span>
|
||||
/
|
||||
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
|
||||
</td>
|
||||
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||
</span>
|
||||
|
||||
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
|
||||
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
|
||||
</td>
|
||||
<img
|
||||
v-if="station.generalInfo?.signalType"
|
||||
class="icon-info"
|
||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||
:alt="station.generalInfo.signalType"
|
||||
:title="
|
||||
$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
|
||||
"
|
||||
/>
|
||||
|
||||
<td
|
||||
class="station-spawns"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
data-tooltip-type="SpawnsTooltip"
|
||||
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
|
||||
>
|
||||
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
|
||||
</td>
|
||||
<img
|
||||
v-if="station.generalInfo?.SUP"
|
||||
class="icon-info"
|
||||
src="/images/icon-SUP.svg"
|
||||
alt="SUP (RASP-UZK)"
|
||||
:title="$t('sceneries.info.SUP')"
|
||||
/>
|
||||
|
||||
<td
|
||||
class="station-schedules all"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
|
||||
</td>
|
||||
<img
|
||||
v-if="station.generalInfo?.ASDEK"
|
||||
class="icon-info"
|
||||
src="/images/icon-ASDEK.svg"
|
||||
alt="dSAT ASDEK"
|
||||
:title="$t('sceneries.info.ASDEK')"
|
||||
/>
|
||||
|
||||
<td
|
||||
class="station-schedules unconfirmed"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
|
||||
</td>
|
||||
<img
|
||||
v-if="!station.generalInfo"
|
||||
class="icon-info"
|
||||
src="/images/icon-unknown.svg"
|
||||
alt="icon-unknown"
|
||||
:title="$t('sceneries.info.unknown')"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-schedules confirmed"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<td
|
||||
class="station-users"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
data-tooltip-type="UsersTooltip"
|
||||
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
|
||||
>
|
||||
<span class="text--primary">{{
|
||||
station.onlineInfo?.stationTrains?.length ?? '-'
|
||||
}}</span>
|
||||
/
|
||||
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
|
||||
</td>
|
||||
|
||||
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
|
||||
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-spawns"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
data-tooltip-type="SpawnsTooltip"
|
||||
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
|
||||
>
|
||||
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-schedules all"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-schedules unconfirmed"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="station-schedules confirmed"
|
||||
style="width: 30px"
|
||||
:class="{ inactive: !station.onlineInfo }"
|
||||
>
|
||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="no-stations" v-else>
|
||||
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
|
||||
>)
|
||||
<div>
|
||||
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
|
||||
>)
|
||||
</div>
|
||||
|
||||
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
|
||||
⚠ {{ $t('sceneries.active-filters') }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { defineComponent, inject, computed } from 'vue';
|
||||
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import dateMixin from '../../mixins/dateMixin';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import { useStationFiltersStore } from '../../store/stationFiltersStore';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import Loading from '../Global/Loading.vue';
|
||||
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
|
||||
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||
import { Station, Status } from '../../typings/common';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { Status } from '../../typings/common';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
import { getChangedFilters } from '../../managers/stationFilterManager';
|
||||
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
||||
import { filterStations, sortStations } from './utils';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['toggleDonationModal'],
|
||||
emits: ['toggleDonationCard'],
|
||||
|
||||
components: { Loading, StationStatusBadge },
|
||||
mixins: [styleMixin, dateMixin],
|
||||
|
||||
data: () => ({
|
||||
headIconsIds,
|
||||
headIds,
|
||||
lastSelectedStationName: ''
|
||||
lastSelectedStationName: '',
|
||||
getChangedFilters
|
||||
}),
|
||||
|
||||
computed: {
|
||||
sorterActive() {
|
||||
return this.stationFiltersStore.sorterActive;
|
||||
},
|
||||
|
||||
displayedStations() {
|
||||
return this.stationFiltersStore.filteredStationList;
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
const apiStore = useApiStore();
|
||||
const tooltipStore = useTooltipStore();
|
||||
|
||||
const stationFiltersStore = useStationFiltersStore();
|
||||
const filters = inject('StationsView_filters') as Record<string, any>;
|
||||
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
|
||||
|
||||
const filteredStationList = computed(() =>
|
||||
mainStore.allStationInfo
|
||||
.filter((station) => filterStations(station, filters))
|
||||
.sort((a, b) => sortStations(a, b, activeSorter))
|
||||
);
|
||||
|
||||
return {
|
||||
Status: Status.Data,
|
||||
stationFiltersStore,
|
||||
mainStore,
|
||||
apiStore,
|
||||
tooltipStore
|
||||
tooltipStore,
|
||||
filters,
|
||||
filteredStationList,
|
||||
activeSorter
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
setScenery(name: string) {
|
||||
const station = this.displayedStations.find((station) => station.name === name);
|
||||
const station = this.filteredStationList.find((station) => station.name === name);
|
||||
|
||||
if (!station) return;
|
||||
|
||||
@@ -376,8 +381,8 @@ export default defineComponent({
|
||||
});
|
||||
},
|
||||
|
||||
openDonationModal(e: Event) {
|
||||
this.$emit('toggleDonationModal', true);
|
||||
openDonationCard(e: Event) {
|
||||
this.$emit('toggleDonationCard', true);
|
||||
this.mainStore.modalLastClickedTarget = e.target;
|
||||
this.tooltipStore.hide();
|
||||
},
|
||||
@@ -388,10 +393,14 @@ export default defineComponent({
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
changeSorter(headerName: HeadIdsTypes) {
|
||||
changeSorter(headerName: HeadIdsType) {
|
||||
if (headerName == 'general') return;
|
||||
|
||||
this.stationFiltersStore.changeSorter(headerName);
|
||||
if (headerName == this.activeSorter.headerName)
|
||||
this.activeSorter.dir = -1 * this.activeSorter.dir;
|
||||
else this.activeSorter.dir = 1;
|
||||
|
||||
this.activeSorter.headerName = headerName;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -406,18 +415,18 @@ $rowCol: #424242;
|
||||
|
||||
.station_table {
|
||||
height: 80vh;
|
||||
min-height: 550px;
|
||||
max-height: 2000px;
|
||||
min-height: 700px;
|
||||
overflow: auto;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-stations {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
|
||||
font-size: 1.25em;
|
||||
padding: 1em;
|
||||
|
||||
background: #1a1a1a;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -5,54 +5,29 @@ export interface FilterOption {
|
||||
defaultValue: boolean;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
[key: string]: boolean | number | string;
|
||||
default: boolean;
|
||||
notDefault: boolean;
|
||||
real: boolean;
|
||||
fictional: boolean;
|
||||
SPK: boolean;
|
||||
SCS: boolean;
|
||||
SPE: boolean;
|
||||
SUP: boolean;
|
||||
noSUP: boolean;
|
||||
ASDEK: boolean;
|
||||
noASDEK: boolean;
|
||||
ręczne: boolean;
|
||||
'ręczne+SPK': boolean;
|
||||
'ręczne+SCS': boolean;
|
||||
mechaniczne: boolean;
|
||||
'mechaniczne+SPK': boolean;
|
||||
'mechaniczne+SCS': boolean;
|
||||
SBL: boolean;
|
||||
PBL: boolean;
|
||||
współczesna: boolean;
|
||||
kształtowa: boolean;
|
||||
historyczna: boolean;
|
||||
mieszana: boolean;
|
||||
minLevel: number;
|
||||
maxLevel: number;
|
||||
minOneWayCatenary: number;
|
||||
minOneWay: number;
|
||||
minTwoWayCatenary: number;
|
||||
minTwoWay: number;
|
||||
minVmax: number;
|
||||
maxVmax: number;
|
||||
'no-1track': boolean;
|
||||
'no-2track': boolean;
|
||||
'include-selected': boolean;
|
||||
free: boolean;
|
||||
occupied: boolean;
|
||||
nonPublic: boolean;
|
||||
unavailable: boolean;
|
||||
abandoned: boolean;
|
||||
endingStatus: boolean;
|
||||
afkStatus: boolean;
|
||||
noSpaceStatus: boolean;
|
||||
unavailableStatus: boolean;
|
||||
unsignedStatus: boolean;
|
||||
authors: string;
|
||||
onlineFromHours: number;
|
||||
withActiveTimetables: boolean;
|
||||
withoutActiveTimetables: boolean;
|
||||
export const headIds = [
|
||||
'station',
|
||||
'min-lvl',
|
||||
'status',
|
||||
'dispatcher',
|
||||
'dispatcher-lvl',
|
||||
'routes-single',
|
||||
'routes-double',
|
||||
'general'
|
||||
] as const;
|
||||
|
||||
export const headIconsIds = [
|
||||
'user',
|
||||
'like',
|
||||
'spawn',
|
||||
'timetableAll',
|
||||
'timetableUnconfirmed',
|
||||
'timetableConfirmed'
|
||||
] as const;
|
||||
|
||||
export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
|
||||
|
||||
export interface ActiveSorter {
|
||||
headerName: HeadIdsType;
|
||||
dir: number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="tooltip" v-show="tooltipStore.type" ref="preview">
|
||||
<div class="tooltip" ref="preview">
|
||||
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,10 +35,7 @@ export default defineComponent({
|
||||
let translateX = '0',
|
||||
translateY = '30px';
|
||||
|
||||
if (clientWidth < 500) {
|
||||
previewEl.style.left = '50%';
|
||||
translateX = '-50%';
|
||||
} else if (val[0] <= boxWidth / 2) {
|
||||
if (val[0] <= boxWidth / 2) {
|
||||
previewEl.style.left = '0';
|
||||
translateX = '0px';
|
||||
} else if (val[0] >= clientWidth - boxWidth / 2) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
import { StationTrain } from '../../typings/common';
|
||||
import { Train } from '../../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
@@ -23,7 +23,7 @@ export default defineComponent({
|
||||
trains() {
|
||||
if (this.tooltipStore.content == '') return [];
|
||||
|
||||
const parsedTrains = JSON.parse(this.tooltipStore.content) as StationTrain[];
|
||||
const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
|
||||
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,20 @@
|
||||
width="300"
|
||||
height="176"
|
||||
class="rounded-md w-full h-auto"
|
||||
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
|
||||
:src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
|
||||
/>
|
||||
|
||||
<div v-if="imageState == 'error'" class="error-placeholder"></div>
|
||||
|
||||
<div class="vehicle-name">
|
||||
{{ tooltipStore.content.replace(/_/g, ' ') }}
|
||||
{{ vehicleName.replace(/_/g, ' ') }}
|
||||
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
||||
</div>
|
||||
|
||||
<div class="vehicle-props" v-if="vehicleData">
|
||||
{{ vehicleData.group.speed }}km/h • {{ vehicleData.group.length }}m •
|
||||
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
|
||||
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -27,11 +34,13 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tooltipStore: useTooltipStore(),
|
||||
apiStore: useApiStore(),
|
||||
imageState: 'loading'
|
||||
};
|
||||
},
|
||||
@@ -41,7 +50,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
watch: {
|
||||
'tooltipStore.type'(prev, val) {
|
||||
vehicleName(prev, val) {
|
||||
if (prev != val) this.imageState = 'loading';
|
||||
}
|
||||
},
|
||||
@@ -56,6 +65,22 @@ export default defineComponent({
|
||||
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
vehicleName() {
|
||||
return this.tooltipStore.content.split(':')[0];
|
||||
},
|
||||
|
||||
vehicleData() {
|
||||
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
|
||||
},
|
||||
|
||||
vehicleCargo() {
|
||||
return this.vehicleData?.group.cargoTypes?.find(
|
||||
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -85,10 +110,13 @@ img {
|
||||
.vehicle-name {
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
color: #ccc;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.vehicle-props {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
height: 176px;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<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
|
||||
v-if="stop.position != 'begin'"
|
||||
class="date arrival"
|
||||
:data-status="
|
||||
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
|
||||
? 'delayed'
|
||||
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
|
||||
? 'preponed'
|
||||
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.arrivalDelay > 0"
|
||||
:data-status-preponed="stop.arrivalDelay < 0"
|
||||
:data-status="stop.status"
|
||||
>
|
||||
p.
|
||||
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
|
||||
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
|
||||
{{ timestampToString(stop.arrivalReal) }}
|
||||
@@ -27,19 +25,16 @@
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="
|
||||
stop.duration ||
|
||||
(stop.status == 'stopped' &&
|
||||
stop.position != 'begin' &&
|
||||
stop.departureDelay != stop.arrivalDelay)
|
||||
"
|
||||
v-if="stop.duration"
|
||||
class="date stop"
|
||||
:data-stop-types="stop.type.replace(', ', '-')"
|
||||
:data-stop-status="
|
||||
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
|
||||
"
|
||||
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
|
||||
>
|
||||
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }}
|
||||
{{
|
||||
stop.duration == 0 && stop.departureDelay > 0
|
||||
? stop.departureDelay - stop.arrivalDelay
|
||||
: stop.duration
|
||||
}}
|
||||
{{ stop.type == '' ? 'pt' : stop.type }}
|
||||
</span>
|
||||
|
||||
@@ -49,16 +44,11 @@
|
||||
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
|
||||
"
|
||||
class="date departure"
|
||||
:data-status="
|
||||
stop.departureDelay > 0 && stop.status == 'confirmed'
|
||||
? 'delayed'
|
||||
: stop.departureDelay < 0 && stop.status == 'confirmed'
|
||||
? 'preponed'
|
||||
: stop.departureDelay == 0 && stop.status == 'confirmed'
|
||||
? 'on-time'
|
||||
: ''
|
||||
"
|
||||
:data-status-delayed="stop.departureDelay > 0"
|
||||
:data-status-preponed="stop.departureDelay < 0"
|
||||
:data-status-confirmed="stop.status == 'confirmed'"
|
||||
>
|
||||
o.
|
||||
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
|
||||
<s>{{ timestampToString(stop.departureScheduled) }}</s>
|
||||
{{ timestampToString(stop.departureReal) }}
|
||||
@@ -96,14 +86,18 @@ $delayedClr: salmon;
|
||||
$dateClr: #525151;
|
||||
$stopExchangeClr: #db8e29;
|
||||
$stopDefaultClr: #252525;
|
||||
$stopNameClr: #22a8d1;
|
||||
$stopNameClr: #303030;
|
||||
|
||||
s {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stop-label {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&[data-sbl='true'] {
|
||||
&[data-minor='true'] {
|
||||
.date {
|
||||
display: none;
|
||||
}
|
||||
@@ -117,6 +111,7 @@ $stopNameClr: #22a8d1;
|
||||
|
||||
.name {
|
||||
background: $stopNameClr;
|
||||
border-radius: 0.5em 0 0 0.5em;
|
||||
padding: 0.3em 0.5em;
|
||||
|
||||
display: flex;
|
||||
@@ -130,6 +125,10 @@ $stopNameClr: #22a8d1;
|
||||
.date {
|
||||
background: $dateClr;
|
||||
padding: 0.3em 0.5em;
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stop {
|
||||
@@ -145,27 +144,44 @@ $stopNameClr: #22a8d1;
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrival,
|
||||
.departure {
|
||||
&[data-status='delayed'] {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
.stop .arrival {
|
||||
&[data-status='confirmed'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='preponed'] {
|
||||
s {
|
||||
color: #999;
|
||||
}
|
||||
&[data-status='confirmed'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
&[data-status='stopped'][data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status='stopped'][data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stop .departure[data-status-confirmed='true'] {
|
||||
&[data-status-delayed='true'] {
|
||||
span {
|
||||
color: $delayedClr;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-status-preponed='true'] {
|
||||
span {
|
||||
color: $preponedClr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,18 @@
|
||||
<div>
|
||||
<img src="/images/icon-speed.svg" alt="speed icon" />
|
||||
{{ train.speed }} km/h
|
||||
|
||||
<span v-if="stockSpeedLimit != Infinity">
|
||||
•
|
||||
<em
|
||||
class="text--grayed"
|
||||
style="text-decoration: underline dotted"
|
||||
tabindex="0"
|
||||
:data-tooltip="$t('trains.vmax-tooltip')"
|
||||
>
|
||||
{{ stockSpeedLimit }} km/h
|
||||
</em>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,6 +203,18 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
stockSpeedLimit() {
|
||||
return this.train.stockList.reduce((acc, stockName) => {
|
||||
const vehicleSpeed =
|
||||
this.apiStore.vehiclesData?.find((v) => v.name == stockName.split(':')[0])?.group.speed ??
|
||||
300;
|
||||
|
||||
return Math.min(vehicleSpeed, acc);
|
||||
}, 300);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToJournal() {
|
||||
this.$router.push({
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineComponent({
|
||||
|
||||
computed: {
|
||||
chosenTrain() {
|
||||
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
|
||||
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,8 +29,15 @@ export default defineComponent({
|
||||
chosenTrain(train: Train | undefined) {
|
||||
this.$nextTick(() => {
|
||||
if (train) {
|
||||
document.body.classList.add('no-scroll');
|
||||
const contentEl = this.$refs['content'] as HTMLElement;
|
||||
contentEl.focus();
|
||||
} else {
|
||||
(this.store.modalLastClickedTarget as any)?.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('no-scroll');
|
||||
}, 90);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -40,20 +47,6 @@ export default defineComponent({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
@import '../../styles/card.scss';
|
||||
|
||||
.top-info-bar-anim {
|
||||
&-enter-active,
|
||||
&-leave-active {
|
||||
transition: all 150ms ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.train-modal {
|
||||
position: fixed;
|
||||
@@ -61,12 +54,14 @@ export default defineComponent({
|
||||
left: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
color: white;
|
||||
z-index: 200;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
text-align: left;
|
||||
}
|
||||
@@ -87,10 +82,10 @@ export default defineComponent({
|
||||
position: relative;
|
||||
overflow-y: scroll;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
width: 95vw;
|
||||
max-height: 95vh;
|
||||
max-height: 95dvh;
|
||||
margin-top: 1em;
|
||||
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
@@ -105,10 +100,4 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
.modal_content {
|
||||
max-height: 85vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,6 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-actions">
|
||||
<div></div>
|
||||
<button class="btn--action" @click="resetAllFilters">
|
||||
{{ $t('options.filter-reset') }}
|
||||
</button>
|
||||
@@ -223,9 +222,6 @@ export default defineComponent({
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
|
||||
margin-top: 1em;
|
||||
|
||||
> * {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
:data-stop-type="stop.type"
|
||||
:data-minor-stop-active="stop.isActive"
|
||||
:data-last-confirmed="stop.isLastConfirmed"
|
||||
x
|
||||
>
|
||||
<span class="stop_info">
|
||||
<span class="distance">
|
||||
@@ -48,26 +47,46 @@
|
||||
<span
|
||||
v-if="
|
||||
stop.departureLine &&
|
||||
stop.departureLine == scheduleStops[i + 1]?.arrivalLine &&
|
||||
!/sbl/gi.test(stop.departureLine)
|
||||
scheduleStops[i + 1] != undefined &&
|
||||
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
|
||||
"
|
||||
>
|
||||
{{ stop.departureLine }}
|
||||
</span>
|
||||
|
||||
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
|
||||
<div>{{ stop.departureLine }}</div>
|
||||
<div
|
||||
class="scenery-change-name"
|
||||
v-if="
|
||||
i < scheduleStops.length - 1 &&
|
||||
stop.sceneryName != scheduleStops[i + 1].sceneryName
|
||||
"
|
||||
>
|
||||
{{ scheduleStops[i + 1].sceneryName }}
|
||||
<div class="scenery-route">
|
||||
<span>{{ stop.departureLine }}</span>
|
||||
<span v-if="stop.departureLineInfo">
|
||||
| {{ stop.departureLineInfo.routeSpeed }}
|
||||
<span v-if="stop.departureLineInfo.isElectric">⚡</span>
|
||||
<img
|
||||
v-else
|
||||
src="/images/icon-we4a.png"
|
||||
:title="$t('trains.we4a-tooltip')"
|
||||
width="12"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ scheduleStops[i + 1].arrivalLine }}
|
||||
|
||||
<div
|
||||
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
|
||||
class="scenery-change-name"
|
||||
>
|
||||
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
|
||||
<span v-if="stop.departureLineInfo?.routeTracks == 1"> ↕</span>
|
||||
<span v-else> ⇅</span>
|
||||
</div>
|
||||
|
||||
<div class="scenery-route">
|
||||
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
|
||||
|
||||
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
|
||||
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
|
||||
<span v-if="scheduleStops[i + 1].arrivalLineInfo!.isElectric">⚡</span>
|
||||
<img
|
||||
v-else
|
||||
src="/images/icon-we4a.png"
|
||||
:title="$t('trains.we4a-tooltip')"
|
||||
width="12"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
@@ -85,7 +104,7 @@ import StopLabel from './StopLabel.vue';
|
||||
import StockList from '../Global/StockList.vue';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { Train } from '../../typings/common';
|
||||
import { StationRoutesInfo, Train } from '../../typings/common';
|
||||
|
||||
export interface TrainScheduleStop {
|
||||
nameHtml: string;
|
||||
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
|
||||
isSBL: boolean;
|
||||
|
||||
sceneryName: string | null;
|
||||
sceneryHash: string;
|
||||
distance: number;
|
||||
|
||||
arrivalLine: string | null;
|
||||
departureLine: string | null;
|
||||
|
||||
arrivalLineInfo?: StationRoutesInfo;
|
||||
departureLineInfo?: StationRoutesInfo;
|
||||
|
||||
isExternal: boolean;
|
||||
|
||||
comments: string | null;
|
||||
}
|
||||
|
||||
@@ -146,13 +169,25 @@ export default defineComponent({
|
||||
|
||||
return (
|
||||
this.train.timetableData?.followingStops.map((stop, i, arr) => {
|
||||
if (
|
||||
const isExternal =
|
||||
i > 0 &&
|
||||
stop.arrivalLine &&
|
||||
stop.arrivalLine != arr[i - 1].departureLine &&
|
||||
!/sbl/gi.test(stop.arrivalLine)
|
||||
)
|
||||
currentSceneryIndex++;
|
||||
stop.arrivalLine != null &&
|
||||
(stop.arrivalLine != arr[i - 1].departureLine ||
|
||||
(stop.arrivalLine == arr[i - 1].departureLine &&
|
||||
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
|
||||
|
||||
if (isExternal) currentSceneryIndex++;
|
||||
|
||||
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
|
||||
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
|
||||
|
||||
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
|
||||
(r) => r.routeName == stop.arrivalLine
|
||||
);
|
||||
|
||||
const departureLineInfo = sceneryInfo?.routesInfo.find(
|
||||
(r) => r.routeName == stop.departureLine
|
||||
);
|
||||
|
||||
return {
|
||||
nameHtml: stop.stopName,
|
||||
@@ -174,14 +209,18 @@ export default defineComponent({
|
||||
arrivalLine: stop.arrivalLine,
|
||||
departureLine: stop.departureLine,
|
||||
|
||||
arrivalLineInfo: arrivalLineInfo,
|
||||
departureLineInfo: departureLineInfo,
|
||||
|
||||
isExternal,
|
||||
|
||||
type: stop.stopType,
|
||||
distance: stop.stopDistance,
|
||||
isActive: this.activeMinorStops.includes(i),
|
||||
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
|
||||
isSBL: /sbl/gi.test(stop.stopName),
|
||||
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
|
||||
sceneryHash: '',
|
||||
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
|
||||
sceneryName,
|
||||
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
|
||||
};
|
||||
}) ?? []
|
||||
@@ -483,22 +522,26 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-line-info {
|
||||
.scenery-change-name {
|
||||
position: relative;
|
||||
margin: 0.25em 0;
|
||||
.scenery-route {
|
||||
img {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 30px;
|
||||
background-color: #aaa;
|
||||
.scenery-change-name {
|
||||
position: relative;
|
||||
margin: 0.25em 0;
|
||||
|
||||
top: 50%;
|
||||
right: calc(100% + 5px);
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 2px;
|
||||
width: 30px;
|
||||
background-color: #aaa;
|
||||
|
||||
top: 50%;
|
||||
right: calc(100% + 5px);
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,17 +8,18 @@
|
||||
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
|
||||
|
||||
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
|
||||
{{ $t('trains.no-trains') }}
|
||||
{{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
|
||||
>)
|
||||
</div>
|
||||
|
||||
<transition-group name="list-anim" tag="ul">
|
||||
<li
|
||||
class="train-row"
|
||||
v-for="train in trains"
|
||||
:key="train.trainId"
|
||||
:key="train.id"
|
||||
tabindex="0"
|
||||
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@click.stop="selectModalTrain(train, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
|
||||
>
|
||||
<TrainInfo :train="train" :extended="false" />
|
||||
</li>
|
||||
@@ -76,17 +77,6 @@ export default defineComponent({
|
||||
|
||||
return Status.Data.Loaded;
|
||||
}
|
||||
},
|
||||
|
||||
activated() {
|
||||
const query = this.$route.query;
|
||||
if (query.trainNo && query.driverName) {
|
||||
this.searchedDriver = query.driverName.toString();
|
||||
this.searchedTrain = query.trainNo.toString();
|
||||
setTimeout(() => {
|
||||
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
|
||||
}, 20);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -108,8 +98,7 @@ export default defineComponent({
|
||||
text-align: center;
|
||||
|
||||
padding: 1em 0;
|
||||
|
||||
font-size: 1.5em;
|
||||
font-size: 1.25em;
|
||||
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
"id": "eu",
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
"filters": "FILTERS",
|
||||
"donate": "DONATE",
|
||||
|
||||
"search-button": "Search",
|
||||
"reset-button": "Reset",
|
||||
"search-button": "SEARCH",
|
||||
"reset-button": "RESET",
|
||||
|
||||
"sort-title": "SORT BY:",
|
||||
"filter-title": "FILTER BY:",
|
||||
@@ -125,7 +125,9 @@
|
||||
"search-dispatcher": "Dispatcher name",
|
||||
"search-station": "Scenery name",
|
||||
"search-author": "Timetable author name",
|
||||
"search-issuedFrom": "Origin scenery name",
|
||||
"search-issuedFrom": "Issuing scenery name",
|
||||
"search-via": "Via scenery name",
|
||||
"search-terminatingAt": "Terminating scenery name",
|
||||
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
|
||||
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
|
||||
"search-date": "Date (UTC+2 / CEST)",
|
||||
@@ -174,8 +176,9 @@
|
||||
|
||||
"sections": {
|
||||
"quick": "QUICK FILTERS",
|
||||
"stationType": "STATION TYPE",
|
||||
"reality": "SCENERY REALITY",
|
||||
"package-access": "IN-GAME AVAILABILITY",
|
||||
"packageAccess": "IN-GAME AVAILABILITY",
|
||||
"access": "GENERAL AVAILABILITY",
|
||||
"control": "CONTROLS",
|
||||
"signals": "SIGNALLING",
|
||||
@@ -186,6 +189,9 @@
|
||||
"spawns": "OPEN SPAWNS"
|
||||
},
|
||||
|
||||
"changed-filters-count": "Changed filters:",
|
||||
"no-changed-filters": "No changed filters",
|
||||
|
||||
"all-available": "ALL AVAILABLE",
|
||||
"all-free": "CURRENTLY FREE",
|
||||
|
||||
@@ -196,11 +202,11 @@
|
||||
|
||||
"title": "STATION FILTERS",
|
||||
"default": "IN-GAME",
|
||||
"not-default": "ADDITIONAL",
|
||||
"notDefault": "ADDITIONAL",
|
||||
"real": "REAL",
|
||||
"fictional": "FICTIONAL",
|
||||
"unavailable": "UNSUPPORTED",
|
||||
"non-public": "NON-PUBLIC",
|
||||
"nonPublic": "NON-PUBLIC",
|
||||
"abandoned": "ABANDONED",
|
||||
|
||||
"SPK": "SPK",
|
||||
@@ -210,7 +216,6 @@
|
||||
"SCS-R": "SCS + MANUAL",
|
||||
"SCS-M": "SCS + MECH.",
|
||||
"SPE": "SPE",
|
||||
|
||||
"manual": "MANUAL",
|
||||
"mechanical": "MECHANICAL",
|
||||
|
||||
@@ -233,15 +238,18 @@
|
||||
"withActiveTimetables": "ACTIVE",
|
||||
"withoutActiveTimetables": "NO ACTIVE",
|
||||
|
||||
"junction": "JUNCTIONS",
|
||||
"nonJunction": "OTHER",
|
||||
|
||||
"sliders": {
|
||||
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
|
||||
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
|
||||
"min-vmax": "MIN. SCENERY ROUTE SPEED",
|
||||
"max-vmax": "MAX. SCENERY ROUTE SPEED",
|
||||
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
|
||||
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
|
||||
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
|
||||
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
|
||||
"minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
|
||||
"maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
|
||||
"minVmax": "MIN. SCENERY ROUTE SPEED",
|
||||
"maxVmax": "MAX. SCENERY ROUTE SPEED",
|
||||
"minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
|
||||
"minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
|
||||
"minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
|
||||
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
|
||||
},
|
||||
|
||||
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
|
||||
@@ -294,16 +302,16 @@
|
||||
"single-track-routes-other": "Not electrified single-track routes count: "
|
||||
},
|
||||
"no-stations": "No stations to show here!",
|
||||
"scenery-search": "Search for scenery..."
|
||||
"scenery-search": "Search for scenery...",
|
||||
"active-filters": "Attention! You got active filters!"
|
||||
},
|
||||
"station-stats": {
|
||||
"u-factor": "U-factor",
|
||||
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
|
||||
"avg-timetable-count": "Average timetable count for one dispatcher:",
|
||||
"single-track-count": "Available single track routes:",
|
||||
"double-track-count": "Available double track routes:",
|
||||
"electrified": "(electrified)",
|
||||
"not-electrified": "(not electr.)",
|
||||
"avg-timetable-count": "Average count of scenery timetables:",
|
||||
"single-track-count": "Single track routes:",
|
||||
"double-track-count": "Double track routes:",
|
||||
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
|
||||
"open-spawns": "Open spawns:"
|
||||
},
|
||||
"trains": {
|
||||
@@ -323,6 +331,9 @@
|
||||
"current-signal": "at signal",
|
||||
"current-track": "on track",
|
||||
|
||||
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
|
||||
"we4a-tooltip": "Non-electrified track",
|
||||
|
||||
"delayed": "Delayed: ",
|
||||
"preponed": "Ahead of schedule: ",
|
||||
"on-time": "On time",
|
||||
@@ -485,21 +496,16 @@
|
||||
"option-timetables-history": "Timetables history PL1",
|
||||
"option-dispatchers-history": "Dispatchers history PL1",
|
||||
|
||||
"timetable-author-title": "Issued by",
|
||||
"timetable-author-unknown": "Author unknown",
|
||||
"timetable-via": "ALL TIMETABLES",
|
||||
"timetable-issuedFrom": "BEGINS HERE",
|
||||
"timetable-terminatingAt": "TERMINATES HERE",
|
||||
|
||||
"timetables-history-id": "ID",
|
||||
"timetables-history-number": "Number",
|
||||
"timetables-history-route": "Route",
|
||||
"timetables-history-driver": "Driver",
|
||||
"timetables-history-author": "TT author",
|
||||
"timetables-history-date": "Date",
|
||||
"timetable-issued-date": "Issued",
|
||||
"timetable-issued-by": " by:",
|
||||
"timetable-issued-for": " for driver:",
|
||||
|
||||
"dispatchers-history-hash": "Hash",
|
||||
"dispatchers-history-dispatcher": "Dispatcher",
|
||||
"dispatchers-history-level": "Level",
|
||||
"dispatchers-history-rate": "Rate",
|
||||
"dispatchers-history-date": "Service date",
|
||||
"dispatcher-rate": "Rate:",
|
||||
"dispatcher-status-changes": "Status changes:",
|
||||
|
||||
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
|
||||
"history-list-empty": "No recorded scenery history!",
|
||||
|
||||
@@ -108,8 +108,8 @@
|
||||
"filters": "FILTRY",
|
||||
"donate": "WESPRZYJ",
|
||||
|
||||
"search-button": "Szukaj",
|
||||
"reset-button": "Zresetuj",
|
||||
"search-button": "SZUKAJ",
|
||||
"reset-button": "ZRESETUJ",
|
||||
|
||||
"sort-title": "SORTUJ WG:",
|
||||
"filter-title": "FILTRUJ WG:",
|
||||
@@ -122,6 +122,8 @@
|
||||
"search-station": "Nazwa scenerii",
|
||||
"search-author": "Nick autora rozkładu jazdy",
|
||||
"search-issuedFrom": "Sceneria początkowa",
|
||||
"search-via": "Przez scenerię",
|
||||
"search-terminatingAt": "Sceneria końcowa",
|
||||
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
|
||||
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
|
||||
"search-date": "Data (UTC+2 / CEST)",
|
||||
@@ -171,8 +173,9 @@
|
||||
|
||||
"sections": {
|
||||
"quick": "SZYBKIE FILTRY",
|
||||
"stationType": "RODZAJ STACJI",
|
||||
"reality": "FIKCYJNOŚĆ SCENERII",
|
||||
"package-access": "DOSTĘPNOŚĆ W PACZCE",
|
||||
"packageAccess": "DOSTĘPNOŚĆ W PACZCE",
|
||||
"access": "DOSTĘPNOŚĆ OGÓLNA",
|
||||
"control": "TYP STEROWANIA",
|
||||
"signals": "TYP SYGNALIZACJI",
|
||||
@@ -183,6 +186,9 @@
|
||||
"spawns": "OTWARTE SPAWNY"
|
||||
},
|
||||
|
||||
"changed-filters-count": "Zmienione filtry:",
|
||||
"no-changed-filters": "Brak zmienionych filtrów",
|
||||
|
||||
"all-available": "WSZYSTKIE DOSTĘPNE",
|
||||
"all-free": "WSZYSTKIE WOLNE",
|
||||
|
||||
@@ -193,11 +199,11 @@
|
||||
|
||||
"title": "FILTRUJ STACJE",
|
||||
"default": "DOMYŚLNA",
|
||||
"not-default": "POZA PACZKĄ",
|
||||
"notDefault": "POZA PACZKĄ",
|
||||
"real": "REALNA",
|
||||
"fictional": "FIKCYJNA",
|
||||
"unavailable": "NIEDOSTĘPNA",
|
||||
"non-public": "NIEPUBLICZNA",
|
||||
"nonPublic": "NIEPUBLICZNA",
|
||||
"abandoned": "WYCOFANA",
|
||||
|
||||
"SPK": "SPK",
|
||||
@@ -229,15 +235,18 @@
|
||||
"withActiveTimetables": "AKTYWNE",
|
||||
"withoutActiveTimetables": "BEZ AKTYWNYCH",
|
||||
|
||||
"junction": "WĘZŁOWE",
|
||||
"nonJunction": "INNE",
|
||||
|
||||
"sliders": {
|
||||
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
|
||||
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
|
||||
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
|
||||
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
|
||||
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
|
||||
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
|
||||
"minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
|
||||
"maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
|
||||
"minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
|
||||
"minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
|
||||
"minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
|
||||
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
|
||||
},
|
||||
|
||||
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
|
||||
@@ -287,16 +296,16 @@
|
||||
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
|
||||
},
|
||||
"no-stations": "Brak stacji do wyświetlenia!",
|
||||
"scenery-search": "Wyszukaj scenerię..."
|
||||
"scenery-search": "Wyszukaj scenerię...",
|
||||
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
|
||||
},
|
||||
"station-stats": {
|
||||
"u-factor": "Współczynnik Ugla",
|
||||
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
|
||||
"avg-timetable-count": "Średnia liczba rozkładów jazdy na dyżurnego:",
|
||||
"single-track-count": "Dostępne szlaki jednotorowe:",
|
||||
"double-track-count": "Dostępne szlaki dwutorowe:",
|
||||
"electrified": "(zelektr.)",
|
||||
"not-electrified": "(niezelektr.)",
|
||||
"avg-timetable-count": "Średnia liczba rozkładów jazdy na sceneriach:",
|
||||
"single-track-count": "Szlaki jednotorowe:",
|
||||
"double-track-count": "Szlaki dwutorowe:",
|
||||
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
|
||||
"open-spawns": "Otwarte spawny:"
|
||||
},
|
||||
"trains": {
|
||||
@@ -308,6 +317,9 @@
|
||||
"current-signal": "przy semaforze",
|
||||
"current-track": "na szlaku",
|
||||
|
||||
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
|
||||
"we4a-tooltip": "Szlak niezelektryfikowany",
|
||||
|
||||
"delayed": "Opóźniony: ",
|
||||
"preponed": "Przed czasem: ",
|
||||
"on-time": "Planowo",
|
||||
@@ -467,21 +479,16 @@
|
||||
"option-timetables-history": "Historia rozkładów PL1",
|
||||
"option-dispatchers-history": "Historia dyżurów PL1",
|
||||
|
||||
"timetable-author-title": "Wydany przez",
|
||||
"timetable-author-unknown": "Autor nieznany",
|
||||
"timetable-via": "WSZYSTKIE RJ",
|
||||
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
|
||||
"timetable-terminatingAt": "KOŃCZY BIEG",
|
||||
|
||||
"timetables-history-id": "ID",
|
||||
"timetables-history-number": "Numer",
|
||||
"timetables-history-route": "Trasa",
|
||||
"timetables-history-driver": "Maszynista",
|
||||
"timetables-history-author": "Autor RJ",
|
||||
"timetables-history-date": "Data",
|
||||
"timetable-issued-date": "Wystawiony",
|
||||
"timetable-issued-by": " przez:",
|
||||
"timetable-issued-for": " dla maszynisty:",
|
||||
|
||||
"dispatchers-history-hash": "Hash",
|
||||
"dispatchers-history-dispatcher": "Dyżurny",
|
||||
"dispatchers-history-level": "Poziom",
|
||||
"dispatchers-history-rate": "Ocena",
|
||||
"dispatchers-history-date": "Data służby",
|
||||
"dispatcher-rate": "Ocena:",
|
||||
"dispatcher-status-changes": "Zmiany statusów:",
|
||||
|
||||
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
|
||||
"history-list-empty": "Brak historii dla tej scenerii!",
|
||||
|
||||
@@ -5,10 +5,15 @@ import router from './router';
|
||||
import i18n from './i18n';
|
||||
|
||||
import { createPinia } from 'pinia';
|
||||
import useCustomSW from './mixins/useCustomSW';
|
||||
import { registerSW } from 'virtual:pwa-register';
|
||||
|
||||
// Service worker
|
||||
useCustomSW();
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh() {
|
||||
console.log('Needs refresh!');
|
||||
}
|
||||
});
|
||||
|
||||
const clickOutsideDirective: Directive = {
|
||||
mounted(el, binding) {
|
||||
|
||||
@@ -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]
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,10 @@ export default class StorageManager {
|
||||
window.localStorage.removeItem(key);
|
||||
}
|
||||
|
||||
static getValue(key: string) {
|
||||
return window.localStorage.getItem(key);
|
||||
}
|
||||
|
||||
static getBooleanValue(key: string): boolean {
|
||||
return window.localStorage.getItem(key) === 'true' ? true : false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import { useTooltipStore } from '../store/tooltipStore';
|
||||
import { Train } from '../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
@@ -11,20 +12,19 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
methods: {
|
||||
selectModalTrain(trainId: string, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = trainId;
|
||||
document.body.classList.add('no-scroll');
|
||||
selectModalTrain(train: Train, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = train.modalId;
|
||||
if (target) this.store.modalLastClickedTarget = target;
|
||||
},
|
||||
|
||||
selectModalTrainById(modalId: string, target?: EventTarget | null) {
|
||||
this.store.chosenModalTrainId = modalId;
|
||||
if (target) this.store.modalLastClickedTarget = target;
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
this.store.chosenModalTrainId = undefined;
|
||||
this.tooltipStore.hide();
|
||||
|
||||
setTimeout(() => {
|
||||
(this.store.modalLastClickedTarget as any)?.focus();
|
||||
document.body.classList.remove('no-scroll');
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,16 +108,19 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
currentDelay(stops: TrainStop[]) {
|
||||
const delay =
|
||||
stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)
|
||||
)?.departureDelay || 0;
|
||||
const lastConfirmedStop = stops.find(
|
||||
(stop, i) =>
|
||||
(i == 0 && !stop.confirmed) ||
|
||||
(i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
|
||||
(stops[i + 1] == undefined && stop.confirmed)
|
||||
);
|
||||
|
||||
if (delay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
|
||||
else if (delay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`;
|
||||
const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
|
||||
|
||||
if (lastDelay > 0)
|
||||
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
|
||||
else if (lastDelay < 0)
|
||||
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
|
||||
else return this.$t('trains.on-time');
|
||||
},
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||
|
||||
export default () => {
|
||||
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
|
||||
immediate: true
|
||||
});
|
||||
|
||||
return {
|
||||
needRefresh,
|
||||
updateServiceWorker,
|
||||
offlineReady
|
||||
};
|
||||
};
|
||||
@@ -58,7 +58,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
const router = createRouter({
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
|
||||
return { el: `.app_main` };
|
||||
return { el: `.app_main`, top: -15 };
|
||||
|
||||
if (savedPosition) return savedPosition;
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
};
|
||||
@@ -4,24 +4,21 @@ import { Status } from '../typings/common';
|
||||
import { StationJSONData } from './typings';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
|
||||
export enum APIMode {
|
||||
PRODUCTION = 0,
|
||||
DEV = 1,
|
||||
MOCK = 2
|
||||
}
|
||||
|
||||
export const useApiStore = defineStore('apiStore', {
|
||||
state: () => ({
|
||||
dataStatuses: {
|
||||
connection: Status.Data.Loading,
|
||||
sceneries: Status.Data.Loading
|
||||
sceneries: Status.Data.Loading,
|
||||
vehicles: Status.Data.Loading
|
||||
},
|
||||
|
||||
activeData: undefined as API.ActiveData.Response | undefined,
|
||||
vehiclesData: undefined as API.Vehicles.Response | undefined,
|
||||
|
||||
donatorsData: [] as API.Donators.Response,
|
||||
sceneryData: [] as StationJSONData[],
|
||||
|
||||
lastFetchData: new Date(),
|
||||
nextUpdateTime: 0,
|
||||
|
||||
client: undefined as AxiosInstance | undefined,
|
||||
|
||||
@@ -54,18 +51,40 @@ export const useApiStore = defineStore('apiStore', {
|
||||
// Static data
|
||||
this.fetchDonatorsData();
|
||||
this.fetchStationsGeneralInfo();
|
||||
this.fetchVehiclesInfo();
|
||||
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
updateTick(t: number) {
|
||||
if (this.dataStatuses.connection == Status.Data.Offline) return;
|
||||
|
||||
if (t >= this.nextUpdateTime) {
|
||||
this.fetchActiveData();
|
||||
this.nextUpdateTime = t + 20000;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(this.updateTick);
|
||||
},
|
||||
|
||||
async fetchActiveData() {
|
||||
// if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
|
||||
// import('../../tests/data/getActiveData.json').then((data) => {
|
||||
// console.warn('activeData: mocking mode');
|
||||
// this.activeData = data.default as API.ActiveData.Response;
|
||||
|
||||
// this.dataStatuses.connection = Status.Data.Loaded;
|
||||
// });
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
try {
|
||||
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
|
||||
|
||||
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
|
||||
|
||||
this.activeData = response.data;
|
||||
this.lastFetchData = new Date();
|
||||
this.dataStatuses.connection = Status.Data.Loaded;
|
||||
} catch (error) {
|
||||
this.dataStatuses.connection = Status.Data.Error;
|
||||
@@ -84,17 +103,39 @@ export const useApiStore = defineStore('apiStore', {
|
||||
},
|
||||
|
||||
async fetchStationsGeneralInfo() {
|
||||
const sceneryData: StationJSONData[] = (
|
||||
await this.client!.get<StationJSONData[]>('api/getSceneries')
|
||||
).data;
|
||||
try {
|
||||
const sceneryData: StationJSONData[] = (
|
||||
await this.client!.get<StationJSONData[]>('api/getSceneries')
|
||||
).data;
|
||||
|
||||
if (!sceneryData) {
|
||||
this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||
this.sceneryData = sceneryData;
|
||||
} catch (error) {
|
||||
this.dataStatuses.sceneries = Status.Data.Error;
|
||||
return;
|
||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o sceneriach:', error);
|
||||
}
|
||||
},
|
||||
|
||||
this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||
this.sceneryData = sceneryData;
|
||||
async fetchVehiclesInfo() {
|
||||
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
|
||||
// import('../../tests/data/vehicles.json').then((data) => {
|
||||
// console.warn('vehicles.json: mocking mode');
|
||||
// this.vehiclesData = data.default;
|
||||
// this.dataStatuses.vehicles = Status.Data.Loaded;
|
||||
// });
|
||||
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
|
||||
|
||||
this.vehiclesData = response.data;
|
||||
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
|
||||
} catch (error) {
|
||||
this.dataStatuses.vehicles = Status.Data.Error;
|
||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
|
||||
import { parseSpawns } from './utils';
|
||||
|
||||
import {
|
||||
ActiveScenery,
|
||||
ScheduledTrain,
|
||||
CheckpointTrain,
|
||||
Station,
|
||||
StationRoutes,
|
||||
Status,
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
import { useApiStore } from './apiStore';
|
||||
import { MainStoreState } from './typings';
|
||||
|
||||
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
|
||||
const sceneriesTrains: Map<string, Train[]> = new Map();
|
||||
|
||||
export const useMainStore = defineStore('mainStore', {
|
||||
state: () =>
|
||||
({
|
||||
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
|
||||
trainList(): Train[] {
|
||||
const apiStore = useApiStore();
|
||||
|
||||
checkpointsTrains.clear();
|
||||
sceneriesTrains.clear();
|
||||
|
||||
return (apiStore.activeData?.trains ?? [])
|
||||
.filter((train) => train.timetable || train.online)
|
||||
.map((train) => {
|
||||
@@ -53,8 +59,9 @@ export const useMainStore = defineStore('mainStore', {
|
||||
sceneryHash
|
||||
) ?? [];
|
||||
|
||||
return {
|
||||
trainId: train.driverName + train.trainNo.toString(),
|
||||
const trainObj = {
|
||||
id: train.id,
|
||||
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
|
||||
|
||||
trainNo: train.trainNo,
|
||||
mass: train.mass,
|
||||
@@ -89,10 +96,65 @@ export const useMainStore = defineStore('mainStore', {
|
||||
followingStops: timetable.stopList,
|
||||
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
|
||||
sceneries: timetable.sceneries,
|
||||
sceneryNames: sceneryNames.reverse()
|
||||
sceneryNames: sceneryNames.reverse(),
|
||||
timetablePath: timetable.path.split(';').map((pathElementString) => {
|
||||
const [arrival, station, departure] = pathElementString.split(',');
|
||||
|
||||
return {
|
||||
arrivalRouteExt: arrival,
|
||||
departureRouteExt: departure,
|
||||
stationName: station.split(' ').slice(0, -1).join(' '),
|
||||
stationHash: station.split(' ').slice(-1).join(' ')
|
||||
};
|
||||
})
|
||||
}
|
||||
: undefined
|
||||
} as Train;
|
||||
|
||||
// Sceneries trains map
|
||||
if (sceneriesTrains.has(train.currentStationName)) {
|
||||
sceneriesTrains.set(train.currentStationName, [
|
||||
...sceneriesTrains.get(train.currentStationName)!,
|
||||
trainObj
|
||||
]);
|
||||
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
|
||||
|
||||
// Checkpoints trains map
|
||||
if (trainObj.timetableData) {
|
||||
let currentSceneryIndex = 0;
|
||||
const timetablePath = trainObj.timetableData.timetablePath;
|
||||
|
||||
trainObj.timetableData.followingStops.forEach((stop, i) => {
|
||||
if (/strong|podg|pe/.test(stop.stopName)) {
|
||||
const checkpointTrain: CheckpointTrain = {
|
||||
train: trainObj,
|
||||
checkpointStop: stop,
|
||||
|
||||
previousSceneryElement:
|
||||
currentSceneryIndex > 0 ? timetablePath[currentSceneryIndex - 1] : null,
|
||||
|
||||
nextSceneryElement:
|
||||
currentSceneryIndex < timetablePath.length - 1
|
||||
? timetablePath[currentSceneryIndex + 1]
|
||||
: null,
|
||||
|
||||
timetablePathElement: timetablePath[currentSceneryIndex]
|
||||
};
|
||||
|
||||
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
|
||||
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
|
||||
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
|
||||
checkpointTrain
|
||||
]);
|
||||
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
|
||||
}
|
||||
|
||||
if (timetablePath[currentSceneryIndex].departureRouteExt == stop.departureLine)
|
||||
currentSceneryIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
return trainObj;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -131,13 +193,14 @@ export const useMainStore = defineStore('mainStore', {
|
||||
dispatcherId: -1,
|
||||
dispatcherExp: -1,
|
||||
dispatcherIsSupporter: false,
|
||||
scheduledTrains: [],
|
||||
stationTrains: [],
|
||||
dispatcherStatus: Status.ActiveDispatcher.FREE,
|
||||
dispatcherTimestamp: -1,
|
||||
|
||||
isOnline: false,
|
||||
|
||||
stationTrains: [],
|
||||
scheduledTrains: [],
|
||||
|
||||
scheduledTrainCount: {
|
||||
all: 0,
|
||||
confirmed: 0,
|
||||
@@ -177,8 +240,9 @@ export const useMainStore = defineStore('mainStore', {
|
||||
|
||||
isOnline: scenery.isOnline == 1,
|
||||
|
||||
scheduledTrains: [],
|
||||
stationTrains: [],
|
||||
scheduledTrains: [],
|
||||
|
||||
scheduledTrainCount: {
|
||||
all: 0,
|
||||
confirmed: 0,
|
||||
@@ -196,37 +260,41 @@ export const useMainStore = defineStore('mainStore', {
|
||||
|
||||
const station = this.stationList.find((s) => s.name === scenery.name);
|
||||
|
||||
const scheduledTrains = getScheduledTrains(
|
||||
this.trainList,
|
||||
station?.generalInfo,
|
||||
scenery.name,
|
||||
scenery.region
|
||||
);
|
||||
let checkpointsSet: Set<string> = new Set();
|
||||
|
||||
const stationTrains = getStationTrains(
|
||||
this.trainList,
|
||||
scheduledTrains,
|
||||
this.region.id,
|
||||
scenery.name
|
||||
);
|
||||
// Add checkpoints to active scenery data
|
||||
checkpointsSet.add(scenery.name.toLowerCase());
|
||||
|
||||
// Remove checkpoint duplicates
|
||||
const uniqueScheduledTrains = scheduledTrains.reduce(
|
||||
(uniqueList, sTrain) =>
|
||||
uniqueList.find((v) => v.trainId === sTrain.trainId)
|
||||
? uniqueList
|
||||
: [...uniqueList, sTrain],
|
||||
[] as ScheduledTrain[]
|
||||
);
|
||||
station?.generalInfo?.checkpoints.forEach((cpName) => {
|
||||
checkpointsSet.add(cpName.toLowerCase());
|
||||
});
|
||||
|
||||
scenery.scheduledTrains = scheduledTrains;
|
||||
scenery.stationTrains = stationTrains;
|
||||
const checkpoints = Array.from(checkpointsSet);
|
||||
|
||||
scenery.scheduledTrainCount = {
|
||||
all: uniqueScheduledTrains.length,
|
||||
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
|
||||
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
|
||||
};
|
||||
scenery.stationTrains =
|
||||
sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
|
||||
|
||||
const uniqueTrainIds: string[] = [];
|
||||
checkpoints.forEach((cp) => {
|
||||
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
|
||||
|
||||
if (!scheduledTrains) return;
|
||||
|
||||
scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
|
||||
if (scenery.name != timetablePathElement.stationName) return;
|
||||
|
||||
scenery.scheduledTrains.push({ train, checkpointStop, timetablePathElement, ...v });
|
||||
|
||||
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
|
||||
|
||||
scenery.scheduledTrainCount.all += 1;
|
||||
|
||||
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
|
||||
else scenery.scheduledTrainCount.unconfirmed++;
|
||||
|
||||
uniqueTrainIds.push(train.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return allActiveSceneries;
|
||||
@@ -281,7 +349,10 @@ export const useMainStore = defineStore('mainStore', {
|
||||
...scenery,
|
||||
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
||||
routes: routes,
|
||||
checkpoints: scenery.checkpoints?.split(';') ?? []
|
||||
checkpoints:
|
||||
scenery.checkpoints && scenery.checkpoints.trim().length > 0
|
||||
? scenery.checkpoints.split(';')
|
||||
: []
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import { API } from '../typings/api';
|
||||
import { Availability, StationRoutesInfo, Status } from '../typings/common';
|
||||
import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
|
||||
|
||||
export interface MainStoreState {
|
||||
region: { id: string; value: string; name: string };
|
||||
|
||||
@@ -1,19 +1,4 @@
|
||||
import {
|
||||
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`;
|
||||
}
|
||||
import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
|
||||
|
||||
export function getStatusTimestamp(stationStatus: any): number {
|
||||
if (!stationStatus) return -2;
|
||||
@@ -63,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
|
||||
export function getTimestamp(date: string | null): number {
|
||||
return date ? new Date(date).getTime() : 0;
|
||||
}
|
||||
|
||||
export function getTrainStopStatus(
|
||||
stopInfo: TrainStop,
|
||||
currentStationName: string,
|
||||
sceneryName: string
|
||||
) {
|
||||
let stopStatus = StopStatus.ARRIVING,
|
||||
stopLabel = '',
|
||||
stopStatusID = -1;
|
||||
|
||||
if (stopInfo.terminatesHere && stopInfo.confirmed) {
|
||||
stopStatus = StopStatus.TERMINATED;
|
||||
stopLabel = 'Skończył bieg';
|
||||
stopStatusID = 5;
|
||||
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
|
||||
stopStatus = StopStatus.DEPARTED;
|
||||
stopLabel = 'Odprawiony';
|
||||
stopStatusID = 2;
|
||||
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
|
||||
stopStatus = StopStatus.DEPARTED_AWAY;
|
||||
stopLabel = 'Odjechał';
|
||||
stopStatusID = 4;
|
||||
} else if (currentStationName == sceneryName && !stopInfo.stopped) {
|
||||
stopStatus = StopStatus.ONLINE;
|
||||
stopLabel = 'Na stacji';
|
||||
stopStatusID = 0;
|
||||
} else if (currentStationName == sceneryName && stopInfo.stopped) {
|
||||
stopStatus = StopStatus.STOPPED;
|
||||
stopLabel = 'Postój';
|
||||
stopStatusID = 1;
|
||||
} else if (currentStationName != sceneryName) {
|
||||
stopStatus = StopStatus.ARRIVING;
|
||||
stopLabel = 'W drodze';
|
||||
stopStatusID = 3;
|
||||
}
|
||||
|
||||
return { stopStatus, stopLabel, stopStatusID };
|
||||
}
|
||||
|
||||
export function getCheckpointTrain(
|
||||
train: Train,
|
||||
trainStopIndex: number,
|
||||
sceneryName: string
|
||||
): ScheduledTrain {
|
||||
const timetable = train.timetableData!;
|
||||
const followingStops = timetable.followingStops;
|
||||
const trainStop = followingStops[trainStopIndex];
|
||||
|
||||
const trainStopStatus = getTrainStopStatus(trainStop, train.currentStationName, sceneryName);
|
||||
|
||||
let prevStationName = '',
|
||||
nextStationName = '';
|
||||
|
||||
let departureLine: string | null = null;
|
||||
let arrivingLine: string | null = null;
|
||||
|
||||
let prevDepartureLine: string | null = null,
|
||||
nextArrivalLine: string | null = null;
|
||||
|
||||
for (let i = trainStopIndex; i >= 0; i--) {
|
||||
const stop = followingStops[i];
|
||||
|
||||
if (/strong|podg\.|pe\./g.test(stop.stopName) && !prevStationName && i <= trainStopIndex - 1)
|
||||
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
|
||||
|
||||
if (stop.arrivalLine != null && !arrivingLine && !/-|_|it|sbl/gi.test(stop.arrivalLine)) {
|
||||
arrivingLine = stop.arrivalLine;
|
||||
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = trainStopIndex; i < followingStops.length; i++) {
|
||||
const stop = followingStops[i];
|
||||
|
||||
if (/strong|podg\.|pe\./g.test(stop.stopName) && !nextStationName && i > trainStopIndex)
|
||||
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
|
||||
|
||||
if (stop.departureLine && !departureLine && !/-|_|it|sbl/gi.test(stop.departureLine)) {
|
||||
departureLine = stop.departureLine;
|
||||
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
checkpointName: trainStop.stopNameRAW,
|
||||
|
||||
trainNo: train.trainNo,
|
||||
trainId: train.trainId,
|
||||
|
||||
signal: train.signal,
|
||||
connectedTrack: train.connectedTrack,
|
||||
|
||||
driverName: train.driverName,
|
||||
driverId: train.driverId,
|
||||
currentStationName: train.currentStationName,
|
||||
currentStationHash: train.currentStationHash,
|
||||
category: timetable.category,
|
||||
beginsAt: timetable.followingStops[0].stopNameRAW,
|
||||
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
|
||||
|
||||
nextStationName,
|
||||
prevStationName,
|
||||
|
||||
stopInfo: trainStop,
|
||||
stopLabel: trainStopStatus.stopLabel,
|
||||
stopStatus: trainStopStatus.stopStatus,
|
||||
stopStatusID: trainStopStatus.stopStatusID,
|
||||
|
||||
region: train.region,
|
||||
|
||||
arrivingLine: arrivingLine,
|
||||
departureLine: departureLine,
|
||||
|
||||
nextArrivalLine,
|
||||
prevDepartureLine
|
||||
};
|
||||
}
|
||||
|
||||
export function getScheduledTrains(
|
||||
trainList: Train[],
|
||||
stationGeneralInfo: Station['generalInfo'],
|
||||
stationName: string,
|
||||
region: string
|
||||
// sceneryData: API.ActiveSceneries.Data,
|
||||
): ScheduledTrain[] {
|
||||
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
|
||||
|
||||
return trainList.reduce((acc: ScheduledTrain[], train) => {
|
||||
if (!train.timetableData) return acc;
|
||||
if (train.region != region) return acc;
|
||||
|
||||
const timetable = train.timetableData;
|
||||
if (!timetable.sceneryNames.includes(stationName)) return acc;
|
||||
|
||||
const checkpoints = [stationName];
|
||||
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
|
||||
|
||||
const checkpointScheduledTrains: ScheduledTrain[] = [];
|
||||
for (let i = 0; i < timetable.followingStops.length; i++) {
|
||||
if (
|
||||
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
|
||||
timetable.followingStops[i].stopNameRAW
|
||||
)
|
||||
) {
|
||||
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
|
||||
}
|
||||
}
|
||||
|
||||
acc.push(...checkpointScheduledTrains);
|
||||
return acc;
|
||||
}, []) as ScheduledTrain[];
|
||||
}
|
||||
|
||||
export function getStationTrains(
|
||||
trainList: Train[],
|
||||
scheduledTrainList: ScheduledTrain[],
|
||||
region: string,
|
||||
stationName: string
|
||||
): StationTrain[] {
|
||||
return trainList
|
||||
.filter(
|
||||
(train) =>
|
||||
train?.region === region && train.online && train.currentStationName === stationName
|
||||
)
|
||||
.map((train) => ({
|
||||
driverName: train.driverName,
|
||||
driverId: train.driverId,
|
||||
trainNo: train.trainNo,
|
||||
trainId: train.trainId,
|
||||
stopStatus:
|
||||
scheduledTrainList.find((st) => st.trainNo === train.trainNo)?.stopStatus || 'no-timetable'
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
.list_wrapper {
|
||||
overflow-y: auto;
|
||||
height: 90vh;
|
||||
min-height: 550px;
|
||||
min-height: 650px;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -121,8 +121,6 @@ input {
|
||||
height: 7px;
|
||||
background-color: lightgreen;
|
||||
border-radius: 50%;
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
@@ -230,6 +228,10 @@ a.a-button {
|
||||
|
||||
background-color: #3c3c3c;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
&.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 {
|
||||
::-webkit-scrollbar {
|
||||
width: 0.5em;
|
||||
@@ -298,24 +321,11 @@ a.a-button {
|
||||
background-color: #777;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Basic tooltip
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:focus::after {
|
||||
position: absolute;
|
||||
transform: translate(10px, -50%);
|
||||
|
||||
content: attr(data-tooltip);
|
||||
color: white;
|
||||
background-color: #171717;
|
||||
border-radius: 0.5em;
|
||||
padding: 0.5em;
|
||||
margin: 0 0.25em;
|
||||
max-width: 300px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
[data-tooltip] {
|
||||
cursor: help;
|
||||
[data-tooltip]:hover::after,
|
||||
[data-tooltip]:focus::after {
|
||||
transform: translate(-50%, 2em);
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
.scenery-table-section {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
table.scenery-history-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
min-width: 900px;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead {
|
||||
@@ -25,7 +21,7 @@ table.scenery-history-table {
|
||||
|
||||
td {
|
||||
padding: 0.75em;
|
||||
border-bottom: solid 5px #111;
|
||||
border-bottom: solid 5px #181818;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Status } from './common';
|
||||
import { Status, VehicleData } from './common';
|
||||
|
||||
export enum APIDataStatus {
|
||||
OK = 'OK',
|
||||
@@ -19,11 +19,12 @@ export namespace API {
|
||||
apiStatuses?: APIStatuses;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DispatcherHistory {
|
||||
export type Response = Data[];
|
||||
|
||||
export interface Data {
|
||||
id: string;
|
||||
id: number;
|
||||
currentDuration: number;
|
||||
dispatcherId: number;
|
||||
dispatcherName: string;
|
||||
@@ -38,6 +39,7 @@ export namespace API {
|
||||
stationName: string;
|
||||
timestampFrom: number;
|
||||
timestampTo?: number;
|
||||
statusHistory: string[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +163,6 @@ export namespace API {
|
||||
stopNameRAW: string;
|
||||
stopType: string;
|
||||
stopDistance: number;
|
||||
pointId: string;
|
||||
|
||||
mainStop: boolean;
|
||||
|
||||
@@ -194,6 +195,8 @@ export namespace API {
|
||||
TWR: boolean;
|
||||
SKR: boolean;
|
||||
sceneries: string[];
|
||||
|
||||
path: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,16 +251,14 @@ export namespace API {
|
||||
hashesString?: string;
|
||||
currentSceneryName?: string;
|
||||
currentSceneryHash?: string;
|
||||
|
||||
routeSceneries?: string;
|
||||
|
||||
checkpointArrivals?: string[];
|
||||
checkpointDepartures?: string[];
|
||||
|
||||
checkpointArrivalsScheduled?: string[];
|
||||
checkpointDeparturesScheduled?: string[];
|
||||
|
||||
checkpointStopTypes?: string[];
|
||||
visitedSceneries?: string[];
|
||||
path: string;
|
||||
}
|
||||
|
||||
export type Response = Data[];
|
||||
@@ -317,6 +318,10 @@ export namespace API {
|
||||
export namespace Donators {
|
||||
export type Response = string[];
|
||||
}
|
||||
|
||||
export namespace Vehicles {
|
||||
export type Response = VehicleData[];
|
||||
}
|
||||
}
|
||||
|
||||
export namespace GithubAPI {
|
||||
|
||||
@@ -39,9 +39,16 @@ export interface RegionCounters {
|
||||
timetablesCount: number;
|
||||
}
|
||||
|
||||
export interface TimetablePathElement {
|
||||
arrivalRouteExt?: string;
|
||||
departureRouteExt?: string;
|
||||
stationName: string;
|
||||
stationHash: string;
|
||||
}
|
||||
|
||||
export interface Train {
|
||||
id: string;
|
||||
trainId: string;
|
||||
modalId: string;
|
||||
mass: number;
|
||||
length: number;
|
||||
speed: number;
|
||||
@@ -73,41 +80,37 @@ export interface Train {
|
||||
routeDistance: number;
|
||||
sceneries: string[];
|
||||
sceneryNames: string[];
|
||||
timetablePath: TimetablePathElement[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface Station {
|
||||
name: string;
|
||||
|
||||
generalInfo?: {
|
||||
name: string;
|
||||
url: string;
|
||||
abbr: string;
|
||||
hash?: string;
|
||||
|
||||
reqLevel: number;
|
||||
// supportersOnly: boolean;
|
||||
|
||||
lines: string;
|
||||
project: string;
|
||||
projectUrl?: string;
|
||||
|
||||
signalType: string;
|
||||
controlType: string;
|
||||
|
||||
SUP: boolean;
|
||||
ASDEK: boolean;
|
||||
authors?: string[];
|
||||
|
||||
availability: Availability;
|
||||
routes: StationRoutes;
|
||||
|
||||
checkpoints: string[];
|
||||
};
|
||||
generalInfo?: StationGeneralInfo;
|
||||
|
||||
onlineInfo?: ActiveScenery;
|
||||
}
|
||||
|
||||
export interface StationGeneralInfo {
|
||||
name: string;
|
||||
url: string;
|
||||
abbr: string;
|
||||
hash?: string;
|
||||
reqLevel: number;
|
||||
lines: string;
|
||||
project: string;
|
||||
projectUrl?: string;
|
||||
signalType: string;
|
||||
controlType: string;
|
||||
SUP: boolean;
|
||||
ASDEK: boolean;
|
||||
authors?: string[];
|
||||
availability: Availability;
|
||||
routes: StationRoutes;
|
||||
checkpoints: string[];
|
||||
}
|
||||
|
||||
export interface StationRoutes {
|
||||
single: StationRoutesInfo[];
|
||||
double: StationRoutesInfo[];
|
||||
@@ -148,8 +151,8 @@ export interface ActiveScenery {
|
||||
dispatcherStatus: Status.ActiveDispatcher | number;
|
||||
dispatcherTimestamp: number | null;
|
||||
isOnline: boolean;
|
||||
stationTrains?: StationTrain[];
|
||||
scheduledTrains?: ScheduledTrain[];
|
||||
stationTrains: Train[];
|
||||
scheduledTrains: CheckpointTrain[];
|
||||
scheduledTrainCount: {
|
||||
all: number;
|
||||
confirmed: number;
|
||||
@@ -164,49 +167,6 @@ export interface ScenerySpawn {
|
||||
spawnType: ScenerySpawnType;
|
||||
}
|
||||
|
||||
export interface StationTrain {
|
||||
driverName: string;
|
||||
driverId: number;
|
||||
trainNo: number;
|
||||
trainId: string;
|
||||
stopStatus: string;
|
||||
}
|
||||
|
||||
export interface ScheduledTrain {
|
||||
checkpointName: string;
|
||||
|
||||
trainId: string;
|
||||
trainNo: number;
|
||||
|
||||
driverName: string;
|
||||
driverId: number;
|
||||
currentStationName: string;
|
||||
currentStationHash: string;
|
||||
category: string;
|
||||
stopInfo: TrainStop;
|
||||
|
||||
terminatesAt: string;
|
||||
beginsAt: string;
|
||||
|
||||
prevStationName: string;
|
||||
nextStationName: string;
|
||||
|
||||
arrivingLine: string | null;
|
||||
departureLine: string | null;
|
||||
|
||||
prevDepartureLine: string | null;
|
||||
nextArrivalLine: string | null;
|
||||
|
||||
signal: string;
|
||||
connectedTrack: string;
|
||||
|
||||
stopLabel: string;
|
||||
stopStatus: StopStatus;
|
||||
stopStatusID: number;
|
||||
|
||||
region: string;
|
||||
}
|
||||
|
||||
export interface TrainStop {
|
||||
stopName: string;
|
||||
stopNameRAW: string;
|
||||
@@ -223,13 +183,50 @@ export interface TrainStop {
|
||||
departureTimestamp: number;
|
||||
departureRealTimestamp: number;
|
||||
departureDelay: number;
|
||||
pointId: number;
|
||||
|
||||
comments?: string;
|
||||
|
||||
beginsHere: boolean;
|
||||
terminatesHere: boolean;
|
||||
confirmed: boolean;
|
||||
stopped: boolean;
|
||||
confirmed: number;
|
||||
stopped: number;
|
||||
stopTime: number | null;
|
||||
}
|
||||
|
||||
export interface CheckpointTrain {
|
||||
checkpointStop: TrainStop;
|
||||
train: Train;
|
||||
timetablePathElement: TimetablePathElement;
|
||||
previousSceneryElement: TimetablePathElement | null;
|
||||
nextSceneryElement: TimetablePathElement | null;
|
||||
}
|
||||
|
||||
// Vehicles Data
|
||||
|
||||
export interface VehicleData {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
cabinName: string | null;
|
||||
restrictions: Record<string, any> | null;
|
||||
vehicleGroupsId: number;
|
||||
group: VehiclesGroup;
|
||||
}
|
||||
|
||||
export interface VehiclesGroup {
|
||||
id: number;
|
||||
name: string;
|
||||
speed: number;
|
||||
length: number;
|
||||
weight: number;
|
||||
cargoTypes: VehicleCargo[] | null;
|
||||
locoProps: {
|
||||
coldStart: boolean;
|
||||
doubleManned: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface VehicleCargo {
|
||||
id: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
<JournalStats :statsButtons="statsButtons" />
|
||||
</div>
|
||||
|
||||
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
||||
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
||||
<div class="journal_refreshed-date">
|
||||
{{ $t('journal.data-refreshed-at') }}:
|
||||
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
|
||||
</div>
|
||||
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
<JournalStats :statsButtons="statsButtons" />
|
||||
</div>
|
||||
|
||||
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
|
||||
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
|
||||
<div class="journal_refreshed-date">
|
||||
{{ $t('journal.data-refreshed-at') }}:
|
||||
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
|
||||
</div>
|
||||
|
||||
<div class="list_wrapper" @scroll="handleScroll">
|
||||
@@ -125,6 +126,8 @@ interface TimetablesQueryParams {
|
||||
dateTo?: string;
|
||||
|
||||
issuedFrom?: string;
|
||||
terminatingAt?: string;
|
||||
via?: string;
|
||||
|
||||
countFrom?: number;
|
||||
countLimit?: number;
|
||||
@@ -205,6 +208,8 @@ export default defineComponent({
|
||||
'search-driver': '',
|
||||
'search-dispatcher': '',
|
||||
'search-issuedFrom': '',
|
||||
'search-via': '',
|
||||
'search-terminatingAt': '',
|
||||
'search-date': ''
|
||||
} as Journal.TimetableSearchType);
|
||||
|
||||
@@ -298,11 +303,17 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
setOptions(options: { [key: string]: string }) {
|
||||
this.searchersValues['search-date'] = options['search-date'] ?? '';
|
||||
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
|
||||
this.searchersValues['search-train'] = options['search-train'] ?? '';
|
||||
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
|
||||
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
|
||||
Object.keys(this.searchersValues).forEach((v) => {
|
||||
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
|
||||
});
|
||||
|
||||
// this.searchersValues['search-date'] = options['search-date'] ?? '';
|
||||
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
|
||||
// this.searchersValues['search-train'] = options['search-train'] ?? '';
|
||||
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
|
||||
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
|
||||
// this.searchersValues['search-via'] = options['search-via'] ?? '';
|
||||
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
|
||||
|
||||
this.sorterActive.id =
|
||||
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
|
||||
@@ -346,6 +357,8 @@ export default defineComponent({
|
||||
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
|
||||
const dateFrom = this.searchersValues['search-date'].trim() || undefined;
|
||||
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
|
||||
const via = this.searchersValues['search-via'].trim() || undefined;
|
||||
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
|
||||
|
||||
let dateTo: string | undefined = undefined;
|
||||
|
||||
@@ -417,6 +430,10 @@ export default defineComponent({
|
||||
queryParams['authorName'] = authorName;
|
||||
queryParams['dateFrom'] = dateFrom;
|
||||
queryParams['dateTo'] = dateTo;
|
||||
queryParams['issuedFrom'] = issuedFrom;
|
||||
queryParams['terminatingAt'] = terminatingAt;
|
||||
queryParams['via'] = via;
|
||||
|
||||
queryParams['issuedFrom'] = issuedFrom;
|
||||
queryParams['sortBy'] =
|
||||
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
v-for="(viewMode, i) in viewModes"
|
||||
:key="i"
|
||||
class="btn btn--option"
|
||||
:class="{ checked: currentMode == viewMode.component }"
|
||||
@click="setViewMode(viewMode.component)"
|
||||
:data-checked="currentMode == viewMode.component"
|
||||
>
|
||||
{{ $t(viewMode.id) }}
|
||||
</button>
|
||||
@@ -121,10 +121,6 @@ export default defineComponent({
|
||||
Status: Status.Data
|
||||
}),
|
||||
|
||||
// activated() {
|
||||
// this.loadSelectedCheckpoint();
|
||||
// },
|
||||
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
|
||||
@@ -200,7 +196,7 @@ button.back-btn {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
font-size: 1.5em;
|
||||
font-size: 1.25em;
|
||||
|
||||
button {
|
||||
margin: 1em auto;
|
||||
@@ -215,11 +211,10 @@ button.back-btn {
|
||||
|
||||
position: relative;
|
||||
|
||||
width: 100%;
|
||||
max-width: var(--max-container-width);
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
|
||||
margin: 1rem 0;
|
||||
padding: 1rem 0;
|
||||
text-align: center;
|
||||
|
||||
&[data-timetable-only='true'] {
|
||||
@@ -228,30 +223,27 @@ button.back-btn {
|
||||
}
|
||||
}
|
||||
|
||||
.scenery-left {
|
||||
.scenery-left,
|
||||
.scenery-right {
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
|
||||
background-color: #181818;
|
||||
padding: 1em 0.5em;
|
||||
|
||||
height: 95vh;
|
||||
min-height: 750px;
|
||||
max-height: 1000px;
|
||||
overflow: auto;
|
||||
height: calc(100vh - 0.5em);
|
||||
min-height: 800px;
|
||||
max-height: 2000px;
|
||||
}
|
||||
|
||||
.scenery-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.scenery-right {
|
||||
background: #181818;
|
||||
padding: 1em 0.5em;
|
||||
|
||||
height: 95vh;
|
||||
min-height: 750px;
|
||||
max-height: 1000px;
|
||||
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
@@ -261,18 +253,12 @@ button.back-btn {
|
||||
|
||||
.info-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.75em;
|
||||
|
||||
.btn {
|
||||
button {
|
||||
padding: 0.5em;
|
||||
box-shadow: 0 0 10px 4px #242424;
|
||||
|
||||
&[data-checked='true'] {
|
||||
color: var(--clr-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
<button
|
||||
class="btn-donation btn--image"
|
||||
ref="btn"
|
||||
@click="isDonationModalOpen = true"
|
||||
@focus="isDonationModalOpen = false"
|
||||
@click="isDonationCardOpen = true"
|
||||
@focus="isDonationCardOpen = false"
|
||||
>
|
||||
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
|
||||
<span>{{ $t('donations.button-title') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
|
||||
<StationTable @toggleDonationModal="toggleDonationModal" />
|
||||
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
|
||||
<StationTable @toggle-donation-card="toggleDonationCard" />
|
||||
<StationStats />
|
||||
</div>
|
||||
</section>
|
||||
@@ -30,34 +30,47 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import StationTable from '../components/StationsView/StationTable.vue';
|
||||
import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
|
||||
import { useStationFiltersStore } from '../store/stationFiltersStore';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import DonationModal from '../components/Global/DonationModal.vue';
|
||||
import DonationCard from '../components/Global/DonationCard.vue';
|
||||
import StationStats from '../components/StationsView/StationStats.vue';
|
||||
import { initFilters, setupFilters } from '../managers/stationFilterManager';
|
||||
import { reactive } from 'vue';
|
||||
import { provide } from 'vue';
|
||||
import { ActiveSorter } from '../components/StationsView/typings';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
const filterInitStates = { ...initFilters };
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
StationTable,
|
||||
StationFilterCard,
|
||||
StationStats,
|
||||
DonationModal
|
||||
DonationCard
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
filterCardOpen: false,
|
||||
isDonationModalOpen: false,
|
||||
isDonationCardOpen: false,
|
||||
|
||||
filterStore: useStationFiltersStore(),
|
||||
store: useMainStore()
|
||||
mainStore: useMainStore()
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.filterStore.setupFilters();
|
||||
setup() {
|
||||
const filters = reactive(filterInitStates);
|
||||
const activeSorter = reactive({ headerName: 'station', dir: 1 }) as ActiveSorter;
|
||||
|
||||
provide('StationsView_filters', filters);
|
||||
provide('StationsView_activeSorter', activeSorter);
|
||||
|
||||
onMounted(() => {
|
||||
setupFilters(filters);
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDonationModal(value: boolean) {
|
||||
this.isDonationModalOpen = value;
|
||||
toggleDonationCard(value: boolean) {
|
||||
this.isDonationCardOpen = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -84,6 +97,7 @@ export default defineComponent({
|
||||
.stations-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5em;
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
@@ -109,7 +109,7 @@ export default defineComponent({
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (this.trainId) {
|
||||
this.selectModalTrain(this.trainId);
|
||||
this.selectModalTrainById(this.trainId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ declare module '*.vue' {
|
||||
export default component;
|
||||
}
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_APP_API_DEV: string;
|
||||
readonly VITE_APP_WS_DEV: string;
|
||||
readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
|
||||
readonly VITE_API_VEHICLES_MODE: 'production' | 'mocking' | 'development';
|
||||
readonly VITE_API_ACTIVE_DATA_MODE: 'production' | 'mocking' | 'development';
|
||||
readonly VITE_UPDATE_TEST: 'test' | 'production';
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -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"]
|
||||
@@ -7,6 +7,10 @@ export default defineConfig({
|
||||
port: 5001,
|
||||
open: true
|
||||
},
|
||||
preview: {
|
||||
port: 4001,
|
||||
open: true
|
||||
},
|
||||
publicDir: 'public',
|
||||
plugins: [
|
||||
vue(),
|
||||
@@ -20,35 +24,16 @@ export default defineConfig({
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'),
|
||||
handler: 'CacheFirst',
|
||||
urlPattern:
|
||||
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
|
||||
handler: 'StaleWhileRevalidate',
|
||||
options: {
|
||||
cacheName: 'spythere-sceneries-cache',
|
||||
cacheName: 'stacjownik-api-cache',
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
urlPattern: 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: {
|
||||
|
||||