Compare commits

...

19 Commits

Author SHA1 Message Date
Spythere af9073ab98 Merge do wersji 1.17.1
Wersja 1.17.1
2023-10-03 21:58:23 +02:00
Spythere 800fc35e63 pliki lock 2023-10-03 21:53:59 +02:00
Spythere d5649d221b hotfixy 2023-10-03 21:51:00 +02:00
Spythere 5b35fac512 miniaturki c.d. 2023-10-03 21:31:06 +02:00
Spythere d5bc90f668 mock data 2023-10-02 22:09:27 +02:00
Spythere 6d663886f0 bump wersji 2023-10-02 22:06:58 +02:00
Spythere 85a1a0216e poprawki miniatur 2023-10-02 22:05:54 +02:00
Spythere 4ac054e947 miniaturki 2EN57 2023-10-01 15:11:53 +02:00
Spythere ba70fa1316 miniaturki pojazdów c.d. 2023-10-01 15:08:01 +02:00
Spythere 77e6b20d0c obsługa niezaładowanych miniaturek pojazdów 2023-10-01 01:41:14 +02:00
Spythere f60263c923 mocking danych 2023-10-01 00:23:17 +02:00
Spythere 6aec1a75c9 cleanup c.d. 2023-09-29 16:49:37 +02:00
Spythere d28d600833 code cleanup dziennika 2023-09-29 03:03:59 +02:00
Spythere a353eb3185 Meta tagi; aktualizacja paczek
Meta tagi; aktualizacja paczek
2023-09-26 14:44:48 +02:00
Spythere c5735a6953 pliki lock 2023-09-26 14:42:11 +02:00
Spythere 7930f7fc8a package.json 2023-09-26 14:36:01 +02:00
Spythere 68f4d54619 index: meta tagi 2023-09-26 14:33:58 +02:00
Spythere c4f9738589 Merge #54 (hotfix)
hotfix: wersja WS
2023-09-07 15:21:28 +02:00
Spythere dd916afd1d hotfix: wersja WS 2023-09-07 15:20:37 +02:00
60 changed files with 21678 additions and 6780 deletions
+1
View File
@@ -31,6 +31,7 @@ node_modules
.firebase .firebase
.firebaserc .firebaserc
# Env
.env .env
.fake .fake
+18 -3
View File
@@ -16,14 +16,30 @@
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#222222" />
<link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" /> <link rel="icon" href="favicon-64.png" sizes="64x64" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" /> <link rel="icon" href="favicon-62.png" sizes="62x62" type="image/png" />
<link rel="icon" href="favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" /> <link rel="icon" href="favicon-16.png" sizes="16x16" type="image/png" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<!-- Static OpenGraph meta -->
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Stacjownik" />
<meta property="og:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Stacjownik" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Stacjownik" />
<meta name="twitter:description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<meta name="twitter:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg" />
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@500;700&display=swap" rel="stylesheet" />
</head> </head>
@@ -32,4 +48,3 @@
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
</html> </html>
+3633 -2973
View File
File diff suppressed because it is too large Load Diff
+19 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.17.0", "version": "1.17.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -9,25 +9,26 @@
"preview": "yarn build && vite preview" "preview": "yarn build && vite preview"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.12.1", "core-js": "^3.32.2",
"dotenv": "^16.0.3", "dotenv": "^16.3.1",
"firebase": "^9.8.1", "firebase": "^10.4.0",
"howler": "^2.2.1", "howler": "^2.2.4",
"pinia": "^2.0.14", "pinia": "^2.1.6",
"sass": "^1.53.0", "sass": "^1.67.0",
"socket.io-client": "^4.4.1", "socket.io-client": "^4.7.2",
"vue": "^3.2.37", "vue": "^3.3.4",
"vue-i18n": "^9.1.6", "vue-i18n": "^9.4.1",
"vue-router": "^4.0.0-0" "vue-router": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.17", "@types/node": "^20.6.2",
"@vitejs/plugin-vue": "^4.0.0", "@vite-pwa/assets-generator": "^0.0.10",
"axios": "^1.2.1", "@vitejs/plugin-vue": "^4.3.4",
"typescript": "^4.9.4", "axios": "^1.5.0",
"vite": "^4.0.3", "typescript": "^5.2.2",
"vite-plugin-pwa": "^0.14.0", "vite": "^4.4.9",
"vue-tsc": "^1.0.18" "vite-plugin-pwa": "^0.16.5",
"vue-tsc": "^1.8.11"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.00251 14.9297L0 1.07422H6.14651L8.00251 4.27503L9.84583 1.07422H16L8.00251 14.9297Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 215 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

+42
View File
@@ -0,0 +1,42 @@
<template>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && list.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
export default defineComponent({
props: {
scrollNoMoreData: {
type: Boolean,
required: true,
},
scrollDataLoaded: {
type: Boolean,
required: true,
},
list: {
type: Array as PropType<any[]>,
required: true,
},
},
emits: ['addHistoryData'],
methods: {
addHistoryData() {
this.$emit('addHistoryData');
},
},
});
</script>
<style scoped></style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<div class="stock-list">
<ul>
<li v-for="(stockName, i) in trainStockList">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
<span>
<img
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${/^EN/.test(stockName) ? 'rb' : ''}.png`"
@error="onImageError($event, stockName)"
width="400"
height="60"
/>
<img
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')"
/>
<img
class="train-thumbnail"
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')"
/>
<img
class="train-thumbnail"
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')"
/>
</span>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { useStore } from '../../store/store';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({
mixins: [imageMixin],
props: {
trainStockList: {
type: Array as PropType<string[]>,
required: true,
},
},
data() {
return {
store: useStore(),
};
},
methods: {
onImageError(event: Event, stockName: string) {
const fallbackName =
Object.keys(this.store.rollingStockData!.info).find((type) => {
return this.store.rollingStockData!.info[type as keyof RollingStockInfo].find((v) => v[0] === stockName.split(':')[0]);
}) || 'vehicle-unknown';
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
},
},
});
</script>
<style lang="scss" scoped>
.stock-list {
display: flex;
justify-content: center;
}
.stock-list ul {
display: flex;
align-items: flex-end;
overflow: auto;
margin: 0 auto;
padding: 1em 0;
}
ul > li > span {
display: flex;
align-items: flex-end;
}
img {
max-height: 60px;
width: auto;
height: auto;
}
p {
text-align: center;
color: #aaa;
font-size: 0.9em;
margin-bottom: 1em;
}
</style>
+82
View File
@@ -0,0 +1,82 @@
<template>
<img class="train-thumbnail" :src="placeholderUrl" v-if="isNotFound" />
<img
class="train-thumbnail"
v-else
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${stockType == 'loco-ezt' ? 'rb' : ''}.png`"
@error="onImageError"
@load="onImageLoad"
width="220"
height="60"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import { useStore } from '../../store/store';
import { RollingStockInfo } from '../../scripts/interfaces/github_api/StockInfoGithubData';
export default defineComponent({
props: {
name: {
type: String,
required: true,
},
onlyFirstSegment: {
type: Boolean,
default: false,
},
},
data() {
return {
store: useStore(),
isNotFound: false,
isLoaded: false,
};
},
computed: {
url() {
return `https://rj.td2.info.pl/dist/img/thumbnails/${this.name.split(':')[0]}.png`;
},
placeholderUrl() {
return `/images/icon-${this.stockType}.png`;
},
stockType() {
if (!this.store.rollingStockData) return 'vehicle-unknown';
return (
Object.keys(this.store.rollingStockData.info).find((type) => {
return this.store.rollingStockData?.info[type as keyof RollingStockInfo].find((v) => v[0] === this.name.split(':')[0]);
}) || 'vehicle-unknown'
);
},
},
methods: {
onImageError() {
this.isNotFound = true;
this.isLoaded = false;
},
onImageLoad() {
this.isNotFound = false;
this.isLoaded = true;
},
},
});
</script>
<style lang="scss" scoped>
.train-thumbnail {
width: auto;
height: auto;
max-height: 60px;
}
</style>
@@ -82,13 +82,12 @@
</tbody> </tbody>
</table> </table>
<button <AddDataButton
class="btn btn--option btn--load-data" :list="dispatcherHistory"
v-if="!scrollNoMoreData && scrollDataLoaded && dispatcherHistory.length > 15" :scrollDataLoaded="scrollDataLoaded"
@click="addHistoryData" :scrollNoMoreData="scrollNoMoreData"
> @addHistoryData="addHistoryData"
{{ $t('journal.load-data') }} />
</button>
</div> </div>
</div> </div>
</transition> </transition>
@@ -113,9 +112,10 @@ import { DataStatus } from '../../scripts/enums/DataStatus';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { regions } from '../../data/options.json'; import { regions } from '../../data/options.json';
import AddDataButton from '../Global/AddDataButton.vue';
export default defineComponent({ export default defineComponent({
components: { Loading }, components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, imageMixin], mixins: [dateMixin, styleMixin, imageMixin],
@@ -195,6 +195,8 @@ table.scenery-history-table {
position: relative; position: relative;
text-align: center; text-align: center;
margin-bottom: 1em;
thead { thead {
position: sticky; position: sticky;
top: 0; top: 0;
@@ -208,7 +210,7 @@ table.scenery-history-table {
tr { tr {
background-color: var(--_bg-row); background-color: var(--_bg-row);
border-bottom: 2px solid black; border-bottom: 2px solid black;
&:last-child { &:last-child {
border: none; border: none;
} }
@@ -0,0 +1,82 @@
<template>
<div>
<transition name="status-anim" mode="out-in">
<div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else>
<TimetableHistoryList :timetableHistory="timetableHistory" />
<AddDataButton
:list="timetableHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
</div>
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { DataStatus } from '../../../scripts/enums/DataStatus';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import { useStore } from '../../../store/store';
import Loading from '../../Global/Loading.vue';
import ProgressBar from '../../Global/ProgressBar.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
export default defineComponent({
components: { ProgressBar, Loading, AddDataButton, TimetableHistoryList },
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
},
data() {
return {
DataStatus,
store: useStore(),
};
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss';
</style>
@@ -0,0 +1,165 @@
<template>
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{ currentHistoryIndex == 0 ? timetable.stockLength : stockHistory[currentHistoryIndex].stockLength || timetable.stockLength }}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor((currentHistoryIndex == 0 ? timetable.stockMass! : stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button class="btn--action" v-for="(sh, i) in stockHistory" :data-checked="i == currentHistoryIndex" @click.stop="currentHistoryIndex = i">
{{ sh.updatedAt }}
</button>
</div>
<!-- <StockList :trainStockList="currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')" /> -->
<StockList :trainStockList="(currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';') " />
<!-- <ul class="stock-list">
<li
v-for="(stockName, i) in (currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')"
:key="i"
>
<div>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</div>
<TrainThumbnail :name="stockName" />
</li>
</ul> -->
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import imageMixin from '../../../mixins/imageMixin';
import TrainThumbnail from '../../Global/TrainThumbnail.vue';
import StockList from '../../Global/StockList.vue';
export default defineComponent({
mixins: [imageMixin],
props: {
showExtraInfo: {
type: Boolean,
required: true,
},
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
data() {
return {
currentHistoryIndex: 0,
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
});
},
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
components: { TrainThumbnail, StockList },
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-extra {
margin-top: 0.5em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen() {
justify-content: center;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
</style>
@@ -0,0 +1,137 @@
<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="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
</span>
<span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</span>
<span class="general-time">
<b class="info-date"
>{{
new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? localeDateTime(timetable.createdAt, $i18n.locale)
: localeDateTime(timetable.beginDate, $i18n.locale)
}}
</b>
<b
class="info-badge"
:class="{
fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin],
props: {
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
methods: {
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-general {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
@include smallScreen() {
justify-content: center;
}
}
.info-date {
margin-right: 0.5em;
}
.info-badge {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25em;
}
</style>
@@ -0,0 +1,100 @@
<template>
<ul class="journal-list">
<transition-group name="list-anim">
<li
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<button class="btn--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Extra -->
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue';
import imageMixin from '../../../mixins/imageMixin';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableExtra from './TimetableExtra.vue';
export default defineComponent({
mixins: [imageMixin],
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false),
}));
},
},
methods: {},
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/JournalSection.scss';
.btn--show {
display: flex;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
hr {
margin: 0.25em 0;
}
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -0,0 +1,68 @@
<template>
<div class="item-status" style="margin: 0.5em 0">
<ProgressBar
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
/>
<span>
<span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }">
{{ timetable.currentDistance + ' km' }}
</span>
<span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span>
|
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span>
</span>
<span class="text--grayed" v-if="timetable.currentSceneryName">
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&lpar;</span>
<span v-if="timetable.currentLocation[1]">
{{ $t('journal.timetable-location-route') }} {{ timetable.currentLocation[1] }}
</span>
<span v-else-if="timetable.currentLocation[0]">
{{ $t('journal.timetable-location-signal') }} {{ timetable.currentLocation[0] }}
</span>
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&rpar;</span>
</b>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
import ProgressBar from '../../Global/ProgressBar.vue';
export default defineComponent({
components: { ProgressBar },
props: {
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
.item-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
@include smallScreen() {
justify-content: center;
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="stop-list" v-if="showExtraInfo == true">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { TimetableHistory } from '../../../scripts/interfaces/api/TimetablesAPIData';
export default defineComponent({
mixins: [dateMixin],
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<TimetableHistory>,
required: true,
},
},
computed: {
timetableStops() {
const timetable = this.timetable;
const stopNames = timetable.sceneriesString.split('%');
const beginDateHTML = ` (o. ${
timetable.beginDate != timetable.scheduledBeginDate
? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML = ` (p. ${
timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
return stopNames.map((stopName, i) => {
const confirmed = i < timetable.confirmedStopsCount;
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(timetable.checkpointDeparturesScheduled?.at(i));
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(timetable.checkpointArrivalsScheduled?.at(i));
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML =
(arrivalDateReal && arrivalDateScheduled && arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML =
(departureDateReal &&
departureDateScheduled &&
departureDateReal?.getTime() != departureDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
if (html) html = ` (${html})`;
return { stopName, html, confirmed };
});
},
},
});
</script>
<style lang="scss" scoped>
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
&-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
}
}
</style>
@@ -1,549 +0,0 @@
<template>
<div class="journal-list">
<transition name="status-anim" mode="out-in">
<div :key="dataStatus">
<div class="journal_warning" v-if="store.isOffline">
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == DataStatus.Loading" />
<div v-else-if="dataStatus == DataStatus.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }}
</div>
<div v-else>
<transition-group tag="ul" name="list-anim">
<li
v-for="{ timetable, stockHistoryComp, stops, showExtraInfo, ...item } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<div class="info-general">
<span
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
</span>
<span>
<strong class="text--primary">
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
</span>
&bull;
<strong
v-if="timetable.driverLevel !== null"
class="level-badge driver"
:style="calculateExpStyle(timetable.driverLevel, timetable.driverIsSupporter)"
>
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</span>
<span class="general-time">
<b class="info-date"
>{{
new Date(timetable.createdAt).getTime() - new Date(timetable.beginDate).getTime() < 0
? localeDateTime(timetable.createdAt, $i18n.locale)
: localeDateTime(timetable.beginDate, $i18n.locale)
}}
</b>
<b
class="info-badge"
:class="{
fulfilled: timetable.fulfilled,
terminated: timetable.terminated && !timetable.fulfilled,
active: !timetable.terminated,
}"
>
{{
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
</span>
</div>
<div class="info-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</div>
<hr />
<!-- Spis postojów -->
<div class="stop-list" v-if="showExtraInfo.value == true">
<span
v-for="(stop, i) in stops.filter((_, i) =>
!showExtraInfo.value ? i == 0 || i == stops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo.value && i == 1 && stops.length > 2">
... (+{{ stops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
<!-- Status RJ -->
<div class="info-status" style="margin: 0.5em 0">
<ProgressBar
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
/>
<span>
<span :style="{ color: timetable.fulfilled ? 'lightgreen' : timetable.terminated ? 'salmon' : '' }">
{{ timetable.currentDistance + ' km' }}
</span>
<span> / </span>
<span class="text--primary">{{ timetable.routeDistance }} km</span>
|
<span class="text--grayed">{{ timetable.confirmedStopsCount }}/{{ timetable.allStopsCount }}</span>
</span>
<span class="text--grayed" v-if="timetable.currentSceneryName">
<b>
{{ $t(`journal.${timetable.terminated ? 'last-seen-at' : 'currently-at'}`) }}
{{ timetable.currentSceneryName.replace(/.[a-zA-Z0-9]+.sc/, '') }}
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&lpar;</span>
<span v-if="timetable.currentLocation[1]">
{{ $t('journal.timetable-location-route') }} {{ timetable.currentLocation[1] }}
</span>
<span v-else-if="timetable.currentLocation[0]">
{{ $t('journal.timetable-location-signal') }} {{ timetable.currentLocation[0] }}
</span>
<span v-if="timetable.currentLocation[0] || timetable.currentLocation[1]">&rpar;</span>
</b>
</span>
</div>
<button class="btn--option btn--show">
{{ $t('journal.stock-info') }}
<img :src="getIcon(`arrow-${showExtraInfo.value ? 'asc' : 'desc'}`)" alt="Arrow" />
</button>
<!-- Dodatkowe informacje -->
<div class="info-extended" v-if="timetable.stockString && timetable.stockMass && showExtraInfo.value">
<hr />
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge specs-badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
item.currentHistoryIndex.value == 0
? timetable.stockLength
: stockHistoryComp[item.currentHistoryIndex.value].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge specs-badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(item.currentHistoryIndex.value == 0
? timetable.stockMass!
: stockHistoryComp[item.currentHistoryIndex.value].stockMass || timetable.stockMass) /
1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistoryComp.length > 1">
<button
class="btn--action"
v-for="(sh, i) in stockHistoryComp"
:data-checked="i == item.currentHistoryIndex.value"
@click.stop="item.currentHistoryIndex.value = i"
>
{{ sh.updatedAt }}
</button>
</div>
<ul class="stock-list">
<li
v-for="(car, i) in (item.currentHistoryIndex.value == 0
? timetable.stockString
: stockHistoryComp[item.currentHistoryIndex.value].stockString
).split(';')"
:key="i"
>
<img
@error="onImageError"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
:alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
</div>
</li>
</transition-group>
<button
class="btn btn--option btn--load-data"
v-if="!scrollNoMoreData && scrollDataLoaded && timetableHistory.length >= 15"
@click="addHistoryData"
>
{{ $t('journal.load-data') }}
</button>
</div>
</div>
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">{{ $t('journal.loading-further-data') }}</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import imageMixin from '../../mixins/imageMixin';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import styleMixin from '../../mixins/styleMixin';
import { DataStatus } from '../../scripts/enums/DataStatus';
import { TimetableHistory } from '../../scripts/interfaces/api/TimetablesAPIData';
import { useStore } from '../../store/store';
import Loading from '../Global/Loading.vue';
import ProgressBar from '../Global/ProgressBar.vue';
export default defineComponent({
components: { ProgressBar, Loading },
mixins: [dateMixin, imageMixin, modalTrainMixin, styleMixin],
props: {
timetableHistory: {
type: Array as PropType<TimetableHistory[]>,
required: true,
},
scrollNoMoreData: {
type: Boolean,
},
scrollDataLoaded: {
type: Boolean,
},
addHistoryData: {
type: Function as PropType<() => void>,
},
dataStatus: {
type: Number as PropType<DataStatus>,
},
},
data() {
return {
DataStatus,
store: useStore(),
};
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
stockHistoryComp: timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit',
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined,
};
}),
showExtraInfo: ref(false),
stops: this.getTimetableStops(timetable),
currentHistoryIndex: ref(0),
}));
},
},
methods: {
getTimetableStops(timetable: TimetableHistory) {
const stopNames = timetable.sceneriesString.split('%');
const beginDateHTML = ` (o. ${
timetable.beginDate != timetable.scheduledBeginDate
? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML = ` (p. ${
timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
return stopNames.map((stopName, i) => {
const confirmed = i < timetable.confirmedStopsCount;
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(timetable.checkpointDeparturesScheduled?.at(i));
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(timetable.checkpointArrivalsScheduled?.at(i));
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML =
(arrivalDateReal && arrivalDateScheduled && arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML =
(departureDateReal &&
departureDateScheduled &&
departureDateReal?.getTime() != departureDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
if (html) html = ` (${html})`;
return { stopName, html, confirmed };
});
},
showTimetable(timetable: TimetableHistory, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = this.getImage('unknown.png');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/animations.scss';
@import '../../styles/variables.scss';
@import '../../styles/responsive.scss';
@import '../../styles/badge.scss';
@import '../../styles/JournalSection.scss';
.journal_item {
cursor: pointer;
}
hr {
margin: 0.25em 0;
}
.info {
&-date {
margin-right: 0.5em;
}
&-badge {
padding: 0.05em 0.35em;
color: black;
&.terminated {
background-color: salmon;
}
&.fulfilled {
background-color: lightgreen;
}
&.active {
background-color: lightblue;
}
}
&-general {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
}
&-route {
margin: 0.25em 0;
}
&-extended {
margin-top: 0.5em;
}
&-status {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25em;
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
margin-top: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
li > img {
vertical-align: text-bottom;
max-height: 60px;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.specs-badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
// badge.scss
.badges {
display: flex;
gap: 0.25em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
&-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
}
}
.btn--show {
display: flex;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
@include smallScreen {
.journal_item-info {
text-align: center;
}
.info-route {
display: flex;
justify-content: center;
}
.info-status {
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
.info-general,
.general-train,
.stock-specs,
.stock-history {
justify-content: center;
}
}
</style>
+7 -19
View File
@@ -2,7 +2,7 @@
<div class="scenery-info"> <div class="scenery-info">
<section v-if="!timetableOnly"> <section v-if="!timetableOnly">
<div class="scenery-info-general" v-if="station.generalInfo"> <div class="scenery-info-general" v-if="station.generalInfo">
<scenery-info-icons :station="station" /> <SceneryInfoIcons :station="station" />
<div class="scenery-general-list"> <div class="scenery-general-list">
<span> <span>
@@ -26,28 +26,16 @@
</span> </span>
<span v-if="station.generalInfo.project"> <span v-if="station.generalInfo.project">
&bull; <b>{{ $t('scenery.project-title') }}: </b> &bull; <b>{{ $t('scenery.project-title') }}: </b>
<a <a style="color: salmon; text-decoration: underline; font-weight: bold" :href="station.generalInfo.projectUrl" target="_blank">
style="color: salmon; text-decoration: underline; font-weight: bold"
:href="station.generalInfo.projectUrl"
target="_blank"
>
{{ station.generalInfo.project }} {{ station.generalInfo.project }}
</a> </a>
</span> </span>
</div> </div>
<scenery-info-routes :station="station" /> <SceneryInfoRoutes :station="station" />
<div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0"> <div class="scenery-authors" v-if="station.generalInfo.authors && station.generalInfo.authors.length > 0">
<b> <b> {{ $t('scenery.authors-title', { authors: station.generalInfo.authors.length }, station.generalInfo.authors.length) }}: </b>
{{
$t(
'scenery.authors-title',
{ authors: station.generalInfo.authors.length },
station.generalInfo.authors.length
)
}}:
</b>
{{ station.generalInfo.authors.join(', ') }} {{ station.generalInfo.authors.join(', ') }}
</div> </div>
</div> </div>
@@ -55,14 +43,14 @@
<div style="margin: 2em 0; height: 2px; background-color: white"></div> <div style="margin: 2em 0; height: 2px; background-color: white"></div>
<!-- info dispatcher --> <!-- info dispatcher -->
<scenery-info-dispatcher :station="station" :onlineFrom="onlineFrom" /> <SceneryInfoDispatcher :station="station" :onlineFrom="onlineFrom" />
<div class="info-lists"> <div class="info-lists">
<!-- user list --> <!-- user list -->
<scenery-info-user-list :station="station" /> <SceneryInfoUserList :station="station" />
<!-- spawn list --> <!-- spawn list -->
<scenery-info-spawn-list :station="station" /> <SceneryInfoSpawnList :station="station" />
</div> </div>
</section> </section>
</div> </div>
+33 -41
View File
@@ -14,19 +14,11 @@
</span> </span>
<span class="header_links"> <span class="header_links">
<a <a :href="`https://pragotron-td2.web.app/board?name=${station.name}`" target="_blank" :title="$t('scenery.pragotron-link')">
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img :src="getIcon('pragotron')" alt="icon-pragotron" /> <img :src="getIcon('pragotron')" alt="icon-pragotron" />
</a> </a>
<a <a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
:href="`https://tablice-td2.web.app/?station=${station.name}`"
target="_blank"
:title="$t('scenery.tablice-link')"
>
<img :src="getIcon('tablice', 'ico')" alt="icon-tablice" /> <img :src="getIcon('tablice', 'ico')" alt="icon-tablice" />
</a> </a>
</span> </span>
@@ -39,8 +31,8 @@
<button <button
:key="cp.checkpointName" :key="cp.checkpointName"
class="checkpoint_item" class="checkpoint_item"
:class="{ current: selectedCheckpoint === cp.checkpointName }" :class="{ current: chosenCheckpoint === cp.checkpointName }"
@click="selectCheckpoint(cp)" @click="setCheckpoint(cp)"
> >
{{ cp.checkpointName }} {{ cp.checkpointName }}
</button> </button>
@@ -106,15 +98,12 @@
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</s>
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
}}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.arrivalDelay }})
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -146,15 +135,12 @@
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</s>
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
}}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' }}{{ scheduledTrain.stopInfo.departureDelay }})
}}{{ scheduledTrain.stopInfo.departureDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -203,16 +189,22 @@ export default defineComponent({
listOpen: false, listOpen: false,
}), }),
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
setup(props) { setup(props) {
const route = useRoute(); const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`); const currentURL = computed(() => `${location.origin}${route.fullPath}`);
const store = useStore(); const store = useStore();
const selectedCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints?.length == 0 ? '' : props.station?.generalInfo?.checkpoints[0].checkpointName || null
? ''
: props.station?.generalInfo?.checkpoints[0].checkpointName || ''
); );
const computedScheduledTrains = computed(() => { const computedScheduledTrains = computed(() => {
@@ -221,8 +213,7 @@ export default defineComponent({
const station = props.station as Station; const station = props.station as Station;
let scheduledTrains = let scheduledTrains =
station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === selectedCheckpoint.value) station.generalInfo?.checkpoints.find((cp) => cp.checkpointName === chosenCheckpoint.value)?.scheduledTrains ||
?.scheduledTrains ||
station.onlineInfo?.scheduledTrains || station.onlineInfo?.scheduledTrains ||
[]; [];
@@ -243,12 +234,21 @@ export default defineComponent({
return { return {
currentURL, currentURL,
selectedCheckpoint, chosenCheckpoint,
computedScheduledTrains, computedScheduledTrains,
store, store,
}; };
}, },
computed: {
tabliceZbiorczeHref() {
let url = `https://tablice-td2.web.app/?station=${this.station.name}`;
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
return url;
},
},
methods: { methods: {
loadSelectedOption() { loadSelectedOption() {
if (!this.station) return; if (!this.station) return;
@@ -256,27 +256,19 @@ export default defineComponent({
if (!this.station.generalInfo.checkpoints) return; if (!this.station.generalInfo.checkpoints) return;
if (this.station.generalInfo.checkpoints.length == 0) return; if (this.station.generalInfo.checkpoints.length == 0) return;
if (this.selectedCheckpoint != '') return; if (this.chosenCheckpoint != '') return;
this.selectedCheckpoint = this.station.generalInfo.checkpoints[0].checkpointName; this.chosenCheckpoint = this.station.generalInfo.checkpoints[0].checkpointName;
}, },
selectCheckpoint(cp: { checkpointName: string }) { setCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName; this.chosenCheckpoint = cp.checkpointName;
}, },
showTimetableOnlyView() { showTimetableOnlyView() {
this.$router.push(`${this.$route.fullPath}&timetableOnly=1`); this.$router.push(`${this.$route.fullPath}&timetableOnly=1`);
}, },
}, },
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
}); });
</script> </script>
+14 -11
View File
@@ -62,15 +62,13 @@
</section> </section>
<section class="train-stats"> <section class="train-stats">
<div> <TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" />
<img :src="train.locoURL" loading="lazy" alt="Loco image not found" @error="onImageError" />
</div>
<div class="text--grayed"> <div class="text--grayed">
{{ train.locoType }} {{ train.locoType }}
<span v-if="train.cars.length > 0"> <span v-if="train.stockList.length > 1">
&nbsp;&bull; {{ $t('trains.cars') }}: &nbsp;&bull; {{ $t('trains.cars') }}:
<span class="count">{{ train.cars.length }}</span> <span class="count">{{ train.stockList.length - 1 }}</span>
</span> </span>
</div> </div>
@@ -91,6 +89,7 @@ import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train'; import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue'; import ProgressBar from '../Global/ProgressBar.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -104,10 +103,17 @@ export default defineComponent({
}, },
}, },
mixins: [trainInfoMixin, imageMixin, styleMixin], mixins: [trainInfoMixin, imageMixin, styleMixin],
components: { ProgressBar }, components: { ProgressBar, TrainThumbnail },
}); });
</script> </script>
<!-- Global style for TrainThumbnail -->
<style lang="scss">
.train-stats .train-thumbnail {
max-width: 100%;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; @import '../../styles/badge.scss';
@@ -121,15 +127,12 @@ export default defineComponent({
.train-stats { .train-stats {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-content: center; align-items: center;
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
img { gap: 0.25em;
margin: 0.5em 0;
width: 12em;
}
} }
.train-info { .train-info {
+14 -62
View File
@@ -1,38 +1,15 @@
<template> <template>
<div class="train-schedule" @click="toggleShowState"> <div class="train-schedule" @click="toggleShowState">
<div class="train-stock"> <StockList :trainStockList="train.stockList" />
<ul class="stock-list">
<li>
<img class="train-image" :src="train.locoURL" alt="loco" @error="onImageError" />
<div>{{ train.locoType }}</div>
</li>
<li v-if="train.locoType.startsWith('EN')"> <!-- <div class="train-stock"> -->
<img :src="train.locoURL.replace('rb', 's')" @error="onImageError" alt="" /> <!-- <ul>
<div>{{ train.locoType }}S</div> <li v-for="(stockName, i) in train.stockList" :key="i">
<p>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</p>
<TrainThumbnail :name="stockName" />
</li> </li>
</ul> -->
<li v-if="train.locoType.startsWith('EN71')"> <!-- </div> -->
<img :src="train.locoURL.replace('rb', 's')" @error="onImageError" alt="" />
<div>{{ train.locoType }}S</div>
</li>
<li v-if="train.locoType.startsWith('EN')">
<img :src="train.locoURL.replace('rb', 'ra')" @error="onImageError" alt="" />
<div>{{ train.locoType }}RA</div>
</li>
<li v-for="(car, i) in train.cars" :key="i">
<img
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${car.split(':')[0]}.png`"
@error="onImageError"
alt="car"
/>
<div>{{ car.replace(/_/g, ' ').split(':')[0] }}</div>
</li>
</ul>
</div>
<div class="schedule-wrapper" v-if="train.timetableData"> <div class="schedule-wrapper" v-if="train.timetableData">
<ul class="stop_list"> <ul class="stop_list">
@@ -60,9 +37,7 @@
<b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span> <b>{{ stop.stopNameRAW }} </b>: <span v-html="stop.comments"></span>
</div> </div>
<span <span v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)">
v-if="stop.departureLine == train.timetableData!.followingStops[i + 1].arrivalLine && !/sbl/gi.test(stop.departureLine!)"
>
{{ stop.departureLine }} {{ stop.departureLine }}
</span> </span>
@@ -91,9 +66,11 @@ import Train from '../../scripts/interfaces/Train';
import TrainStop from '../../scripts/interfaces/TrainStop'; import TrainStop from '../../scripts/interfaces/TrainStop';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import StopDate from '../Global/StopDate.vue'; import StopDate from '../Global/StopDate.vue';
import TrainThumbnail from '../Global/TrainThumbnail.vue';
import StockList from '../Global/StockList.vue';
export default defineComponent({ export default defineComponent({
components: { StopDate }, components: { StopDate, TrainThumbnail, StockList },
props: { props: {
train: { train: {
type: Object as PropType<Train>, type: Object as PropType<Train>,
@@ -145,8 +122,7 @@ export default defineComponent({
end: stop.terminatesHere, end: stop.terminatesHere,
delayed: stop.departureDelay > 0, delayed: stop.departureDelay > 0,
sbl: /sbl/gi.test(stop.stopName), sbl: /sbl/gi.test(stop.stopName),
[stop.stopType.replaceAll(', ', '-')]: [stop.stopType.replaceAll(', ', '-')]: stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
stop.stopType.match(new RegExp('ph|pm|pt')) && !stop.confirmed && !stop.beginsHere,
'minor-stop-active': this.activeMinorStops.includes(index), 'minor-stop-active': this.activeMinorStops.includes(index),
'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere, 'last-confirmed': index == this.lastConfirmed && !stop.terminatesHere,
}; };
@@ -179,30 +155,7 @@ $stopNameClr: #22a8d1;
} }
.train-schedule { .train-schedule {
padding: 0 0.25em; padding: 0 1em;
}
.train-stock {
padding: 0.25em 0.5em;
display: flex;
justify-content: center;
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 1em;
li > div {
text-align: center;
color: #aaa;
font-size: 0.9em;
}
img {
max-height: 60px;
}
} }
.schedule-wrapper { .schedule-wrapper {
@@ -426,4 +379,3 @@ ul.stop_list > li.stop {
} }
} }
</style> </style>
-337
View File
@@ -1,337 +0,0 @@
<template>
<div class="train-stats" v-click-outside="closeStats">
<action-button class="stats_button" @click="toggleStatsOpen">
<img :src="getIcon('stats')" :alt="$t('trains.stats')" />
<p>{{ $t('trains.stats') }}</p>
</action-button>
<transition name="stats-anim" class="stats_wrapper" tag="div">
<div class="stats-body" v-if="trainStatsOpen">
<h2 class="stats-header">
<img :src="getIcon('stats')" :alt="$t('trains.stats')" />
{{ $t('trains.stats') }}
</h2>
<div class="stats-speed">
<div class="title stats-title">
{{ $t('trains.stats-speed') }}
</div>
<div class="stats-content">{{ speedStats.min }} | {{ speedStats.avg }} | {{ speedStats.max }}</div>
</div>
<div class="stats-length">
<div class="title stats-title">
{{ $t('trains.stats-length') }}
</div>
<div class="stats-content">
{{ timetableStats.min }} | {{ timetableStats.avg }} |
{{ timetableStats.max }}
</div>
</div>
<div class="stats-categories">
<div class="title stats-title">
{{ $t('trains.stats-categories') }}
</div>
<div class="category-list">
<span class="category" v-for="[key, value] of categoryList" :key="key">
<span class="category-type">{{ key }}</span>
<span class="category-count">{{ value }}</span>
</span>
</div>
<div class="special-list">
<span class="special twr">
<span class="special-type">{{ $t('trains.stats-special-twr') }}</span>
<span class="special-count">{{ specialTrainCount[0] }}</span>
</span>
<span class="special skr">
<span class="special-type">{{ $t('trains.stats-special-skr') }}</span>
<span class="special-count">{{ specialTrainCount[1] }}</span>
</span>
</div>
</div>
<div class="stats-locos">
<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">{{ loco[0] }} | {{ loco[1] }}</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, inject } from 'vue';
import imageMixin from '../../mixins/imageMixin';
import Train from '../../scripts/interfaces/Train';
import ActionButton from '../Global/ActionButton.vue';
export default defineComponent({
components: { ActionButton },
mixins: [imageMixin],
props: {
trains: {
type: Array as () => Train[],
required: true,
},
},
data: () => ({
trainStatsOpen: false,
}),
methods: {
toggleStatsOpen() {
this.trainStatsOpen = !this.trainStatsOpen;
},
closeStats() {
this.trainStatsOpen = false;
},
},
setup(props) {
const speedStats = computed(() => {
if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const trainList = props.trains.filter((train) => train.timetableData);
const avg = (trainList.reduce((acc, train) => acc + train.speed, 0) / trainList.length).toFixed(2);
const minMaxSpeed = trainList.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] = !acc[0] || train.speed < acc[0] ? train.speed : acc[0];
acc[1] = !acc[1] || train.speed > acc[1] ? train.speed : acc[1];
return acc;
}, [] as any);
return {
avg,
min: minMaxSpeed[0].toString(),
max: minMaxSpeed[1].toString(),
};
});
const timetableStats = computed(() => {
if (props.trains.length == 0) return { avg: '0', min: '0', max: '0' };
const activeTrainsLength = props.trains.filter((train) => train.timetableData).length;
const avg = (
props.trains.reduce((acc, train) => (train.timetableData ? acc + train.timetableData.routeDistance : acc), 0) /
activeTrainsLength
).toFixed(2);
const minMaxDistance = props.trains.reduce((acc, train) => {
if (!train.timetableData) return acc;
acc[0] = !acc[0] || train.timetableData.routeDistance < acc[0] ? train.timetableData.routeDistance : acc[0];
acc[1] = !acc[1] || train.timetableData.routeDistance > acc[1] ? train.timetableData.routeDistance : acc[1];
return acc;
}, [] as any);
return {
avg,
min: minMaxDistance[0].toString(),
max: minMaxDistance[1].toString(),
};
});
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
);
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 [twrList.length, skrList.length];
});
/* Inject list from TrainsView for category filter */
const chosenTrainCategories = inject('chosenTrainCategories') as string[];
return {
speedStats,
timetableStats,
categoryList,
locoList,
specialTrainCount,
chosenTrainCategories,
};
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive';
.stats-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateY(30px);
}
}
.train-stats {
position: relative;
top: 0;
z-index: 15;
}
.stats {
&_wrapper {
margin-bottom: 0.5em;
outline: none;
}
&-header {
display: flex;
margin-bottom: 0.85em;
img {
vertical-align: middle;
margin-right: 0.35em;
}
}
&-body {
position: absolute;
display: inline-block;
max-width: 700px;
width: 100%;
top: 100%;
left: 0;
background: #222;
border-radius: 0 1em 1em 1em;
padding: 1em;
}
&-content {
color: #ddd;
}
}
/* .category {
cursor: pointer;
} */
.category,
.special {
&-list {
display: flex;
flex-wrap: wrap;
}
margin-right: 0.4em;
margin-bottom: 0.4em;
&-type,
&-count {
display: inline-block;
padding: 0.2em 0.4em;
}
&-type {
background: #585858;
font-weight: 600;
}
&-count {
background: #ffc014;
color: black;
}
}
.special {
&-list {
font-size: 0.85em;
}
&-count {
background: gray;
color: white;
}
&.twr > &-type {
background-color: var(--clr-twr);
color: black;
}
&.skr > &-type {
background-color: var(--clr-skr);
color: white;
}
}
.warning {
display: inline-block;
margin-right: 0.4em;
padding: 0.2em 0.3em;
color: black;
font-weight: bold;
font-size: 0.85em;
}
@include smallScreen {
.stats-body {
display: block;
width: 100%;
border-radius: 0 0 1em 1em;
}
.train-stats {
display: flex;
justify-content: center;
}
}
</style>
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -15,12 +15,11 @@ export default interface Train {
driverLevel: number; driverLevel: number;
currentStationName: string; currentStationName: string;
currentStationHash: string; currentStationHash: string;
locoURL: string;
locoType: string; locoType: string;
online: boolean; online: boolean;
lastSeen: number; lastSeen: number;
region: string; region: string;
cars: string[]; stockList: string[];
isTimeout: boolean; isTimeout: boolean;
isSupporter: boolean; isSupporter: boolean;
+1 -1
View File
@@ -11,7 +11,7 @@ export default interface StationAPIData {
lastSeen: number; lastSeen: number;
dispatcherExp: number; dispatcherExp: number;
nameFromHeader: string; nameFromHeader: string;
spawnString: string; spawnString: string | null;
networkConnectionString: string; networkConnectionString: string;
isOnline: number; isOnline: number;
dispatcherRate: number; dispatcherRate: number;
@@ -0,0 +1,13 @@
export interface RollingStockGithubData {
usage: Record<string, string>;
info: RollingStockInfo;
}
export interface RollingStockInfo {
'loco-e': [string, string, string, string, boolean][];
'loco-s': [string, string, string, string, boolean][];
'loco-szt': [string, string, string, string, boolean][];
'loco-ezt': [string, string, string, string, boolean][];
'car-passenger': [string, string, boolean, boolean, string][];
'car-cargo': [string, string, boolean, boolean, string][];
}
+2 -1
View File
@@ -6,7 +6,7 @@ import Station from '../Station';
import Train from '../Train'; import Train from '../Train';
import { DispatcherStatsAPIData } from '../api/DispatcherStatsAPIData'; import { DispatcherStatsAPIData } from '../api/DispatcherStatsAPIData';
import { DriverStatsAPIData } from '../api/DriverStatsAPIData'; import { DriverStatsAPIData } from '../api/DriverStatsAPIData';
import { Ref } from 'vue'; import { RollingStockGithubData } from '../github_api/StockInfoGithubData';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
@@ -14,6 +14,7 @@ export interface StoreState {
stationList: Station[]; stationList: Station[];
trainList: Train[]; trainList: Train[];
apiData: APIData; apiData: APIData;
rollingStockData?: RollingStockGithubData;
lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[]; lastDispatcherStatuses: { hash: string; statusTimestamp: number; statusID: string }[];
+1 -1
View File
@@ -1,6 +1,6 @@
export const URLs = { export const URLs = {
stacjownikAPI: stacjownikAPI:
import.meta.env.VITE_APP_API_DEV == 1 && !import.meta.env.PROD import.meta.env.VITE_APP_API_DEV === "1" && !import.meta.env.PROD
? 'http://localhost:3001' ? 'http://localhost:3001'
: 'https://stacjownik.spythere.pl', : 'https://stacjownik.spythere.pl',
stacjownikAPIDev: 'localhost:3000', stacjownikAPIDev: 'localhost:3000',
+2 -3
View File
@@ -2,8 +2,7 @@ import { ScheduledTrain, StopStatus } from '../interfaces/ScheduledTrain';
import Train from '../interfaces/Train'; import Train from '../interfaces/Train';
import TrainStop from '../interfaces/TrainStop'; import TrainStop from '../interfaces/TrainStop';
export const getLocoURL = (locoType: string): string => export const getLocoURL = (locoType: string): string => `https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes('EN') ? locoType + 'rb' : locoType}.png`;
`https://rj.td2.info.pl/dist/img/thumbnails/${locoType.includes('EN') ? locoType + 'rb' : locoType}.png`;
export const getStatusID = (stationStatus: any): string => { export const getStatusID = (stationStatus: any): string => {
if (!stationStatus) return 'unknown'; if (!stationStatus) return 'unknown';
@@ -58,7 +57,7 @@ export const getStatusTimestamp = (stationStatus: any): number => {
return -1; return -1;
}; };
export const parseSpawns = (spawnString: string) => { export const parseSpawns = (spawnString: string | null) => {
if (!spawnString) return []; if (!spawnString) return [];
if (spawnString === 'NO_SPAWN') return []; if (spawnString === 'NO_SPAWN') return [];
-9
View File
@@ -1,9 +0,0 @@
import { defineStore } from 'pinia';
export const useJournalFiltersStore = defineStore('journalFiltersStore', {
state: () => ({
timetableFilters: {
},
}),
});
+44 -54
View File
@@ -8,19 +8,16 @@ import Station from '../scripts/interfaces/Station';
import StationRoutes from '../scripts/interfaces/StationRoutes'; import StationRoutes from '../scripts/interfaces/StationRoutes';
import Train from '../scripts/interfaces/Train'; import Train from '../scripts/interfaces/Train';
import { URLs } from '../scripts/utils/apiURLs'; import { URLs } from '../scripts/utils/apiURLs';
import { import { getStatusTimestamp, getStatusID, getScheduledTrain, parseSpawns } from '../scripts/utils/storeUtils';
getLocoURL,
getStatusTimestamp,
getStatusID,
getScheduledTrain,
parseSpawns,
} from '../scripts/utils/storeUtils';
import { APIData, StationJSONData, StoreState } from '../scripts/interfaces/store/storeTypes'; import { APIData, StationJSONData, StoreState } from '../scripts/interfaces/store/storeTypes';
import packageInfo from '../../package.json';
import { RollingStockInfo, RollingStockGithubData } from '../scripts/interfaces/github_api/StockInfoGithubData';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => state: () =>
({ ({
apiData: {} as unknown, apiData: {} as unknown,
rollingStockData: undefined,
stationList: [], stationList: [],
trainList: [], trainList: [],
@@ -58,7 +55,7 @@ export const useStore = defineStore('store', {
blockScroll: false, blockScroll: false,
listenerLaunched: false, listenerLaunched: false,
modalLastClickedTarget: null modalLastClickedTarget: null,
} as StoreState), } as StoreState),
actions: { actions: {
@@ -68,10 +65,7 @@ export const useStore = defineStore('store', {
if (!trains) return []; if (!trains) return [];
this.trainList = trains this.trainList = trains
.filter( .filter((train) => train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000))
(train) =>
train.region === this.region.id && (train.online || train.timetable || train.lastSeen > Date.now() - 180000)
)
.map((train) => { .map((train) => {
const stock = train.stockString.split(';'); const stock = train.stockString.split(';');
const locoType = stock ? stock[0] : train.stockString; const locoType = stock ? stock[0] : train.stockString;
@@ -95,9 +89,8 @@ export const useStore = defineStore('store', {
currentStationName: train.currentStationName, currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash, currentStationHash: train.currentStationHash,
connectedTrack: train.connectedTrack, connectedTrack: train.connectedTrack,
stockList: stock,
locoType, locoType,
locoURL: getLocoURL(locoType),
cars: stock.slice(1),
lastSeen: train.lastSeen, lastSeen: train.lastSeen,
isTimeout: train.isTimeout, isTimeout: train.isTimeout,
@@ -124,20 +117,12 @@ export const useStore = defineStore('store', {
getDispatcherStatus(onlineStationData: StationAPIData) { getDispatcherStatus(onlineStationData: StationAPIData) {
const { dispatchers } = this.apiData; const { dispatchers } = this.apiData;
const prevDispatcherStatus = this.lastDispatcherStatuses.find( const prevDispatcherStatus = this.lastDispatcherStatuses.find((dispatcher) => dispatcher.hash === onlineStationData.stationHash);
(dispatcher) => dispatcher.hash === onlineStationData.stationHash
);
const stationStatus = !dispatchers const stationStatus = !dispatchers ? undefined : dispatchers.find((status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id) || -1;
? undefined
: dispatchers.find(
(status: string[]) => status[0] == onlineStationData.stationHash && status[1] == this.region.id
) || -1;
const statusTimestamp = const statusTimestamp = prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus);
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusTimestamp : getStatusTimestamp(stationStatus); const statusID = prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
const statusID =
prevDispatcherStatus && !dispatchers ? prevDispatcherStatus.statusID : getStatusID(stationStatus);
return { return {
hash: onlineStationData.stationHash, hash: onlineStationData.stationHash,
@@ -161,26 +146,17 @@ export const useStore = defineStore('store', {
const stopName = stop.stopNameRAW.toLowerCase(); const stopName = stop.stopNameRAW.toLowerCase();
if (stationName === stopName) return true; if (stationName === stopName) return true;
if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) if (stopName.includes(stationName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) return true;
return true;
if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) if (stationName.includes(stopName) && !stop.stopName.includes('po.') && !stop.stopName.includes('podg.')) return true;
return true;
if ( if (stopName.includes('podg.') && stopName.split(', podg.')[0] && stationName.includes(stopName.split(', podg.')[0])) return true;
stopName.includes('podg.') &&
stopName.split(', podg.')[0] &&
stationName.includes(stopName.split(', podg.')[0])
)
return true;
if ( if (
stationGeneralInfo && stationGeneralInfo &&
stationGeneralInfo.checkpoints && stationGeneralInfo.checkpoints &&
stationGeneralInfo.checkpoints.length > 0 && stationGeneralInfo.checkpoints.length > 0 &&
stationGeneralInfo.checkpoints.some((cp) => stationGeneralInfo.checkpoints.some((cp) => cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase()))
cp.checkpointName.toLowerCase().includes(stop.stopNameRAW.toLowerCase())
)
) )
return true; return true;
@@ -193,9 +169,7 @@ export const useStore = defineStore('store', {
if (stationGeneralInfo?.checkpoints) { if (stationGeneralInfo?.checkpoints) {
for (const checkpoint of stationGeneralInfo.checkpoints) { for (const checkpoint of stationGeneralInfo.checkpoints) {
const index = timetable.followingStops.findIndex( const index = timetable.followingStops.findIndex((stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase());
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase()
);
if (index == -1) continue; if (index == -1) continue;
@@ -211,10 +185,7 @@ export const useStore = defineStore('store', {
getStationTrains(stationAPIData: StationAPIData) { getStationTrains(stationAPIData: StationAPIData) {
return this.trainList return this.trainList
.filter( .filter((train) => train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName)
(train) =>
train?.region === this.region.id && train.online && train.currentStationName === stationAPIData.stationName
)
.map((train) => ({ .map((train) => ({
driverName: train.driverName, driverName: train.driverName,
driverId: train.driverId, driverId: train.driverId,
@@ -304,9 +275,7 @@ export const useStore = defineStore('store', {
routes: routes:
scenery.routesInfo.reduce( scenery.routesInfo.reduce(
(acc, route) => { (acc, route) => {
const propName: keyof StationRoutes = `${route.routeTracks == 2 ? 'twoWay' : 'oneWay'}${ const propName: keyof StationRoutes = `${route.routeTracks == 2 ? 'twoWay' : 'oneWay'}${route.isElectric ? '' : 'No'}CatenaryRouteNames`;
route.isElectric ? '' : 'No'
}CatenaryRouteNames`;
acc[route.routeTracks == 2 ? 'twoWay' : 'oneWay'].push({ acc[route.routeTracks == 2 ? 'twoWay' : 'oneWay'].push({
name: route.routeName, name: route.routeName,
@@ -335,19 +304,31 @@ export const useStore = defineStore('store', {
twoWayNoCatenaryRouteNames: [], twoWayNoCatenaryRouteNames: [],
} as StationRoutes } as StationRoutes
) || {}, ) || {},
checkpoints: scenery.checkpoints checkpoints: scenery.checkpoints ? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] })) : [],
? scenery.checkpoints.split(';').map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: [],
}, },
}; };
}); });
}, },
connectToWebsocket() { async connectToWebsocket() {
if (import.meta.env.VITE_APP_WS_DEV === '1') {
const mockWebsocketData = await import('../data/mockWebsocketData.json');
this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = mockWebsocketData as any;
this.setOnlineData();
console.warn('Stacjownik działa w trybie mockowania danych z WS');
return;
}
const socket = io(URLs.stacjownikAPI, { const socket = io(URLs.stacjownikAPI, {
// transports: ['websocket', 'polling'], // transports: ['websocket', 'polling'],
rememberUpgrade: true, rememberUpgrade: true,
reconnection: true, reconnection: true,
extraHeaders: {
version: packageInfo.version,
},
}); });
socket.on('connect_error', (err) => { socket.on('connect_error', (err) => {
@@ -360,7 +341,7 @@ export const useStore = defineStore('store', {
this.setOnlineData(); this.setOnlineData();
}); });
socket.emit('FETCH_DATA', {}, (data: APIData) => { socket.emit('FETCH_DATA', { version: packageInfo.version }, (data: APIData) => {
this.dataStatuses.connection = DataStatus.Loaded; this.dataStatuses.connection = DataStatus.Loaded;
this.apiData = data; this.apiData = data;
@@ -372,6 +353,7 @@ export const useStore = defineStore('store', {
async connectToAPI() { async connectToAPI() {
await this.fetchStationsGeneralInfo(); await this.fetchStationsGeneralInfo();
await this.fetchStockInfoData();
this.connectToWebsocket(); this.connectToWebsocket();
}, },
@@ -382,6 +364,14 @@ export const useStore = defineStore('store', {
await this.setOnlineData(); await this.setOnlineData();
}, },
async fetchStockInfoData() {
try {
this.rollingStockData = (await axios.get<RollingStockGithubData>('https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json')).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async setOnlineData() { async setOnlineData() {
if (!this.apiData.stations) { if (!this.apiData.stations) {
this.dataStatuses.sceneries = DataStatus.Error; this.dataStatuses.sceneries = DataStatus.Error;
+3 -18
View File
@@ -1,6 +1,5 @@
@import 'responsive.scss'; @import 'responsive.scss';
@import 'animations.scss'; @import 'animations.scss';
//Styles
.list_wrapper { .list_wrapper {
overflow-y: auto; overflow-y: auto;
@@ -10,10 +9,6 @@
padding-right: 0.2em; padding-right: 0.2em;
} }
.journal-list {
position: relative;
}
.journal_wrapper { .journal_wrapper {
max-width: 1350px; max-width: 1350px;
width: 100%; width: 100%;
@@ -41,8 +36,8 @@
} }
} }
.schedule-dates > * { .journal_item {
margin-right: 0.25em; cursor: pointer;
} }
.journal_item, .journal_item,
@@ -50,6 +45,7 @@
background-color: #1a1a1a; background-color: #1a1a1a;
padding: 1em; padding: 1em;
margin-bottom: 1em; margin-bottom: 1em;
cursor: pointer;
} }
.journal_top-bar { .journal_top-bar {
@@ -59,7 +55,6 @@
gap: 0.5em; gap: 0.5em;
position: relative; position: relative;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
@@ -72,10 +67,6 @@
} }
@include smallScreen() { @include smallScreen() {
.list_wrapper {
font-size: 1.1em;
}
.journal_top-bar { .journal_top-bar {
justify-content: center; justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -85,9 +76,3 @@
text-align: center; text-align: center;
} }
} }
@media (orientation: landscape) {
.list_wrapper {
font-size: 1em;
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<section class="journal-timetables"> <section class="journal-dispatchers">
<JournalHeader /> <JournalHeader />
<div class="journal_wrapper"> <div class="journal_wrapper">
+2 -2
View File
@@ -46,7 +46,6 @@ import DriverStats from '../components/JournalView/JournalDriverStats.vue';
import JournalOptions from '../components/JournalView/JournalOptions.vue'; import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalStats from '../components/JournalView/JournalStats.vue'; import JournalStats from '../components/JournalView/JournalStats.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue'; import JournalHeader from '../components/JournalView/JournalHeader.vue';
import JournalTimetablesList from '../components/JournalView/JournalTimetablesList.vue';
import Loading from '../components/Global/Loading.vue'; import Loading from '../components/Global/Loading.vue';
import { DataStatus } from '../scripts/enums/DataStatus'; import { DataStatus } from '../scripts/enums/DataStatus';
@@ -63,11 +62,12 @@ import {
JournalTimetableSorter, JournalTimetableSorter,
} from '../scripts/types/JournalTimetablesTypes'; } from '../scripts/types/JournalTimetablesTypes';
import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts'; import { journalTimetableFilters } from '../constants/Journal/JournalTimetablesConsts';
import JournalTimetablesList from '../components/JournalView/JournalTimetables/JournalTimetablesList.vue';
const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`; const TIMETABLES_API_URL = `${URLs.stacjownikAPI}/api/getTimetables`;
export default defineComponent({ export default defineComponent({
components: { DriverStats, Loading, JournalOptions, JournalTimetablesList, JournalStats, JournalHeader }, components: { DriverStats, Loading, JournalOptions, JournalStats, JournalHeader, JournalTimetablesList },
mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin], mixins: [dateMixin, routerMixin, modalTrainMixin, imageMixin],
name: 'JournalTimetables', name: 'JournalTimetables',
+1
View File
@@ -45,6 +45,7 @@ import { computed, defineComponent, PropType } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import routerMixin from '../mixins/routerMixin'; import routerMixin from '../mixins/routerMixin';
import { useStore } from '../store/store'; import { useStore } from '../store/store';
import SceneryInfo from '../components/SceneryView/SceneryInfo.vue'; import SceneryInfo from '../components/SceneryView/SceneryInfo.vue';
import SceneryHeader from '../components/SceneryView/SceneryHeader.vue'; import SceneryHeader from '../components/SceneryView/SceneryHeader.vue';
import SceneryTimetable from '../components/SceneryView/SceneryTimetable.vue'; import SceneryTimetable from '../components/SceneryView/SceneryTimetable.vue';
-2
View File
@@ -14,7 +14,6 @@
<script lang="ts"> <script lang="ts">
import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue'; import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainStats from '../components/TrainsView/TrainStats.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import { trainFilters } from '../constants/Trains/TrainOptionsConsts'; import { trainFilters } from '../constants/Trains/TrainOptionsConsts';
import modalTrainMixin from '../mixins/modalTrainMixin'; import modalTrainMixin from '../mixins/modalTrainMixin';
@@ -26,7 +25,6 @@ import { TrainFilter } from '../scripts/interfaces/Trains/TrainFilter';
export default defineComponent({ export default defineComponent({
components: { components: {
TrainTable, TrainTable,
TrainStats,
TrainOptions, TrainOptions,
}, },
+5 -9
View File
@@ -1,19 +1,15 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' { declare module '*.vue' {
import type { DefineComponent } from 'vue' import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>;
export default component export default component;
} }
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_APP_API_DEV: number; readonly VITE_APP_API_DEV: string;
readonly VITE_APP_WS_DEV: number; readonly VITE_APP_WS_DEV: string;
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv; readonly env: ImportMetaEnv;
} }
+2888 -2652
View File
File diff suppressed because it is too large Load Diff