Compare commits

..

23 Commits

Author SHA1 Message Date
Spythere 3ef27e1d69 Merge pull request #85 from Spythere/development
Wersja 1.23.1
2024-04-01 13:00:28 +02:00
Spythere f53993c717 hotfix 2024-03-31 21:55:33 +02:00
Spythere 235c16e30f train modal 2024-03-31 21:37:14 +02:00
Spythere c3533f07ad literówka 2024-03-30 17:48:34 +01:00
Spythere d05579c5ee popupy 2024-03-30 13:24:39 +01:00
Spythere c8f53c2f06 hotfixy designu 2024-03-30 00:18:54 +01:00
Spythere b44f88ebcd src miniaturek 2024-03-29 23:37:26 +01:00
Spythere 7805d1350c responsywność 2024-03-29 23:35:56 +01:00
Spythere b17bd19433 zmiana położenia przycisku RJ ONLINE w dzienniku 2024-03-29 23:23:14 +01:00
Spythere c12a6cbacd zmiana rozłożenia elementów w modalu aktywnego pociągu 2024-03-29 23:21:15 +01:00
Spythere ba650238db poprawki rozmieszczenia popupu 2024-03-29 23:04:08 +01:00
Spythere d5ec9919e2 update modal (wip) 2024-03-29 20:34:56 +01:00
Spythere 20cd393e05 Merge pull request #84 from Spythere/development
Wersja 1.23.0
2024-03-24 01:30:03 +01:00
Spythere 31e65c09d6 hotfix: podgląd pojazdów 2024-03-24 00:05:39 +01:00
Spythere fb2348e774 hotfixy designu 2024-03-23 23:55:18 +01:00
Spythere 1ec75bda70 poprawki do popupów 2024-03-23 16:47:57 +01:00
Spythere 6b6b837dde bump: v1.23.0 2024-03-23 00:01:15 +01:00
Spythere 66a02d76bd dodano odnośnik do dziennika RJ maszynisty 2024-03-23 00:00:52 +01:00
Spythere c7162dbd14 dodano dymki kontekstowe oraz podgląd pojazdu 2024-03-22 23:41:43 +01:00
Spythere 1cfe073bab Merge pull request #83 from Spythere/development
Wersja 1.22.3
2024-03-17 18:38:27 +01:00
Spythere e3b72c81ea bump: 1.22.3 2024-03-17 18:37:58 +01:00
Spythere 5552995564 fix: duplikujące się aktywne RJ scenerii 2024-03-17 18:37:45 +01:00
Spythere 623d5dd2ce fix: RJ scenerii offline 2024-03-17 17:33:19 +01:00
24 changed files with 668 additions and 181 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.22.2",
"version": "1.23.1",
"private": true,
"scripts": {
"dev": "vite",
+34 -13
View File
@@ -1,8 +1,10 @@
<template>
<div class="app_container" v-cloak>
<PopUp />
<transition name="modal-anim">
<keep-alive>
<TrainModal v-if="store.chosenModalTrainId" />
<TrainModal />
</keep-alive>
</transition>
@@ -32,19 +34,19 @@
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue';
import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '.././package.json';
import Clock from './components/App/Clock.vue';
import { useMainStore } from './store/mainStore';
import popupMixin from './mixins/popupMixin';
import Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue';
import StorageManager from './managers/storageManager';
import PopUp from './components/PopUp/PopUp.vue';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common';
@@ -55,9 +57,12 @@ export default defineComponent({
Clock,
StatusIndicator,
AppHeader,
TrainModal
TrainModal,
PopUp
},
mixins: [popupMixin],
data: () => ({
VERSION: version,
store: useMainStore(),
@@ -79,13 +84,7 @@ export default defineComponent({
this.apiStore.fetchActiveData();
});
watch(
() => this.store.blockScroll,
(value) => {
if (value) document.body.classList.add('no-scroll');
else document.body.classList.remove('no-scroll');
}
);
window.addEventListener('mousemove', (e: MouseEvent) => this.handlePopUpEvents(e));
},
methods: {
@@ -138,6 +137,27 @@ export default defineComponent({
this.apiStore.connectToAPI();
},
handlePopUpEvents(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-popup-key'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.store.popUpData.key != null) this.hidePopUp();
return;
}
const popupComponentKey = targetEl.getAttribute('data-popup-key');
const popupContent = targetEl.getAttribute('data-popup-content');
if (popupComponentKey && popupContent) this.showPopUp(e, popupComponentKey, popupContent);
else if (this.store.popUpData.key != null) this.hidePopUp();
this.store.mousePos.x = e.pageX;
this.store.mousePos.y = e.pageY;
},
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
@@ -217,6 +237,7 @@ export default defineComponent({
grid-template-columns: 100%;
min-height: 100vh;
overflow: hidden;
}
.app_main {
+93 -8
View File
@@ -1,15 +1,36 @@
<template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
<div class="modal_content">
<h1 class="header">Aktualizacja Stacjownika</h1>
<h2>wersja {{ version }}</h2>
<div>
<h1 style="margin-bottom: 0.5em">{{ $t('update.title') }}</h1>
<h2 class="text--primary">{{ $t('update.version', [version]) }}</h2>
<hr class="separator" />
</div>
<b>Co nowego?</b>
<p>
<ul>
<li>test</li>
</ul>
</p>
<div class="features-list">
<h2>Nowości i zmiany:</h2>
<ul>
<li v-for="content in localeChangesArray" :key="content">{{ content }}</li>
</ul>
</div>
<div class="modal_actions">
<button class="btn--action">Przyjąłem!</button>
<p>Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony!</p>
<!-- <div class="actions-checkboxes">
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
</div> -->
</div>
</div>
</AnimatedModal>
</template>
@@ -30,6 +51,12 @@ export default defineComponent({
};
},
computed: {
localeChangesArray() {
return this.$t('update.content').split('\n');
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
@@ -40,9 +67,67 @@ export default defineComponent({
<style lang="scss" scoped>
.modal_content {
font-size: 1.2em;
text-align: center;
padding: 1em;
height: 80vh;
min-height: 550px;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
}
hr.separator {
margin: 0.5em 0;
padding: 0;
height: 3px;
background-color: #fff;
}
.features-list {
margin-top: 0.5em;
overflow: auto;
ul {
text-align: left;
list-style: '\21D2 ';
padding: 1em;
}
li {
margin: 0.5em 0;
}
}
.modal_actions {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
button {
font-weight: bold;
padding: 0.35em;
}
p {
font-size: 0.9em;
}
}
.actions-checkboxes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1em;
label {
font-size: 0.9em;
}
label > input {
margin-right: 0.5em;
}
}
</style>
+23 -3
View File
@@ -7,11 +7,12 @@
</p>
<img
class="traction-only"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
/^EN/.test(computedStockList[0]) ? 'rb' : ''
}.png`"
@error="onImageError($event, computedStockList[0])"
width="400"
width="300"
height="60"
/>
</div>
@@ -25,39 +26,53 @@
<span>
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : ''
}.png`"
@error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400"
height="60"
/>
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
class="train-thumbnail"
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
class="train-thumbnail"
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@click.stop="() => {}"
/>
<!-- /// -->
</span>
@@ -139,6 +154,7 @@ export default defineComponent({
ul > li > span {
display: flex;
align-items: flex-end;
cursor: crosshair;
}
img {
@@ -147,6 +163,10 @@ img {
height: auto;
}
img.traction-only {
max-width: 100%;
}
p {
text-align: center;
color: #aaa;
@@ -1,11 +1,6 @@
<template>
<div class="item-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<span class="general-train">
<span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
@@ -66,6 +61,15 @@
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
<button
v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('journal.timetable-online-button') }}
</button>
</span>
</div>
</template>
@@ -140,12 +144,24 @@ export default defineComponent({
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.btn-timetable {
display: inline-block;
padding: 0.1em 0.4em;
margin-left: 0.5em;
img {
vertical-align: top;
}
}
@include smallScreen {
+39
View File
@@ -0,0 +1,39 @@
<template>
<div class="popup-content">
<img src="/images/icon-diamond.svg" alt="" />
<span>{{ store.popUpData.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
store: useMainStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
gap: 0.5em;
padding: 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
}
img {
vertical-align: middle;
height: 1em;
margin-right: 0.5em;
}
</style>
+71
View File
@@ -0,0 +1,71 @@
<template>
<div class="popup" v-show="store.popUpData.key" ref="preview">
<component v-if="store.popUpData.key" :is="store.popUpData.key" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import DonatorPopUp from './DonatorPopUp.vue';
import TrainCommentsPopUp from './TrainCommentsPopUp.vue';
import VehiclePreviewPopUp from './VehiclePreviewPopUp.vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp },
data() {
return {
store: useMainStore()
};
},
watch: {
'store.mousePos': {
deep: true,
handler(val: typeof this.store.mousePos) {
this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0px',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val.x <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val.x >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%';
translateX = '-100%';
} else {
previewEl.style.left = `${val.x}px`;
translateX = '-50%';
}
previewEl.style.top = `${val.y}px`;
const isOutside =
val.y + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)';
previewEl.style.transform = `translate(${translateX}, ${translateY})`;
});
}
}
}
});
</script>
<style lang="scss" scoped>
.popup {
position: absolute;
z-index: 250;
max-width: 400px;
text-align: center;
}
</style>
@@ -0,0 +1,38 @@
<template>
<div class="popup-content">
<span>{{ store.popUpData.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
store: useMainStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
}
img {
height: 1em;
}
</style>
@@ -0,0 +1,85 @@
<template>
<div class="popup-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="store.popUpData.key"
@load="onImageLoad"
@error="onImageError"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${store.popUpData.content}--300px.jpg`"
/>
<div class="vehicle-name" v-if="imageState != 'error'">
{{ store.popUpData.content.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
store: useMainStore(),
imageState: 'loading'
};
},
mounted() {
this.imageState = 'loading';
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
},
onImageError(e: Event) {
this.imageState = 'error';
(e.target as HTMLElement).style.display = 'none';
}
}
});
</script>
<style lang="scss" scoped>
.popup-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
width: 300px;
min-height: 200px;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em;
border-radius: 0.5em;
}
.loading-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
img {
width: 100%;
height: auto;
}
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
</style>
@@ -74,7 +74,7 @@
class="timetable-item"
v-else
v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
+6 -2
View File
@@ -119,8 +119,9 @@
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
:title="$t('donations.dispatcher-message')"
@click.stop="openDonationModal"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
@@ -311,6 +312,7 @@ import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationH
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import popupMixin from '../../mixins/popupMixin';
export default defineComponent({
props: {
@@ -322,7 +324,7 @@ export default defineComponent({
emits: ['toggleDonationModal'],
components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin],
mixins: [styleMixin, dateMixin, stationInfoMixin, popupMixin],
data: () => ({
headIconsIds,
@@ -339,6 +341,7 @@ export default defineComponent({
setup() {
const mainStore = useMainStore();
const apiStore = useApiStore();
const stationFiltersStore = useStationFiltersStore();
return {
@@ -368,6 +371,7 @@ export default defineComponent({
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
this.mainStore.modalLastClickedTarget = e.target;
this.hidePopUp();
},
openForumSite(e: Event, url: string | undefined) {
+155 -82
View File
@@ -1,60 +1,86 @@
<template>
<div class="train-info">
<div class="train-info" :data-extended="extended">
<section class="train-general">
<div class="general-info">
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }}
</span>
<span
class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
>
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">
TWR
<div class="general-top-bar">
<div>
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }}
</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">
SKR
</span>
</span>
<strong>
<span v-if="train.timetableData" class="text--primary"
>{{ train.timetableData.category }}&nbsp;</span
<span
class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<span
class="train-badge twr"
v-if="train.timetableData?.TWR"
:title="$t('general.TWR')"
>
TWR
</span>
<span
class="train-badge skr"
v-if="train.timetableData?.SKR"
:title="$t('general.SKR')"
>
SKR
</span>
</span>
<div class="train-driver">
<strong>
<span v-if="train.timetableData" class="text--primary"
>{{ train.timetableData.category }}&nbsp;</span
>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
:title="$t('donations.driver-message')"
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<span v-else>{{ train.driverName }}</span>
<div class="train-driver">
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.driver-message')"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</b>
<span v-else>{{ train.driverName }}</span>
</div>
</div>
<div v-if="extended">
<button class="btn-timetable btn--image btn--action" @click="navigateToJournal">
<img src="/images/icon-train.svg" alt="train icon" />
<span>
{{ $t('trains.journal-button') }}
</span>
</button>
<button class="btn-exit btn--image btn--action" @click="closeModal">
<img src="/images/icon-exit.svg" alt="modal exit icon" />
</button>
</div>
</div>
<div class="general-timetable" v-if="train.timetableData">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<img
<span
v-if="getSceneriesWithComments(train.timetableData).length > 0"
class="image-warning"
src="/images/icon-warning.svg"
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
data-popup-key="TrainCommentsPopUp"
:data-popup-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData
)})`"
/>
>
<img class="image-warning" src="/images/icon-warning.svg" />
</span>
</div>
<hr style="margin: 0.25em 0" />
@@ -67,7 +93,7 @@
</div>
<div class="general-status">
<div class="timetable-progress" v-if="train.timetableData">
<div class="status-timetable-progress" v-if="train.timetableData">
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="progress-distance">
@@ -91,12 +117,29 @@
</div>
</div>
<div class="driver_position text--grayed" style="margin-top: 0.25em">
<div class="general-stats" v-if="extended">
<div>
<img src="/images/icon-length.svg" alt="length icon" />
{{ train.length }}m
</div>
<div>
<img src="/images/icon-mass.svg" alt="mass icon" />
{{ (train.mass / 1000).toFixed(1) }}t
</div>
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h
</div>
</div>
<div class="text--grayed" style="margin-top: 0.25em">
{{ displayTrainPosition(train) }}
</div>
</section>
<section class="train-stats">
<section class="train-stats" v-if="!extended">
<StockList :trainStockList="train.stockList" :tractionOnly="true" />
<div>
@@ -125,9 +168,10 @@ import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin],
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
components: { ProgressBar, StockList },
props: {
@@ -136,8 +180,7 @@ export default defineComponent({
required: true
},
extended: {
type: Boolean,
default: true
type: Boolean
}
},
@@ -146,6 +189,19 @@ export default defineComponent({
store: useMainStore(),
apiStore: useApiStore()
};
},
methods: {
navigateToJournal() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-driver': this.train.driverName
}
});
this.closeModal();
}
}
});
</script>
@@ -156,8 +212,8 @@ export default defineComponent({
.image-warning {
height: 1em;
margin-left: 0.5em;
vertical-align: middle;
}
.train-stats {
@@ -176,6 +232,10 @@ export default defineComponent({
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr;
&[data-extended='true'] {
grid-template-columns: 1fr;
}
padding: 1em;
background-color: #1a1a1a;
@@ -210,14 +270,29 @@ export default defineComponent({
font-size: 0.8em;
}
.general-info {
.general-top-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
gap: 0.25em;
margin-right: 1.5em;
& > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
}
}
.btn-timetable {
padding: 0.25em;
}
.btn-exit {
padding: 0.25em;
}
.general-status {
display: flex;
align-items: center;
@@ -226,6 +301,27 @@ export default defineComponent({
gap: 0.25em;
}
.general-stats {
display: flex;
gap: 0.5em;
flex-wrap: wrap;
& > div {
display: flex;
align-items: center;
gap: 0.25em;
}
img {
width: 1.5em;
}
}
.general-timetable {
display: flex;
align-items: center;
}
.status-badges {
display: flex;
flex-wrap: wrap;
@@ -237,17 +333,7 @@ export default defineComponent({
}
}
.general-timetable {
display: flex;
align-items: center;
}
.timetable-warnings {
display: flex;
gap: 0.25em;
}
.timetable-progress {
.status-timetable-progress {
display: flex;
align-items: center;
flex-wrap: wrap;
@@ -257,32 +343,19 @@ export default defineComponent({
margin-right: 0.25em;
}
.timetable-warnings {
display: flex;
gap: 0.25em;
}
@include smallScreen() {
.train-info {
grid-template-columns: 1fr;
gap: 1em 0;
text-align: center;
font-size: 1.15em;
}
.general-info,
.general-status,
.general-timetable {
justify-content: center;
}
.timetable-progress {
justify-content: center;
}
.comments {
flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
.btn-timetable > span {
display: none;
}
}
</style>
+16 -27
View File
@@ -2,11 +2,7 @@
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0">
<button class="btn exit" @click="closeModal">
<img src="/images/icon-exit.svg" alt="close card" />
</button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
</div>
@@ -17,17 +13,27 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
import Train from '../../scripts/interfaces/Train';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [modalTrainMixin],
activated() {
const contentEl = this.$refs['content'] as HTMLElement;
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
}
},
this.$nextTick(() => {
contentEl.focus();
});
watch: {
chosenTrain(train: Train | undefined) {
this.$nextTick(() => {
if (train) {
const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus();
}
});
}
}
});
</script>
@@ -49,23 +55,6 @@ export default defineComponent({
}
}
.exit {
position: absolute;
top: 0;
right: 0;
margin: 0.5em 1em;
padding: 0.25em;
z-index: 201;
img {
width: 1.5rem;
vertical-align: middle;
}
}
.train-modal {
position: fixed;
top: 0;
+4 -4
View File
@@ -1,13 +1,13 @@
<template>
<transition name="status-anim" mode="out-in" tag="div" class="train-table">
<div :key="apiStore.dataStatuses.connection">
<div class="table-info" key="offline" v-if="store.isOffline">
<div class="table-warning" key="offline" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-info" key="no-trains" v-else-if="trains.length == 0">
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }}
</div>
@@ -20,7 +20,7 @@
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
>
<TrainInfo :train="train" />
<TrainInfo :train="train" :extended="false" />
</li>
</transition-group>
</div>
@@ -105,7 +105,7 @@ export default defineComponent({
overflow-x: hidden;
}
.table-info {
.table-warning {
text-align: center;
padding: 1em 0;
+15 -7
View File
@@ -26,6 +26,13 @@
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
},
"update": {
"title": "Stacjownik app update!",
"version": "Version {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "context tooltips when hovering over project sponsors and timetable comment warnings\nvehicle image preview when hovering over its thumbnail in the active timetable card view and timetable journal\nlink to the driver's timetable history in the active timetable card view\nlink to the driver's active timetable card view in the timetable journal (available for online trains only)\nnew update card with version changelog"
},
"app": {
"sceneries": "SCENERIES",
"trains": "TRAINS",
@@ -41,12 +48,10 @@
"footer": {
"discord": "Stacjownik Discord server"
},
"update": {
"title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "UPDATE NOW",
"later-button": "LATER"
"vehicle-preview": {
"loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/"
},
"data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
@@ -315,7 +320,9 @@
"last-seen-ago": "since {minutes} minutes",
"scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!"
"timeout": "An error occured while trying to refresh SWDR timetable data!",
"journal-button": "DRIVER'S JOURNAL"
},
"train-stats": {
"stats-button": "STATISTICS",
@@ -351,6 +358,7 @@
"timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED",
"timetable-abandoned": "ABANDONED",
"timetable-online-button": "ONLINE TIMETABLE",
"online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted",
+16 -2
View File
@@ -4,7 +4,7 @@
"header": "Grosza daj Stacjownikowi!",
"donator-title": "Projekt ma już ponad <b>{count}</b> wspierających, w tym:",
"p1": "<b>Hej o7!</b> Z tej strony Spythere, twórca Stacjownika, Pojazdownika oraz kilku innych aplikacji wspomagających rozgrywkę symulatora Train Driver 2!",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdeczne zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdecznie zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2-b1": "Stacjownik",
"p2-b2": "Dziennik",
"p2-b3": "Stacjobot",
@@ -26,6 +26,13 @@
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
},
"update": {
"title": "Aktualizacja Stacjownika!",
"version": "Wersja {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "dymki kontekstowe po najechaniu kursorem na m.in. sponsorów projektu i uwagi eksploatacyjne\npodgląd pojazdu po najechaniu kursorem na jego miniaturkę w karcie aktywnego rozkładu jazdy oraz dzienniku RJ\nodnośnik do historii RJ maszynisty w widoku karty aktywnego rozkładu jazdy\nodnośnik do karty aktywnego rozkładu jazdy maszynisty w dzienniku (dostępny tylko dla pociągów online)\nnowa karta ze zmianami w aktualizacji"
},
"app": {
"sceneries": "SCENERIE",
"trains": "POCIĄGI",
@@ -38,6 +45,10 @@
"footer": {
"discord": "Serwer Discord Stacjownika"
},
"vehicle-preview": {
"loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
},
"data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
@@ -295,7 +306,9 @@
"scenery-offline": "Przejazd offline",
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
"journal-button": "DZIENNIK MASZYNISTY"
},
"train-stats": {
"stats-button": "STATYSTYKI",
@@ -337,6 +350,7 @@
"timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE",
"stock-length": "Długość",
+1 -6
View File
@@ -8,12 +8,6 @@ export default defineComponent({
};
},
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
}
},
methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId;
@@ -23,6 +17,7 @@ export default defineComponent({
closeModal() {
this.store.chosenModalTrainId = undefined;
this.store.popUpData.key = null;
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
+27
View File
@@ -0,0 +1,27 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { PopUpType, popupKeys } from '../store/typings';
const isPopUp = (v: any): v is PopUpType => popupKeys.includes(v);
export default defineComponent({
data() {
return {
store: useMainStore()
};
},
methods: {
showPopUp(e: MouseEvent, componentKey: string, value?: string) {
if (!isPopUp(componentKey)) return;
this.store.popUpData['key'] = componentKey;
this.store.popUpData['content'] = value ?? '';
},
hidePopUp() {
this.store.popUpData['key'] = null;
this.store.popUpData['content'] = '';
}
}
});
-3
View File
@@ -4,9 +4,6 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
// Update seconds cron for active data scheduler
const UPDATE_SECONDS = [3, 23, 43];
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
+10 -4
View File
@@ -26,8 +26,10 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined,
blockScroll: false,
modalLastClickedTarget: null
modalLastClickedTarget: null,
mousePos: { x: 0, y: 0 },
popUpData: { key: null, content: '' }
}) as StoreState,
getters: {
@@ -45,7 +47,8 @@ export const useMainStore = defineStore('store', {
const sceneryNames =
train.timetable?.sceneries?.map(
(sceneryHash) =>
this.activeSceneryList.find((st) => st.hash === sceneryHash)?.name ??
apiStore.activeData?.activeSceneries?.find((st) => st.stationHash === sceneryHash)
?.stationName ??
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
sceneryHash
) ?? [];
@@ -107,7 +110,10 @@ export const useMainStore = defineStore('store', {
if (
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
apiStore.activeData?.activeSceneries?.findIndex(
(sc) => sc.stationName === name && sc.region == train.region
(sc) =>
sc.stationName === name &&
sc.region == train.region &&
Date.now() - sc.lastSeen < 1000 * 60 * 2
) != -1
)
return acc;
+4 -7
View File
@@ -1,7 +1,9 @@
import { API } from '../typings/api';
import { Status } from '../typings/common';
export const popupKeys = ['DonatorPopUp', 'TrainCommentsPopUp', 'VehiclePreviewPopUp'] as const;
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type PopUpType = (typeof popupKeys)[number];
export interface RegionCounters {
stationCount: number;
@@ -11,22 +13,17 @@ export interface RegionCounters {
export interface StoreState {
region: { id: string; value: string };
isOffline: boolean;
isNewUpdate: boolean;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
driverStatsData?: API.DriverStats.Response;
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
blockScroll: boolean;
modalLastClickedTarget: EventTarget | null;
mousePos: { x: number; y: number };
popUpData: { key: PopUpType | null; content: string };
}
export interface StationRoutesInfo {
+1
View File
@@ -6,6 +6,7 @@
height: 90vh;
min-height: 550px;
margin-top: 0.5em;
position: relative;
padding-right: 0.2em;
}
+1 -2
View File
@@ -1,4 +1,4 @@
$animDuration: 150ms;
$animDuration: 95ms;
$animType: ease-in-out;
// List animation
@@ -72,7 +72,6 @@ $animType: ease-in-out;
&-enter-from,
&-leave-to {
transform: translateY(-25%);
opacity: 0;
}
}
+3 -1
View File
@@ -55,6 +55,8 @@ body {
-webkit-font-smoothing: antialiased !important;
overflow-y: scroll;
overflow-x: hidden;
position: relative;
&.no-scroll {
overflow-y: hidden;
@@ -234,7 +236,7 @@ a.a-button {
padding: 0.35em 0.75em;
img {
width: 1.5em;
width: 1.35em;
vertical-align: middle;
}
}