Migracja z wersji Vue 2 na Vue 3

This commit is contained in:
2021-06-29 02:26:36 +02:00
parent 6391b997b1
commit 26ae065837
49 changed files with 2906 additions and 3279 deletions
+162
View File
@@ -0,0 +1,162 @@
@import "./styles/responsive.scss";
@import "./styles/variables.scss";
@import "./styles/global.scss";
@import "./styles/scenery_status.scss";
:root {
--clr-primary: #ffc014;
--clr-secondary: #2f2f2f;
--clr-bg: #333;
--clr-accent: #1085b3;
--clr-accent2: #ff3d5d;
--clr-skr: #ff5100;
--clr-twr: #ffbb00;
}
// VUE ROUTE CHANGE ANIMATION
.view-anim {
&-enter-from,
&-leave-to {
opacity: 0.02;
}
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
min-height: 100%;
}
}
.route {
margin: 0 0.2em;
&-active {
color: $accentCol;
font-weight: bold;
}
}
// APP
.app {
background: $bgCol;
color: white;
overflow: hidden;
font-size: 1rem;
@include smallScreen() {
font-size: calc(0.45rem + 1vw);
}
}
// CONTAINER
.app_container {
display: flex;
flex-flow: column;
min-width: 0;
min-height: 100vh;
header {
flex: 0 0 auto;
}
main {
flex: 1 1 auto;
}
footer {
flex: 0 1 0.2em;
}
}
// HEADER
.app_header {
background: $primaryCol;
padding: 0.15em;
border-radius: 0 0 1em 1em;
display: flex;
justify-content: center;
}
.header {
&_brand {
position: relative;
width: 100%;
font-size: 4.25em;
text-align: center;
img {
width: 0.8em;
}
.brand_lang {
position: absolute;
right: 0;
transform: translate(110%, -35%);
img {
width: 0.6em;
}
cursor: pointer;
}
}
&_info {
display: flex;
justify-content: space-between;
font-size: 1.25em;
margin: 0 0.3em;
padding: 0.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
}
// COUNTER
.info_counter {
display: flex;
align-items: center;
color: $accentCol;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// FOOTER
footer.app_footer {
max-width: 100%;
padding: 0.5em;
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
+213 -379
View File
@@ -1,379 +1,213 @@
git <template>
<div class="app">
<UpdateModal
:currentVersion="VERSION"
@toggleUpdateModal="toggleUpdateModal"
v-if="updateModalVisible"
/>
<div class="app_container">
<header class="app_header">
<div class="header_body">
<span class="header_brand">
<span>
<span>Stacj</span>
<img
src="@/assets/trainlogo.png"
alt="trainlogo"
/>
<span>wnik</span>
</span>
<span class="brand_lang">
<span
class="lang pl"
@click="changeLang('en')"
:class="{ current: currentLang == 'pl' }"
v-if="currentLang == 'pl'"
>
<img
:src="iconPL"
alt="icon-pl"
/>
</span>
<span
class="lang en"
@click="changeLang('pl')"
:class="{ current: currentLang == 'en' }"
v-if="currentLang == 'en'"
>
<img
:src="iconEN"
alt="icon-en"
/>
</span>
</span>
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img
src="@/assets/icon-dispatcher.svg"
alt="icon dispatcher"
/>
<span>{{ data.activeStationCount }}</span>
<span>{{ data.activeTrainCount }}</span>
<img
src="@/assets/icon-train.svg"
alt="icon train"
/>
</div>
</span>
<span class="header_links">
<router-link
class="route"
active-class="route-active"
to="/"
exact
>{{ $t("app.sceneries") }}
</router-link>
/
<router-link
class="route"
active-class="route-active"
to="/trains"
>{{ $t("app.trains") }}
</router-link>
</span>
</div>
</header>
<main class="app_main">
<transition
name="view-anim"
mode="out-in"
>
<keep-alive>
<router-view />
</keep-alive>
</transition>
</main>
<footer class="app_footer">
&copy;
<a
href="https://td2.info.pl/profile/?u=20777"
target="_blank"
>
Spythere
</a>
2021 | v{{ VERSION }}
</footer>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { Action, Getter } from "vuex-class";
import UpdateModal from "@/components/Global/UpdateModal.vue";
import Clock from "@/components/App/Clock.vue";
import StorageManager from "@/scripts/managers/storageManager";
import { StoreData } from "./scripts/interfaces/StoreData";
@Component({
components: { Clock, UpdateModal },
})
export default class App extends Vue {
@Action("synchronizeData") synchronizeData;
@Getter("getAllData") data!: StoreData;
private VERSION = "1.4.7";
hasReleaseNotes = false;
updateModalVisible = false;
currentLang = "pl";
iconEN = require("@/assets/icon-en.jpg");
iconPL = require("@/assets/icon-pl.svg");
toggleUpdateModal() {
this.updateModalVisible = !this.updateModalVisible;
StorageManager.setBooleanValue("version_notes_read", true);
}
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
StorageManager.setStringValue("lang", lang);
}
loadLang() {
const storageLang = StorageManager.getStringValue("lang");
if (storageLang) {
this.changeLang(storageLang);
return;
}
if (!window.navigator.language) {
this.changeLang("pl");
return;
}
switch (window.navigator.language) {
case "pl-PL":
this.changeLang("pl");
break;
case "en-EN":
default:
this.changeLang("en");
break;
}
return;
}
created() {
this.loadLang();
this.synchronizeData();
}
mounted() {
if (this.detectIEVersion() != -1)
alert(
"Stacjownik nie wspiera reliktów przeszłości. Przesiądź się na nowszą przeglądarkę!"
);
if (StorageManager.getStringValue("version") != this.VERSION) {
StorageManager.setStringValue("version", this.VERSION);
if (this.hasReleaseNotes)
StorageManager.setBooleanValue("version_notes_read", false);
}
this.updateModalVisible =
this.hasReleaseNotes &&
!StorageManager.getBooleanValue("version_notes_read");
}
detectIEVersion() {
var rv = -1;
if (navigator.appName == "Microsoft Internet Explorer") {
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\\.0-9]{0,})");
if (re.exec(ua) != null) rv = parseFloat(RegExp.$1);
} else if (navigator.appName == "Netscape") {
var ua = navigator.userAgent;
var re = new RegExp("Trident/.*rv:([0-9]{1,}[\\.0-9]{0,})");
if (re.exec(ua) != null) rv = parseFloat(RegExp.$1);
}
return rv;
}
}
</script>
<style lang="scss">
@import "./styles/responsive.scss";
@import "./styles/variables.scss";
@import "./styles/global.scss";
@import "./styles/scenery_status.scss";
:root {
--clr-primary: #ffc014;
--clr-secondary: #2f2f2f;
--clr-bg: #333;
--clr-accent: #1085b3;
--clr-accent2: #ff3d5d;
--clr-skr: #ff5100;
--clr-twr: #ffbb00;
}
// VUE ROUTE CHANGE ANIMATION
.view-anim {
&-enter,
&-leave-to {
opacity: 0.02;
}
&-enter-active,
&-leave-active {
transition: all $animDuration $animType;
min-height: 100%;
}
}
.route {
margin: 0 0.2em;
&-active {
color: $accentCol;
font-weight: bold;
}
}
// APP
.app {
background: $bgCol;
color: white;
overflow: hidden;
font-size: 1rem;
@include smallScreen() {
font-size: calc(0.45rem + 1vw);
}
}
// CONTAINER
.app_container {
display: flex;
flex-flow: column;
min-width: 0;
min-height: 100vh;
header {
flex: 0 0 auto;
}
main {
flex: 1 1 auto;
}
footer {
flex: 0 1 0.2em;
}
}
// HEADER
.app_header {
background: $primaryCol;
padding: 0.15em;
border-radius: 0 0 1em 1em;
display: flex;
justify-content: center;
}
.header {
&_brand {
position: relative;
width: 100%;
font-size: 4.25em;
text-align: center;
img {
width: 0.8em;
}
.brand_lang {
position: absolute;
right: 0;
transform: translate(110%, -35%);
img {
width: 0.6em;
}
cursor: pointer;
}
}
&_info {
display: flex;
justify-content: space-between;
font-size: 1.25em;
margin: 0 0.3em;
padding: 0.2em;
}
&_links {
display: flex;
justify-content: center;
border-radius: 0.7em;
font-size: 1.25em;
padding: 0.5em;
}
}
// COUNTER
.info_counter {
display: flex;
align-items: center;
color: $accentCol;
span {
margin: 0 0.15em;
}
img {
width: 1.35em;
}
}
// FOOTER
footer.app_footer {
max-width: 100%;
padding: 0.5em;
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
</style>
<template>
<div class="app">
<!-- <UpdateModal
:currentVersion="VERSION"
@toggleUpdateModal="toggleUpdateModal"
v-if="updateModalVisible"
/> -->
<div class="app_container">
<header class="app_header">
<div class="header_body">
<span class="header_brand">
<span>
<span>Stacj</span>
<img
src="@/assets/trainlogo.png"
alt="trainlogo"
/>
<span>wnik</span>
</span>
<span class="brand_lang">
<span
class="lang pl"
@click="changeLang('en')"
:class="{ current: currentLang == 'pl' }"
v-if="currentLang == 'pl'"
>
<img
:src="iconPL"
alt="icon-pl"
/>
</span>
<span
class="lang en"
@click="changeLang('pl')"
:class="{ current: currentLang == 'en' }"
v-if="currentLang == 'en'"
>
<img
:src="iconEN"
alt="icon-en"
/>
</span>
</span>
</span>
<span class="header_info">
<Clock />
<div class="info_counter">
<img
src="@/assets/icon-dispatcher.svg"
alt="icon dispatcher"
/>
<span>{{ data.activeStationCount }}</span>
<span>{{ data.activeTrainCount }}</span>
<img
src="@/assets/icon-train.svg"
alt="icon train"
/>
</div>
</span>
<span class="header_links">
<router-link
class="route"
active-class="route-active"
to="/"
exact
>{{ $t("app.sceneries") }}
</router-link>
/
<router-link
class="route"
active-class="route-active"
to="/trains"
>{{ $t("app.trains") }}
</router-link>
</span>
</div>
</header>
<main class="app_main">
<router-view v-slot="{ Component }">
<transition
name="view-anim"
mode="out-in"
>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</main>
<footer class="app_footer">
&copy;
<a
href="https://td2.info.pl/profile/?u=20777"
target="_blank"
>
Spythere
</a>
2021 | v{{ VERSION }}
</footer>
</div>
</div>
</template>
<script lang="ts">
// import UpdateModal from "@/components/Global/UpdateModal.vue";
import Clock from "@/components/App/Clock.vue";
import StorageManager from "@/scripts/managers/storageManager";
import { computed, ComputedRef, defineComponent } from "vue";
import { GETTERS } from "./constants/storeConstants";
import { StoreData } from "./scripts/interfaces/StoreData";
import { useStore } from "./store";
// import { StoreData } from "./scripts/interfaces/StoreData";
export default defineComponent({
components: {
Clock,
},
setup() {
const store = useStore();
store.dispatch("synchronizeData");
const data: ComputedRef<StoreData> = computed(
() => store.getters[GETTERS.allData]
);
return {
data,
};
},
data: () => ({
VERSION: "1.4.7",
updateModalVisible: false,
hasReleaseNotes: false,
currentLang: "pl",
iconEN: require("@/assets/icon-en.jpg"),
iconPL: require("@/assets/icon-pl.svg"),
}),
created() {
this.loadLang();
},
mounted() {
if (StorageManager.getStringValue("version") != this.VERSION) {
StorageManager.setStringValue("version", this.VERSION);
if (this.hasReleaseNotes)
StorageManager.setBooleanValue("version_notes_read", false);
}
this.updateModalVisible =
this.hasReleaseNotes &&
!StorageManager.getBooleanValue("version_notes_read");
},
methods: {
toggleUpdateModal() {
this.updateModalVisible = !this.updateModalVisible;
StorageManager.setBooleanValue("version_notes_read", true);
},
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
StorageManager.setStringValue("lang", lang);
},
loadLang() {
const storageLang = StorageManager.getStringValue("lang");
if (storageLang) {
this.changeLang(storageLang);
return;
}
if (!window.navigator.language) {
this.changeLang("pl");
return;
}
switch (window.navigator.language) {
case "pl-PL":
this.changeLang("pl");
break;
case "en-EN":
default:
this.changeLang("en");
break;
}
return;
},
},
});
</script>
<style lang="scss" src="./App.scss"></style>
+13 -12
View File
@@ -1,27 +1,28 @@
<template>
<div class="clock">{{ formattedDate }}</div>
<div class="clock">{{ computedDate }}</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
name: "clock",
data: () => ({
timestamp: Date.now(),
}),
computed: {
formattedDate() {
return new Date(this.timestamp).toLocaleString("pl-PL", {
setup() {
let timestamp = ref(Date.now());
const computedDate = computed(() => new Date(timestamp.value).toLocaleString("pl-PL", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
},
},
mounted() {
setInterval(() => (this.timestamp = Date.now()), 1000);
},
}));
setInterval(() => (timestamp.value = Date.now()), 1000);
return { computedDate }
}
});
</script>
+4 -5
View File
@@ -3,12 +3,11 @@
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
import { defineComponent } from "@vue/runtime-core";
@Component
export default class Loading extends Vue {
@Prop() readonly message!: string;
}
export default defineComponent({
props: ["message"],
});
</script>
<style lang="scss" scoped>
+16 -5
View File
@@ -1,14 +1,23 @@
<template>
<section class="updates card" v-if="cardOpen">
<section
class="updates card"
v-if="cardOpen"
>
<h2>Ostatnie aktualizacje w Stacjowniku</h2>
<p>Tutaj będą pojawiać się informacje o kolejnych nowościach na stronie :)</p>
<ul>
<li v-for="(update, i) in updates" :key="i">
<li
v-for="(update, i) in updates"
:key="i"
>
<div>{{update.date}}</div>
<div>
<span v-for="(line, l) in content" :key="l">{{line}}</span>
<span
v-for="(line, l) in content"
:key="l"
>{{line}}</span>
</div>
</li>
</ul>
@@ -16,7 +25,9 @@
</template>
<script>
export default {
import { defineComponent } from "@vue/runtime-core";
export default defineComponent({
data() {
return {
updates: {
@@ -29,7 +40,7 @@ export default {
},
};
},
};
});
</script>
<style lang="scss" scoped>
+3 -7
View File
@@ -5,13 +5,12 @@
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { defineComponent } from "vue";
@Component
export default class ActionButton extends Vue {}
export default defineComponent({});
</script>
<style lang="scss" scoped>
<style lang="scss">
@import "../../styles/variables";
@import "../../styles/responsive";
@@ -40,15 +39,12 @@ export default class ActionButton extends Vue {}
img {
width: 1.25em;
vertical-align: middle;
margin-right: 0.35em;
}
p {
font-size: 1em;
overflow: hidden;
transition: max-width 0.35s ease-in-out;
}
&.open {
-66
View File
@@ -1,66 +0,0 @@
<template>
<div class="search-box">
<input v-model="searchedItem" :placeholder="title" />
<img
class="search-exit"
:src="exitIcon"
alt="exit-icon"
@click="() => (searchedItem = '')"
/>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Model } from "vue-property-decorator";
@Component
export default class SearchBox extends Vue {
@Prop({ required: true }) title!: string;
@Model("changed") readonly searchedItem!: string;
// @Emit("changed")
// onItemChanged() {
// return this.searchedItem;
// }
exitIcon = require("@/assets/icon-exit.svg");
// searchedItem: string = "";
// @Watch("searchedItem")
// watchSelectedItem(item) {
// this.onItemChanged();
// }
}
</script>
<style lang="scss" scoped>
.search {
&-box {
position: relative;
background: #333;
border-radius: 0.5em;
min-width: 200px;
}
&-exit {
position: absolute;
cursor: pointer;
top: 50%;
right: 10px;
transform: translateY(-50%);
width: 1em;
}
}
input {
border: none;
min-width: 85%;
padding: 0.35em 0.5em;
}
</style>
+81 -43
View File
@@ -1,12 +1,22 @@
<template>
<div class="select-box">
<div class="select-box_content">
<button class="selected" @click="toggleBox">
{{ selectedItemComp ? selectedItemComp.value : "" }}
<button
class="selected"
@click="toggleBox"
>
{{ computedSelectedItem.value }}
</button>
<div class="options" v-if="boxVisible">
<div class="option" v-for="item in itemList" :key="item.id">
<div
class="options"
v-if="boxVisible"
>
<div
class="option"
v-for="item in itemList"
:key="item.id"
>
<label :for="item.id">
<input
type="button"
@@ -15,7 +25,7 @@
@click="selectOption(item)"
/>
<span :style="selectedItemComp.id == item.id ? 'color: gold;' : ''">
<span :style="computedSelectedItem.id == item.id ? 'color: gold;' : ''">
{{ item.value }}
</span>
</label>
@@ -24,51 +34,79 @@
</div>
<div class="arrow">
<img :src="boxVisible ? ascIcon : descIcon" alt="arrow-icon" />
<img
:src="boxVisible ? ascIcon : descIcon"
alt="arrow-icon"
/>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop, Emit } from "vue-property-decorator";
import {
computed,
defineComponent,
defineEmit,
Ref,
ref,
} from "@vue/runtime-core";
@Component
export default class SelectBox extends Vue {
@Prop({ required: true }) itemList!: { id: string | number; value: string }[];
@Prop({ default: 0 }) defaultItemIndex!: number;
boxVisible = false;
test() {
console.log("test");
}
@Emit("selected")
onItemSelected() {
return this.selectedItem;
}
ascIcon = require("@/assets/icon-arrow-asc.svg");
descIcon = require("@/assets/icon-arrow-desc.svg");
selectedItem: { id: string | number; value: string } | null = null;
get selectedItemComp() {
if (!this.selectedItem) return this.itemList[this.defaultItemIndex];
return this.itemList.find((item) => item.id === this.selectedItem?.id);
}
toggleBox() {
this.boxVisible = !this.boxVisible;
}
selectOption(item: { id: string | number; value: string }) {
this.selectedItem = item;
this.boxVisible = false;
this.onItemSelected();
}
interface Item {
id: string | number;
value: string;
}
export default defineComponent({
emits: ["selected"],
props: {
itemList: {
type: Array as () => Item[],
required: true,
},
defaultItemIndex: {
type: Number,
default: 0,
},
},
data: () => ({
ascIcon: require("@/assets/icon-arrow-asc.svg"),
descIcon: require("@/assets/icon-arrow-desc.svg"),
}),
setup(props) {
let boxVisible = ref(false);
let selectedItem: Ref<Item> = ref(props.itemList[props.defaultItemIndex]);
const computedSelectedItem = computed(() => {
return (
props.itemList.find((item) => item.id === selectedItem.value.id) ||
props.itemList[props.defaultItemIndex]
);
});
return {
computedSelectedItem,
boxVisible,
selectedItem,
};
},
methods: {
selectOption(item: Item) {
this.selectedItem = item;
this.boxVisible = false;
this.$emit("selected", item);
},
toggleBox() {
this.boxVisible = !this.boxVisible;
},
},
});
</script>
<style lang="scss" scoped>
-97
View File
@@ -1,97 +0,0 @@
<template>
<div class="modal">
<div class="header">
<span>Stacj</span>
<img src="@/assets/trainlogo.png" alt="trainlogo" />
<span>wnik</span>
<sup style="font-size: 0.5em; margin-left: 10px;" class="title">1.4</sup>
</div>
<div class="title">Dziennik Aktywności Scenerii dostępny w wersji beta!</div>
<div class="content">
Do użytku został oddany Dziennik Aktywności Scenerii, który pozwala na dostęp do informacji kto i kiedy dyżurował na danej stacji.
Aby przejść do zakładki z dziennikiem wystarczy wybrać opcję "DZIENNIK" w menu na górze strony. Funkcjonalność ta jest nadal w trakcie prac,
więc informacje, które pokazuje, mogą być niepoprawne, a dane kasowane w ramach dalszych testów.
<div style="text-align: center; font-weight: bold; margin: 0.5em 0;">Miłego korzystania!</div>
</div>
<button class="button" @click="toggleUpdateModal">PRZYJĄŁEM!</button>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
@Component
export default class UpdateModal extends Vue {
@Prop() currentVersion!: string;
STORAGE_ID = "modal_update";
toggleUpdateModal(type: string) {
this.$emit("toggleUpdateModal");
}
}
</script>
<style lang="scss" scoped>
@import "../../styles/responsive";
.modal {
z-index: 100;
padding: 1em;
border-radius: 1em;
position: fixed;
top: 50%;
left: 50%;
width: 65%;
max-width: 950px;
max-height: 95vh;
overflow: auto;
transform: translate(-50%, -50%);
background: rgba(black, 0.85);
color: white;
text-align: center;
@include smallScreen() {
font-size: 0.8em;
width: 95%;
}
}
.header {
font-size: 4.5em;
img {
width: 0.8em;
}
}
.title {
font-size: 2em;
}
.content {
font-size: 1.4em;
text-align: justify;
ul {
list-style: square inside;
}
}
.button {
font-size: 1.25em;
margin: 0 auto;
}
</style>
+79 -41
View File
@@ -7,12 +7,14 @@
:href="stationInfo.stationURL"
target="_blank"
rel="noopener noreferrer"
>{{ stationInfo.stationName }}</a
>
>{{ stationInfo.stationName }}</a>
<span v-else>{{ stationInfo.stationName }}</span>
</div>
<div class="scenery-hash" v-if="stationInfo.stationHash">
<div
class="scenery-hash"
v-if="stationInfo.stationHash"
>
#{{ stationInfo.stationHash }}
</div>
</div>
@@ -23,24 +25,36 @@
:class="!stationInfo.stationHash ? 'no-stats' : ''"
>
<span class="likes">
<img :src="likeIcon" alt="icon-like" />
<img
:src="likeIcon"
alt="icon-like"
/>
<span>{{ stationInfo.dispatcherRate }}</span>
</span>
<span class="users">
<img :src="userIcon" alt="icon-user" />
<img
:src="userIcon"
alt="icon-user"
/>
<span>{{ stationInfo.currentUsers }}</span>
/
<span>{{ stationInfo.maxUsers }}</span>
</span>
<span class="spawns">
<img :src="spawnIcon" alt="icon-spawn" />
<img
:src="spawnIcon"
alt="icon-spawn"
/>
<span>{{ stationInfo.spawns.length }}</span>
</span>
<span class="schedules">
<img :src="timetableIcon" alt="icon-timetable" />
<img
:src="timetableIcon"
alt="icon-timetable"
/>
<span v-if="stationInfo.scheduledTrains">
<span style="color: #eee">{{
stationInfo.scheduledTrains.length
@@ -111,7 +125,10 @@
</div>
<div class="info-dispatcher">
<div class="dispatcher" v-if="stationInfo.stationHash">
<div
class="dispatcher"
v-if="stationInfo.stationHash"
>
<span
class="dispatcher_level"
:style="
@@ -129,7 +146,10 @@
<span class="dispatcher_name">{{ stationInfo.dispatcherName }}</span>
</div>
<span class="status-badge" :class="stationInfo.statusID">
<span
class="status-badge"
:class="stationInfo.statusID"
>
{{ $t(`status.${stationInfo.statusID}`) }}
{{
stationInfo.statusID == "online" ? stationInfo.statusTimeString : ""
@@ -141,7 +161,10 @@
<div class="user-list">
<h3 class="user-header">
{{ $t("scenery.users") }}
<img :src="userIcon" alt="icon-user" />
<img
:src="userIcon"
alt="icon-user"
/>
</h3>
<div
@@ -166,7 +189,10 @@
<div class="spawn-list">
<h3 class="spawn-header">
{{ $t("scenery.spawns") }}
<img :src="spawnIcon" alt="icon-spawn" />
<img
:src="spawnIcon"
alt="icon-spawn"
/>
</h3>
<span
@@ -181,7 +207,7 @@
<span
class="spawn none"
v-if="!stationInfo.spawns || stationInfo.spawns.length == 0"
>{{ $t("scenery.no-spawns") }}
>{{ $t("scenery.no-spawns") }}
</span>
</div>
</div>
@@ -190,44 +216,56 @@
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
import Station from "@/scripts/interfaces/Station";
import styleMixin from "@/mixins/styleMixin";
import { computed, defineComponent } from "@vue/runtime-core";
@Component
export default class SceneryInfo extends styleMixin {
@Prop() readonly stationInfo!: Station;
@Prop() readonly timetableOnly!: boolean;
export default defineComponent({
props: {
stationInfo: {
type: Object as () => Station,
},
likeIcon: string = require("@/assets/icon-like.svg");
spawnIcon: string = require("@/assets/icon-spawn.svg");
timetableIcon: string = require("@/assets/icon-timetable.svg");
userIcon: string = require("@/assets/icon-user.svg");
timetableOnly: Boolean,
},
get computedStationTrains() {
if (!this.stationInfo) return null;
mixins: [styleMixin],
return this.stationInfo.stationTrains.map((stationTrain) => {
const scheduledData = this.stationInfo?.scheduledTrains.find(
(scheduledTrain) => scheduledTrain.trainNo === stationTrain.trainNo
);
data: () => ({
likeIcon: require("@/assets/icon-like.svg"),
spawnIcon: require("@/assets/icon-spawn.svg"),
timetableIcon: require("@/assets/icon-timetable.svg"),
userIcon: require("@/assets/icon-user.svg"),
}),
return {
...stationTrain,
stopStatus: scheduledData?.stopStatus || "no-timetable",
};
setup(props) {
const computedStationTrains = computed(() => {
if (!props.stationInfo) return [];
return props.stationInfo.stationTrains.map((train) => {
const scheduledTrainStatus = props.stationInfo?.scheduledTrains.find(
(st) => st.trainNo === train.trainNo
);
return {
...train,
stopStatus: scheduledTrainStatus?.stopStatus || "no-timetable",
};
});
});
}
navigateToTrain(trainNo: number) {
this.$router.push({
name: "TrainsView",
params: { queryTrain: trainNo.toString() },
});
}
}
return { computedStationTrains };
},
methods: {
navigateToTrain(trainNo: number) {
this.$router.push({
name: "TrainsView",
params: { queryTrain: trainNo.toString() },
});
},
},
});
</script>
<style lang="scss" scoped>
+100 -72
View File
@@ -25,11 +25,14 @@
value: cp.checkpointName,
}))
"
@selected="chooseOption"
@selected="selectCheckpoint"
></select-box>
</div>
<span class="timetable-item loading" v-if="dataStatus == 0">{{
<span
class="timetable-item loading"
v-if="dataStatus == 0"
>{{
$t("app.loading")
}}</span>
@@ -48,14 +51,12 @@
>
<span class="timetable-general">
<span class="general-info">
<router-link
:to="{
<router-link :to="{
name: 'TrainsView',
params: {
queryTrain: scheduledTrain.trainNo.toString(),
},
}"
>
}">
<span>
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
@@ -68,21 +69,17 @@
'https://td2.info.pl/profile/?u=' + scheduledTrain.driverId
"
target="_blank"
>{{ scheduledTrain.driverName }}</a
>
>{{ scheduledTrain.driverName }}</a>
</span>
<div class="info-route">
<strong
>{{ scheduledTrain.beginsAt }} -
{{ scheduledTrain.terminatesAt }}</strong
>
<strong>{{ scheduledTrain.beginsAt }} -
{{ scheduledTrain.terminatesAt }}</strong>
</div>
</span>
<span class="general-status">
<span :class="scheduledTrain.stopStatus"
>{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
<span :class="scheduledTrain.stopStatus">{{ $t(`timetables.${scheduledTrain.stopStatus}`) }}
</span>
</span>
</span>
@@ -95,7 +92,10 @@
v-html="$t('timetables.begins')"
>
</span>
<span class="arrival-time" v-else>
<span
class="arrival-time"
v-else
>
{{ scheduledTrain.stopInfo.arrivalTimeString }} ({{
scheduledTrain.stopInfo.arrivalDelay
}})
@@ -103,7 +103,10 @@
</span>
<span class="schedule-stop">
<span class="stop-time" v-if="scheduledTrain.stopInfo.stopTime">
<span
class="stop-time"
v-if="scheduledTrain.stopInfo.stopTime"
>
{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType }}
</span>
@@ -116,7 +119,10 @@
v-html="$t('timetables.terminates')"
>
</span>
<span class="departure-time" v-else>
<span
class="departure-time"
v-else
>
{{ scheduledTrain.stopInfo.departureTimeString }} ({{
scheduledTrain.stopInfo.departureDelay
}})
@@ -129,74 +135,96 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import Station from "@/scripts/interfaces/Station";
import ScheduledTrain from "@/scripts/interfaces/ScheduledTrain";
import SelectBox from "../Global/SelectBox.vue";
import { computed, defineComponent, ref } from "@vue/runtime-core";
import { useRoute } from "vue-router";
@Component({ components: { SelectBox } })
export default class SceneryTimetable extends Vue {
@Prop() readonly stationInfo!: Station;
@Prop() readonly timetableOnly!: boolean;
@Prop() readonly dataStatus!: number;
export default defineComponent({
components: { SelectBox },
viewIcon: string = require("@/assets/icon-view.svg");
props: {
stationInfo: {
type: Object as () => Station,
},
timetableOnly: {
type: Boolean,
},
dataStatus: {
type: Number,
},
},
listOpen: boolean = false;
selectedOption: string = "";
data: () => ({
viewIcon: require("@/assets/icon-view.svg"),
listOpen: false,
}),
loadSelectedOption() {
if (!this.stationInfo) return;
if (!this.stationInfo.checkpoints) return;
if (this.selectedOption != "") return;
setup(props) {
const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
this.selectedOption = this.stationInfo.checkpoints[0].checkpointName;
}
const selectedCheckpoint = ref("");
const computedScheduledTrains = computed(() => {
if (!props.stationInfo) return [];
let scheduledTrains = props.stationInfo.checkpoints?.find(
(cp) => cp.checkpointName === selectedCheckpoint.value
)?.scheduledTrains;
// if (props.stationInfo.checkpoints)
// scheduledTrains = props.stationInfo.checkpoints.find(
// (cp) => cp.checkpointName === selectedCheckpoint.value
// )?.scheduledTrains;
// else scheduledTrains = props.stationInfo.scheduledTrains;
return (
scheduledTrains?.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
else if (a.stopStatusID < b.stopStatusID) return -1;
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp)
return 1;
else if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp)
return -1;
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp
? 1
: -1;
}) || []
);
});
return {
currentURL,
selectedCheckpoint,
computedScheduledTrains,
};
},
methods: {
loadSelectedOption() {
if (!this.stationInfo) return;
if (!this.stationInfo.checkpoints) return;
if (this.selectedCheckpoint != "") return;
this.selectedCheckpoint = this.stationInfo.checkpoints[0].checkpointName;
},
selectCheckpoint(item: { id: number | string; value: string }) {
this.selectedCheckpoint = item.value;
},
},
mounted() {
this.loadSelectedOption();
}
},
activated() {
this.loadSelectedOption();
}
chooseOption(item: { id: number | string; value: string }) {
this.selectedOption = item.value;
}
get currentURL() {
return `${location.origin}${this.$route.fullPath}`;
}
get computedScheduledTrains() {
if (!this.stationInfo) return [];
let scheduledTrains: ScheduledTrain[] | undefined;
if (this.stationInfo.checkpoints)
scheduledTrains = this.stationInfo.checkpoints.find(
(cp) => cp.checkpointName === this.selectedOption
)?.scheduledTrains;
else scheduledTrains = this.stationInfo.scheduledTrains;
return (
scheduledTrains?.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
else if (a.stopStatusID < b.stopStatusID) return -1;
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
else if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp)
return -1;
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp
? 1
: -1;
}) || []
);
}
}
},
});
</script>
<style lang="scss" scoped>
+64 -59
View File
@@ -1,5 +1,8 @@
<template>
<section class="card">
<section
class="card"
v-if="showCard"
>
<div
class="card-exit"
@click="exit"
@@ -79,13 +82,13 @@
<div class="card-actions flex">
<action-button
class="outlined"
@click.native="resetFilters"
@click="resetFilters"
>
{{ $t("filters.reset") }}
</action-button>
<action-button
class="outlined"
@click.native="exit"
@click="exit"
>{{
$t("filters.close")
}}</action-button>
@@ -94,83 +97,85 @@
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
import inputData from "@/data/options.json";
import StorageManager from "@/scripts/managers/storageManager";
import { defineComponent } from "@vue/runtime-core";
import ActionButton from "../Global/ActionButton.vue";
@Component({ components: { ActionButton } })
export default class FilterCard extends Vue {
inputs = { ...inputData };
saveOptions: boolean = false;
STORAGE_KEY: string = "options_saved";
export default defineComponent({
components: { ActionButton },
props: ["showCard", "exit"],
@Prop() exit!: () => void;
data: () => ({
inputs: { ...inputData },
saveOptions: false,
STORAGE_KEY: "options_saved",
}),
mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
}
},
handleChange(e: Event): void {
const target = <HTMLInputElement>e.target;
methods: {
handleChange(e: Event) {
const target = e.target as HTMLInputElement;
this.$emit("changeFilterValue", {
name: target.name,
value: !target.checked,
});
this.$emit("changeFilterValue", {
name: target.name,
value: !target.checked,
});
if (this.saveOptions)
StorageManager.setBooleanValue(target.name, target.checked);
},
if (this.saveOptions)
StorageManager.setBooleanValue(target.name, target.checked);
}
handleInput(e: Event) {
const target = e.target as HTMLInputElement;
handleInput(e: Event): void {
const target = <HTMLInputElement>e.target;
this.$emit("changeFilterValue", {
name: target.name,
value: target.value,
});
this.$emit("changeFilterValue", {
name: target.name,
value: target.value,
});
if (this.saveOptions)
StorageManager.setStringValue(target.name, target.value);
},
if (this.saveOptions)
StorageManager.setStringValue(target.name, target.value);
}
saveFilters() {
if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY);
return;
}
saveFilters(): void {
if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY);
return;
}
StorageManager.registerStorage(this.STORAGE_KEY);
StorageManager.registerStorage(this.STORAGE_KEY);
this.inputs.options.forEach((option) =>
StorageManager.setBooleanValue(option.name, option.value)
);
this.inputs.options.forEach((option) =>
StorageManager.setBooleanValue(option.name, option.value)
);
this.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
},
this.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
}
resetFilters() {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
resetFilters(): void {
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, option.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.value);
});
this.$emit("resetFilters");
},
this.$emit("resetFilters");
}
closeCard(): void {
this.exit();
}
}
closeCard() {
this.exit();
},
},
});
</script>
<style lang="scss" scoped>
-139
View File
@@ -1,139 +0,0 @@
<template>
<div class="options">
<div class="options-actions">
<button
class="action-btn"
:class="{'open': filterCardOpen}"
@click="() => toggleCardsState('filter')"
>
<img :src="require('@/assets/icon-filter2.svg')" alt="icon-filter" />
<p>FILTRY</p>
</button>
<button
class="action-btn"
:class="{'open': legendCardOpen}"
@click="() => toggleCardsState('legend')"
>
<img :src="require('@/assets/icon-legend.svg')" alt="icon legend" />
<p>LEGENDA</p>
</button>
</div>
<div class="options-content">
<transition name="card-anim"></transition>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
@Component({})
export default class Options extends Vue {
filterCardOpen: boolean = false;
legendCardOpen: boolean = false;
toggleCardsState(name: string): void {
if (name == "filter") {
this.legendCardOpen = false;
this.filterCardOpen = !this.filterCardOpen;
}
if (name == "legend") {
this.filterCardOpen = false;
this.legendCardOpen = !this.legendCardOpen;
}
}
toggleCardState(): void {
this.filterCardOpen = !this.filterCardOpen;
}
toggleLegendCardState(): void {
this.legendCardOpen = !this.legendCardOpen;
}
}
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
@import "../../styles/responsive.scss";
.options {
display: flex;
z-index: 5;
}
.card-anim {
&-enter-active,
&-leave-active {
transition: all 0.25s ease-in-out;
}
&-enter,
&-leave-to {
transform: translate(-45%, -50%);
opacity: 0;
}
}
.options {
&-actions {
display: flex;
}
}
.action-btn {
display: flex;
align-items: center;
background: #333;
border: none;
color: #e0e0e0;
font-size: 0.4em;
padding: 0.3em;
outline: none;
cursor: pointer;
transition: all 0.3s;
img {
width: 1.3em;
margin-right: 0.2em;
}
p {
max-width: 0;
font-size: 1em;
overflow: hidden;
transition: max-width 0.35s ease-in-out;
}
&:hover > p,
&.open > p {
max-width: 500px;
color: $accentCol;
}
&:hover {
background: rgba(#e0e0e0, 0.4);
}
}
@include smallScreen {
.options {
display: flex;
justify-content: center;
}
.action-btn {
font-size: 0.8rem;
}
}
</style>
+69 -60
View File
@@ -160,6 +160,7 @@
$t('desc.signals-type') + $t(`signals.${station.signalType}`)
"
/>
<img
v-if="station.SBL && station.SBL !== ''"
:src="require(`@/assets/icon-SBL.svg`)"
@@ -236,80 +237,87 @@
</template>
<script lang="ts">
import { Component, Prop } from "vue-property-decorator";
import Station from "@/scripts/interfaces/Station";
import styleMixin from "@/mixins/styleMixin";
import { Getter } from "vuex-class";
import Options from "@/components/StationsView/Options.vue";
import { StoreData } from "@/scripts/interfaces/StoreData";
import { DataStatus } from "@/scripts/enums/DataStatus";
import { computed, ComputedRef, defineComponent } from "@vue/runtime-core";
import { useStore } from "@/store";
import { GETTERS } from "@/constants/storeConstants";
import Station from "@/scripts/interfaces/Station";
@Component({
components: { Options },
})
export default class StationTable extends styleMixin {
@Prop() readonly stations!: Station[];
@Prop() readonly sorterActive!: number;
export default defineComponent({
props: {
stations: {
type: Array as () => Station[],
required: true,
},
@Prop() readonly setFocusedStation!: () => void;
@Prop() readonly changeSorter!: () => void;
sorterActive: {
type: Object as () => { id: string; dir: number },
required: true,
},
@Getter("getAllData") storeAPIData!: StoreData;
setFocusedStation: Function,
changeSorter: Function,
},
likeIcon: string = require("@/assets/icon-like.svg");
spawnIcon: string = require("@/assets/icon-spawn.svg");
timetableIcon: string = require("@/assets/icon-timetable.svg");
userIcon: string = require("@/assets/icon-user.svg");
trainIcon: string = require("@/assets/icon-train.svg");
mixins: [styleMixin],
ascIcon: string = require("@/assets/icon-arrow-asc.svg");
descIcon: string = require("@/assets/icon-arrow-desc.svg");
data: () => ({
likeIcon: require("@/assets/icon-like.svg"),
spawnIcon: require("@/assets/icon-spawn.svg"),
timetableIcon: require("@/assets/icon-timetable.svg"),
userIcon: require("@/assets/icon-user.svg"),
trainIcon: require("@/assets/icon-train.svg"),
headIds = [
"station",
"min-lvl",
"status",
"dispatcher",
"dispatcher-lvl",
"routes",
"general",
];
ascIcon: require("@/assets/icon-arrow-asc.svg"),
descIcon: require("@/assets/icon-arrow-desc.svg"),
headIconsIds = ["user", "spawn", "timetable"];
headIds: [
"station",
"min-lvl",
"status",
"dispatcher",
"dispatcher-lvl",
"routes",
"general",
],
headTitles: string[][] = [
["Stacja"],
["Min. poziom", "dyżurnego"],
["Status"],
["Dyżurny"],
["Poziom", "dyżurnego"],
["Szlaki", "2tor | 1tor"],
["Informacje", "ogólne"],
[this.userIcon, "Mechanicy online"],
[this.spawnIcon, "Otwarte spawny"],
[this.timetableIcon, "Aktywne RJ"],
];
headIconsIds: ["user", "spawn", "timetable"],
}),
setScenery(name: string) {
const station = this.stations.find(
(station) => station.stationName === name
setup() {
const store = useStore();
const dataConnectionStatus: ComputedRef<DataStatus> = computed(
() => store.getters[GETTERS.dataStatus]
);
if (!station) return;
const isDataLoaded = () =>
computed(() => {
dataConnectionStatus.value == DataStatus.Loaded;
});
this.$router.push({
name: "SceneryView",
query: { station: station.stationName.replaceAll(" ", "_") },
});
}
return {
isDataLoaded,
};
},
get isDataLoaded() {
return this.storeAPIData.dataConnectionStatus == DataStatus.Loaded;
}
}
methods: {
setScenery(name: string) {
const station = this.stations.find(
(station) => station.stationName === name
);
if (!station) return;
this.$router.push({
name: "SceneryView",
query: { station: station.stationName.replaceAll(" ", "_") },
});
},
},
});
</script>
<style lang="scss" scoped>
@@ -358,6 +366,7 @@ table {
padding: 0.5em;
background-color: $primaryCol;
white-space: pre-wrap;
cursor: pointer;
user-select: none;
@@ -442,8 +451,8 @@ td.station {
}
.track {
margin: 0 0.3em;
padding: 0.35em 0.1em;
margin: 0 0.35em;
padding: 0.35em;
font-size: 1.05em;
white-space: pre-wrap;
}
@@ -1,286 +0,0 @@
<template>
<div class="station-timetable">
<div class="timetable-wrapper">
<div class="timetable-title title">
<div style="font-size: 1.5em">{{ stationName.toUpperCase() }}</div>
<div style="font-size: 0.7em">AKTYWNE ROZKŁADY JAZDY</div>
</div>
<div class="timetable-content">
<div
class="timetable-item"
v-for="(scheduledTrain, i) in computedScheduledTrains"
:key="i"
>
<span class="timetable-general">
<span class="general-info">
<router-link
:to="{
name: 'TrainsView',
params: {
passedSearchedTrain: scheduledTrain.trainNo.toString(),
},
}"
>
<span>
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
</span>
</router-link>
|
<span>
<a
:href="
'https://td2.info.pl/profile/?u=' + scheduledTrain.driverId
"
target="_blank"
>{{ scheduledTrain.driverName }}</a
>
</span>
</span>
<span class="general-status">
<span :class="scheduledTrain.stopStatus">{{
scheduledTrain.stopLabel
}}</span>
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span
class="arrival-time begins"
v-if="scheduledTrain.stopInfo.beginsHere"
>ROZPOCZYNA BIEG</span
>
<span class="arrival-time" v-else
>{{ scheduledTrain.stopInfo.arrivalTimeString }} ({{
scheduledTrain.stopInfo.arrivalDelay
}})</span
>
</span>
<span class="schedule-stop">
<span class="stop-time" v-if="scheduledTrain.stopInfo.stopTime"
>{{ scheduledTrain.stopInfo.stopTime }}
{{ scheduledTrain.stopInfo.stopType }}</span
>
<span class="stop-arrow arrow"></span>
</span>
<span class="schedule-departure">
<span
class="departure-time terminates"
v-if="scheduledTrain.stopInfo.terminatesHere"
>KOŃCZY BIEG</span
>
<span class="departure-time" v-else
>{{ scheduledTrain.stopInfo.departureTimeString }} ({{
scheduledTrain.stopInfo.departureDelay
}})</span
>
</span>
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
@Component
export default class StationTimetable extends Vue {
@Prop() readonly scheduledTrains;
@Prop() readonly stationName;
get computedScheduledTrains() {
return this.scheduledTrains.sort((a, b) => {
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
else if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp)
return -1;
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp
? 1
: -1;
});
}
}
</script>
<style lang="scss" scoped>
@import "../../styles/variables.scss";
@import "../../styles/responsive.scss";
.station-timetable {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
transform: translateY(-100%);
-webikit-transform: translateY(-100%);
&.show {
transform: translateY(0);
-webkit-transform: translateY(0);
}
transition: transform 150ms ease-out;
background: #333;
@include smallScreen() {
font-size: 1.3em;
}
}
.timetable {
&-content {
width: 100%;
height: 100%;
overflow: auto;
}
&-title {
padding-top: 2rem;
padding-bottom: 0.3rem;
font-size: 1.6em;
}
&-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
&-item {
margin: 1em auto;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
padding: 0 1rem;
@include smallScreen() {
display: flex;
flex-direction: column;
align-items: center;
}
}
}
.timetable {
&-general {
padding: 0.3rem 0.7rem;
border: 2px solid white;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: space-between;
@include smallScreen() {
width: 95%;
font-size: 0.85em;
}
}
&-schedule {
@include smallScreen() {
width: 80%;
margin: 0.7em 0;
font-size: 0.9em;
}
display: grid;
grid-template-columns: repeat(auto-fit, minmax(50px, 1fr));
font-size: 1.35em;
}
}
.arrow {
border: solid white;
border-width: 0 2px 2px 0;
display: inline-block;
padding: 2px;
margin-left: 50px;
position: relative;
transform: rotate(-45deg);
&::before {
content: "";
position: absolute;
display: block;
width: 55px;
height: 3px;
top: 4px;
left: 4px;
transform: translate(-100%, -1px) rotate(45deg);
transform-origin: right bottom;
background: white;
}
}
.general-info {
span {
color: $accentCol;
}
}
.general-status {
span.arriving {
color: #aaa;
}
span.departed {
color: lime;
}
span.stopped {
color: #ffa600;
}
span.online {
color: gold;
}
span.terminated {
color: red;
}
}
.schedule {
&-arrival,
&-stop,
&-departure {
display: flex;
justify-content: center;
align-items: center;
margin: 0 0.3rem;
}
&-stop {
display: flex;
flex-direction: column;
.stop-time {
font-size: 0.7em;
}
}
}
.arrival-time.begins,
.departure-time.terminates {
font-size: 0.75em;
}
</style>
+73 -71
View File
@@ -42,96 +42,98 @@
</template>
<script lang="ts">
import { Component, Vue, Watch, Prop, Emit } from "vue-property-decorator";
import { computed, defineComponent } from "vue";
import { useI18n } from "vue-i18n";
import SelectBox from "../Global/SelectBox.vue";
@Component({ components: { SelectBox } })
export default class TrainOptions extends Vue {
// Passed as component parameters
@Prop() readonly queryTrain!: string;
export default defineComponent({
components: { SelectBox },
props: ["queryTrain"],
emits: ["changeSearchedTrain", "changeSearchedDriver", "changeSorter"],
exitIcon = require("@/assets/icon-exit.svg");
data: () => ({
exitIcon: require("@/assets/icon-exit.svg"),
searchedTrain: "",
searchedDriver: "",
}),
searchedTrain = "";
searchedDriver = "";
setup() {
const { t } = useI18n();
sorterOptions: { id: string; value: string }[] = [
{
id: "mass",
value: "masa",
},
{
id: "speed",
value: "prędkość",
},
{
id: "length",
value: "długość",
},
{
id: "distance",
value: "kilometraż",
},
{
id: "timetable",
value: "numer pociągu",
},
];
const sorterOptions = [
{
id: "mass",
value: "masa",
},
{
id: "speed",
value: "prędkość",
},
{
id: "length",
value: "długość",
},
{
id: "distance",
value: "kilometraż",
},
{
id: "timetable",
value: "numer pociągu",
},
];
get translatedSorterOptions() {
return this.sorterOptions.map((option) => ({
id: option.id,
value: this.$t(`trains.option-${option.id}`),
}));
}
const translatedSorterOptions = computed(() =>
sorterOptions.map(({ id }) => ({
id,
value: t(`trains.option-${id}`),
}))
);
return {
translatedSorterOptions,
};
},
mounted() {
if (this.queryTrain) {
this.searchedTrain = this.queryTrain;
this.searchedDriver = "";
}
}
},
/* Emitters to TrainsView managing variables */
methods: {
chooseTrain(train: string) {
this.$emit("changeSearchedTrain", train);
},
@Emit("changeSearchedTrain")
chooseTrain(train: string) {
return train;
}
chooseDriver(driverName: string) {
this.$emit("changeSearchedDriver", driverName);
},
@Emit("changeSearchedDriver")
chooseDriver(driverName: string) {
return driverName;
}
changeSorter(item: { id: string | number; value: string }) {
this.$emit("changeSorter", { id: item.id, dir: -1 });
},
},
@Emit()
changeSorter(item: { id: string | number; value: string }) {
return { id: item.id, dir: -1 };
}
watch: {
searchedTrain(value: string) {
this.chooseTrain(value);
},
/* Watchers for search boxes */
searchedDriver(value: string) {
this.chooseDriver(value);
},
@Watch("searchedTrain")
watchSearchedTrain(train: string) {
this.chooseTrain(train);
}
queryTrain(train: string) {
if (!train) return;
if (train == "") return;
@Watch("searchedDriver")
watchSearchedDriver(driver: string) {
this.chooseDriver(driver);
}
/* Watcher for train no passed in link params */
@Watch("queryTrain")
onQueryTrainChanged(train: string | undefined) {
if (!train) return;
if (train == "") return;
this.searchedTrain = train;
this.searchedDriver = "";
}
}
this.searchedTrain = train;
this.searchedDriver = "";
},
},
});
</script>
<style lang="scss" scoped>
+28 -30
View File
@@ -1,5 +1,8 @@
<template>
<div class="train-schedule" @click="click">
<div
class="train-schedule"
@click="this.$emit('click')"
>
<div class="schedule-wrapper">
<ul class="stop_list">
<li
@@ -19,11 +22,17 @@
<div class="stop-bar"></div>
<span class="distance" v-if="stop.stopDistance">
<span
class="distance"
v-if="stop.stopDistance"
>
{{ Math.floor(stop.stopDistance) }}
</span>
<span class="stop-name" v-html="stop.stopName"></span>
<span
class="stop-name"
v-html="stop.stopName"
></span>
<span class="stop-date">
<span
class="date arrival"
@@ -79,9 +88,7 @@
<div class="progress-bar"></div>
<span v-if="i < followingStops.length - 1">
<span
v-if="stop.departureLine == followingStops[i + 1].arrivalLine"
>
<span v-if="stop.departureLine == followingStops[i + 1].arrivalLine">
{{ stop.departureLine }}
</span>
@@ -98,32 +105,23 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { defineComponent } from "@vue/runtime-core";
import TrainStop from "@/scripts/interfaces/TrainStop";
export default defineComponent({
props: ["followingStops", "currentStationName"],
emits: ["click"],
@Component
export default class TrainSchedule extends Vue {
@Prop() readonly followingStops!: TrainStop[];
@Prop() readonly currentStationName!: string;
stylizeTime(timeString: string, delay: number, confirmed: boolean) {
return (
timeString +
(delay != 0 && confirmed
? " (" + (delay > 0 ? "+" : "") + delay.toString() + ")"
: "")
);
}
click() {
this.$emit("click");
}
mounted() {
console.log("mounted");
}
}
methods: {
stylizeTime(timeString: string, delay: number, confirmed: boolean) {
return (
timeString +
(delay != 0 && confirmed
? " (" + (delay > 0 ? "+" : "") + delay.toString() + ")"
: "")
);
},
},
});
</script>
<style lang="scss" scoped>
+141 -99
View File
@@ -1,16 +1,29 @@
<template>
<div class="train-stats">
<div class="stats_button">
<action-button @click.native="toggleStatsOpen">
<img :src="statsIcon" :alt="$t('trains.stats')" />
{{ $t("trains.stats") }}
<action-button @click="toggleStatsOpen">
<img
:src="statsIcon"
:alt="$t('trains.stats')"
/>
<p class="xd">{{ $t("trains.stats") }}</p>
</action-button>
</div>
<transition name="stats-anim" class="stats_wrapper" tag="div">
<div class="stats-body" v-if="trainStatsOpen">
<transition
name="stats-anim"
class="stats_wrapper"
tag="div"
>
<div
class="stats-body"
v-if="trainStatsOpen"
>
<h2 class="stats-header">
<img :src="statsIcon" :alt="$t('trains.stats')" />
<img
:src="statsIcon"
:alt="$t('trains.stats')"
/>
{{ $t("trains.stats") }}
</h2>
@@ -70,7 +83,11 @@
<div class="title stats-title">{{ $t("trains.stats-locos") }}</div>
<div class="loco-list stats-content">
<div class="loco-item" v-for="(loco, i) in locoList" :key="i">
<div
class="loco-item"
v-for="(loco, i) in locoList"
:key="i"
>
{{ loco[0] }} | {{ loco[1] }}
</div>
</div>
@@ -81,120 +98,145 @@
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import ActionButton from "@/components/Global/ActionButton.vue";
import Train from "@/scripts/interfaces/Train";
import { computed, defineComponent } from "@vue/runtime-core";
@Component({ components: { ActionButton } })
export default class TrainStats extends Vue {
@Prop() readonly trains!: Train[];
trainStatsOpen = false;
export default defineComponent({
components: { ActionButton },
props: {
trains: {
type: Array as () => Train[],
required: true,
},
},
toggleStatsOpen() {
this.trainStatsOpen = !this.trainStatsOpen;
}
data: () => ({
trainStatsOpen: false,
statsIcon: require("@/assets/icon-stats.svg"),
}),
statsIcon = require("@/assets/icon-stats.svg");
methods: {
toggleStatsOpen() {
this.trainStatsOpen = !this.trainStatsOpen;
},
},
get speedStats(): { avg: string; min: string; max: string } {
if (this.trains.length == 0) return { avg: "0", min: "0", max: "0" };
setup(props) {
const speedStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" };
const avg = (
this.trains.reduce((acc, train) => acc + train.speed, 0) /
this.trains.length
).toFixed(2);
const avg = (
props.trains.reduce((acc, train) => acc + train.speed, 0) /
props.trains.length
).toFixed(2);
const minMax = this.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
const minMaxSpeed = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] =
acc[0] === undefined || train.speed < acc[0] ? train.speed : acc[0];
acc[0] = !acc[0] || train.speed < acc[0] ? train.speed : acc[0];
acc[1] =
acc[1] === undefined || train.speed > acc[1] ? train.speed : acc[1];
return acc;
}, [] as any);
acc[1] = !acc[1] || train.speed > acc[1] ? train.speed : acc[1];
return acc;
}, [] as any);
return { avg, min: minMax[0].toString(), max: minMax[1].toString() };
}
return {
avg,
min: minMaxSpeed[0].toString(),
max: minMaxSpeed[1].toString(),
};
});
get timetableStats(): { avg: string; min: string; max: string } {
if (this.trains.length == 0) return { avg: "0", min: "0", max: "0" };
const timetableStats = computed(() => {
if (props.trains.length == 0) return { avg: "0", min: "0", max: "0" };
const avg = (
this.trains.reduce(
(acc, train) =>
train.timetableData ? acc + train.timetableData.routeDistance : acc,
0
) / this.trains.length
).toFixed(2);
const avg = (
props.trains.reduce(
(acc, train) =>
train.timetableData ? acc + train.timetableData.routeDistance : acc,
0
) / props.trains.length
).toFixed(2);
const minMax = this.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
const minMaxDistance = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] =
acc[0] === undefined || train.timetableData.routeDistance < acc[0]
? train.timetableData.routeDistance
: acc[0];
acc[0] =
!acc[0] || train.timetableData.routeDistance < acc[0]
? train.timetableData.routeDistance
: acc[0];
acc[1] =
acc[1] === undefined || train.timetableData.routeDistance > acc[1]
? train.timetableData.routeDistance
: acc[1];
return acc;
}, [] as any);
acc[1] =
!acc[1] || train.timetableData.routeDistance > acc[1]
? train.timetableData.routeDistance
: acc[1];
return acc;
}, [] as any);
return { avg, min: minMax[0].toString(), max: minMax[1].toString() };
}
return {
avg,
min: minMaxDistance[0].toString(),
max: minMaxDistance[1].toString(),
};
});
get categoryList(): Map<string, number> {
const map = this.trains.reduce((acc, train) => {
if (!train.timetableData || !train.timetableData.category) return acc;
const categoryList = computed(() => {
const map = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.timetableData.category) return acc;
acc.set(
train.timetableData.category,
acc.get(train.timetableData.category)
? acc.get(train.timetableData.category) + 1
: 1
acc.set(
train.timetableData.category,
acc.get(train.timetableData.category)
? acc.get(train.timetableData.category) + 1
: 1
);
return acc;
}, new Map());
return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
});
const locoList = computed(() => {
const map: Map<string, number> = props.trains.reduce((acc, train) => {
if (!train.timetableData || !train.locoType) return acc;
acc.set(
train.locoType,
acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1
);
return acc;
}, new Map());
const sorted = [...map.entries()]
.sort((a, b) => b[1] - a[1])
.filter((v, i) => i < 3);
return sorted;
});
const specialTrainCount = computed(() => {
const twrList = props.trains.filter(
(train) => train.timetableData && train.timetableData.TWR
);
const skrList = props.trains.filter(
(train) => train.timetableData && train.timetableData.SKR
);
return acc;
}, new Map());
return [twrList.length, skrList.length];
});
return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
}
get locoList(): any[] {
const map = this.trains.reduce((acc, train) => {
if (!train.timetableData || !train.locoType) return acc;
acc.set(
train.locoType,
acc.get(train.locoType) ? acc.get(train.locoType) + 1 : 1
);
return acc;
}, new Map());
const sorted = [...map.entries()]
.sort((a, b) => b[1] - a[1])
.filter((v, i) => i < 3);
return sorted;
}
get specialTrainCount(): [number, number] {
const twrList = this.trains.filter(
(train) => train.timetableData && train.timetableData.TWR
);
const skrList = this.trains.filter(
(train) => train.timetableData && train.timetableData.SKR
);
return [twrList.length, skrList.length];
}
}
return {
speedStats,
timetableStats,
categoryList,
locoList,
specialTrainCount,
};
},
});
</script>
<style lang="scss" scoped>
@@ -206,7 +248,7 @@ export default class TrainStats extends Vue {
transition: all 150ms ease-out;
}
&-enter,
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(30px);
+134 -88
View File
@@ -26,7 +26,7 @@
class="train-row"
v-for="(train, i) in computedTrains"
:key="i"
:ref="train.timetableData ? train.timetableData.timetableId : -1"
:ref="train.timetableData && (el => { elList[train.timetableData.timetableId] = el })"
>
<div
class="wrapper no-timetable"
@@ -319,117 +319,163 @@
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
import Train from "@/scripts/interfaces/Train";
import TrainStop from "@/scripts/interfaces/TrainStop";
import TrainSchedule from "@/components/TrainsView/TrainSchedule.vue";
import { DataStatus } from "@/scripts/enums/DataStatus";
import {
computed,
ComputedRef,
defineComponent,
onBeforeUpdate,
Ref,
ref,
} from "@vue/runtime-core";
import { useStore } from "@/store";
import { GETTERS } from "@/constants/storeConstants";
@Component({
components: { TrainSchedule },
})
export default class TrainTable extends Vue {
@Prop() computedTrains!: Train[];
@Prop() timetableDataStatus!: DataStatus;
@Prop() queryTrain!: string;
export default defineComponent({
components: {
TrainSchedule,
},
showedSchedule = 0;
props: {
computedTrains: {
type: Array as () => Train[],
required: true,
},
defaultLocoImage = require("@/assets/unknown.png");
queryTrain: {
type: String,
required: false,
},
},
ascSVG = require("@/assets/icon-arrow-asc.svg");
descSVG = require("@/assets/icon-arrow-desc.svg");
data: () => ({
showedSchedule: 0,
defaultLocoImage: require("@/assets/unknown.png"),
speedIcon: string = require("@/assets/icon-speed.svg");
massIcon: string = require("@/assets/icon-mass.svg");
lengthIcon: string = require("@/assets/icon-length.svg");
ascSVG: require("@/assets/icon-arrow-asc.svg"),
descSVG: require("@/assets/icon-arrow-desc.svg"),
distanceIcon: string = require("@/assets/icon-distance.svg");
sceneryIcon: string = require("@/assets/icon-scenery.svg");
signalIcon: string = require("@/assets/icon-signal.svg");
routeIcon: string = require("@/assets/icon-route.svg");
speedIcon: require("@/assets/icon-speed.svg"),
massIcon: require("@/assets/icon-mass.svg"),
lengthIcon: require("@/assets/icon-length.svg"),
get timetableLoaded() {
return this.timetableDataStatus == DataStatus.Loaded;
}
distanceIcon: require("@/assets/icon-distance.svg"),
sceneryIcon: require("@/assets/icon-scenery.svg"),
signalIcon: require("@/assets/icon-signal.svg"),
routeIcon: require("@/assets/icon-route.svg"),
}),
get timetableError() {
return this.timetableDataStatus == DataStatus.Error;
}
setup(props) {
const store = useStore();
const elList: Ref<HTMLElement[]> = ref([]);
get distanceLimitExceeded() {
return (
this.computedTrains.findIndex(
(train) =>
train.timetableData && train.timetableData.routeDistance > 200
) != -1
);
}
changeScheduleShowState(elementId: number) {
if (elementId < 0) return;
this.showedSchedule = this.showedSchedule == elementId ? 0 : elementId;
this.$nextTick(() => {
const currentEl: HTMLElement = this.$refs[elementId][0];
currentEl.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
onBeforeUpdate(() => {
elList.value.length = 0;
});
}
@Watch("queryTrain")
onSearchedTrainChange(trainNo: string) {
const timetableId = this.computedTrains.find(
(train) => train.trainNo == parseInt(trainNo)
)?.timetableData?.timetableId;
const timetableDataStatus: ComputedRef<DataStatus> = computed(
() => store.getters[GETTERS.timetableDataStatus]
);
if (!timetableId) return;
const timetableLoaded = computed(
() => timetableDataStatus.value === DataStatus.Loaded
);
const timetableError = computed(
() => timetableDataStatus.value === DataStatus.Error
);
this.changeScheduleShowState(timetableId);
}
const distanceLimitExceeded = computed(
() =>
props.computedTrains.findIndex(
({ timetableData }) =>
timetableData && timetableData.routeDistance > 200
) != -1
);
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
return {
timetableLoaded,
timetableError,
distanceLimitExceeded,
elList,
};
},
imageEl.src = this.defaultLocoImage;
}
methods: {
changeScheduleShowState(timetableId: number) {
if (timetableId < 0) return;
generateStopList(stops: any): string | undefined {
if (!stops) return "";
this.showedSchedule =
this.showedSchedule == timetableId ? 0 : timetableId;
return stops
.reduce((acc, stop: TrainStop, i) => {
if (stop.stopType.includes("ph"))
acc.push(
`<strong style='color:${
stop.confirmed ? "springgreen" : "white"
}'>${stop.stopName}</strong>`
);
else if (
i > 0 &&
i < stops.length - 1 &&
!stop.stopNameRAW.includes("po.") &&
!stop.stopNameRAW.includes("SBL")
)
acc.push(`<span style='color:${ stop.confirmed ? "springgreen" : "lightgray" }'>${stop.stopName}</span>`);
return acc;
}, [])
.join(" > ");
}
this.$nextTick(() => {
const currentEl: HTMLElement = this.elList[timetableId];
calculateCars(locoType: string, cars: string[]) {
if (cars.length == 0 && locoType.includes("EN")) return "EZT";
else if (cars.length == 0) return "LOK";
currentEl.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
});
},
return `${this.$t("trains.cars")}: <span style='color:gold'> ${cars.length}</span>`;
}
}
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.defaultLocoImage;
},
generateStopList(stops: TrainStop[]): string | undefined {
if (!stops) return "";
return stops
.reduce((acc: string[], stop: TrainStop, i: number) => {
if (stop.stopType.includes("ph"))
acc.push(
`<strong style='color:${
stop.confirmed ? "springgreen" : "white"
}'>${stop.stopName}</strong>`
);
else if (
i > 0 &&
i < stops.length - 1 &&
!stop.stopNameRAW.includes("po.") &&
!stop.stopNameRAW.includes("SBL")
)
acc.push(
`<span style='color:${
stop.confirmed ? "springgreen" : "lightgray"
}'>${stop.stopName}</span>`
);
return acc;
}, [])
.join(" > ");
},
calculateCars(locoType: string, cars: string[]) {
if (cars.length == 0 && locoType.includes("EN")) return "EZT";
else if (cars.length == 0) return "LOK";
return `${this.$t("trains.cars")}: <span style='color:gold'> ${
cars.length
}</span>`;
},
},
watch: {
queryTrain(trainNo: string) {
const timetableId = this.computedTrains.find(
(train) => train.trainNo == parseInt(trainNo)
)?.timetableData?.timetableId;
if (!timetableId) return;
this.changeScheduleShowState(timetableId);
},
},
});
</script>
<style lang="scss" scoped>
+23
View File
@@ -0,0 +1,23 @@
export const ACTIONS = {
synchronizeData: "synchronizeData",
fetchOnlineData: "fetchOnlineData",
fetchTimetableData: "fetchTimetableData"
}
export const MUTATIONS = {
SET_SCENERY_DATA: "SET_SCENERY_DATA",
SET_SCENERY_DATA_STATUS: "SET_SCENERY_DATA_STATUS",
SET_DATA_CONNECTION_STATUS: "SET_DATA_CONNECTION_STATUS",
UPDATE_STATIONS: "UPDATE_STATIONS",
UPDATE_TRAINS: "UPDATE_TRAINS",
UPDATE_TIMETABLES: "UPDATE_TIMETABLES"
}
export const GETTERS = {
stationList: "stationList",
trainList: "trainList",
allData: "allData",
timetableDataStatus: "timetableDataStatus",
sceneryDataStatus: "sceneryDataStatus",
dataStatus: "dataStatus"
}
-1
View File
@@ -1 +0,0 @@
{"success":true,"respCode":10,"message":[{"dispatcherId":13983,"dispatcherName":"Don350","dispatcherIsSupporter":true,"stationName":"Hetmanice","stationHash":"f7389af5","region":"eu","maxUsers":8,"currentUsers":0,"spawn":1,"lastSeen":1606776450625,"dispatcherExp":14,"nameFromHeader":"Hetmanice","spawnString":"He_Tm1,1,550,True,False,False,ALL","networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":22},{"dispatcherId":22620,"dispatcherName":"Calleman","dispatcherIsSupporter":true,"stationName":"Wielichowo","stationHash":"e4f61c89","region":"ru","maxUsers":29,"currentUsers":1,"spawn":0,"lastSeen":1606776448323,"dispatcherExp":11,"nameFromHeader":"Wielichowo","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0},{"dispatcherId":10048,"dispatcherName":"Sprytny_Zbys","dispatcherIsSupporter":false,"stationName":"Piaskowo","stationHash":"74a6c5ba","region":"eu","maxUsers":29,"currentUsers":0,"spawn":1,"lastSeen":1606776447873,"dispatcherExp":11,"nameFromHeader":"Piaskowo","spawnString":"Ps_G3,1,650,True,False,False,ALL;Ps_G4,1,650,True,False,False,ALL;Ps_N,-1,185,True,False,False,PAS","networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":23},{"dispatcherId":4310,"dispatcherName":"jedrek386","dispatcherIsSupporter":true,"stationName":"Niedoradz","stationHash":"2a7a048e","region":"eu","maxUsers":7,"currentUsers":0,"spawn":1,"lastSeen":1606776454351,"dispatcherExp":1,"nameFromHeader":"Niedoradz","spawnString":"Ne_H,1,200,True,False,False,H","networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":2},{"dispatcherId":10048,"dispatcherName":"Sprytny_Zbys","dispatcherIsSupporter":false,"stationName":"LCS Skrzynki","stationHash":"c9d5dc18","region":"eu","maxUsers":10,"currentUsers":1,"spawn":0,"lastSeen":1606776460199,"dispatcherExp":11,"nameFromHeader":"LCS Skrzynki","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0},{"dispatcherId":6975,"dispatcherName":"ziolek","dispatcherIsSupporter":false,"stationName":"Kcynia","stationHash":"64f1a3ba","region":"ru","maxUsers":9,"currentUsers":0,"spawn":0,"lastSeen":1606775846330,"dispatcherExp":4,"nameFromHeader":"Kcynia","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":0,"dispatcherRate":0},{"dispatcherId":7240,"dispatcherName":"ASkier","dispatcherIsSupporter":false,"stationName":"Głębce","stationHash":"3f7e4639","region":"ru","maxUsers":4,"currentUsers":0,"spawn":0,"lastSeen":1606775877861,"dispatcherExp":1,"nameFromHeader":"Głębce","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":0,"dispatcherRate":0},{"dispatcherId":16658,"dispatcherName":"NIEMIEC141","dispatcherIsSupporter":false,"stationName":"Buk 2018","stationHash":"4c831fc3","region":"eu","maxUsers":4,"currentUsers":0,"spawn":0,"lastSeen":1606775836686,"dispatcherExp":7,"nameFromHeader":"Buk 2018","spawnString":"NO_SPAWN","networkConnectionString":"2020.1.1Stable004","isOnline":0,"dispatcherRate":0},{"dispatcherId":6975,"dispatcherName":"ziolek","dispatcherIsSupporter":false,"stationName":"Kolsko","stationHash":"687dcf5b","region":"ru","maxUsers":6,"currentUsers":0,"spawn":0,"lastSeen":1606776448954,"dispatcherExp":4,"nameFromHeader":"Kolsko","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0},{"dispatcherId":7240,"dispatcherName":"ASkier","dispatcherIsSupporter":false,"stationName":"Karszynek","stationHash":"c0e19184","region":"ru","maxUsers":5,"currentUsers":0,"spawn":0,"lastSeen":1606776456733,"dispatcherExp":1,"nameFromHeader":"Karszynek","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0},{"dispatcherId":3619,"dispatcherName":"SzybkiWiewiór","dispatcherIsSupporter":false,"stationName":"Knot","stationHash":"cbaad885","region":"ru","maxUsers":14,"currentUsers":1,"spawn":0,"lastSeen":1606776460872,"dispatcherExp":7,"nameFromHeader":"Knot","spawnString":null,"networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0},{"dispatcherId":16658,"dispatcherName":"NIEMIEC141","dispatcherIsSupporter":false,"stationName":"Parzęczewo","stationHash":"325b1a74","region":"eu","maxUsers":29,"currentUsers":0,"spawn":1,"lastSeen":1606776392453,"dispatcherExp":7,"nameFromHeader":"Parzęczewo","spawnString":"LUZ1,-1,50,True,False,True,;LUZ2,-1,50,True,False,True,;POSP1,-1,200,True,False,False,;POSP2,-1,300,True,False,False,;POSP3,-1,200,True,False,False,;Pr_P20,1,200,True,True,False,;Pr_T28,1,750,True,False,False,;Pr_T34,1,750,True,False,False,;Pr_U38,-1,750,True,False,False,","networkConnectionString":"2020.1.1Stable004","isOnline":1,"dispatcherRate":0}]}
+27
View File
@@ -0,0 +1,27 @@
import { createI18n, LocaleMessages, VueMessageType } from 'vue-i18n'
/**
* Load locale messages
*
* The loaded `JSON` locale messages is pre-compiled by `@intlify/vue-i18n-loader`, which is integrated into `vue-cli-plugin-i18n`.
* See: https://github.com/intlify/vue-i18n-loader#rocket-i18n-resource-pre-compilation
*/
function loadLocaleMessages(): LocaleMessages<VueMessageType> {
const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i)
const messages: LocaleMessages<VueMessageType> = {}
locales.keys().forEach(key => {
const matched = key.match(/([A-Za-z0-9-_]+)\./i)
if (matched && matched.length > 1) {
const locale = matched[1]
messages[locale] = locales(key)
}
})
return messages
}
export default createI18n({
legacy: false,
locale: process.env.VUE_APP_I18N_LOCALE || 'en',
fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en',
messages: loadLocaleMessages()
})
+142 -142
View File
@@ -1,142 +1,142 @@
{
"app": {
"sceneries": "SCENERIES",
"trains": "TRAINS",
"journal": "JOURNAL",
"loading": "Loading data...",
"support": "Support the project"
},
"desc": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic line blockade system on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unavailable": "This scenery is unavailable",
"real": "This scenery is real"
},
"signals": {
"współczesna": "modern",
"mieszana": "mixed",
"kształtowa": "mechanical",
"historyczna": "historyczna"
},
"controls": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "SCS/SPK",
"SPE": "SPE",
"ręczne": "manual",
"ręczne+SPK": "manual + SPK",
"ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK",
"mechaniczne+SCS": "levers + SCS"
},
"status": {
"online": "UNTIL ",
"free": "FREE",
"ending": "ENDS SOON",
"not-signed": "NOT SIGNED IN",
"no-limit": "NO LIMIT",
"unavailable": "UNAVAILABLE",
"brb": "AFK",
"no-space": "NO SPACE"
},
"options": {
"filters": "FILTERS",
"donate": "DONATE"
},
"filters": {
"title": "STATION FILTER",
"default": "DEFAULT",
"not-default": "OTHER",
"real": "REAL",
"fictional": "FICTIONAL",
"SPK": "SPK",
"SCS": "SCS",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
"modern": "MODERN",
"semaphores": "SEMAPHORES",
"mixed": "MIXED",
"historical": "HISTORICAL",
"free": "FREE",
"occupied": "OCCUPIED",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"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"
},
"save": "SAVE FILTERS",
"reset": "RESET FILTERS",
"close": "CLOSE FILTERS"
},
"sceneries": {
"station": "Station",
"min-lvl": "Min. dispatcher <br> level",
"status": "Status",
"dispatcher": "Dispatcher",
"dispatcher-lvl": "Dispatcher <br> level",
"routes": "Routes <br> double | single",
"general": "General info",
"users": "Drivers online",
"spawns": "Spawns online",
"timetables": "Active timetables",
"no-stations": "No stations to show here!"
},
"trains": {
"no-trains": "Oops! No trains online!",
"loading": "Loading train data...",
"stats": "TRAFFIC STATISTICS",
"stats-speed": "TRAINS SPEED (MIN | AVG | MAX) [km/h]",
"stats-length": "TIMETABLES LENGTH (MIN | AVG | MAX) [km]",
"stats-categories": "TIMETABLE CATEGORIES",
"stats-special-twr": "HIGH RISK",
"stats-special-skr": "EXCEEDED STRUCT. GAUGE",
"stats-locos": "MOST COMMON UNITS",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"search-no": "Search for train no...",
"search-driver": "Search for driver...",
"detailed-timetable": "Detailed timetable for train no. ",
"via-title": "Via: ",
"no-timetable": "no current timetable",
"distance-exceeded": "Attention! Due to an internal error, timetables with route distance greater than 200km might be incorrect!",
"cars": "Cars"
},
"journal": {
"title": "SCENERY ACTIVITY JOURNAL",
"subtitle": "Shows all recent dispatchers on a selected scenery",
"disclaimer": "<b>This functionality is unfinished!</b> <br> Information shown here could be false or incorrect!",
"select": "Select a scenery"
},
"scenery": {
"users": "PLAYERS ONLINE",
"spawns": "OPEN SPAWNS",
"timetables": "ACTIVE TIMETABLES",
"no-timetables": "No active timetables!",
"no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!",
"return-btn": "Return to main site"
},
"timetables": {
"timetable-only": "Switch to timetable-only view",
"online": "At station",
"departed": "Dispatched",
"departed-away": "Departed",
"arriving": "En route",
"stopped": "Stopped",
"terminated": "Terminated",
"begins": "BEGINS HERE",
"terminates": "TERMINATES <br /> HERE"
}
}
{
"app": {
"sceneries": "SCENERIES",
"trains": "TRAINS",
"journal": "JOURNAL",
"loading": "Loading data...",
"support": "Support the project"
},
"desc": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic line blockade system on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unavailable": "This scenery is unavailable",
"real": "This scenery is real"
},
"signals": {
"współczesna": "modern",
"mieszana": "mixed",
"kształtowa": "mechanical",
"historyczna": "historyczna"
},
"controls": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "SCS/SPK",
"SPE": "SPE",
"ręczne": "manual",
"ręczne+SPK": "manual + SPK",
"ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK",
"mechaniczne+SCS": "levers + SCS"
},
"status": {
"online": "UNTIL ",
"free": "FREE",
"ending": "ENDS SOON",
"not-signed": "NOT SIGNED IN",
"no-limit": "NO LIMIT",
"unavailable": "UNAVAILABLE",
"brb": "AFK",
"no-space": "NO SPACE"
},
"options": {
"filters": "FILTERS",
"donate": "DONATE"
},
"filters": {
"title": "STATION FILTER",
"default": "DEFAULT",
"not-default": "OTHER",
"real": "REAL",
"fictional": "FICTIONAL",
"SPK": "SPK",
"SCS": "SCS",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
"modern": "MODERN",
"semaphores": "SEMAPHORES",
"mixed": "MIXED",
"historical": "HISTORICAL",
"free": "FREE",
"occupied": "OCCUPIED",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"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"
},
"save": "SAVE FILTERS",
"reset": "RESET FILTERS",
"close": "CLOSE FILTERS"
},
"sceneries": {
"station": "Station",
"min-lvl": "Min. dispatcher\nlevel",
"status": "Status",
"dispatcher": "Dispatcher",
"dispatcher-lvl": "Dispatcher\nlevel",
"routes": "Routes\ndouble / single",
"general": "General info",
"users": "Drivers online",
"spawns": "Spawns online",
"timetables": "Active timetables",
"no-stations": "No stations to show here!"
},
"trains": {
"no-trains": "Oops! No trains online!",
"loading": "Loading train data...",
"stats": "TRAFFIC STATISTICS",
"stats-speed": "TRAINS SPEED (MIN, AVG, MAX) [km/h]",
"stats-length": "TIMETABLES LENGTH (MIN, AVG, MAX) [km]",
"stats-categories": "TIMETABLE CATEGORIES",
"stats-special-twr": "HIGH RISK",
"stats-special-skr": "EXCEEDED STRUCT. GAUGE",
"stats-locos": "MOST COMMON UNITS",
"option-mass": "mass",
"option-speed": "speed",
"option-length": "length",
"option-distance": "distance",
"option-timetable": "train no.",
"search-no": "Search for train no...",
"search-driver": "Search for driver...",
"detailed-timetable": "Detailed timetable for train no. ",
"via-title": "Via: ",
"no-timetable": "no current timetable",
"distance-exceeded": "Attention! Due to an internal error, timetables with route distance greater than 200km might be incorrect!",
"cars": "Cars"
},
"journal": {
"title": "SCENERY ACTIVITY JOURNAL",
"subtitle": "Shows all recent dispatchers on a selected scenery",
"disclaimer": "<b>This functionality is unfinished!</b> \n Information shown here could be false or incorrect!",
"select": "Select a scenery"
},
"scenery": {
"users": "PLAYERS ONLINE",
"spawns": "OPEN SPAWNS",
"timetables": "ACTIVE TIMETABLES",
"no-timetables": "No active timetables!",
"no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!",
"return-btn": "Return to main site"
},
"timetables": {
"timetable-only": "Switch to timetable-only view",
"online": "At station",
"departed": "Dispatched",
"departed-away": "Departed",
"arriving": "En route",
"stopped": "Stopped",
"terminated": "Terminated",
"begins": "BEGINS HERE",
"terminates": "TERMINATES\nHERE"
}
}
+142 -142
View File
@@ -1,142 +1,142 @@
{
"app": {
"sceneries": "SCENERIE",
"trains": "POCIĄGI",
"journal": "DZIENNIK",
"loading": "Pobieranie danych...",
"support": "Wspomóż projekt"
},
"desc": {
"control-type": "Sterowanie: ",
"signals-type": "Sygnalizacja: ",
"SBL": "Sceneria posiada SBL na szlakach: ",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"real": "Sceneria realna"
},
"signals": {
"współczesna": "współczesna",
"mieszana": "mieszana",
"kształtowa": "kształtowa",
"historyczna": "historyczna"
},
"controls": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "SCS/SPK",
"SPE": "SPE",
"ręczne": "ręczne",
"ręczne+SPK": "ręczne + SPK",
"ręczne+SCS": "ręczne + SCS",
"mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne + SPK",
"mechaniczne+SCS": "mechaniczne + SCS"
},
"status": {
"online": "DO ",
"free": "WOLNA",
"ending": "KOŃCZY",
"not-signed": "NIEZALOGOWANY",
"no-limit": "BEZ LIMITU",
"unavailable": "NIEDOSTĘPNY",
"brb": "Z/W",
"no-space": "BRAK MIEJSCA"
},
"options": {
"filters": "FILTRY",
"donate": "WESPRZYJ"
},
"filters": {
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"SPK": "SPK",
"SCS": "SCS",
"SPE": "SPE",
"manual": "RĘCZNE",
"mechanical": "MECHANICZNE",
"modern": "WSPÓŁCZESNA",
"semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA",
"historical": "HISTORYCZNA",
"free": "WOLNA",
"occupied": "ZAJĘTA",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"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)"
},
"save": "ZAPISZ FILTRY",
"reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY"
},
"sceneries": {
"station": "Stacja",
"min-lvl": "Min. poziom <br/> dyżurnego",
"status": "Status",
"dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom <br> dyżurnego",
"routes": "Szlaki <br> 2tor | 1tor",
"general": "Informacje <br> ogólne",
"users": "Maszyniści online",
"spawns": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!"
},
"trains": {
"no-trains": "Brak pociągów online!",
"loading": "Pobieranie danych o pociągach...",
"stats": "STATYSTYKI RUCHU",
"stats-speed": "PRĘDKOŚCI POCIĄGÓW (MIN | ŚR | MAX) [km/h]",
"stats-length": "DŁUGOŚCI ROZKŁADÓW (MIN | ŚR | MAX) [km]",
"stats-categories": "KATEGORIE RJ",
"stats-special-twr": "WYSOKIEGO RYZYKA",
"stats-special-skr": "PRZEKROCZONA SKRAJNIA",
"stats-locos": "NAJCZĘSTSZE JEDNOSTKI",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "numer pociągu",
"search-no": "Szukaj nr pociągu...",
"search-driver": "Szukaj maszynisty...",
"detailed-timetable": "Szczegółowy rozkład jazdy pociągu ",
"via-title": "Przez: ",
"no-timetable": "brak rozkładu jazdy",
"distance-exceeded": "Uwaga! Z powodu wewnętrznego błędu serwera TD2, rozkłady jazdy o kilometrażu powyżej 200km mogą być niepoprawne!",
"cars": "Wagony"
},
"journal": {
"title": "DZIENNIK AKTYWNOŚCI SCENERII",
"subtitle": "Pokazuje dyżurnych, którzy ostatnio byli aktywni na wybranej scenerii",
"disclaimer": "<b>Ta funkcjonalność jest w testach beta!</b> <br> Informacje pokazywane na ekranie mogą znikać, a ich zawartość może być fałszywa!",
"select": "Wybierz scenerię"
},
"scenery": {
"users": "GRACZE ONLINE",
"spawns": "OTWARTE SPAWNY",
"timetables": "AKTYWNE ROZKŁADY JAZDY",
"no-timetables": "Brak aktywnych rozkładów!",
"no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "Wróć na stronę główną"
},
"timetables": {
"timetable-only": "Wyodrębnij rozkłady jazdy",
"online": "Na stacji",
"departed": "Odprawiony",
"departed-away": "Odjechał",
"arriving": "W drodze",
"stopped": "Postój",
"terminated": "Skończył bieg",
"begins": "ROZPOCZYNA <br /> BIEG",
"terminates": "KOŃCZY BIEG"
}
}
{
"app": {
"sceneries": "SCENERIE",
"trains": "POCIĄGI",
"journal": "DZIENNIK",
"loading": "Pobieranie danych...",
"support": "Wspomóż projekt"
},
"desc": {
"control-type": "Sterowanie: ",
"signals-type": "Sygnalizacja: ",
"SBL": "Sceneria posiada SBL na szlakach: ",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"real": "Sceneria realna"
},
"signals": {
"współczesna": "współczesna",
"mieszana": "mieszana",
"kształtowa": "kształtowa",
"historyczna": "historyczna"
},
"controls": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "SCS/SPK",
"SPE": "SPE",
"ręczne": "ręczne",
"ręczne+SPK": "ręczne + SPK",
"ręczne+SCS": "ręczne + SCS",
"mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne + SPK",
"mechaniczne+SCS": "mechaniczne + SCS"
},
"status": {
"online": "DO ",
"free": "WOLNA",
"ending": "KOŃCZY",
"not-signed": "NIEZALOGOWANY",
"no-limit": "BEZ LIMITU",
"unavailable": "NIEDOSTĘPNY",
"brb": "Z/W",
"no-space": "BRAK MIEJSCA"
},
"options": {
"filters": "FILTRY",
"donate": "WESPRZYJ"
},
"filters": {
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"SPK": "SPK",
"SCS": "SCS",
"SPE": "SPE",
"manual": "RĘCZNE",
"mechanical": "MECHANICZNE",
"modern": "WSPÓŁCZESNA",
"semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA",
"historical": "HISTORYCZNA",
"free": "WOLNA",
"occupied": "ZAJĘTA",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"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)"
},
"save": "ZAPISZ FILTRY",
"reset": "RESETUJ FILTRY",
"close": "ZAMKNIJ FILTRY"
},
"sceneries": {
"station": "Stacja",
"min-lvl": "Min. poziom\ndyżurnego",
"status": "Status",
"dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor",
"general": "Informacje\nogólne",
"users": "Maszyniści online",
"spawns": "Otwarte spawny",
"timetables": "Aktywne rozkłady jazdy",
"no-stations": "Brak stacji do wyświetlenia!"
},
"trains": {
"no-trains": "Brak pociągów online!",
"loading": "Pobieranie danych o pociągach...",
"stats": "STATYSTYKI RUCHU",
"stats-speed": "PRĘDKOŚCI POCIĄGÓW (MIN, ŚR, MAX) [km/h]",
"stats-length": "DŁUGOŚCI ROZKŁADÓW (MIN, ŚR, MAX) [km]",
"stats-categories": "KATEGORIE RJ",
"stats-special-twr": "WYSOKIEGO RYZYKA",
"stats-special-skr": "PRZEKROCZONA SKRAJNIA",
"stats-locos": "NAJCZĘSTSZE JEDNOSTKI",
"option-mass": "masa",
"option-speed": "prędkość",
"option-length": "długość",
"option-distance": "kilometraż",
"option-timetable": "numer pociągu",
"search-no": "Szukaj nr pociągu...",
"search-driver": "Szukaj maszynisty...",
"detailed-timetable": "Szczegółowy rozkład jazdy pociągu ",
"via-title": "Przez: ",
"no-timetable": "brak rozkładu jazdy",
"distance-exceeded": "Uwaga! Z powodu wewnętrznego błędu serwera TD2, rozkłady jazdy o kilometrażu powyżej 200km mogą być niepoprawne!",
"cars": "Wagony"
},
"journal": {
"title": "DZIENNIK AKTYWNOŚCI SCENERII",
"subtitle": "Pokazuje dyżurnych, którzy ostatnio byli aktywni na wybranej scenerii",
"disclaimer": "<b>Ta funkcjonalność jest w testach beta!</b> \n Informacje pokazywane na ekranie mogą znikać, a ich zawartość może być fałszywa!",
"select": "Wybierz scenerię"
},
"scenery": {
"users": "GRACZE ONLINE",
"spawns": "OTWARTE SPAWNY",
"timetables": "AKTYWNE ROZKŁADY JAZDY",
"no-timetables": "Brak aktywnych rozkładów!",
"no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "Wróć na stronę główną"
},
"timetables": {
"timetable-only": "Wyodrębnij rozkłady jazdy",
"online": "Na stacji",
"departed": "Odprawiony",
"departed-away": "Odjechał",
"arriving": "W drodze",
"stopped": "Postój",
"terminated": "Skończył bieg",
"begins": "ROZPOCZYNA\nBIEG",
"terminates": "KOŃCZY BIEG"
}
}
+15 -18
View File
@@ -1,28 +1,25 @@
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import VueI18n from 'vue-i18n';
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { store, key } from './store'
import enLang from '@/lang/en.json';
import plLang from '@/lang/pl.json';
import enLang from '@/locales/en.json';
import plLang from '@/locales/pl.json';
Vue.use(VueI18n);
import { createI18n } from 'vue-i18n'
const i18n = new VueI18n({
const i18n = createI18n({
locale: 'pl',
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang,
},
});
enableLegacy: false
})
Vue.config.productionTip = false;
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app');
createApp(App)
.use(store, key)
.use(router)
.use(i18n)
.mount('#app')
View File
+40 -41
View File
@@ -1,47 +1,46 @@
import Vue from 'vue';
import Component from 'vue-class-component';
import { defineComponent } from 'vue';
// You can declare mixins as the same style as components.
@Component
export default class styleMixin extends Vue {
calculateExpStyle(exp: string | number, isSupporter: boolean = false): string {
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
export default defineComponent({
methods: {
calculateExpStyle(exp: string | number, isSupporter = false): string {
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
const boxShadow = isSupporter ? `box-shadow: 0 0 10px 2px ${bgColor};` : '';
return `background-color: ${bgColor}; color: ${fontColor}; ${boxShadow}`;
},
const fontColor = exp > 15 || exp == -1 ? 'white' : 'black';
const boxShadow = isSupporter ? `0 0 10px 2px ${bgColor}` : '';
statusClasses(occupiedTo: string) {
let className = '';
return `backgroundColor: ${bgColor}; color: ${fontColor}; box-shadow: ${boxShadow};`;
}
switch (occupiedTo) {
case 'WOLNA':
className = 'free';
break;
case 'KOŃCZY':
className = 'ending';
break;
case 'NIEZALOGOWANY':
className = 'not-signed';
break;
case 'BEZ LIMITU':
className = 'no-limit';
break;
case 'NIEDOSTĘPNY':
className = 'unavailable';
break;
case 'Z/W':
className = 'brb';
break;
case 'BRAK MIEJSCA':
className = 'no-space';
break;
default:
break;
}
statusClasses(occupiedTo: string) {
let className = '';
switch (occupiedTo) {
case 'WOLNA':
className = 'free';
break;
case 'KOŃCZY':
className = 'ending';
break;
case 'NIEZALOGOWANY':
className = 'not-signed';
break;
case 'BEZ LIMITU':
className = 'no-limit';
break;
case 'NIEDOSTĘPNY':
className = 'unavailable';
break;
case 'Z/W':
className = 'brb';
break;
case 'BRAK MIEJSCA':
className = 'no-space';
break;
default:
break;
return className;
}
return className;
}
}
})
+14 -23
View File
@@ -1,22 +1,19 @@
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import StationsView from "../views/StationsView.vue";
import TrainsView from "../views/TrainsView.vue";
import StationsView from "@/views/StationsView.vue";
Vue.use(VueRouter);
const routes: Array<RouteConfig> = [
const routes: Array<RouteRecordRaw> = [
{
path: "/",
name: "StationsView",
path: '/',
name: 'StationsView',
component: StationsView
},
{
path: "/trains",
name: "TrainsView",
component: TrainsView,
props: true
component: () => import("@/views/TrainsView.vue"),
props: true,
},
{
path: "/scenery",
@@ -24,17 +21,11 @@ const routes: Array<RouteConfig> = [
component: () => import("@/views/SceneryView.vue"),
props: true
},
{
path: "/history",
name: "HistoryView",
component: () => import("@/views/HistoryView.vue")
}
];
]
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router;
export default router
+5 -3
View File
@@ -1,11 +1,12 @@
export default interface Filter {
[key: string]: (boolean | number),
default: boolean;
notDefault: boolean;
real: boolean;
fictional: boolean;
SPK: boolean;
SCS: boolean;
SPE: boolean;
"SPK": boolean;
"SCS": boolean;
"SPE": boolean;
ręczne: boolean;
mechaniczne: boolean;
współczesna: boolean;
@@ -23,4 +24,5 @@ export default interface Filter {
free: boolean;
occupied: boolean;
ending: boolean;
nonPublic: boolean;
}
+1
View File
@@ -54,6 +54,7 @@ export default interface Station {
stationTrains: {
driverName: number;
trainNo: number;
stopStatus?: string;
}[];
scheduledTrains: ScheduledTrain[];
+17
View File
@@ -0,0 +1,17 @@
import TrainStop from "./TrainStop";
export default interface Timetable {
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
timetableId: number;
category: string;
route: string;
TWR: boolean;
SKR: boolean;
routeDistance: number;
followingStops: TrainStop[];
followingSceneries: string[];
}
@@ -0,0 +1,18 @@
export default interface StationAPIData {
dispatcherId: number;
dispatcherName: string;
dispatcherIsSupporter: boolean;
stationName: string;
stationHash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawn: number;
lastSeen: number;
dispatcherExp: number;
nameFromHeader: string;
spawnString: string;
networkConnectionString: string;
isOnline: number;
dispatcherRate: number;
}
@@ -0,0 +1,34 @@
export default interface TimetableAPIData {
trainInfo: {
timetableId: number;
trainNo: number;
trainCategoryCode: string;
driverId: number;
driverName: string;
route: string;
twr: boolean;
skr: boolean;
sceneries: string[];
};
stopPoints: {
arrivalLine: string | null;
arrivalTime: string | null;
arrivalDelay: number;
arrivalRealTime: string | null;
pointDistance: number;
pointName: string;
pointNameRAW: string;
entryId: number;
pointId: number;
comments: string | null;
confirmed: boolean;
isStopped: boolean;
pointStopTime: number | null;
pointStopType: string;
departureLine: string | null;
departureTime: string | null;
departureDelay: number;
departureRealTime: string | null;
}[];
}
@@ -0,0 +1,19 @@
import StationAPIData from "./StationAPIData";
export default interface TrainAPIData {
trainNo: number;
driverId: number;
driverName: string;
driverIsSupporter: boolean;
station: StationAPIData;
dataSignal: string;
dataSceneryConnection: string;
dataDistance: number;
dataCon: string;
dataSpeed: number;
dataMass: number;
dataLength: number;
region: string;
isOnline: boolean;
lastSeen: number;
}
@@ -122,6 +122,7 @@ export default class StationFilterManager {
free: true,
occupied: false,
ending: false,
nonPublic: false
};
private filters: Filter = { ...this.filterInitStates };
+6
View File
@@ -0,0 +1,6 @@
export const URLs = {
stations: "https://api.td2.info.pl:9640/?method=getStationsOnline",
trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
dispatchers: "https://api.td2.info.pl:9640/?method=readFromSWDR&value=getDispatcherStatusList%3B1",
getTimetableURL: (trainNo: string | number) => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3Beu`
};
+11 -23
View File
@@ -1,10 +1,9 @@
import Station from "../interfaces/Station";
import TrainStop from "../interfaces/TrainStop";
const timetableURL = (trainNo: number) => `https://api.td2.info.pl:9640/?method=readFromSWDR&value=getTimetable%3B${trainNo}%3Beu`;
const getLocoURL = (locoType: string) => `https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes("EN") ? locoType + "rb" : locoType}.png`;
export const getLocoURL = (locoType: string): string => (`https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes("EN") ? locoType + "rb" : locoType}.png`)
const getStatusID = (stationStatus: any) => {
export const getStatusID = (stationStatus: any): string => {
if (!stationStatus) return "not-signed";
const statusCode = stationStatus[2];
@@ -33,7 +32,7 @@ const getStatusID = (stationStatus: any) => {
return "unavailable";
};
const getStatusTimestamp = (stationStatus: any) => {
export const getStatusTimestamp = (stationStatus: any): number => {
if (!stationStatus) return -2;
const statusCode = stationStatus[2];
@@ -56,7 +55,7 @@ const getStatusTimestamp = (stationStatus: any) => {
return -1;
};
const parseSpawns = (spawnString: string) => {
export const parseSpawns = (spawnString: string) => {
if (!spawnString) return [];
if (spawnString === "NO_SPAWN") return [];
@@ -69,9 +68,9 @@ const parseSpawns = (spawnString: string) => {
});
};
const getTimestamp = (date: string | null) => (date ? new Date(date).getTime() : 0);
export const getTimestamp = (date: string | null): number => (date ? new Date(date).getTime() : 0);
const timestampToString = (timestamp: number | null) =>
export const timestampToString = (timestamp: number | null): string =>
timestamp
? new Date(timestamp).toLocaleTimeString("pl-PL", {
hour: "2-digit",
@@ -79,10 +78,10 @@ const timestampToString = (timestamp: number | null) =>
})
: "";
const getTrainStopStatus = (stopInfo: TrainStop, timetableData: { currentStationName: string }, station: Station) => {
let stopStatus: string = "",
stopLabel: string = "",
stopStatusID: number = -1;
export const getTrainStopStatus = (stopInfo: TrainStop, timetableData: { currentStationName: string }, station: Station) => {
let stopStatus = "",
stopLabel = "",
stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = "terminated";
@@ -111,15 +110,4 @@ const getTrainStopStatus = (stopInfo: TrainStop, timetableData: { currentStation
}
return { stopStatus, stopLabel, stopStatusID };
};
export default {
timetableURL,
getLocoURL,
getStatusID,
parseSpawns,
getTimestamp,
timestampToString,
getStatusTimestamp,
getTrainStopStatus
};
};
-13
View File
@@ -1,13 +0,0 @@
import Vue, { VNode } from 'vue'
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
+4 -2
View File
@@ -1,4 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import Vue from 'vue'
export default Vue
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
+455 -9
View File
@@ -1,12 +1,458 @@
import Vue from 'vue';
import Vuex from 'vuex';
/* eslint-disable */
import Store from '@/store/store';
import { InjectionKey } from 'vue'
import { createStore, useStore as baseUseStore, Store } from 'vuex'
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
Store,
import axios from "axios";
import JSONStationData from "@/data/stationData.json";
import Station from "@/scripts/interfaces/Station";
import Train from "@/scripts/interfaces/Train";
import TrainStop from "@/scripts/interfaces/TrainStop";
import { StoreData } from "@/scripts/interfaces/StoreData";
import TimetableAPIData from '@/scripts/interfaces/api/TimetableAPIData';
import StationAPIData from '@/scripts/interfaces/api/StationAPIData';
import TrainAPIData from '@/scripts/interfaces/api/TrainAPIData';
import Timetable from '@/scripts/interfaces/Timetable';
import { ACTIONS, MUTATIONS } from "@/constants/storeConstants";
import { DataStatus } from "@/scripts/enums/DataStatus";
import { getLocoURL, getStatusID, getStatusTimestamp, getTimestamp, getTrainStopStatus, parseSpawns, timestampToString } from "@/scripts/utils/storeUtils";
import { URLs } from '@/scripts/utils/apiURLs';
export interface State {
stationList: Station[],
trainList: Train[],
trainCount: number;
stationCount: number;
dataConnectionStatus: DataStatus;
sceneryDataStatus: DataStatus;
timetableDataStatus: DataStatus;
listenerLaunched: boolean;
}
export const key: InjectionKey<Store<State>> = Symbol()
export const store = createStore<State>({
state: () => ({
stationList: [],
trainList: [],
trainCount: 0,
stationCount: 0,
dataConnectionStatus: DataStatus.Loading,
sceneryDataStatus: DataStatus.Loading,
timetableDataStatus: DataStatus.Loading,
listenerLaunched: false
}),
getters: {
stationList: (state) => state.stationList,
trainList: (state) => state.trainList,
allData: (state): StoreData => ({
stationList: state.stationList,
trainList: state.trainList,
activeTrainCount: state.trainCount,
activeStationCount: state.stationCount,
dataConnectionStatus: state.dataConnectionStatus,
timetableDataStatus: state.timetableDataStatus
}),
timetableDataStatus: (state): DataStatus => state.timetableDataStatus,
sceneryDataStatus: (state): DataStatus => state.sceneryDataStatus,
dataStatus: (state): DataStatus => state.dataConnectionStatus
},
});
export default store;
actions: {
async synchronizeData({ commit, dispatch, state }) {
if(state.listenerLaunched) return;
commit(MUTATIONS.SET_SCENERY_DATA);
commit(MUTATIONS.SET_SCENERY_DATA_STATUS, DataStatus.Loaded);
dispatch(ACTIONS.fetchOnlineData);
setInterval(() => dispatch(ACTIONS.fetchOnlineData), 30000);
},
async fetchOnlineData({ commit, dispatch }) {
Promise.all([axios.get(URLs.stations), axios.get(URLs.trains), axios.get(URLs.dispatchers)])
.then(async response => {
const onlineStationsData: StationAPIData[] = response[0].data.message;
const onlineTrainsData: TrainAPIData[] = await response[1].data.message;
const onlineDispatchersData: string[][] = await response[2].data.message;
const updatedStationList = onlineStationsData.reduce((acc, station) => {
if (station.region !== "eu" || !station.isOnline) return acc;
const stationStatus = onlineDispatchersData.find((status: string[]) => status[0] == station.stationHash && status[1] == "eu");
const statusTimestamp = getStatusTimestamp(stationStatus);
const statusID = getStatusID(stationStatus);
const stationTrains = onlineTrainsData
.filter(train => train.region === "eu" && train.isOnline && train.station.stationName === station.stationName)
.map(train => ({ driverName: train.driverName, trainNo: train.trainNo }));
acc.push({
stationName: station.stationName,
stationHash: station.stationHash,
maxUsers: station.maxUsers,
currentUsers: station.currentUsers,
spawns: parseSpawns(station.spawnString),
dispatcherName: station.dispatcherName,
dispatcherRate: station.dispatcherRate,
dispatcherId: station.dispatcherId,
dispatcherExp: station.dispatcherExp,
dispatcherIsSupporter: station.dispatcherIsSupporter,
stationTrains,
statusTimestamp,
statusID,
statusTimeString: timestampToString(statusTimestamp)
});
return acc;
}, [] as any);
const updatedTrainList = await Promise.all(
onlineTrainsData
.filter(train => train.region === "eu")
.map(async train => {
const locoType = train.dataCon.split(";") ? train.dataCon.split(";")[0] : train.dataCon;
return {
trainNo: train.trainNo,
mass: train.dataMass,
length: train.dataLength,
speed: train.dataSpeed,
distance: train.dataDistance,
signal: train.dataSignal,
online: train.isOnline,
driverId: train.driverId,
driverName: train.driverName,
currentStationName: train.station.stationName,
currentStationHash: train.station.stationHash,
connectedTrack: train.dataSceneryConnection,
locoType,
locoURL: getLocoURL(locoType),
cars: train.dataCon.split(";").filter((train, i) => i > 0) || []
};
})
);
// Pass reduced lists to mutations
commit(MUTATIONS.UPDATE_STATIONS, updatedStationList);
commit(MUTATIONS.UPDATE_TRAINS, updatedTrainList);
dispatch(ACTIONS.fetchTimetableData);
})
.catch(() => {
commit(MUTATIONS.SET_DATA_CONNECTION_STATUS, DataStatus.Error);
});
},
async fetchTimetableData({ commit }) {
const reducedList = this.state.trainList.reduce(async (acc: Promise<Timetable[]>, train: Train) => {
const timetable: TimetableAPIData = await (await axios.get(URLs.getTimetableURL(train.trainNo))).data.message;
const trainInfo = timetable.trainInfo;
if (!timetable || !trainInfo) return acc;
const followingStops: TrainStop[] = timetable.stopPoints.reduce((stopsAcc: TrainStop[], point) => {
if (point.pointNameRAW.toLowerCase().includes("sbl")) return stopsAcc;
const arrivalTimestamp = getTimestamp(point.arrivalTime);
const arrivalRealTimestamp = getTimestamp(point.arrivalRealTime);
const departureTimestamp = getTimestamp(point.departureTime);
const departureRealTimestamp = getTimestamp(point.departureRealTime);
stopsAcc.push({
stopName: point.pointName,
stopNameRAW: point.pointNameRAW,
stopType: point.pointStopType,
stopDistance: point.pointDistance,
mainStop: point.pointName.includes("strong"),
arrivalLine: point.arrivalLine,
arrivalTimeString: timestampToString(arrivalTimestamp),
arrivalTimestamp: arrivalTimestamp,
arrivalRealTimeString: timestampToString(arrivalRealTimestamp),
arrivalRealTimestamp: arrivalRealTimestamp,
arrivalDelay: point.arrivalDelay,
departureLine: point.departureLine,
departureTimeString: timestampToString(departureTimestamp),
departureTimestamp: departureTimestamp,
departureRealTimeString: timestampToString(departureRealTimestamp),
departureRealTimestamp: departureRealTimestamp,
departureDelay: point.departureDelay,
beginsHere: arrivalTimestamp == 0,
terminatesHere: departureTimestamp == 0,
confirmed: point.confirmed,
stopped: point.isStopped,
stopTime: point.pointStopTime
});
return stopsAcc;
}, []);
(await acc).push({
trainNo: train.trainNo,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
timetableId: trainInfo.timetableId,
category: trainInfo.trainCategoryCode,
route: trainInfo.route,
TWR: trainInfo.twr,
SKR: trainInfo.skr,
routeDistance: timetable.stopPoints[timetable.stopPoints.length - 1].pointDistance,
followingStops,
followingSceneries: trainInfo.sceneries
});
return acc;
}, Promise.resolve([]));
commit(MUTATIONS.UPDATE_TIMETABLES, (await reducedList));
}
},
mutations: {
SET_SCENERY_DATA(state) {
state.stationList = JSONStationData.map(station => ({
stationName: station[0] as string,
stationURL: station[1] as string,
stationLines: station[2] as string,
stationProject: station[3] as string,
reqLevel: station[4] as string,
supportersOnly: station[5] == "TAK",
signalType: station[6] as string,
controlType: station[7] as string,
SBL: station[8] as string,
TWB: station[9] as string,
routes: {
oneWay: {
catenary: station[10] as number,
noCatenary: station[11] as number
},
twoWay: {
catenary: station[12] as number,
noCatenary: station[13] as number
}
},
checkpoints: station[14] ? (station[14] as string[]).map(sub => ({ checkpointName: sub, scheduledTrains: [] })) : null,
stops: station[15] as string[],
default: station[16] as boolean,
nonPublic: station[17] as boolean,
unavailable: station[18] as boolean,
stationHash: "",
maxUsers: 0,
currentUsers: 0,
dispatcherName: "",
dispatcherRate: 0,
dispatcherExp: -1,
dispatcherId: 0,
dispatcherIsSupporter: false,
online: false,
statusTimestamp: -3,
statusID: "free",
statusTimeString: "",
stationTrains: [],
scheduledTrains: [],
spawns: []
}));
},
SET_SCENERY_DATA_STATUS(state, status: DataStatus) {
state.sceneryDataStatus = status;
},
SET_DATA_CONNECTION_STATUS(state, status: DataStatus) {
state.dataConnectionStatus = status;
},
UPDATE_STATIONS(state, updatedStationList: any[]) {
state.stationList = state.stationList.reduce((acc: Station[], station) => {
const onlineStationData = updatedStationList.find(updatedStation => updatedStation.stationName === station.stationName);
const listedStationData = JSONStationData.find(data => data[0] === station.stationName);
if (onlineStationData)
acc.push({
...station,
...onlineStationData,
online: true
});
else if (listedStationData)
acc.push({
...station,
stationProject: "",
stationHash: "",
maxUsers: 0,
currentUsers: 0,
dispatcherName: "",
dispatcherRate: 0,
dispatcherExp: -1,
dispatcherId: 0,
dispatcherIsSupporter: false,
online: false,
statusID: "free",
statusTimestamp: -3,
statusTimeString: "",
stationTrains: [],
scheduledTrains: [],
checkpoints: null
});
return acc;
}, [] as Station[]);
updatedStationList
.filter(uStation => !state.stationList.some(station => uStation.stationName === station.stationName))
.forEach(uStation => {
state.stationList.push({
...uStation,
scheduledTrains: [],
stationTrains: uStation.stationTrains || [],
subStations: [],
online: true,
reqLevel: "-1",
nonPublic: true
});
});
state.stationCount = state.stationList.filter(station => station.online).length;
state.dataConnectionStatus = DataStatus.Loaded;
},
UPDATE_TRAINS(state, updatedTrainList: any[]) {
state.trainList = updatedTrainList.reduce((acc, updatedTrain) => {
const trainData = state.trainList.find(train => train.trainNo === updatedTrain.trainNo);
if (trainData) acc.push({ ...trainData, ...updatedTrain });
else acc.push({ ...updatedTrain });
return acc;
}, [] as Train[]);
state.trainCount = state.trainList.filter(train => train.online).length;
state.dataConnectionStatus = DataStatus.Loaded;
},
UPDATE_TIMETABLES(state, timetableList: Timetable[]) {
state.stationList = state.stationList.map(station => {
const stationName = station.stationName.toLowerCase();
const scheduledTrains: Station["scheduledTrains"] = timetableList.reduce((acc: Station["scheduledTrains"], timetable: Timetable) => {
if (!timetable.followingSceneries.includes(station.stationHash)) return acc;
const stopInfoIndex = timetable.followingStops.findIndex(stop => {
const stopName = stop.stopNameRAW.toLowerCase();
if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes("po.") && !stop.stopName.includes("podg.")) return true;
if (stationName.includes(stopName) && !stop.stopName.includes("po.") && !stop.stopName.includes("podg.")) return true;
if (stopName.includes("podg.") && stopName.split(", podg.")[0] && stationName.includes(stopName.split(", podg.")[0])) return true;
if (station.stops && station.stops.includes(stop.stopNameRAW)) return true;
return false;
});
if (stopInfoIndex == -1) return acc;
const trainStop = timetable.followingStops[stopInfoIndex];
const trainStopStatus = getTrainStopStatus(trainStop, timetable, station);
acc.push({
trainNo: timetable.trainNo,
driverName: timetable.driverName,
driverId: timetable.driverId,
currentStationName: timetable.currentStationName,
currentStationHash: timetable.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nearestStop: "",
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID
});
return acc;
}, []);
if (station.checkpoints) {
station.checkpoints.forEach(cp => (cp.scheduledTrains.length = 0));
for (const checkpoint of station.checkpoints) {
timetableList.forEach(timetable => {
timetable.followingStops
.filter(trainStop => trainStop.stopNameRAW.toLowerCase() === checkpoint.checkpointName.toLowerCase())
.forEach(trainStop => {
const trainStopStatus = getTrainStopStatus(trainStop, timetable, station);
checkpoint.scheduledTrains.push({
trainNo: timetable.trainNo,
driverName: timetable.driverName,
driverId: timetable.driverId,
currentStationName: timetable.currentStationName,
currentStationHash: timetable.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nearestStop: "",
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID
});
});
});
}
}
return { ...station, scheduledTrains };
});
state.trainList = state.trainList.reduce((acc, train) => {
const timetableData = timetableList.find(data => data && data.trainNo === train.trainNo);
const trainStopData = state.stationList
.find(station => station.stationName === train.currentStationName)
?.scheduledTrains.find(stationTrain => stationTrain.trainNo === train.trainNo);
acc.push({ ...train, timetableData, stopStatus: trainStopData?.stopStatus || "", stopLabel: trainStopData?.stopLabel || "" });
return acc;
}, [] as Train[]);
state.timetableDataStatus = DataStatus.Loaded;
}
}
})
export function useStore(): Store<State> {
return baseUseStore(key)
}
-591
View File
@@ -1,591 +0,0 @@
import { Module, VuexModule, Mutation, Action } from "vuex-module-decorators";
import axios from "axios";
import JSONStationData from "@/data/stationData.json";
import Station from "@/scripts/interfaces/Station";
import Train from "@/scripts/interfaces/Train";
import TrainStop from "@/scripts/interfaces/TrainStop";
import utils from "@/scripts/utils/storeUtils";
import { DataStatus } from "@/scripts/enums/DataStatus";
import { StoreData } from "@/scripts/interfaces/StoreData";
interface TimetableAPIData {
trainInfo: {
timetableId: number;
trainNo: number;
trainCategoryCode: string;
driverId: number;
driverName: string;
route: string;
twr: boolean;
skr: boolean;
sceneries: string[];
};
stopPoints: {
arrivalLine: string | null;
arrivalTime: string | null;
arrivalDelay: number;
arrivalRealTime: string | null;
pointDistance: number;
pointName: string;
pointNameRAW: string;
entryId: number;
pointId: number;
comments: string | null;
confirmed: boolean;
isStopped: boolean;
pointStopTime: number | null;
pointStopType: string;
departureLine: string | null;
departureTime: string | null;
departureDelay: number;
departureRealTime: string | null;
}[];
}
interface StationAPIData {
dispatcherId: number;
dispatcherName: string;
dispatcherIsSupporter: boolean;
stationName: string;
stationHash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawn: number;
lastSeen: number;
dispatcherExp: number;
nameFromHeader: string;
spawnString: string;
networkConnectionString: string;
isOnline: number;
dispatcherRate: number;
}
interface TrainAPIData {
trainNo: number;
driverId: number;
driverName: string;
driverIsSupporter: boolean;
station: StationAPIData;
dataSignal: string;
dataSceneryConnection: string;
dataDistance: number;
dataCon: string;
dataSpeed: number;
dataMass: number;
dataLength: number;
region: string;
isOnline: boolean;
lastSeen: number;
}
interface Timetable {
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
timetableId: number;
category: string;
route: string;
TWR: boolean;
SKR: boolean;
routeDistance: number;
followingStops: TrainStop[];
followingSceneries: string[];
}
// interface OnlineStationData {
// dispatcherId: number;
// dispatcherName: string;
// dispatcherIsSupporter: boolean;
// stationName: string;
// stationHash: string;
// region: string;
// maxUsers: number;
// currentUsers: number;
// spawn: number;
// lastSeen: number;
// dispatcherExp: number;
// nameFromHeader: string;
// spawnString: string;
// networkConnectionString: string;
// isOnline: number;
// dispatcherRate: number;
// }
// interface TrainData {
// trainNo: number;
// driverId: number;
// driverName: string;
// driverIsSupporter: boolean;
// dataSignal: string;
// dataSceneryConnection: string;
// dataDistance: number;
// dataCon: string;
// dataSpeed: number;
// dataMass: number;
// dataLength: number;
// station: OnlineStationData;
// region: string;
// isOnline: number;
// lastSeen: number;
// }
const URLs = {
stations: "https://api.td2.info.pl:9640/?method=getStationsOnline",
trains: "https://api.td2.info.pl:9640/?method=getTrainsOnline",
dispatchers: "https://api.td2.info.pl:9640/?method=readFromSWDR&value=getDispatcherStatusList%3B1"
};
@Module
export default class Store extends VuexModule {
private trainCount: number = 0;
private stationCount: number = 0;
private dataConnectionStatus: DataStatus = DataStatus.Loading;
private sceneryDataStatus: DataStatus = DataStatus.Loading;
private timetableDataStatus: DataStatus = DataStatus.Loading;
private stationList: Station[] = [];
private trainList: Train[] = [];
//GETTERS
get getAllData(): StoreData {
return {
stationList: this.stationList,
trainList: this.trainList,
activeTrainCount: this.trainCount,
activeStationCount: this.stationCount,
dataConnectionStatus: this.dataConnectionStatus,
timetableDataStatus: this.timetableDataStatus
};
}
get getStationList() {
return this.stationList;
}
get getTrainList() {
return this.trainList;
}
get getTimetableDataStatus() {
return this.timetableDataStatus;
}
get getDataStatus() {
return this.dataConnectionStatus;
}
get getSceneryDataStatus() {
return this.sceneryDataStatus;
}
//ACTIONS
@Action
async synchronizeData() {
this.context.commit("setSceneryData");
this.context.commit("setSceneryDataStatus", DataStatus.Loaded);
this.context.dispatch("fetchOnlineData");
setInterval(() => this.context.dispatch("fetchOnlineData"), 20000);
}
// Fetching all station and train data from API
@Action
async fetchOnlineData() {
Promise.all([axios.get(URLs.stations), axios.get(URLs.trains), axios.get(URLs.dispatchers)])
.then(async response => {
const onlineStationsData: StationAPIData[] = response[0].data.message;
const onlineTrainsData: TrainAPIData[] = await response[1].data.message;
const onlineDispatchersData: string[][] = await response[2].data.message;
const updatedStationList = onlineStationsData.reduce((acc, station) => {
if (station.region !== "eu" || !station.isOnline) return acc;
const stationStatus = onlineDispatchersData.find((status: string[]) => status[0] == station.stationHash && status[1] == "eu");
const statusTimestamp = utils.getStatusTimestamp(stationStatus);
const statusID = utils.getStatusID(stationStatus);
const stationTrains = onlineTrainsData
.filter(train => train.region === "eu" && train.isOnline && train.station.stationName === station.stationName)
.map(train => ({ driverName: train.driverName, trainNo: train.trainNo }));
acc.push({
stationName: station.stationName,
stationHash: station.stationHash,
maxUsers: station.maxUsers,
currentUsers: station.currentUsers,
spawns: utils.parseSpawns(station.spawnString),
dispatcherName: station.dispatcherName,
dispatcherRate: station.dispatcherRate,
dispatcherId: station.dispatcherId,
dispatcherExp: station.dispatcherExp,
dispatcherIsSupporter: station.dispatcherIsSupporter,
stationTrains,
statusTimestamp,
statusID,
statusTimeString: utils.timestampToString(statusTimestamp)
});
return acc;
}, [] as any);
const updatedTrainList = await Promise.all(
onlineTrainsData
.filter(train => train.region === "eu")
.map(async train => {
const locoType = train.dataCon.split(";") ? train.dataCon.split(";")[0] : train.dataCon;
return {
trainNo: train.trainNo,
mass: train.dataMass,
length: train.dataLength,
speed: train.dataSpeed,
distance: train.dataDistance,
signal: train.dataSignal,
online: train.isOnline,
driverId: train.driverId,
driverName: train.driverName,
currentStationName: train.station.stationName,
currentStationHash: train.station.stationHash,
connectedTrack: train.dataSceneryConnection,
locoType,
locoURL: utils.getLocoURL(locoType),
cars: train.dataCon.split(";").filter((train, i) => i > 0) || []
};
})
);
this.context.commit("updateOnlineStations", updatedStationList);
this.context.commit("updateOnlineTrains", updatedTrainList);
this.context.dispatch("fetchTimetableData");
})
.catch(() => {
this.context.commit("setDataConnectionStatus", DataStatus.Error);
});
}
// Fetching timetable data from API basing on online trains
@Action({ commit: "updateTimetableData" })
async fetchTimetableData() {
return this.trainList.reduce(async (acc: Promise<Timetable[]>, train: Train) => {
const timetable: TimetableAPIData = await (await axios.get(utils.timetableURL(train.trainNo))).data.message;
const trainInfo = timetable.trainInfo;
if (!timetable || !trainInfo) return acc;
const followingStops: TrainStop[] = timetable.stopPoints.reduce((stopsAcc: TrainStop[], point) => {
if (point.pointNameRAW.toLowerCase().includes("sbl")) return stopsAcc;
const arrivalTimestamp = utils.getTimestamp(point.arrivalTime);
const arrivalRealTimestamp = utils.getTimestamp(point.arrivalRealTime);
const departureTimestamp = utils.getTimestamp(point.departureTime);
const departureRealTimestamp = utils.getTimestamp(point.departureRealTime);
stopsAcc.push({
stopName: point.pointName,
stopNameRAW: point.pointNameRAW,
stopType: point.pointStopType,
stopDistance: point.pointDistance,
mainStop: point.pointName.includes("strong"),
arrivalLine: point.arrivalLine,
arrivalTimeString: utils.timestampToString(arrivalTimestamp),
arrivalTimestamp: arrivalTimestamp,
arrivalRealTimeString: utils.timestampToString(arrivalRealTimestamp),
arrivalRealTimestamp: arrivalRealTimestamp,
arrivalDelay: point.arrivalDelay,
departureLine: point.departureLine,
departureTimeString: utils.timestampToString(departureTimestamp),
departureTimestamp: departureTimestamp,
departureRealTimeString: utils.timestampToString(departureRealTimestamp),
departureRealTimestamp: departureRealTimestamp,
departureDelay: point.departureDelay,
beginsHere: arrivalTimestamp == 0,
terminatesHere: departureTimestamp == 0,
confirmed: point.confirmed,
stopped: point.isStopped,
stopTime: point.pointStopTime
});
return stopsAcc;
}, []);
(await acc).push({
trainNo: train.trainNo,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
timetableId: trainInfo.timetableId,
category: trainInfo.trainCategoryCode,
route: trainInfo.route,
TWR: trainInfo.twr,
SKR: trainInfo.skr,
routeDistance: timetable.stopPoints[timetable.stopPoints.length - 1].pointDistance,
followingStops,
followingSceneries: trainInfo.sceneries
});
return acc;
}, Promise.resolve([]));
}
//MUTATIONS
@Mutation
private setDataConnectionStatus(status: DataStatus) {
this.dataConnectionStatus = status;
}
@Mutation
private setSceneryDataStatus(status: DataStatus) {
this.sceneryDataStatus = status;
}
@Mutation
private setSceneryData() {
/*
0: stationName,
1: stationURL,
2: stationlines,
3: stationProject?,
4: reqLevel,
5: supportersOnly,
6: signalType,
7: controlType,
8: SBL,
9: two-way block,
10: routes, one-way, catenary,
11: routes, one-way, no catenary,
12: routes, two-way, catenary,
13: routes, two-way, no catenary,
14: subStations?,
15: stops?,
16: default,
17: nonPublic,
18: unavailable
*/
this.stationList = JSONStationData.map(station => ({
stationName: station[0] as string,
stationURL: station[1] as string,
stationLines: station[2] as string,
stationProject: station[3] as string,
reqLevel: station[4] as string,
supportersOnly: station[5] == "TAK",
signalType: station[6] as string,
controlType: station[7] as string,
SBL: station[8] as string,
TWB: station[9] as string,
routes: {
oneWay: {
catenary: station[10] as number,
noCatenary: station[11] as number
},
twoWay: {
catenary: station[12] as number,
noCatenary: station[13] as number
}
},
checkpoints: station[14] ? (station[14] as string[]).map(sub => ({ checkpointName: sub, scheduledTrains: [] })) : null,
stops: station[15] as string[],
default: station[16] as boolean,
nonPublic: station[17] as boolean,
unavailable: station[18] as boolean,
stationHash: "",
maxUsers: 0,
currentUsers: 0,
dispatcherName: "",
dispatcherRate: 0,
dispatcherExp: -1,
dispatcherId: 0,
dispatcherIsSupporter: false,
online: false,
statusTimestamp: -3,
statusID: "free",
statusTimeString: "",
stationTrains: [],
scheduledTrains: [],
spawns: []
}));
}
@Mutation
private updateOnlineStations(updatedStationList: any[]) {
this.stationList = this.stationList.reduce((acc: Station[], station) => {
const onlineStationData = updatedStationList.find(updatedStation => updatedStation.stationName === station.stationName);
const listedStationData = JSONStationData.find(data => data[0] === station.stationName);
if (onlineStationData)
acc.push({
...station,
...onlineStationData,
online: true
});
else if (listedStationData)
acc.push({
...station,
stationProject: "",
stationHash: "",
maxUsers: 0,
currentUsers: 0,
dispatcherName: "",
dispatcherRate: 0,
dispatcherExp: -1,
dispatcherId: 0,
dispatcherIsSupporter: false,
online: false,
statusID: "free",
statusTimestamp: -3,
statusTimeString: "",
stationTrains: [],
scheduledTrains: [],
checkpoints: null
});
return acc;
}, [] as Station[]);
updatedStationList
.filter(uStation => !this.stationList.some(station => uStation.stationName === station.stationName))
.forEach(uStation => {
this.stationList.push({
...uStation,
scheduledTrains: [],
stationTrains: uStation.stationTrains || [],
subStations: [],
online: true,
reqLevel: "-1",
nonPublic: true
});
});
this.stationCount = this.stationList.filter(station => station.online).length;
this.dataConnectionStatus = DataStatus.Loaded;
}
@Mutation
private updateOnlineTrains(updatedTrainList) {
this.trainList = updatedTrainList.reduce((acc, updatedTrain) => {
const trainData = this.trainList.find(train => train.trainNo === updatedTrain.trainNo);
if (trainData) acc.push({ ...trainData, ...updatedTrain });
else acc.push({ ...updatedTrain });
return acc;
}, [] as Train[]);
this.trainCount = this.trainList.filter(train => train.online).length;
this.dataConnectionStatus = DataStatus.Loaded;
}
@Mutation
private updateTimetableData(timetableList: Timetable[]) {
this.stationList = this.stationList.map(station => {
const stationName = station.stationName.toLowerCase();
const scheduledTrains: Station["scheduledTrains"] = timetableList.reduce((acc: Station["scheduledTrains"], timetable: Timetable, index) => {
if (!timetable.followingSceneries.includes(station.stationHash)) return acc;
const stopInfoIndex = timetable.followingStops.findIndex(stop => {
const stopName = stop.stopNameRAW.toLowerCase();
if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes("po.") && !stop.stopName.includes("podg.")) return true;
if (stationName.includes(stopName) && !stop.stopName.includes("po.") && !stop.stopName.includes("podg.")) return true;
if (stopName.includes("podg.") && stopName.split(", podg.")[0] && stationName.includes(stopName.split(", podg.")[0])) return true;
if (station.stops && station.stops.includes(stop.stopNameRAW)) return true;
return false;
});
if (stopInfoIndex == -1) return acc;
const trainStop = timetable.followingStops[stopInfoIndex];
const trainStopStatus = utils.getTrainStopStatus(trainStop, timetable, station);
acc.push({
trainNo: timetable.trainNo,
driverName: timetable.driverName,
driverId: timetable.driverId,
currentStationName: timetable.currentStationName,
currentStationHash: timetable.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nearestStop: "",
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID
});
return acc;
}, []);
if (station.checkpoints) {
station.checkpoints.forEach(cp => (cp.scheduledTrains.length = 0));
for (let checkpoint of station.checkpoints) {
timetableList.forEach(timetable => {
timetable.followingStops
.filter(trainStop => trainStop.stopNameRAW.toLowerCase() === checkpoint.checkpointName.toLowerCase())
.forEach(trainStop => {
const trainStopStatus = utils.getTrainStopStatus(trainStop, timetable, station);
checkpoint.scheduledTrains.push({
trainNo: timetable.trainNo,
driverName: timetable.driverName,
driverId: timetable.driverId,
currentStationName: timetable.currentStationName,
currentStationHash: timetable.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nearestStop: "",
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID
});
});
});
}
}
return { ...station, scheduledTrains };
});
this.trainList = this.trainList.reduce((acc, train) => {
const timetableData = timetableList.find(data => data && data.trainNo === train.trainNo);
const trainStopData = this.stationList
.find(station => station.stationName === train.currentStationName)
?.scheduledTrains.find(stationTrain => stationTrain.trainNo === train.trainNo);
acc.push({ ...train, timetableData, stopStatus: trainStopData?.stopStatus || "", stopLabel: trainStopData?.stopLabel || "" });
return acc;
}, [] as Train[]);
this.timetableDataStatus = DataStatus.Loaded;
}
}
-333
View File
@@ -1,333 +0,0 @@
<template>
<div class="history_view">
<div class="history_wrapper">
<div class="header">
<h2>{{ $t("journal.title") }}</h2>
<p style="color: #ccc">
{{ $t("journal.subtitle") }}
</p>
<!-- <select-box
:itemList="filteredStationList"
:title="$t('journal.select')"
@itemSelected="itemSelected"
/> -->
<div class="disclaimer" v-html="$t('journal.disclaimer')"></div>
</div>
<div class="list">
<div class="list_wrapper">
<!-- <div class="list_loading" v-if="dataLoading">POBIERANIE DANYCH...</div> -->
<transition name="list-anim" mode="out-in">
<ul
class="list_content"
v-if="
!dataLoading &&
!historyLoading &&
computedHistoryList.length != 0
"
:key="inputStationName"
>
<li v-if="currentDispatcherFrom != -1" class="current">
<div class="dispatcher-name">
<a
:href="`https://td2.info.pl/profile/?u=${currentDispatcherId}`"
>{{ currentDispatcher }}</a
>
</div>
<div class="dispatcher-date">
<span style="color: #bbb">{{
new Date(currentDispatcherFrom).toLocaleDateString("pl-PL")
}}</span>
{{
new Date(currentDispatcherFrom).toLocaleTimeString(
"pl-PL",
{ hour: "2-digit", minute: "2-digit" }
)
}}
</div>
</li>
<li v-for="(history, i) in computedHistoryList" :key="i">
<div class="dispatcher-name">
<a
:href="`https://td2.info.pl/profile/?u=${history.dispatcherId}`"
>{{ history.dispatcherName }}</a
>
</div>
<div class="dispatcher-date">
<div>
<span style="color: #888">{{
history.dispatcherFromDate
}}</span>
{{ history.dispatcherFromTime }}
</div>
<div>
<span style="color: #888">{{
history.dispatcherToDate
}}</span>
{{ history.dispatcherToTime }}
</div>
</div>
</li>
</ul>
</transition>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import axios from "axios";
import { Component, Vue, Watch } from "vue-property-decorator";
import { Getter } from "vuex-class";
import Station from "@/scripts/interfaces/Station";
import Scenery from "@/scripts/interfaces/Scenery";
import SelectBox from "@/components/Global/SelectBox.vue";
@Component({ components: { SelectBox } })
export default class HistoryView extends Vue {
@Getter("getStationList") stationList!: Station[];
sceneryHistoryList: Scenery[] = [];
currentSceneryHistory: Scenery["dispatcherHistory"] = [];
currentDispatcher: string = "";
currentDispatcherId: number = 0;
currentDispatcherFrom: number = -1;
inputStationName = "";
dataLoading = true; /* Initial data */
historyLoading = false; /* History loaded after input is checked */
async mounted() {
try {
const responseData: Scenery[] = await (
await axios.get(
"https://stacjownik.herokuapp.com/api/getSceneryInfo?items=-1"
)
).data;
this.sceneryHistoryList = responseData;
} catch (error) {
console.error(error);
}
this.dataLoading = false;
}
get filteredStationList() {
return this.sceneryHistoryList
.map((station) => station.stationName)
.sort((a, b) => (a.toLowerCase() >= b.toLowerCase() ? 1 : -1));
}
get computedHistoryList() {
return this.currentSceneryHistory
.map(
({ dispatcherName, dispatcherFrom, dispatcherTo, dispatcherId }) => ({
dispatcherName,
dispatcherFrom,
dispatcherTo,
dispatcherId,
dispatcherFromDate: new Date(dispatcherFrom).toLocaleDateString(
"pl-PL"
),
dispatcherFromTime: new Date(dispatcherFrom).toLocaleTimeString(
"pl-PL",
{ hour: "2-digit", minute: "2-digit" }
),
dispatcherToDate: new Date(dispatcherTo).toLocaleDateString("pl-PL"),
dispatcherToTime: new Date(dispatcherTo).toLocaleTimeString("pl-PL", {
hour: "2-digit",
minute: "2-digit",
}),
})
)
.reverse();
}
async itemSelected(itemName: string) {
try {
this.historyLoading = true;
const selectedScenery: Scenery = await (
await axios.get(
`https://stacjownik.herokuapp.com/api/getSceneryInfo?name=${itemName}&items=10`
)
).data;
this.currentSceneryHistory = selectedScenery.dispatcherHistory;
this.currentDispatcher = selectedScenery.currentDispatcher;
this.currentDispatcherId = selectedScenery.currentDispatcherId;
this.currentDispatcherFrom = selectedScenery.currentDispatcherFrom;
} catch (error) {
console.error(error);
}
this.historyLoading = false;
}
}
</script>
<style scoped lang="scss">
@import "../styles/responsive.scss";
.history {
&_view {
font-size: 1.2em;
}
&_wrapper {
width: 100%;
height: 100%;
text-align: center;
margin-top: 0.5em;
}
}
.list-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-out;
}
&-enter,
&-leave-to {
opacity: 0.1;
transform: scale(0.95);
}
&-move {
transition: transform 100ms;
}
}
.disclaimer {
color: #aaa;
}
.search-box {
display: flex;
justify-content: center;
align-items: center;
&_content {
position: relative;
margin: 1em 0;
font-size: 1em;
}
select {
border: none;
font-size: 1em;
background-color: rgb(87, 87, 87);
padding: 0.3em;
padding-right: 50px;
outline: none;
color: white;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
}
label {
position: relative;
&.disabled::after {
color: gray;
}
&::after {
content: "<>";
position: absolute;
top: -5%;
right: 0.3em;
font-weight: bold;
}
}
}
.list {
text-align: center;
margin: 1em 0;
display: flex;
justify-content: center;
&_loading,
&_no-info {
margin: 0.3em 0;
padding: 0.5em 2em;
color: white;
}
&_loading {
background-color: #b96b11;
}
&_no-info {
background-color: firebrick;
}
&_wrapper {
@include smallScreen() {
width: 95%;
font-size: 0.9em;
}
}
&_content {
max-height: 75vh;
overflow: auto;
padding: 0.2em 0.5em;
}
&_content > li {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
background: #222;
padding: 0.3em 0.8em;
margin: 0.3em 0;
gap: 10em;
@include smallScreen() {
gap: 1em;
}
& > div {
margin: 0 1em;
}
&.current {
background: #007200;
}
& > .dispatcher-name {
display: flex;
justify-content: center;
align-items: center;
font-size: 1.1em;
font-weight: 500;
}
}
}
</style>
+51 -36
View File
@@ -11,8 +11,14 @@
</action-button>
</div>
<div class="scenery-wrapper" v-if="stationInfo">
<SceneryInfo :stationInfo="stationInfo" :timetableOnly="timetableOnly" />
<div
class="scenery-wrapper"
v-if="stationInfo"
>
<SceneryInfo
:stationInfo="stationInfo"
:timetableOnly="timetableOnly"
/>
<SceneryTimetable
:stationInfo="stationInfo"
@@ -24,49 +30,58 @@
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { Getter } from "vuex-class";
import Station from "@/scripts/interfaces/Station";
import { StoreData } from "@/scripts/interfaces/StoreData";
import { DataStatus } from "@/scripts/enums/DataStatus";
import SceneryInfo from "@/components/SceneryView/SceneryInfo.vue";
import SceneryTimetable from "@/components/SceneryView/SceneryTimetable.vue";
import { StoreData } from "@/scripts/interfaces/StoreData";
import { DataStatus } from "@/scripts/enums/DataStatus";
import ActionButton from "@/components/Global/ActionButton.vue";
@Component({
import { computed, ComputedRef, defineComponent } from "@vue/runtime-core";
import { useStore } from "@/store";
import { GETTERS } from "@/constants/storeConstants";
import { useRoute } from "vue-router";
export default defineComponent({
components: { SceneryInfo, SceneryTimetable, ActionButton },
})
export default class SceneryView extends Vue {
@Getter("getAllData") data!: StoreData;
timetableOnly: boolean = false;
setup() {
const route = useRoute();
const store = useStore();
activated() {
this.timetableOnly =
this.$route.query["timetable_only"] == "1" ? true : false;
}
get isComponentVisible() {
return this.$route.path === "/scenery";
}
get isDataLoaded() {
return this.data.dataConnectionStatus == DataStatus.Loaded;
}
get stationInfo(): Station | undefined {
if (!this.$route.query.station) return;
return this.data.stationList.find(
(station) =>
station.stationName ===
this.$route.query.station.toString().replaceAll("_", " ")
const data: ComputedRef<StoreData> = computed(
() => store.getters[GETTERS.allData]
);
}
}
const timetableOnly = computed(() =>
route.query["timetable_only"] == "1" ? true : false
);
const isComponentVisible = computed(() => route.path === "/scenery");
const isDataLoaded = computed(
() => data.value.dataConnectionStatus === DataStatus.Loaded
);
const stationInfo = computed(() => {
if (!route.query.station) return;
return data.value.stationList.find(
(station) =>
station.stationName ===
route.query.station?.toString().replaceAll("_", " ")
);
});
return {
data,
timetableOnly,
isComponentVisible,
isDataLoaded,
stationInfo,
};
},
});
</script>
<style lang="scss" scoped>
+82 -113
View File
@@ -1,14 +1,9 @@
<template>
<div class="stations-view">
<DonationModal
:modalHidden="modalHidden"
@toggleModal="toggleModal"
/>
<div class="wrapper">
<div class="body">
<div class="options-bar">
<action-button @click.native="() => toggleCardsState('filter')">
<action-button @click="() => toggleCardsState('filter')">
<img
class="button_icon"
:src="require('@/assets/icon-filter2.svg')"
@@ -42,7 +37,7 @@
<transition name="card-anim">
<FilterCard
v-if="filterCardOpen"
:showCard="filterCardOpen"
:exit="() => toggleCardsState('filter')"
@changeFilterValue="changeFilterValue"
@resetFilters="resetFilters"
@@ -52,10 +47,6 @@
</template>
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import { Getter } from "vuex-class";
import Station from "@/scripts/interfaces/Station";
import StorageManager from "@/scripts/managers/storageManager";
@@ -68,128 +59,106 @@ import FilterCard from "@/components/StationsView/FilterCard.vue";
import ActionButton from "@/components/Global/ActionButton.vue";
import { StoreData } from "@/scripts/interfaces/StoreData";
import { DataStatus } from "@/scripts/enums/DataStatus";
import { computed, ComputedRef, defineComponent, reactive } from "vue";
import { useStore } from "@/store";
import { GETTERS } from "@/constants/storeConstants";
@Component({
export default defineComponent({
components: {
StationTable,
FilterCard,
ActionButton,
},
})
export default class StationsView extends Vue {
STORAGE_KEY: string = "options_saved";
STORAGE_MODAL: string = "modal";
data: () => ({
trainIcon: require("@/assets/icon-train.svg"),
timetableIcon: require("@/assets/icon-timetable.svg"),
dolarIcon: require("@/assets/icon-dolar.svg"),
filterCardOpen: false,
modalHidden: true,
STORAGE_KEY: "options_saved",
inputs: inputData,
}),
setup() {
const store = useStore();
const filterManager = reactive(new StationFilterManager());
const focusedStationName = "";
trainIcon: string = require("@/assets/icon-train.svg");
timetableIcon: string = require("@/assets/icon-timetable.svg");
dolarIcon: string = require("@/assets/icon-dolar.svg");
const data: ComputedRef<StoreData> = computed(
() => store.getters[GETTERS.allData]
);
filterManager: StationFilterManager = new StationFilterManager();
focusedStationName: string = "";
filterCardOpen: boolean = false;
modalHidden: boolean = true;
inputs = inputData;
@Getter("getStationList") stationList!: Station[];
@Getter("getAllData") data!: StoreData;
get dataStatusClass() {
if (this.data.dataConnectionStatus == DataStatus.Loading) return "loading";
if (this.data.dataConnectionStatus == DataStatus.Error) return "error";
return "success";
}
get timetableDataStatusClass() {
if (this.data.timetableDataStatus == DataStatus.Loading) return "loading";
if (this.data.timetableDataStatus == DataStatus.Error) return "error";
return "success";
}
mounted() {
this.initializeOptionsStorage();
// this.initializeModalStorage();
window.addEventListener("keydown", (e: KeyboardEvent) => {
if (e.keyCode == 27 && this.focusedStationName != "") {
this.focusedStationName = "";
}
const computedStations: ComputedRef<Station[]> = computed(() => {
return filterManager.getFilteredStationList(
store.getters[GETTERS.stationList]
);
});
}
initializeOptionsStorage() {
const getStatusClass = computed(() => {
if (data.value.dataConnectionStatus == DataStatus.Loading)
return "loading";
if (data.value.dataConnectionStatus == DataStatus.Error) return "error";
return "success";
});
const timetableDataStatusClass = computed(() => {
if (data.value.timetableDataStatus == DataStatus.Loading)
return "loading";
if (data.value.timetableDataStatus == DataStatus.Error) return "error";
return "success";
});
const focusedStationInfo = computed(() =>
computedStations.value.find(
(station) => station.stationName === focusedStationName
)
);
return {
data,
computedStations,
filterManager,
getStatusClass,
timetableDataStatusClass,
focusedStationName,
focusedStationInfo,
};
},
mounted() {
if (!StorageManager.isRegistered(this.STORAGE_KEY)) return;
this.inputs.options.forEach((option) => {
const value = StorageManager.getBooleanValue(option.name);
this.changeFilterValue({ name: option.name, value: value ? 0 : 1 });
option.value = value;
});
this.inputs.sliders.forEach((slider) => {
const value = StorageManager.getNumericValue(slider.name);
this.changeFilterValue({ name: slider.name, value });
slider.value = value;
});
}
initializeModalStorage() {
if (StorageManager.isRegistered(`${this.STORAGE_MODAL}_hidden`))
this.modalHidden = StorageManager.getBooleanValue(
`${this.STORAGE_MODAL}_hidden`
);
}
toggleModal() {
this.modalHidden = !this.modalHidden;
StorageManager.setBooleanValue(
`${this.STORAGE_MODAL}_hidden`,
this.modalHidden
);
}
toggleCardsState(name: string): void {
if (name == "filter") {
this.filterCardOpen = !this.filterCardOpen;
}
}
changeSorter(index: number) {
this.filterManager.changeSorter(index);
}
changeFilterValue(filter: { name: string; value: number }) {
this.filterManager.changeFilterValue(filter);
}
resetFilters() {
this.filterManager.resetFilters();
}
get computedStations() {
return this.filterManager.getFilteredStationList(this.stationList);
}
closeCard() {
this.focusedStationName = "";
}
setFocusedStation(name: string) {
this.focusedStationName = this.focusedStationName == name ? "" : name;
}
get focusedStationInfo() {
return this.computedStations.find(
(station) => station.stationName === this.focusedStationName
);
}
}
},
methods: {
toggleCardsState(name: string): void {
if (name == "filter") {
this.filterCardOpen = !this.filterCardOpen;
}
},
changeSorter(index: number) {
this.filterManager.changeSorter(index);
},
changeFilterValue(filter: { name: string; value: number }) {
this.filterManager.changeFilterValue(filter);
},
resetFilters() {
this.filterManager.resetFilters();
},
closeCard() {
this.focusedStationName = "";
},
setFocusedStation(name: string) {
this.focusedStationName = this.focusedStationName == name ? "" : name;
},
},
});
</script>
<style lang="scss" scoped>
@@ -202,7 +171,7 @@ export default class StationsView extends Vue {
transition: all $animDuration $animType;
}
&-enter,
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.85);
opacity: 0;
+110 -78
View File
@@ -2,11 +2,14 @@
<section class="trains-view">
<div class="wrapper">
<div class="options-bar">
<TrainStats :trains="trains" :trainStatsOpen="trainStatsOpen" />
<TrainStats
:trains="trainList"
:trainStatsOpen="trainStatsOpen"
/>
<TrainOptions
:queryTrain="queryTrain"
@change-sorter="changeSorter"
@changeSorter="changeSorter"
@changeSearchedTrain="changeSearchedTrain"
@changeSearchedDriver="changeSearchedDriver"
/>
@@ -14,7 +17,6 @@
<TrainTable
:computedTrains="computedTrains"
:timetableDataStatus="timetableDataStatus"
:queryTrain="queryTrain"
/>
</div>
@@ -22,105 +24,135 @@
</template>
<script lang="ts">
import Vue from "vue";
import { Component, Prop } from "vue-property-decorator";
import { Getter } from "vuex-class";
import { computed, ComputedRef, defineComponent, ref } from "vue";
import { DataStatus } from "@/scripts/enums/DataStatus";
import Train from "@/scripts/interfaces/Train";
import TrainTable from "@/components/TrainsView/TrainTable.vue";
import TrainStats from "@/components/TrainsView/TrainStats.vue";
import TrainOptions from "@/components/TrainsView/TrainOptions.vue";
import ActionButton from "@/components/Global/ActionButton.vue";
import { DataStatus } from "@/scripts/enums/DataStatus";
@Component({
import { useStore } from "@/store";
import { GETTERS } from "@/constants/storeConstants";
const filteredTrainList = (
trainList: Train[],
searchedTrain: string,
searchedDriver: string,
sorterActive: { id: string; dir: number }
) => {
return trainList
.filter(
(train) =>
train.online &&
(searchedTrain.length > 0
? train.trainNo.toString().includes(searchedTrain)
: true) &&
(searchedDriver.length > 0
? train.driverName
.toLowerCase()
.includes(searchedDriver.toLowerCase())
: true)
)
.sort((a: Train, b: Train) => {
switch (sorterActive.id) {
case "mass":
if (a.mass > b.mass) return sorterActive.dir;
return -sorterActive.dir;
case "distance":
if (
(a.timetableData?.routeDistance || -1) >
(b.timetableData?.routeDistance || -1)
)
return sorterActive.dir;
return -sorterActive.dir;
case "speed":
if (a.speed > b.speed) return sorterActive.dir;
return -sorterActive.dir;
case "timetable":
if (a.trainNo > b.trainNo) return sorterActive.dir;
return -sorterActive.dir;
case "length":
if (a.length > b.length) return sorterActive.dir;
return -sorterActive.dir;
default:
break;
}
return 0;
});
};
export default defineComponent({
components: {
TrainTable,
TrainStats,
TrainOptions,
ActionButton,
},
})
export default class TrainsView extends Vue {
@Getter("getTrainList") trains!: Train[];
@Getter("getTimetableDataStatus") timetableDataStatus!: DataStatus;
// Passed in route as query parameters
@Prop() readonly queryTrain!: string;
props: ["queryTrain"],
statsIcon = require("@/assets/icon-stats.svg");
data: () => ({
statsIcon: require("@/assets/icon-stats.svg"),
trainStatsOpen: false,
}),
sorterActive: { id: string; dir: number } = { id: "distance", dir: -1 };
setup() {
const store = useStore();
trainStatsOpen: boolean = false;
const trainList: ComputedRef<Train[]> = computed(
() => store.getters[GETTERS.trainList]
);
searchedTrain: string = "";
searchedDriver: string = "";
const timetableDataStatus: ComputedRef<DataStatus> = computed(
() => store.getters[GETTERS.timetableDataStatus]
);
changeSearchedTrain(trainNo: string) {
this.searchedTrain = trainNo;
}
const sorterActive = ref({ id: "distance", dir: -1 });
const searchedDriver = ref("");
const searchedTrain = ref("");
changeSearchedDriver(name: string) {
this.searchedDriver = name;
}
const computedTrains: ComputedRef<Train[]> = computed(() => {
if (timetableDataStatus.value != DataStatus.Loaded) return [];
changeSorter(sorter: { id: string; dir: number }) {
this.sorterActive = sorter;
}
return filteredTrainList(
trainList.value,
searchedTrain.value,
searchedDriver.value,
sorterActive.value
);
});
get computedTrains() {
return this.timetableDataStatus != DataStatus.Loaded
? []
: this.trains
.filter(
(train) =>
train.online &&
(this.searchedTrain.length > 0
? train.trainNo.toString().includes(this.searchedTrain)
: true) &&
(this.searchedDriver.length > 0
? train.driverName
.toLowerCase()
.includes(this.searchedDriver.toLowerCase())
: true)
)
.sort((a, b) => {
switch (this.sorterActive.id) {
case "mass":
if (a.mass > b.mass) return this.sorterActive.dir;
return -this.sorterActive.dir;
return {
trainList,
computedTrains,
searchedTrain,
searchedDriver,
sorterActive,
};
},
case "distance":
if (
(a.timetableData?.routeDistance || -1) >
(b.timetableData?.routeDistance || -1)
)
return this.sorterActive.dir;
methods: {
changeSearchedTrain(trainNo: string) {
this.searchedTrain = trainNo;
},
return -this.sorterActive.dir;
changeSearchedDriver(name: string) {
this.searchedDriver = name;
},
case "speed":
if (a.speed > b.speed) return this.sorterActive.dir;
return -this.sorterActive.dir;
case "timetable":
if (a.trainNo > b.trainNo) return this.sorterActive.dir;
return -this.sorterActive.dir;
case "length":
if (a.length > b.length) return this.sorterActive.dir;
return -this.sorterActive.dir;
default:
break;
}
return 0;
});
}
}
changeSorter(sorter: { id: string; dir: number }) {
this.sorterActive = sorter;
},
},
});
</script>
<style lang="scss" scoped>
+14
View File
@@ -0,0 +1,14 @@
import { ComponentCustomProperties } from 'vue'
import { Store } from 'vuex'
declare module '@vue/runtime-core' {
// declare your own store states
interface State {
count: number
}
// provide typings for `this.$store`
interface ComponentCustomProperties {
$store: Store<State>
}
}