Compare commits

...

80 Commits

Author SHA1 Message Date
Spythere 916f6070ac Merge pull request #103 from Spythere/development
chore: added back merge gh workflow
2024-08-10 14:18:45 +02:00
Spythere a74ab6eb2a chore: added back merge gh workflow 2024-08-10 14:18:00 +02:00
Spythere 985c699ced Merge pull request #102 from Spythere/development
v1.26.1
2024-08-10 14:16:06 +02:00
Spythere 7e0e9146a5 fix: vehicle thumbnail cargo info 2024-08-10 14:08:32 +02:00
Spythere 30a0f05922 feat: journal dispatchers filtering by sc. hash 2024-08-10 14:00:25 +02:00
Spythere a30e04ca96 bump: v1.26.1 2024-08-09 15:24:48 +02:00
Spythere 1852d3e234 feat: category codes explanation tooltips 2024-08-09 15:24:26 +02:00
Spythere a17bf6c03f Merge branch 'development' 2024-08-09 14:12:07 +02:00
Spythere 766b08bc15 Merge branch 'master' of github.com:Spythere/stacjownik 2024-08-09 14:11:34 +02:00
Spythere cd1a4fa734 hotfix: checkpoint trains filtering 2024-08-06 14:18:26 +02:00
Spythere 619ce97b52 Merge pull request #101 from Spythere/development
v1.26.0
2024-08-05 16:11:07 +02:00
Spythere acbe761068 Merge branch 'development' 2024-08-05 16:02:20 +02:00
Spythere 47d35f335f chore: deleted merge workflow 2024-08-05 16:01:54 +02:00
Spythere 8fda8fa0df chore: package scripts 2024-08-05 15:59:54 +02:00
Spythere 71d697eda5 chore: changed workflow npm to yarn 2024-08-05 15:55:15 +02:00
Spythere f2b1fc5369 chore: disabled workflow on master push 2024-08-05 15:54:02 +02:00
Spythere 4a9b142e16 hotfix: lock files 2024-08-03 01:56:58 +02:00
Spythere 08d8bf3c57 bump: v1.26.0 2024-08-03 01:55:12 +02:00
Spythere 0ee90357aa chore: code structure 2024-08-03 01:53:36 +02:00
Spythere c8964dc20f chore: dispatcher history revamp & statuses 2024-08-02 02:00:44 +02:00
Spythere 6a62276d95 fix: vehicle preview loading 2024-08-01 19:26:25 +02:00
Spythere b8550eed9a chore: cleanup 2024-08-01 19:22:54 +02:00
Spythere 27b23ccc95 chore: lazy thumbnail loading & animations 2024-08-01 19:22:43 +02:00
Spythere b49517aded chore: packages upgrade 2024-07-24 18:55:41 +02:00
Spythere ed2b8be4dc chore: offline & fetching fixes 2024-07-24 17:52:20 +02:00
Spythere 54c1dbbf15 fix: stop labels statuses 2024-07-16 21:39:54 +02:00
Spythere 0ac7ba51e5 Merge pull request #100 from Spythere/development
v1.25.2
2024-07-12 16:13:06 +02:00
Spythere bdf85cd8ec bump: 1.25.2 2024-07-12 16:01:45 +02:00
Spythere 63b268d9b9 feat: added journal timetable path 2024-07-12 15:59:08 +02:00
Spythere d73c8ef112 fix: update modal won't open on first visit 2024-07-12 15:11:17 +02:00
Spythere 3d1c66b420 fix: cache control 2024-07-12 14:50:01 +02:00
Spythere b3f7108979 fix: detecting podg in timetables 2024-07-12 13:58:43 +02:00
Spythere feabfd29e0 Merge pull request #99 from Spythere/development
fix: recognizing timetables for sceneries with the same stop names
2024-07-09 20:33:16 +02:00
Spythere f17fedc976 fix: recognizing timetables for sceneries with the same stop names; optimization 2024-07-09 19:15:04 +02:00
Spythere c83c75e014 Merge pull request #98 from Spythere/development
hotfix: thumbnails v2 src
2024-07-08 22:12:41 +02:00
Spythere e57143f517 hotfix: thumbnails v2 src 2024-07-08 22:12:05 +02:00
Spythere fb45a783ee Merge pull request #97 from Spythere/development
v1.25.1
2024-07-08 21:40:50 +02:00
Spythere 71476e9552 bump: v1.25.1 2024-07-08 21:38:05 +02:00
Spythere 922a338143 hotfix: stock naming 2024-07-08 21:37:51 +02:00
Spythere 231d36e877 chore: adjusted for new vehicle thumbnails 2024-07-08 21:35:22 +02:00
Spythere 27d6ac9f14 Merge pull request #96 from Spythere/development
hotfix: scenery timetable train statuses
2024-06-11 20:56:25 +02:00
Spythere a6029da2cc hotfix: scenery timetable train statuses 2024-06-11 20:55:07 +02:00
Spythere a3f3790205 Merge pull request #95 from Spythere/development
hotfix: timetables for unknown sceneries
2024-06-10 20:19:20 +02:00
Spythere ebfb24f729 hotfix: timetables for unknown sceneries 2024-06-10 20:18:09 +02:00
Spythere e521736618 Merge pull request #94 from Spythere/development
hotfix: changed pwa strategy
2024-06-10 00:37:21 +02:00
Spythere fc7662e431 chore: changed pwa strategy 2024-06-10 00:36:30 +02:00
Spythere a459fdf178 Merge pull request #93 from Spythere/development
v1.25.0
2024-06-09 23:40:54 +02:00
Spythere 4e7fba89ee chore: improved stop label information 2024-06-09 00:58:45 +02:00
Spythere 6084e5876d chore: changed default history mode 2024-06-08 21:38:05 +02:00
Spythere 44f548c7b7 chore: scenery history locales 2024-06-08 21:37:28 +02:00
Spythere 59a5fbe5ac chore: adjusted to new version of API vehicles data 2024-06-08 20:53:22 +02:00
Spythere c252213ed9 hotfix 2024-06-07 18:31:20 +02:00
Spythere fb56378f18 chore: redesigned scenery history tables 2024-06-07 16:44:09 +02:00
Spythere e9635eae06 chore: redesigned train schedule list 2024-06-06 17:11:52 +02:00
Spythere 1fc98a8f99 chore: added test data mocks 2024-06-06 14:41:54 +02:00
Spythere c9de1a48ce chore: scenery timetables history translation; layout fixes 2024-06-06 14:19:17 +02:00
Spythere fee9774f88 chore: layout fixes 2024-06-06 14:12:21 +02:00
Spythere 7c974e8d0e bump: 1.25.0 2024-06-06 14:04:07 +02:00
Spythere c84fbbcf42 chore: added scenery timetables history modes 2024-06-05 20:03:05 +02:00
Spythere 45af649505 chore: changes in scenery view layout 2024-06-05 16:01:17 +02:00
Spythere 6c1e00d002 chore: layout & design fixes 2024-06-04 15:57:17 +02:00
Spythere 69ff85cfb1 chore: added route electrification indicators in train schedule 2024-06-03 22:26:58 +02:00
Spythere bdc2ca784c chore: missing translations 2024-06-03 21:37:33 +02:00
Spythere dbd73d448d chore: added active train's rolling stock vmax 2024-06-03 20:09:15 +02:00
Spythere 26b1ec246d chore: added extra data to vehicles tooltip 2024-06-03 18:10:45 +02:00
Spythere 8190dfa2cb chore: fetching & caching vehicles data information 2024-06-03 01:31:31 +02:00
Spythere 44df685606 Merge pull request #92 from Spythere/development
v1.24.4
2024-05-30 14:38:04 +02:00
Spythere 785a42b849 hotfix: detecting user timetable status at checkpoints 2024-05-30 14:29:09 +02:00
Spythere ccfcca8728 hotfix: scenery timetable duplicating 2024-05-30 14:24:18 +02:00
Spythere d9a7ba122c Merge pull request #91 from Spythere/development
v1.24.3
2024-05-26 01:44:45 +02:00
Spythere bf8d4a9ef4 chore: global font sizing; chore: train modal dvh 2024-05-25 18:06:01 +02:00
Spythere 6ea1e91d1d hotfix: card positioning 2024-05-25 17:57:25 +02:00
Spythere 813b557455 chore: improved card positioning 2024-05-25 17:55:18 +02:00
Spythere 834b14da69 fix: card dvh 2024-05-25 17:26:27 +02:00
Spythere c809b2146d chore: locale update 2024-05-25 17:12:19 +02:00
Spythere 33b98ca313 chore: added text color for active filters info 2024-05-25 17:11:28 +02:00
Spythere bcb9c63cb0 chore: reactive hiding body scroll on modal 2024-05-25 17:05:41 +02:00
Spythere 17d77a80d8 bump: 1.24.3 2024-05-25 16:02:40 +02:00
Spythere 65b159f8fd fix: scenery timetable duplicates; fix: not opening train modal for queries 2024-05-25 16:02:20 +02:00
Spythere 44f6cf4232 Merge branch 'development' 2024-05-12 15:22:28 +02:00
73 changed files with 9428 additions and 14718 deletions
+1 -4
View File
@@ -1,6 +1,3 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge name: Deploy to Firebase Hosting on merge
'on': 'on':
push: push:
@@ -11,7 +8,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: npm ci && npm run build - run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0 - uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: npm ci && npm run build - run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0 - uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
-1
View File
@@ -19,7 +19,6 @@
<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="#222222" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
+457 -1761
View File
File diff suppressed because it is too large Load Diff
+13 -16
View File
@@ -1,11 +1,13 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.24.2", "version": "1.26.1",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting", "deploy:prod": "yarn build && firebase deploy --only hosting",
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
"preview": "yarn build && vite preview", "preview": "yarn build && vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -19,25 +21,20 @@
"showdown": "^2.1.0", "showdown": "^2.1.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1", "vue-i18n": "^9.4.1",
"vue-router": "^4.2.4" "vue-router": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3", "@types/node": "^20.14.12",
"@types/node": "^20.6.2",
"@types/showdown": "^2.0.6", "@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^0.2.4", "@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^5.1.0",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/tsconfig": "^0.5.1",
"@vue/eslint-config-typescript": "^12.0.0", "axios": "^1.7.2",
"@vue/tsconfig": "^0.4.0", "prettier": "^3.3.3",
"axios": "^1.5.0", "typescript": "^5.5.4",
"eslint": "^8.49.0", "vite": "^5.3.4",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.20.0", "vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11" "vue-tsc": "^2.0.28"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+9 -17
View File
@@ -81,9 +81,7 @@ export default defineComponent({
isUpdateCardOpen: false, isUpdateCardOpen: false,
currentLang: 'pl', currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
nextUpdateTime: 0
}), }),
created() { created() {
@@ -96,22 +94,13 @@ export default defineComponent({
methods: { methods: {
init() { init() {
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
this.loadLang(); this.loadLang();
this.setupOfflineHandling(); this.setupOfflineHandling();
this.checkAppVersion(); this.checkAppVersion();
this.apiStore.setupAPIData(); this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
update(t: number) {
if (t >= this.nextUpdateTime) {
this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.update);
}, },
async checkAppVersion() { async checkAppVersion() {
@@ -131,7 +120,8 @@ export default defineComponent({
}; };
this.isUpdateCardOpen = this.isUpdateCardOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test'; (storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) { } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
} }
@@ -157,6 +147,7 @@ export default defineComponent({
handleOnlineMode() { handleOnlineMode() {
this.store.isOffline = false; this.store.isOffline = false;
this.apiStore.dataStatuses.connection = Status.Data.Loading;
this.apiStore.connectToAPI(); this.apiStore.connectToAPI();
}, },
@@ -180,7 +171,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString(); const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) { if (naviLanguage.startsWith('en')) {
this.changeLang('en'); this.changeLang('en');
return; return;
} }
@@ -210,7 +201,7 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.65rem + 0.8vw); font-size: calc(0.65rem + 0.85vw);
} }
@include screenLandscape() { @include screenLandscape() {
@@ -226,6 +217,7 @@ export default defineComponent({
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow: hidden;
position: relative;
} }
.app_main { .app_main {
+19 -13
View File
@@ -2,10 +2,9 @@
<transition name="modal-anim" tag="div"> <transition name="modal-anim" tag="div">
<div class="card" v-if="isOpen"> <div class="card" v-if="isOpen">
<div class="card-background" @click="toggleCard(false)"></div> <div class="card-background" @click="toggleCard(false)"></div>
<div class="card-body" ref="wrapper" tabindex="0"> <div class="card-body" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleCard(false)"></div>
</div> </div>
</transition> </transition>
</template> </template>
@@ -52,8 +51,12 @@ export default defineComponent({
left: 0; left: 0;
width: 100%; width: 100%;
height: 100vh; height: 100%;
z-index: 200; z-index: 200;
display: flex;
justify-content: center;
align-items: center;
} }
.card-background { .card-background {
@@ -69,19 +72,22 @@ export default defineComponent({
} }
.card-body { .card-body {
position: absolute; position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
max-height: 95vh;
margin: 1em;
max-height: 95vh;
max-height: 95dvh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; box-shadow: 0 0 15px 10px #0e0e0e;
& > :slotted(div) { overflow: auto;
background-color: #1a1a1a; }
width: 95vw;
@include smallScreen {
.card {
align-items: flex-start;
} }
} }
</style> </style>
+5 -4
View File
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
.status-badge { .status-badge {
border-radius: 1em; border-radius: 1em;
font-weight: 500; font-weight: 500;
text-wrap: nowrap;
padding: 0.2em 0.55em; padding: 0.2rem 0.55rem;
background-color: $online; background-color: $online;
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
&.no-limit { &.no-limit {
background-color: $no-limit; background-color: $no-limit;
font-size: 0.85em; font-size: 0.9em;
} }
&.not-signed, &.not-signed,
&.unavailable { &.unavailable {
background-color: $unav; background-color: $unav;
font-size: 0.85em; font-size: 0.9em;
} }
&.afk { &.afk {
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
background-color: $no-space; background-color: $no-space;
border: 1px solid white; border: 1px solid white;
color: white; color: white;
font-size: 0.85em; font-size: 0.9em;
} }
&.unknown, &.unknown,
+128 -99
View File
@@ -1,83 +1,24 @@
<template> <template>
<div class="stock-list"> <div class="stock-list">
<div v-if="tractionOnly"> <ul>
<p> <li
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }} v-for="(
{{ computedStockList[0].split(':')[1] }} { vehicleName, vehicleCargo, images, imagesFallbacks, vehicleString }, i
</p> ) in thumbnailNames"
:key="i"
<img >
class="traction-only" <div class="stock-text">
:src=" <p>{{ vehicleName.replace(/_/g, ' ') }}</p>
getVehicleThumbnailURL( <small v-if="vehicleCargo">({{ vehicleCargo }})</small>
computedStockList[0].split(':')[0],
/^EN/.test(computedStockList[0]) ? 'rb' : ''
)
"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
/>
</div> </div>
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
</p>
<span> <span>
<img <VehicleThumbnail
:data-mouseover="stockName" v-for="(thumbnailImage, imageIndex) in images"
data-tooltip-type="VehiclePreviewTooltip" :vehicle-name="vehicleString"
:data-tooltip-content="stockName.split(':')[0]" :img-name="thumbnailImage"
:src=" :fallback-name="imagesFallbacks[imageIndex]"
getVehicleThumbnailURL(stockName.split(':')[0], /^EN/.test(stockName) ? 'rb' : '')
"
@error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400"
height="60"
/> />
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@click.stop="() => {}"
/>
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="getVehicleThumbnailURL(stockName, 'ra')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@click.stop="() => {}"
/>
<!-- /// -->
</span> </span>
</li> </li>
</ul> </ul>
@@ -87,8 +28,11 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import VehicleThumbnail from './VehicleThumbnail.vue';
export default defineComponent({ export default defineComponent({
components: { VehicleThumbnail },
props: { props: {
trainStockList: { trainStockList: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
@@ -109,32 +53,117 @@ export default defineComponent({
computed: { computed: {
computedStockList() { computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList; return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
},
thumbnailNames() {
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
.filter((v) => v.length != 0)
.map((vehicleString) => {
const [vehicleName, vehicleCargo] = vehicleString.split(':');
const vehicleThumbnailData = {
images: [] as string[],
imagesFallbacks: [] as string[],
vehicleName,
vehicleCargo,
vehicleString
};
// Generowanie członów EN57
if (vehicleName.startsWith('EN57')) {
vehicleThumbnailData['images'] = [
vehicleName + 'ra',
vehicleName + 's',
vehicleName + 'rb'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb'
];
}
// Generowanie członów EN71
else if (vehicleName.startsWith('EN71')) {
vehicleThumbnailData['images'] = [
vehicleName + 'ra',
vehicleName + 'sa',
vehicleName + 'sb',
vehicleName + 'rb'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-sa',
'unknown_ezt-sb',
'unknown_ezt-rb'
];
}
// Generowanie pojazdów i członów 2EN57
else if (vehicleString.startsWith('2EN57')) {
const [firstVehicleNumber, secondVehicleNumber] = vehicleString
.replace('2EN57-', '')
.split('+');
vehicleThumbnailData['images'] = [
`EN57-${firstVehicleNumber}ra`,
`EN57-${firstVehicleNumber}s`,
`EN57-${firstVehicleNumber}rb`,
`EN57-${secondVehicleNumber}ra`,
`EN57-${secondVehicleNumber}s`,
`EN57-${secondVehicleNumber}rb`
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb',
'unknown_ezt-ra',
'unknown_ezt-s',
'unknown_ezt-rb'
];
}
// Generowanie członów Gor77
else if (vehicleString.startsWith('Gor77')) {
vehicleThumbnailData['images'] = [
vehicleName + '-A',
vehicleName + '-B',
vehicleName + '-C',
vehicleName + '-D'
];
vehicleThumbnailData['imagesFallbacks'] = [
'unknown_Gor77-A',
'unknown_Gor77-B',
'unknown_Gor77-C',
'unknown_Gor77-D'
];
}
// Generowanie członów ET41
else if (vehicleString.startsWith('ET41')) {
vehicleThumbnailData['images'] = [vehicleName + '-A', vehicleName + '-B'];
vehicleThumbnailData['imagesFallbacks'] = ['unknown_ET41-A', 'unknown_ET41-B'];
}
// Generowanie pozostałych pojazdów
else {
let fallbackVehicleImage = 'unknown_cargo';
if (/^(EP|EU|ET|201E)/.test(vehicleName)) fallbackVehicleImage = 'unknown_train';
else if (/^(SM42)/.test(vehicleName)) fallbackVehicleImage = 'unknown_SM42';
else if (/(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(vehicleName))
fallbackVehicleImage = 'unknown_passenger';
vehicleThumbnailData['images'] = [vehicleName];
vehicleThumbnailData['imagesFallbacks'] = [fallbackVehicleImage];
}
if (this.tractionOnly) vehicleThumbnailData['images'].length = 1;
return vehicleThumbnailData;
});
} }
}, },
methods: { methods: {
getVehicleThumbnailURL(locoType: string, suffix?: string) { onImageError(event: Event, fallbackImage: string) {
return `https://static.spythere.eu/thumbnails/${locoType}${suffix}.png`; (event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
},
onImageError(event: Event, stockName: string) {
let fallbackName = '';
const isLoco = /.-\d{3}/.test(stockName);
if (isLoco) {
if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
else fallbackName = 'loco-s';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
fallbackName += 'car-';
fallbackName += isCarPassenger ? 'passenger' : 'cargo';
}
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
} }
} }
}); });
@@ -170,10 +199,10 @@ img.traction-only {
max-width: 100%; max-width: 100%;
} }
p { .stock-text {
text-align: center; text-align: center;
color: #aaa; color: #aaa;
font-size: 0.95em; font-size: 0.9em;
margin-bottom: 1em; margin-bottom: 0.25em;
} }
</style> </style>
@@ -0,0 +1,61 @@
<template>
<div class="vehicle-thumbnail">
<img
ref="imgRef"
:src="`https://static.spythere.eu/thumbnails/v2/${imgName}.png`"
height="60"
loading="lazy"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="vehicleName"
:data-load-status="imgStatus"
@error="onImageError"
@load="onImageLoad"
/>
</div>
</template>
<script setup lang="ts">
import { onMounted, Ref, ref } from 'vue';
const props = defineProps({
vehicleName: { type: String, required: true },
imgName: { type: String, required: true },
fallbackName: { type: String, required: true },
placeholderName: String
});
const imgRef = ref(null) as Ref<HTMLElement | null>;
const imgStatus = ref('loading');
function onImageError(event: Event) {
console.log('error');
(event.target as HTMLImageElement).src = `/images/${props.fallbackName}.png`;
imgStatus.value = 'error';
}
function onImageLoad() {
if (imgStatus.value != 'error') {
imgStatus.value = 'loaded';
}
imgRef.value!.style.opacity = '1';
}
</script>
<style lang="scss" scoped>
.vehicle-thumbnail {
position: relative;
}
img {
opacity: 0;
transition: opacity 100ms ease-in-out;
&[data-load-status='loading'] {
min-height: 60px;
min-width: 150px;
}
}
</style>
@@ -0,0 +1,217 @@
<template>
<li class="dispatcher-history-entry">
<div class="entry-info">
<span>
<span>
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
<b>{{ entry.stationName }}</b>
</router-link>
<b class="text--grayed"> #{{ entry.stationHash }}</b>
</span>
&bull;
<b
v-if="entry.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
>
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
</b>
<b style="margin-left: 5px">
<span
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<router-link
class="text--donator"
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
>
{{ entry.dispatcherName }}
</router-link>
</span>
<router-link
v-else
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
>
{{ entry.dispatcherName }}
</router-link>
</b>
<div>
<span v-if="entry.timestampTo">
<b>{{ $d(entry.timestampFrom) }}</b>
{{ timestampToString(entry.timestampFrom) }}
-
<b
v-if="
new Date(entry.timestampFrom).getDate() != new Date(entry.timestampTo).getDate()
"
>
{{ $d(entry.timestampTo) }}
</b>
{{ timestampToString(entry.timestampTo) }} ({{
calculateDuration(entry.currentDuration)
}})
</span>
<router-link
:to="`/scenery?station=${entry.stationName}`"
class="dispatcher-online"
v-else
>
{{ $t('journal.online-since') }}
<b>
{{
new Date().getDate() != new Date(entry.timestampFrom).getDate()
? $d(entry.timestampFrom)
: ''
}}
{{ timestampToString(entry.timestampFrom) }}
</b>
({{ calculateDuration(entry.currentDuration) }})
</router-link>
</div>
</span>
<span class="entry-info-right">
<div>
<span>
{{ $t('scenery.dispatcher-rate') }}
<b class="text--primary"> {{ entry.dispatcherRate }}</b>
</span>
<button class="btn btn--option" @click="toggleExtraInfo">
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ entry.statusHistory.length }}</b>
</button>
</div>
<b class="region-badge" :aria-describedby="entry.region">
REGION: {{ regions.find((r) => r.id == entry.region)?.name }}
</b>
</span>
</div>
<div class="entry-extra" v-if="showExtraInfo">
<ul class="status-list">
<li v-for="statusItem in entry.statusHistory">
<b style="margin-right: 0.5em">{{
timestampToString(parseInt(statusItem.split('@')[0]))
}}</b>
<StationStatusBadge
:dispatcher-status="Number(statusItem.split('@')[1])"
:is-online="true"
/>
</li>
</ul>
</div>
</li>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
export default defineComponent({
props: {
entry: {
type: Object as PropType<API.DispatcherHistory.Data>,
required: true
},
showExtraInfo: {
type: Boolean,
required: true
}
},
components: { StationStatusBadge },
mixins: [dateMixin, styleMixin],
emits: ['toggleShowExtraInfo'],
data() {
return {
regions,
apiStore: useApiStore()
};
},
methods: {
toggleExtraInfo() {
this.$emit('toggleShowExtraInfo', this.entry.id);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.region-badge {
padding: 0 0.25em;
}
.level-badge {
text-align: center;
display: inline-block;
line-height: 1.6em;
}
.dispatcher-online {
color: springgreen;
}
.dispatcher-history-entry {
background-color: #1a1a1a;
padding: 1em;
}
.entry-info {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
line-height: 1.75em;
gap: 0.5em;
}
.entry-info-right {
display: flex;
flex-wrap: wrap;
align-items: center;
text-align: center;
gap: 1em;
}
.entry-extra {
margin-top: 1em;
}
.status-list {
display: flex;
overflow: auto;
gap: 0.5em;
}
.status-list > li {
background-color: #313131;
padding: 0.2rem 0 0.2rem 0.5em;
margin: 0.5em 0;
border-radius: 1em;
}
@include smallScreen {
.entry-info {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
</style>
@@ -15,90 +15,16 @@
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <ul v-else class="journal-list">
<table class="dispatchers-table">
<thead>
<th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th>
</thead>
<tbody>
<transition-group name="list-anim"> <transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id"> <JournalDispatcherEntry
<td> v-for="entry in dispatcherHistory"
<router-link :key="entry.id"
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`" :entry="entry"
> :onToggleShowExtraInfo="toggleExtraInfo"
<b>{{ historyItem.stationName }}</b> :showExtraInfo="extraInfoIndexes.includes(entry.id)"
</router-link> />
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
{{ historyItem.dispatcherName }}
</b>
<b v-else>
{{ historyItem.dispatcherName }}
</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="
calculateExpStyle(
historyItem.dispatcherLevel,
historyItem.dispatcherIsSupporter
)
"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group> </transition-group>
</tbody>
</table>
<AddDataButton <AddDataButton
:list="dispatcherHistory" :list="dispatcherHistory"
@@ -106,7 +32,7 @@
:scrollNoMoreData="scrollNoMoreData" :scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData" @addHistoryData="addHistoryData"
/> />
</div> </ul>
<div class="journal_warning" v-if="scrollNoMoreData"> <div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }} {{ $t('journal.no-further-data') }}
@@ -121,20 +47,15 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { useMainStore } from '../../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
import { Status } from '../../../typings/common'; import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin'; import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
components: { Loading, AddDataButton }, components: { Loading, AddDataButton, JournalDispatcherEntry },
mixins: [dateMixin, styleMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
@@ -159,99 +80,30 @@ export default defineComponent({
return { return {
Status, Status,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(),
regions extraInfoIndexes: [] as number[]
}; };
}, },
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce(
(acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i))
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
},
[] as (API.DispatcherHistory.Data | string)[]
);
}
},
methods: { methods: {
navigateToScenery(name: string, isOnline: boolean) { toggleExtraInfo(id: number) {
if (!isOnline) return; const existingIdx = this.extraInfoIndexes.indexOf(id);
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`); if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
}, else this.extraInfoIndexes.push(id);
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/animations.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection.scss';
table.dispatchers-table { .journal-list {
--_bg-table: #111; display: flex;
--_bg-head: #101010; flex-direction: column;
--_bg-row: #2f2f2f; gap: 0.5em;
text-align: left;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
margin-bottom: 1em;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
} }
</style> </style>
@@ -9,7 +9,11 @@
</span> </span>
<span> <span>
<strong class="text--primary"> <strong
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(timetable.trainCategoryCode)"
class="text--primary tooltip-help"
>
{{ timetable.trainCategoryCode }} {{ timetable.trainCategoryCode }}
</strong> </strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong> <strong>&nbsp;{{ timetable.trainNo }}</strong>
@@ -26,7 +30,8 @@
<strong <strong
v-if="apiStore.donatorsData.includes(timetable.driverName)" v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator" class="text--donator"
:title="$t('donations.driver-message')" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
> >
{{ timetable.driverName }} {{ timetable.driverName }}
</strong> </strong>
@@ -82,9 +87,10 @@ import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin], mixins: [dateMixin, modalTrainMixin, styleMixin, trainCategoryMixin],
data() { data() {
return { return {
@@ -103,7 +109,7 @@ export default defineComponent({
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) { showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return; if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target); this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
} }
} }
}); });
@@ -1,101 +0,0 @@
<template>
<ul class="journal-list">
<transition-group name="list-anim">
<li
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<button class="btn--action btn--show">
{{ $t('journal.stock-info') }}
<img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
alt="Arrow icon"
/>
</button>
<!-- Extra -->
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableExtra from './TimetableExtra.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
props: {
timetableHistory: {
type: Array as PropType<API.TimetableHistory.Response>,
required: true
}
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables';
@import '../../../styles/responsive';
@import '../../../styles/JournalSection';
.btn--show {
display: flex;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
hr {
margin: 0.25em 0;
}
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -1,5 +1,6 @@
<template> <template>
<div class="stop-list" v-if="showExtraInfo == true"> <div class="timetable-stops">
<div class="stop-list">
<span <span
v-for="(stop, i) in timetableStops.filter((_, i) => v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true !showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
@@ -19,6 +20,23 @@
<span v-html="stop.html"></span> <span v-html="stop.html"></span>
</span> </span>
</div> </div>
<div class="path-details" v-if="showExtraInfo && timetablePathDetails">
<span
v-for="(pathData, i) in timetablePathDetails"
:data-visited="pathData.isVisited"
:data-next-visited="
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
"
>
<span class="path-arrival" v-if="pathData.arrival">/ {{ pathData.arrival }} &RightArrow; </span>
<b class="path-scenery">{{ pathData.sceneryName }}</b>
<span class="path-departure" v-if="pathData.departure">
&RightArrow; {{ pathData.departure }}&nbsp;
</span>
</span>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -42,6 +60,24 @@ export default defineComponent({
}, },
computed: { computed: {
timetablePathDetails() {
if (!this.timetable.path || this.timetable.path == '') return null;
return this.timetable.path.split(';').map((pathEl, i) => {
const [arrival, name, departure] = pathEl.split(',');
const sceneryName = name.split(' ').slice(0, -1).join(' ');
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
return {
arrival,
sceneryName,
sceneryHash,
departure,
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
};
});
},
timetableStops() { timetableStops() {
const timetable = this.timetable; const timetable = this.timetable;
@@ -94,13 +130,14 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.stop-list { .timetable-stops {
word-wrap: break-word; word-wrap: break-word;
gap: 0.25em; gap: 0.25em;
font-size: 0.95em; font-size: 0.95em;
color: #adadad; color: #adadad;
}
.stop-list {
&-item[data-confirmed='true'] { &-item[data-confirmed='true'] {
color: lightgreen; color: lightgreen;
@@ -109,4 +146,19 @@ export default defineComponent({
} }
} }
} }
.path-details {
margin-top: 0.5em;
}
.path-details > span[data-visited='true'] {
.path-arrival,
.path-scenery {
color: lightgreen;
}
&[data-next-visited='true'] .path-departure {
color: lightgreen;
}
}
</style> </style>
+3 -1
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train' | 'search-train'
| 'search-date' | 'search-date'
| 'search-dispatcher' | 'search-dispatcher'
| 'search-issuedFrom'; | 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = { export type TimetableSearchType = {
[key in TimetableSearchKey]: string; [key in TimetableSearchKey]: string;
@@ -1,31 +1,18 @@
<template> <template>
<section class="scenery-table-section"> <div class="scenery-dispatchers-history">
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" /> <Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0"> <div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }} {{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div v-else class="journal-list">
<thead> <div v-for="historyItem in historyList" :key="historyItem.id">
<th>{{ $t('scenery.dispatchers-history-hash') }}</th> <span>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th> <span class="text--grayed" style="margin-right: 10px">
<th>{{ $t('scenery.dispatchers-history-level') }}</th> #{{ historyItem.stationHash }}
<th>{{ $t('scenery.dispatchers-history-rate') }}</th> </span>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<b <b
v-if="historyItem.dispatcherLevel !== null" v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher" class="level-badge dispatcher"
@@ -35,38 +22,53 @@
> >
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b> </b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b> <div>
</td> <span>
<td class="text--primary"> {{ $t('scenery.dispatcher-rate') }}
<b>{{ historyItem.dispatcherRate }}</b> <b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</td> </span>
<td style="min-width: 300px"> |
<div v-if="historyItem.timestampTo"> <span>
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
</span>
</div>
</span>
<span>
<span v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b> <b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }} {{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{ - {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration) calculateDuration(historyItem.currentDuration)
}}) }})
</div> </span>
<div class="dispatcher-online" v-else> <span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }} {{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }}) ({{ calculateDuration(historyItem.currentDuration) }})
</span>
</span>
</div>
</div>
</div> </div>
</td>
</tr>
</tbody>
</table>
</section>
<div class="bottom-info"> <div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory"> <button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }} {{ $t('scenery.bottom-info') }}
</button> </button>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -149,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss'; @import '../../styles/sceneryViewTables.scss';
.scenery-dispatchers-history {
height: 100%;
overflow: auto;
display: grid;
gap: 0.5em;
grid-template-rows: auto 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.journal-list > div {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.75em;
}
.level-badge { .level-badge {
margin: 0 auto; text-align: center;
display: inline-block;
line-height: 1.6em;
} }
.dispatcher-online { .dispatcher-online {
@@ -158,13 +195,10 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.history-list { .journal-list > div {
font-size: 1.1em;
}
.list-item {
align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center;
text-align: center;
} }
} }
</style> </style>
../../store/storeTypes
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center; align-items: center;
font-size: 1.2em; font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
} }
.info-lists { .info-lists {
@@ -18,8 +18,8 @@
tabindex="0" tabindex="0"
:key="train.id" :key="train.id"
:data-status="status" :data-status="status"
@click.prevent="selectModalTrain(train.id, $event.currentTarget)" @click.prevent="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.id, $event.currentTarget)" @keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span> <span class="user_name">{{ train.driverName }}</span>
@@ -63,7 +63,9 @@ export default defineComponent({
return this.onlineScenery.stationTrains.map((train) => { return this.onlineScenery.stationTrains.map((train) => {
const stop = train.timetableData?.followingStops.find( const stop = train.timetableData?.followingStops.find(
(stop) => stop.stopNameRAW.toLowerCase() == name.toLowerCase() (stop) =>
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
); );
const status = stop const status = stop
+30 -78
View File
@@ -39,8 +39,8 @@
<div class="timetable-list"> <div class="timetable-list">
<transition-group name="list-anim"> <transition-group name="list-anim">
<div <div
style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0" v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em"
key="list-loading" key="list-loading"
> >
<Loading /> <Loading />
@@ -65,23 +65,28 @@
<div <div
class="timetable-item" class="timetable-item"
v-else v-else
v-for="row in sceneryTimetables" v-for="(row, i) in sceneryTimetables"
:key="row.train.id + row.checkpointStop.arrivalTimestamp" :key="row.train.id + i"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(row.train.id, $event.currentTarget)" @click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(row.train.id, $event.currentTarget)" @keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
<span class="info-number"> <span>
<strong>{{ row.train.timetableData!.category }}</strong> <b
{{ row.train.trainNo }} data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(row.train.timetableData!.category)"
class="text--primary tooltip-help"
>
{{ row.train.timetableData!.category }}
</b>
<b>&nbsp;{{ row.train.trainNo }}</b>
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments"> <span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
<img src="/images/icon-warning.svg" /> <img src="/images/icon-warning.svg" />
</span> </span>
</span> </span>
&nbsp;|&nbsp; &nbsp;&bull;&nbsp;
<span> <span>
{{ row.train.driverName }} {{ row.train.driverName }}
</span> </span>
@@ -180,13 +185,14 @@ import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common'; import { ActiveScenery, Station } from '../../typings/common';
import { SceneryTimetableRow } from './typings'; import { SceneryTimetableRow } from './typings';
import { getTrainStopStatus, stopStatusPriority } from './utils'; import { getTrainStopStatus, stopStatusPriority } from './utils';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
components: { Loading, ScheduledTrainStatus }, components: { Loading, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, modalTrainMixin], mixins: [dateMixin, routerMixin, modalTrainMixin, trainCategoryMixin],
props: { props: {
station: { station: {
@@ -213,7 +219,10 @@ export default defineComponent({
const mainStore = useMainStore(); const mainStore = useMainStore();
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints[0] ?? props.station?.name ?? '' props.station?.generalInfo?.checkpoints[0] ??
props.station?.name ??
route.query['station']?.toString() ??
''
); );
return { return {
@@ -233,14 +242,14 @@ export default defineComponent({
}, },
sceneryTimetables(): SceneryTimetableRow[] { sceneryTimetables(): SceneryTimetableRow[] {
if (!this.station) return [];
if (!this.onlineScenery) return []; if (!this.onlineScenery) return [];
console.log(this.onlineScenery.scheduledTrains, this.chosenCheckpoint); const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
return this.onlineScenery.scheduledTrains return this.onlineScenery.scheduledTrains
.filter( .filter(
(ct) => (ct) =>
// ct.timetablePathElement.stationName == sceneryName &&
ct.train.region == this.mainStore.region.id && ct.train.region == this.mainStore.region.id &&
this.chosenCheckpoint && this.chosenCheckpoint &&
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase() ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
@@ -249,75 +258,18 @@ export default defineComponent({
const trainStopStatus = getTrainStopStatus( const trainStopStatus = getTrainStopStatus(
ct.checkpointStop, ct.checkpointStop,
ct.train.currentStationName, ct.train.currentStationName,
this.station!.name sceneryName
); );
const trainStopIndex =
ct.train.timetableData?.followingStops.findIndex(
(stop) => stop.stopName == ct.checkpointStop.stopName
) ?? -1;
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
if (trainStopIndex > -1 && ct.train.timetableData?.followingStops !== undefined) {
for (let i = trainStopIndex; i >= 0; i--) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!prevStationName &&
i <= trainStopIndex - 1
)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.arrivalLine != null &&
!arrivingLine &&
!/-|_|it|sbl/gi.test(stop.arrivalLine)
) {
arrivingLine = stop.arrivalLine;
prevDepartureLine =
ct.train.timetableData.followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < ct.train.timetableData.followingStops.length; i++) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!nextStationName &&
i > trainStopIndex
)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.departureLine &&
!departureLine &&
!/-|_|it|sbl/gi.test(stop.departureLine)
) {
departureLine = stop.departureLine;
nextArrivalLine = ct.train.timetableData.followingStops[i + 1]?.arrivalLine || null;
}
}
}
return { return {
checkpointStop: ct.checkpointStop, checkpointStop: ct.checkpointStop,
train: ct.train, train: ct.train,
prevDepartureLine, prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
nextArrivalLine, nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
departureLine, departureLine: ct.timetablePathElement.departureRouteExt ?? null,
arrivingLine, arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
prevStationName, prevStationName: ct.previousSceneryElement?.stationName ?? null,
nextStationName, nextStationName: ct.nextSceneryElement?.stationName ?? null,
status: trainStopStatus status: trainStopStatus
}; };
}) })
@@ -1,70 +1,98 @@
<template> <template>
<!-- WIP --> <div class="scenery-timetables-history">
<!-- <div class="top-filters"> <div class="history-modes">
<button class="btn btn--option">ROZPOCZYNA BIEG</button> <button
class="btn btn--option"
v-for="mode in historyModeList"
:key="mode"
:class="{ checked: checkedHistoryMode == mode }"
@click="checkHistoryMode(mode)"
>
{{ $t(`scenery.timetable-${mode}`) }}
</button>
</div>
<button class="btn btn--option">PRZEZ</button> <div class="history-wrapper">
<button class="btn btn--option">KOŃCZY BIEG</button>
</div> -->
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded" /> <Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0"> <div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }} {{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div v-else class="journal-list">
<thead> <div v-for="timetableHistory in historyList" :key="timetableHistory.id">
<th>{{ $t('scenery.timetables-history-id') }}</th> <span>
<th>{{ $t('scenery.timetables-history-number') }}</th> <div>
<th>{{ $t('scenery.timetables-history-route') }}</th> <span
<th>{{ $t('scenery.timetables-history-driver') }}</th> class="timetable-status-indicator"
<th>{{ $t('scenery.timetables-history-author') }}</th> :data-terminated="timetableHistory.terminated"
<th>{{ $t('scenery.timetables-history-date') }}</th> :data-fulfilled="timetableHistory.fulfilled"
</thead> >
&ofcir;
</span>
#{{ timetableHistory.id }} |
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
{{ timetableHistory.trainNo }}
{{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</div>
<tbody> <div class="text--grayed">
<tr v-for="historyItem in historyList" :key="historyItem.id"> <span>
<td> {{ $t('scenery.timetable-issued-date') }}
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`"> <b>
#{{ historyItem.id }} {{
</router-link> localeDateTime(
</td> timetableHistory.createdAt > timetableHistory.beginDate
<td> ? timetableHistory.beginDate
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br /> : timetableHistory.createdAt,
{{ historyItem.trainNo }} $i18n.locale
</td> )
<td>{{ historyItem.route.replace('|', ' -> ') }}</td> }}
<td> </b></span
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`"> >
{{ historyItem.driverName }} <span v-if="timetableHistory.authorName">
</router-link> {{ $t('scenery.timetable-issued-by') }}
</td> <b>
<td>
<router-link <router-link
v-if="historyItem.authorName" :to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`" >
>{{ historyItem.authorName }} {{ timetableHistory.authorName }}
</router-link> </router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> </b>
</td> </span>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> <span>
{{ localeTime(historyItem.beginDate, $i18n.locale) }} {{ $t('scenery.timetable-issued-for') }}
</td> <b>
</tr> <router-link
</tbody> :to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
</table> >
</section> {{ timetableHistory.driverName }}
</router-link>
</b>
</span>
</div>
</span>
<button
@click="
navigateTo(`/journal/timetables`, {
'search-train': `#${timetableHistory.id}`
})
"
>
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
</button>
</div>
</div>
</div>
<div class="bottom-info"> <div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()"> <button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }} {{ $t('scenery.bottom-info') }}
</button> </button>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { ActiveScenery, Station, Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import routerMixin from '../../mixins/routerMixin';
import { useMainStore } from '../../store/mainStore';
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
type HistoryMode = (typeof historyModeList)[number];
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, routerMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -91,9 +124,14 @@ export default defineComponent({
data() { data() {
return { return {
historyList: [] as API.TimetableHistory.Response, historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(), apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
DataStatus: Status.Data DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
}; };
}, },
@@ -103,17 +141,22 @@ export default defineComponent({
methods: { methods: {
async fetchAPIData() { async fetchAPIData() {
if (!this.station && !this.onlineScenery) { const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
return; return;
} }
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try { try {
const response: API.TimetableHistory.Response = await ( const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: { params: requestFilters
issuedFrom: this.station?.name || this.onlineScenery?.name
}
}) })
).data; ).data;
@@ -125,11 +168,17 @@ export default defineComponent({
} }
}, },
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() { navigateToHistory() {
this.$router.push({ this.$router.push({
path: '/journal/timetables', path: '/journal/timetables',
query: { query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name [`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
} }
}); });
} }
@@ -142,13 +191,66 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss'; @import '../../styles/sceneryViewTables.scss';
.top-filters { .scenery-timetables-history {
height: 100%;
overflow: auto;
display: grid;
gap: 1em;
grid-template-rows: auto 1fr 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-modes {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
padding: 0.25em;
button { button {
padding: 0.35em;
min-width: 120px;
}
}
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.journal-list > div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5em; padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.5em;
}
.journal-list > div > button > img {
width: 2em;
transform: rotate(180deg);
}
.timetable-status-indicator {
&[data-fulfilled='true'] {
color: lightgreen;
}
&[data-terminated='false'] {
color: lightblue;
}
&[data-terminated='true'][data-fulfilled='false'] {
color: lightcoral;
} }
} }
</style> </style>
@@ -33,12 +33,13 @@
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
<div class="changed-filters" v-if="changedFilters.length > 0"> <div class="changed-filters" :data-active="changedFilters.length > 0">
<template v-if="changedFilters.length > 0">
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b> {{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
</template>
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
</div> </div>
<div class="changed-filters" v-else>{{ $t('filters.no-changed-filters') }}</div>
<section class="card_options"> <section class="card_options">
<div <div
class="option-section" class="option-section"
@@ -371,6 +372,7 @@ export default defineComponent({
@import '../../styles/responsive'; @import '../../styles/responsive';
@import '../../styles/card'; @import '../../styles/card';
@import '../../styles/animations'; @import '../../styles/animations';
@import '../../styles/variables';
h3.section-header { h3.section-header {
text-align: center; text-align: center;
@@ -390,6 +392,10 @@ h3.section-header {
.changed-filters { .changed-filters {
background-color: #111; background-color: #111;
padding: 0.5em; padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
} }
.card_controls { .card_controls {
+3 -5
View File
@@ -4,8 +4,7 @@
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0" v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/> />
<div class="table_wrapper" v-else-if="filteredStationList.length > 0"> <table v-else-if="filteredStationList.length > 0">
<table>
<thead> <thead>
<tr> <tr>
<th <th
@@ -217,8 +216,7 @@
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title=" :title="
$t('sceneries.info.signals-type') + $t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
$t(`signals.${station.generalInfo.signalType}`)
" "
/> />
@@ -299,7 +297,6 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div>
<div class="no-stations" v-else> <div class="no-stations" v-else>
<div> <div>
@@ -418,6 +415,7 @@ $rowCol: #424242;
.station_table { .station_table {
height: 80vh; height: 80vh;
max-height: 2000px;
min-height: 700px; min-height: 700px;
overflow: auto; overflow: auto;
font-weight: 500; font-weight: 500;
+1
View File
@@ -23,6 +23,7 @@ export default defineComponent({
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
white-space: pre-line;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
border-radius: 0.25em; border-radius: 0.25em;
+2 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview"> <div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" /> <component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div> </div>
</template> </template>
@@ -35,10 +35,7 @@ export default defineComponent({
let translateX = '0', let translateX = '0',
translateY = '30px'; translateY = '30px';
if (clientWidth < 500) { if (val[0] <= boxWidth / 2) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0'; previewEl.style.left = '0';
translateX = '0px'; translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) { } else if (val[0] >= clientWidth - boxWidth / 2) {
@@ -13,13 +13,20 @@
width="300" width="300"
height="176" height="176"
class="rounded-md w-full h-auto" class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`" :src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
/> />
<div v-if="imageState == 'error'" class="error-placeholder"></div> <div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name"> <div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }} {{ vehicleName.replace(/_/g, ' ') }}
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
</div>
<div class="vehicle-props" v-if="vehicleData">
{{ vehicleData.group.speed }}km/h &bull; {{ vehicleData.group.length }}m &bull;
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
</div> </div>
</div> </div>
</template> </template>
@@ -27,11 +34,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading' imageState: 'loading'
}; };
}, },
@@ -41,7 +50,7 @@ export default defineComponent({
}, },
watch: { watch: {
'tooltipStore.type'(prev, val) { vehicleName(prev, val) {
if (prev != val) this.imageState = 'loading'; if (prev != val) this.imageState = 'loading';
} }
}, },
@@ -56,6 +65,22 @@ export default defineComponent({
(e.target as HTMLElement).style.display = 'none'; (e.target as HTMLElement).style.display = 'none';
} }
},
computed: {
vehicleName() {
return this.tooltipStore.content.split(':')[0];
},
vehicleData() {
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
},
vehicleCargo() {
return this.vehicleData?.group.cargoTypes?.find(
(c) => c.id == this.tooltipStore.content.split(':')[1]
);
}
} }
}); });
</script> </script>
@@ -85,10 +110,13 @@ img {
.vehicle-name { .vehicle-name {
text-align: center; text-align: center;
margin-top: 0.5em; margin-top: 0.5em;
color: #ccc;
text-wrap: wrap; text-wrap: wrap;
} }
.vehicle-props {
color: #ccc;
}
.error-placeholder { .error-placeholder {
height: 176px; height: 176px;
} }
+57 -41
View File
@@ -1,20 +1,18 @@
<template> <template>
<span class="stop-label" :data-sbl="stop.isSBL"> <span
class="stop-label"
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po.') && !stop.duration)"
>
<span class="name" v-html="stop.nameHtml"></span> <span class="name" v-html="stop.nameHtml"></span>
<span <span
v-if="stop.position != 'begin'" v-if="stop.position != 'begin'"
class="date arrival" class="date arrival"
:data-status=" :data-status-delayed="stop.arrivalDelay > 0"
stop.arrivalDelay > 0 && stop.status != 'unconfirmed' :data-status-preponed="stop.arrivalDelay < 0"
? 'delayed' :data-status="stop.status"
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
> >
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'"> <span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s> <s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }} {{ timestampToString(stop.arrivalReal) }}
@@ -27,19 +25,16 @@
</span> </span>
<span <span
v-if=" v-if="stop.duration"
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
"
class="date stop" class="date stop"
:data-stop-types="stop.type.replace(', ', '-')" :data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status=" :data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
> >
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }} {{
stop.duration == 0 && stop.departureDelay > 0
? stop.departureDelay - stop.arrivalDelay
: stop.duration
}}
{{ stop.type == '' ? 'pt' : stop.type }} {{ stop.type == '' ? 'pt' : stop.type }}
</span> </span>
@@ -49,16 +44,11 @@
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay) (stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
" "
class="date departure" class="date departure"
:data-status=" :data-status-delayed="stop.departureDelay > 0"
stop.departureDelay > 0 && stop.status == 'confirmed' :data-status-preponed="stop.departureDelay < 0"
? 'delayed' :data-status-confirmed="stop.status == 'confirmed'"
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
> >
o.
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'"> <span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
<s>{{ timestampToString(stop.departureScheduled) }}</s> <s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }} {{ timestampToString(stop.departureReal) }}
@@ -96,14 +86,18 @@ $delayedClr: salmon;
$dateClr: #525151; $dateClr: #525151;
$stopExchangeClr: #db8e29; $stopExchangeClr: #db8e29;
$stopDefaultClr: #252525; $stopDefaultClr: #252525;
$stopNameClr: #22a8d1; $stopNameClr: #303030;
s {
color: #ccc;
}
.stop-label { .stop-label {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
&[data-sbl='true'] { &[data-minor='true'] {
.date { .date {
display: none; display: none;
} }
@@ -117,6 +111,7 @@ $stopNameClr: #22a8d1;
.name { .name {
background: $stopNameClr; background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
display: flex; display: flex;
@@ -130,6 +125,10 @@ $stopNameClr: #22a8d1;
.date { .date {
background: $dateClr; background: $dateClr;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
} }
.stop { .stop {
@@ -145,28 +144,45 @@ $stopNameClr: #22a8d1;
color: $delayedClr; color: $delayedClr;
} }
} }
.arrival,
.departure {
&[data-status='delayed'] {
s {
color: #999;
} }
.stop .arrival {
&[data-status='confirmed'][data-status-delayed='true'] {
span { span {
color: $delayedClr; color: $delayedClr;
} }
} }
&[data-status='preponed'] { &[data-status='confirmed'][data-status-preponed='true'] {
s { span {
color: #999; color: $preponedClr;
}
} }
&[data-status='stopped'][data-status-preponed='true'] {
span {
color: $preponedClr;
}
}
&[data-status='stopped'][data-status-delayed='true'] {
span {
color: $delayedClr;
}
}
}
.stop .departure[data-status-confirmed='true'] {
&[data-status-delayed='true'] {
span {
color: $delayedClr;
}
}
&[data-status-preponed='true'] {
span { span {
color: $preponedClr; color: $preponedClr;
} }
} }
} }
}
</style> </style>
+34 -6
View File
@@ -28,12 +28,15 @@
</span> </span>
</span> </span>
<strong> <b
<span v-if="train.timetableData" class="text--primary" v-if="train.timetableData"
>{{ train.timetableData.category }}&nbsp;</span data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(train.timetableData.category)"
class="text--primary tooltip-help"
> >
<span class="train-number">{{ train.trainNo }}</span> {{ train.timetableData.category }}
</strong> </b>
<b class="train-number">{{ train.trainNo }}</b>
<span>&bull;</span> <span>&bull;</span>
<b <b
class="level-badge driver" class="level-badge driver"
@@ -131,6 +134,18 @@
<div> <div>
<img src="/images/icon-speed.svg" alt="speed icon" /> <img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h {{ train.speed }} km/h
<span v-if="stockSpeedLimit != Infinity">
&bull;
<em
class="text--grayed"
style="text-decoration: underline dotted"
tabindex="0"
:data-tooltip="$t('trains.vmax-tooltip')"
>
{{ stockSpeedLimit }} km/h
</em>
</span>
</div> </div>
</div> </div>
@@ -169,9 +184,10 @@ import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common'; import { Train } from '../../typings/common';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin], mixins: [trainInfoMixin, styleMixin, modalTrainMixin, trainCategoryMixin],
components: { ProgressBar, StockList }, components: { ProgressBar, StockList },
props: { props: {
@@ -191,6 +207,18 @@ export default defineComponent({
}; };
}, },
computed: {
stockSpeedLimit() {
return this.train.stockList.reduce((acc, stockName) => {
const vehicleSpeed =
this.apiStore.vehiclesData?.find((v) => v.name == stockName.split(':')[0])?.group.speed ??
300;
return Math.min(vehicleSpeed, acc);
}, 300);
}
},
methods: { methods: {
navigateToJournal() { navigateToJournal() {
this.$router.push({ this.$router.push({
+12 -9
View File
@@ -21,7 +21,7 @@ export default defineComponent({
computed: { computed: {
chosenTrain() { chosenTrain() {
return this.store.trainList.find((train) => train.id == this.store.chosenModalTrainId); return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
} }
}, },
@@ -29,8 +29,15 @@ export default defineComponent({
chosenTrain(train: Train | undefined) { chosenTrain(train: Train | undefined) {
this.$nextTick(() => { this.$nextTick(() => {
if (train) { if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement; const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus(); contentEl.focus();
} else {
(this.store.modalLastClickedTarget as any)?.focus();
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 90);
} }
}); });
} }
@@ -47,12 +54,14 @@ export default defineComponent({
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%;
color: white; color: white;
z-index: 200; z-index: 200;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: flex-start;
text-align: left; text-align: left;
} }
@@ -73,10 +82,10 @@ export default defineComponent({
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
margin-top: 1em;
width: 95vw; width: 95vw;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; box-shadow: 0 0 15px 10px #0e0e0e;
@@ -91,10 +100,4 @@ export default defineComponent({
} }
} }
} }
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style> </style>
+72 -29
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type" :data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive" :data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed" :data-last-confirmed="stop.isLastConfirmed"
x
> >
<span class="stop_info"> <span class="stop_info">
<span class="distance"> <span class="distance">
@@ -48,26 +47,46 @@
<span <span
v-if=" v-if="
stop.departureLine && stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine && scheduleStops[i + 1] != undefined &&
!/sbl/gi.test(stop.departureLine) !/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
" "
> >
{{ stop.departureLine }} <div class="scenery-route">
<span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
| {{ stop.departureLineInfo.routeSpeed }}
<span v-if="stop.departureLineInfo.isElectric">⚡</span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span> </span>
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
<div>{{ stop.departureLine }}</div>
<div
class="scenery-change-name"
v-if="
i < scheduleStops.length - 1 &&
stop.sceneryName != scheduleStops[i + 1].sceneryName
"
>
{{ scheduleStops[i + 1].sceneryName }}
</div> </div>
<div>
{{ scheduleStops[i + 1].arrivalLine }} <div
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
class="scenery-change-name"
>
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
<span v-if="stop.departureLineInfo?.routeTracks == 1"> &UpDownArrow;</span>
<span v-else> &UpArrowDownArrow;</span>
</div>
<div class="scenery-route">
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
<span v-if="scheduleStops[i + 1].arrivalLineInfo!.isElectric">⚡</span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span>
</div> </div>
</span> </span>
</div> </div>
@@ -85,7 +104,7 @@ import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common'; import { StationRoutesInfo, Train } from '../../typings/common';
export interface TrainScheduleStop { export interface TrainScheduleStop {
nameHtml: string; nameHtml: string;
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
isSBL: boolean; isSBL: boolean;
sceneryName: string | null; sceneryName: string | null;
sceneryHash: string;
distance: number; distance: number;
arrivalLine: string | null; arrivalLine: string | null;
departureLine: string | null; departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null; comments: string | null;
} }
@@ -146,13 +169,25 @@ export default defineComponent({
return ( return (
this.train.timetableData?.followingStops.map((stop, i, arr) => { this.train.timetableData?.followingStops.map((stop, i, arr) => {
if ( const isExternal =
i > 0 && i > 0 &&
stop.arrivalLine && stop.arrivalLine != null &&
stop.arrivalLine != arr[i - 1].departureLine && (stop.arrivalLine != arr[i - 1].departureLine ||
!/sbl/gi.test(stop.arrivalLine) (stop.arrivalLine == arr[i - 1].departureLine &&
) !/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
currentSceneryIndex++;
if (isExternal) currentSceneryIndex++;
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.arrivalLine
);
const departureLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.departureLine
);
return { return {
nameHtml: stop.stopName, nameHtml: stop.stopName,
@@ -174,14 +209,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine, arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine, departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType, type: stop.stopType,
distance: stop.stopDistance, distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i), isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere, isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName), isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route', position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '', sceneryName,
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed' status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
}; };
}) ?? [] }) ?? []
@@ -483,7 +522,12 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
} }
} }
.bottom-line-info { .scenery-route {
img {
vertical-align: middle;
}
}
.scenery-change-name { .scenery-change-name {
position: relative; position: relative;
margin: 0.25em 0; margin: 0.25em 0;
@@ -500,5 +544,4 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
transform: translate(0, -50%); transform: translate(0, -50%);
} }
} }
}
</style> </style>
+2 -13
View File
@@ -18,8 +18,8 @@
v-for="train in trains" v-for="train in trains"
:key="train.id" :key="train.id"
tabindex="0" tabindex="0"
@click.stop="selectModalTrain(train.id, $event.currentTarget)" @click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.id, $event.currentTarget)" @keydown.enter="selectModalTrain(train, $event.currentTarget)"
> >
<TrainInfo :train="train" :extended="false" /> <TrainInfo :train="train" :extended="false" />
</li> </li>
@@ -77,17 +77,6 @@ export default defineComponent({
return Status.Data.Loaded; return Status.Data.Loaded;
} }
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
} }
}); });
</script> </script>
File diff suppressed because it is too large Load Diff
-365
View File
@@ -1,369 +1,4 @@
{ {
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"station-type",
"access",
"control",
"blockades",
"signals",
"addons"
],
"options": [
{
"id": "real",
"name": "real",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "fictional",
"name": "fictional",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "non-public",
"name": "nonPublic",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "unavailable",
"name": "unavailable",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "abandoned",
"name": "abandoned",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "junction",
"name": "junction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "nonJunction",
"name": "nonJunction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "SPK",
"name": "SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS",
"name": "SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPE",
"name": "SPE",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-M",
"name": "mechaniczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "mechanical",
"name": "mechaniczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SUP",
"name": "SUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "ASDEK",
"name": "ASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noASDEK",
"name": "noASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "SBL",
"name": "SBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "modern",
"name": "współczesna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "semaphores",
"name": "kształtowa",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "mixed",
"name": "mieszana",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "historical",
"name": "historyczna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "free",
"name": "free",
"section": "status",
"value": false,
"defaultValue": false
},
{
"id": "occupied",
"name": "occupied",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "endingStatus",
"name": "endingStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "afkStatus",
"name": "afkStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "noSpaceStatus",
"name": "noSpaceStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "unavailableStatus",
"name": "unavailableStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
}
],
"sliders": [
{
"id": "min-lvl",
"name": "minLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "max-lvl",
"name": "maxLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 20,
"defaultValue": 20
},
{
"id": "min-vmax",
"name": "minVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 0,
"defaultValue": 0
},
{
"id": "max-vmax",
"name": "maxVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 200,
"defaultValue": 200
},
{
"id": "routes-1t-cat",
"name": "minOneWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-1t-other",
"name": "minOneWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-cat",
"name": "minTwoWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-other",
"name": "minTwoWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
}
],
"modes": [
{
"id": "include-selected",
"name": "include-selected",
"section": "mode",
"value": true,
"defaultValue": true
},
{
"id": "save",
"name": "save",
"section": "mode",
"value": true,
"defaultValue": true
}
],
"regions": [ "regions": [
{ {
"id": "eu", "id": "eu",
+57 -17
View File
@@ -48,7 +48,47 @@
"footer": { "footer": {
"discord": "Stacjownik Discord server" "discord": "Stacjownik Discord server"
}, },
"categories": {
"EI": "domestic express",
"EC": "international express",
"EN": "domestic night express",
"MP": "intervoivodeship bullet",
"MO": "intervoivodeship regio",
"MM": "international bullet",
"MH": "intervoivodeship night bullet",
"RP": "voivodeship bullet",
"RM": "international voivodeship regio",
"RO": "voivodeship regio",
"RA": "voivodeship regio (urban)",
"PW": "empty passenger",
"PX": "empty passenger test drive",
"TC": "international freight (intermodal)",
"TG": "international freight (organized cargo)",
"TR": "international freight (unorganized cargo)",
"TD": "domestic freight (intermodal)",
"TM": "domestic freight (organized cargo)",
"TN": "domestic freight (unorganized cargo)",
"TK": "freight (for stations & sidings)",
"TS": "empty freight test drive",
"TH": "locomotive rolling stock (over 3 vehicles)",
"LT": "freight locomotive only",
"LP": "passenger locomotive only",
"LS": "shunting locomotive only",
"LZ": "shunting locomotive only",
"ZN": "inspection / diagnostic type",
"ZU": "other maintenance type",
"E": "electric loco",
"J": "EMU",
"S": "diesel loco",
"M": "DMU"
},
"vehicle-preview": { "vehicle-preview": {
"loading": "Loading preview...", "loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/" "error": "Oops! The vehicle preview seems to be missing! :/"
@@ -112,8 +152,8 @@
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE", "donate": "DONATE",
"search-button": "Search", "search-button": "SEARCH",
"reset-button": "Reset", "reset-button": "RESET",
"sort-title": "SORT BY:", "sort-title": "SORT BY:",
"filter-title": "FILTER BY:", "filter-title": "FILTER BY:",
@@ -123,9 +163,11 @@
"search-train": "Train no.", "search-train": "Train no.",
"search-driver": "Driver name", "search-driver": "Driver name",
"search-dispatcher": "Dispatcher name", "search-dispatcher": "Dispatcher name",
"search-station": "Scenery name", "search-station": "Scenery name / #",
"search-author": "Timetable author name", "search-author": "Timetable author name",
"search-issuedFrom": "Origin scenery name", "search-issuedFrom": "Issuing scenery name",
"search-via": "Via scenery name",
"search-terminatingAt": "Terminating scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)", "search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)", "search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)", "search-date": "Date (UTC+2 / CEST)",
@@ -329,6 +371,9 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
"we4a-tooltip": "Non-electrified track",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -491,21 +536,16 @@
"option-timetables-history": "Timetables history PL1", "option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1", "option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by", "timetable-via": "ALL TIMETABLES",
"timetable-author-unknown": "Author unknown", "timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID", "timetable-issued-date": "Issued",
"timetables-history-number": "Number", "timetable-issued-by": " by:",
"timetables-history-route": "Route", "timetable-issued-for": " for driver:",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Rate:",
"dispatchers-history-dispatcher": "Dispatcher", "dispatcher-status-changes": "Status changes:",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No recorded scenery history!",
+55 -14
View File
@@ -45,6 +45,47 @@
"footer": { "footer": {
"discord": "Serwer Discord Stacjownika" "discord": "Serwer Discord Stacjownika"
}, },
"categories": {
"EI": "ekspres krajowy",
"EC": "ekspres międzynarodowy",
"EN": "ekspres krajowy nocny",
"MP": "międzywojewódzki pospieszny",
"MO": "międzywojewódzki osobowy",
"MM": "międzynarodowy pospieszny",
"MH": "międzywojewódzki pospieszny (nocny)",
"RP": "wojewódzki pospieszny",
"RO": "wojewódzki osobowy",
"RM": "wojewódzki osobowy międzynarodowy",
"RA": "wojewódzki osobowy algomeracyjny",
"PW": "pasażerski próżny - służbowy",
"PX": "pasażerski próżny próbny",
"TC": "towarowy międzynarodowy intermodalny",
"TG": "towarowy międzynarodowy masowy",
"TR": "towarowy międzynarodowy niemasowy",
"TD": "towarowy krajowy intermodalny",
"TM": "towarowy krajowy masowy",
"TN": "towarowy krajowy niemasowy",
"TK": "towarowy zdawczy",
"TS": "towarowy próżny próbny",
"TH": "skład lokomotyw (powyżej 3 pojazdów)",
"LT": "lokomotywa towarowa luzem",
"LP": "lokomotywa pasażerska luzem",
"LS": "lokomotywa manewrowa luzem",
"LZ": "lokomotywa dla poc. utrzymaniowo-naprawczych",
"ZN": "inspekcyjny / diagnostyczny",
"ZU": "inny utrzymaniowy",
"E": "elektrowóz",
"J": "EZT",
"S": "spalinowóz",
"M": "SZT"
},
"vehicle-preview": { "vehicle-preview": {
"loading": "Ładowanie podglądu...", "loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/" "error": "Ups! Nie znaleziono podglądu pojazdu! :/"
@@ -119,9 +160,11 @@
"search-train": "Nr pociągu / #", "search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty", "search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego", "search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii", "search-station": "Nazwa scenerii / #",
"search-author": "Nick autora rozkładu jazdy", "search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa", "search-issuedFrom": "Sceneria początkowa",
"search-via": "Przez scenerię",
"search-terminatingAt": "Sceneria końcowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)", "search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)", "search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)", "search-date": "Data (UTC+2 / CEST)",
@@ -315,6 +358,9 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
"we4a-tooltip": "Szlak niezelektryfikowany",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -474,21 +520,16 @@
"option-timetables-history": "Historia rozkładów PL1", "option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1", "option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez", "timetable-via": "WSZYSTKIE RJ",
"timetable-author-unknown": "Autor nieznany", "timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID", "timetable-issued-date": "Wystawiony",
"timetables-history-number": "Numer", "timetable-issued-by": " przez:",
"timetables-history-route": "Trasa", "timetable-issued-for": " dla maszynisty:",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Ocena:",
"dispatchers-history-dispatcher": "Dyżurny", "dispatcher-status-changes": "Zmiany statusów:",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!", "history-list-empty": "Brak historii dla tej scenerii!",
+8 -8
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore'; import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -11,20 +12,19 @@ export default defineComponent({
}, },
methods: { methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) { selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = train.modalId;
document.body.classList.add('no-scroll'); if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target; if (target) this.store.modalLastClickedTarget = target;
}, },
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide(); this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
} }
} }
}); });
+12
View File
@@ -0,0 +1,12 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getCategoryExplanation(categoryCode: string) {
const categoryKey = categoryCode.slice(0, 2);
const vehicleTypeKey = categoryCode.slice(-1);
return `${this.$t('categories.' + categoryKey)}\n(${this.$t('categories.' + vehicleTypeKey)})`;
}
}
});
+11 -8
View File
@@ -108,16 +108,19 @@ export default defineComponent({
}, },
currentDelay(stops: TrainStop[]) { currentDelay(stops: TrainStop[]) {
const delay = const lastConfirmedStop = stops.find(
stops.find(
(stop, i) => (stop, i) =>
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed) (i == 0 && !stop.confirmed) ||
)?.departureDelay || 0; (i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
(stops[i + 1] == undefined && stop.confirmed)
);
if (delay > 0) const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
else if (delay < 0) if (lastDelay > 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`; return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
else if (lastDelay < 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
else return this.$t('trains.on-time'); else return this.$t('trains.on-time');
}, },
+1 -1
View File
@@ -58,7 +58,7 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined) if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` }; return { el: `.app_main`, top: -15 };
if (savedPosition) return savedPosition; if (savedPosition) return savedPosition;
}, },
+57 -16
View File
@@ -4,24 +4,21 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings'; import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', { export const useApiStore = defineStore('apiStore', {
state: () => ({ state: () => ({
dataStatuses: { dataStatuses: {
connection: Status.Data.Loading, connection: Status.Data.Loading,
sceneries: Status.Data.Loading sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
}, },
activeData: undefined as API.ActiveData.Response | undefined, activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response, donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[], sceneryData: [] as StationJSONData[],
lastFetchData: new Date(), nextUpdateTime: 0,
client: undefined as AxiosInstance | undefined, client: undefined as AxiosInstance | undefined,
@@ -54,18 +51,40 @@ export const useApiStore = defineStore('apiStore', {
// Static data // Static data
this.fetchDonatorsData(); this.fetchDonatorsData();
this.fetchStationsGeneralInfo(); this.fetchStationsGeneralInfo();
this.fetchVehiclesInfo();
window.requestAnimationFrame(this.updateTick);
},
updateTick(t: number) {
if (this.dataStatuses.connection == Status.Data.Offline) return;
if (t >= this.nextUpdateTime) {
this.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.updateTick);
}, },
async fetchActiveData() { async fetchActiveData() {
// if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
// import('../../tests/data/getActiveData.json').then((data) => {
// console.warn('activeData: mocking mode');
// this.activeData = data.default as API.ActiveData.Response;
// this.dataStatuses.connection = Status.Data.Loaded;
// });
// return;
// }
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response.data;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded; this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) { } catch (error) {
this.dataStatuses.connection = Status.Data.Error; this.dataStatuses.connection = Status.Data.Error;
@@ -84,17 +103,39 @@ export const useApiStore = defineStore('apiStore', {
}, },
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
try {
const sceneryData: StationJSONData[] = ( const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries') await this.client!.get<StationJSONData[]>('api/getSceneries')
).data; ).data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
}
this.dataStatuses.sceneries = Status.Data.Loaded; this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData; this.sceneryData = sceneryData;
} catch (error) {
this.dataStatuses.sceneries = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o sceneriach:', error);
}
},
async fetchVehiclesInfo() {
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
// import('../../tests/data/vehicles.json').then((data) => {
// console.warn('vehicles.json: mocking mode');
// this.vehiclesData = data.default;
// this.dataStatuses.vehicles = Status.Data.Loaded;
// });
// return;
// }
try {
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
this.vehiclesData = response.data;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) {
this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
}
} }
} }
}); });
+52 -19
View File
@@ -36,17 +36,13 @@ export const useMainStore = defineStore('mainStore', {
}) as MainStoreState, }) as MainStoreState,
getters: { getters: {
checkpointsTrains() {
return checkpointsTrains;
},
trainList(): Train[] { trainList(): Train[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
checkpointsTrains.clear(); checkpointsTrains.clear();
sceneriesTrains.clear(); sceneriesTrains.clear();
const x = (apiStore.activeData?.trains ?? []) return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online) .filter((train) => train.timetable || train.online)
.map((train) => { .map((train) => {
const stock = train.stockString.split(';'); const stock = train.stockString.split(';');
@@ -65,6 +61,7 @@ export const useMainStore = defineStore('mainStore', {
const trainObj = { const trainObj = {
id: train.id, id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo, trainNo: train.trainNo,
mass: train.mass, mass: train.mass,
@@ -99,7 +96,17 @@ export const useMainStore = defineStore('mainStore', {
followingStops: timetable.stopList, followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
sceneries: timetable.sceneries, sceneries: timetable.sceneries,
sceneryNames: sceneryNames.reverse() sceneryNames: sceneryNames.reverse(),
timetablePath: timetable.path.split(';').map((pathElementString) => {
const [arrival, station, departure] = pathElementString.split(',');
return {
arrivalRouteExt: arrival,
departureRouteExt: departure,
stationName: station.split(' ').slice(0, -1).join(' '),
stationHash: station.split(' ').slice(-1).join(' ').replace('.sc', '')
};
})
} }
: undefined : undefined
} as Train; } as Train;
@@ -113,11 +120,25 @@ export const useMainStore = defineStore('mainStore', {
} else sceneriesTrains.set(train.currentStationName, [trainObj]); } else sceneriesTrains.set(train.currentStationName, [trainObj]);
// Checkpoints trains map // Checkpoints trains map
timetable?.stopList.forEach((stop, i) => { if (trainObj.timetableData) {
if (/strong|podg\.|pe\./.test(stop.stopName)) { let currentSceneryIndex = 0;
const timetablePath = trainObj.timetableData.timetablePath;
trainObj.timetableData.followingStops.forEach((stop, i) => {
if (/strong|podg|pe/.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = { const checkpointTrain: CheckpointTrain = {
train: trainObj, train: trainObj,
checkpointStop: stop checkpointStop: stop,
previousSceneryElement:
currentSceneryIndex > 0 ? timetablePath[currentSceneryIndex - 1] : null,
nextSceneryElement:
currentSceneryIndex < timetablePath.length - 1
? timetablePath[currentSceneryIndex + 1]
: null,
timetablePathElement: timetablePath[currentSceneryIndex]
}; };
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) { if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
@@ -127,12 +148,14 @@ export const useMainStore = defineStore('mainStore', {
]); ]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]); } else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
} }
if (timetablePath[currentSceneryIndex].departureRouteExt == stop.departureLine)
currentSceneryIndex++;
}); });
}
return trainObj; return trainObj;
}); });
return x;
}, },
// computed active sceneries // computed active sceneries
@@ -143,7 +166,6 @@ export const useMainStore = defineStore('mainStore', {
if (!apiStore.activeData?.activeSceneries) return []; if (!apiStore.activeData?.activeSceneries) return [];
console.time('activeSceneryList');
const offlineActiveSceneries = this.trainList.reduce((acc, train) => { const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
@@ -238,9 +260,16 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name); const station = this.stationList.find((s) => s.name === scenery.name);
const checkpoints = [scenery.name]; let checkpointsSet: Set<string> = new Set();
if (station?.generalInfo?.checkpoints && station.generalInfo.checkpoints.length > 0)
checkpoints.push(...station.generalInfo.checkpoints.filter((cp) => cp !== station.name)); // Add checkpoints to active scenery data
checkpointsSet.add(scenery.name.toLowerCase());
station?.generalInfo?.checkpoints.forEach((cpName) => {
checkpointsSet.add(cpName.toLowerCase());
});
const checkpoints = Array.from(checkpointsSet);
scenery.stationTrains = scenery.stationTrains =
sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? []; sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
@@ -251,8 +280,14 @@ export const useMainStore = defineStore('mainStore', {
if (!scheduledTrains) return; if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop }) => { scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
scenery.scheduledTrains.push({ train, checkpointStop }); if (
scenery.name != timetablePathElement.stationName &&
scenery.hash != timetablePathElement.stationHash
)
return;
scenery.scheduledTrains.push({ train, checkpointStop, timetablePathElement, ...v });
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return; if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
@@ -266,8 +301,6 @@ export const useMainStore = defineStore('mainStore', {
}); });
} }
console.timeEnd('activeSceneryList');
return allActiveSceneries; return allActiveSceneries;
}, },
+1 -1
View File
@@ -4,7 +4,7 @@
.list_wrapper { .list_wrapper {
overflow-y: auto; overflow-y: auto;
height: 90vh; height: 90vh;
min-height: 550px; min-height: 650px;
margin-top: 0.5em; margin-top: 0.5em;
position: relative; position: relative;
+32 -16
View File
@@ -228,6 +228,10 @@ a.a-button {
background-color: #3c3c3c; background-color: #3c3c3c;
} }
&:hover {
background-color: #555;
}
} }
&.btn--image { &.btn--image {
@@ -283,6 +287,31 @@ a.a-button {
} }
} }
// Basic tooltip
[data-tooltip] {
cursor: help;
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(0, -50%);
content: attr(data-tooltip);
color: white;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.5em;
max-width: 300px;
z-index: 100;
}
.tooltip-help {
cursor: help;
}
@include smallScreen { @include smallScreen {
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.5em; width: 0.5em;
@@ -296,24 +325,11 @@ a.a-button {
background-color: #777; background-color: #777;
} }
} }
}
// Basic tooltip
[data-tooltip]:hover::after, [data-tooltip]:hover::after,
[data-tooltip]:focus::after { [data-tooltip]:focus::after {
position: absolute; transform: translate(-50%, 2em);
transform: translate(10px, -50%); left: 50%;
width: 100%;
content: attr(data-tooltip);
color: white;
background-color: #171717;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.25em;
max-width: 300px;
z-index: 100;
} }
[data-tooltip] {
cursor: help;
} }
+3 -7
View File
@@ -1,11 +1,7 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table { table.scenery-history-table {
width: 100%; width: 100%;
table-layout: fixed;
min-width: 900px;
border-collapse: collapse; border-collapse: collapse;
thead { thead {
@@ -25,7 +21,7 @@ table.scenery-history-table {
td { td {
padding: 0.75em; padding: 0.75em;
border-bottom: solid 5px #111; border-bottom: solid 5px #181818;
} }
} }
+12 -6
View File
@@ -1,4 +1,4 @@
import { Status } from './common'; import { Status, VehicleData } from './common';
export enum APIDataStatus { export enum APIDataStatus {
OK = 'OK', OK = 'OK',
@@ -19,11 +19,12 @@ export namespace API {
apiStatuses?: APIStatuses; apiStatuses?: APIStatuses;
} }
} }
export namespace DispatcherHistory { export namespace DispatcherHistory {
export type Response = Data[]; export type Response = Data[];
export interface Data { export interface Data {
id: string; id: number;
currentDuration: number; currentDuration: number;
dispatcherId: number; dispatcherId: number;
dispatcherName: string; dispatcherName: string;
@@ -38,6 +39,7 @@ export namespace API {
stationName: string; stationName: string;
timestampFrom: number; timestampFrom: number;
timestampTo?: number; timestampTo?: number;
statusHistory: string[];
} }
} }
@@ -193,6 +195,8 @@ export namespace API {
TWR: boolean; TWR: boolean;
SKR: boolean; SKR: boolean;
sceneries: string[]; sceneries: string[];
path: string;
} }
} }
@@ -247,16 +251,14 @@ export namespace API {
hashesString?: string; hashesString?: string;
currentSceneryName?: string; currentSceneryName?: string;
currentSceneryHash?: string; currentSceneryHash?: string;
routeSceneries?: string; routeSceneries?: string;
checkpointArrivals?: string[]; checkpointArrivals?: string[];
checkpointDepartures?: string[]; checkpointDepartures?: string[];
checkpointArrivalsScheduled?: string[]; checkpointArrivalsScheduled?: string[];
checkpointDeparturesScheduled?: string[]; checkpointDeparturesScheduled?: string[];
checkpointStopTypes?: string[]; checkpointStopTypes?: string[];
visitedSceneries?: string[];
path: string;
} }
export type Response = Data[]; export type Response = Data[];
@@ -316,6 +318,10 @@ export namespace API {
export namespace Donators { export namespace Donators {
export type Response = string[]; export type Response = string[];
} }
export namespace Vehicles {
export type Response = VehicleData[];
}
} }
export namespace GithubAPI { export namespace GithubAPI {
+42
View File
@@ -39,8 +39,16 @@ export interface RegionCounters {
timetablesCount: number; timetablesCount: number;
} }
export interface TimetablePathElement {
arrivalRouteExt?: string;
departureRouteExt?: string;
stationName: string;
stationHash: string;
}
export interface Train { export interface Train {
id: string; id: string;
modalId: string;
mass: number; mass: number;
length: number; length: number;
speed: number; speed: number;
@@ -72,6 +80,7 @@ export interface Train {
routeDistance: number; routeDistance: number;
sceneries: string[]; sceneries: string[];
sceneryNames: string[]; sceneryNames: string[];
timetablePath: TimetablePathElement[];
}; };
} }
@@ -187,4 +196,37 @@ export interface TrainStop {
export interface CheckpointTrain { export interface CheckpointTrain {
checkpointStop: TrainStop; checkpointStop: TrainStop;
train: Train; train: Train;
timetablePathElement: TimetablePathElement;
previousSceneryElement: TimetablePathElement | null;
nextSceneryElement: TimetablePathElement | null;
}
// Vehicles Data
export interface VehicleData {
id: number;
name: string;
type: string;
cabinName: string | null;
restrictions: Record<string, any> | null;
vehicleGroupsId: number;
group: VehiclesGroup;
}
export interface VehiclesGroup {
id: number;
name: string;
speed: number;
length: number;
weight: number;
cargoTypes: VehicleCargo[] | null;
locoProps: {
coldStart: boolean;
doubleManned: boolean;
} | null;
}
export interface VehicleCargo {
id: string;
weight: number;
} }
+4 -1
View File
@@ -267,7 +267,10 @@ export default defineComponent({
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined; const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
if (dispatcher) queries.push(`dispatcherName=${dispatcher}`); if (dispatcher) queries.push(`dispatcherName=${dispatcher}`);
if (station) queries.push(`stationName=${station}`);
if (station.startsWith("#")) queries.push(`stationHash=${station.slice(1)}`);
else if (station.length > 0) queries.push(`stationName=${station}`);
if (timestampFrom && timestampTo) if (timestampFrom && timestampTo)
queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`); queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
+21 -5
View File
@@ -126,6 +126,8 @@ interface TimetablesQueryParams {
dateTo?: string; dateTo?: string;
issuedFrom?: string; issuedFrom?: string;
terminatingAt?: string;
via?: string;
countFrom?: number; countFrom?: number;
countLimit?: number; countLimit?: number;
@@ -206,6 +208,8 @@ export default defineComponent({
'search-driver': '', 'search-driver': '',
'search-dispatcher': '', 'search-dispatcher': '',
'search-issuedFrom': '', 'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-date': '' 'search-date': ''
} as Journal.TimetableSearchType); } as Journal.TimetableSearchType);
@@ -299,11 +303,17 @@ export default defineComponent({
}, },
setOptions(options: { [key: string]: string }) { setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? ''; Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues['search-driver'] = options['search-driver'] ?? ''; this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? ''; });
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? ''; // this.searchersValues['search-date'] = options['search-date'] ?? '';
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
// this.searchersValues['search-train'] = options['search-train'] ?? '';
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
// this.searchersValues['search-via'] = options['search-via'] ?? '';
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
this.sorterActive.id = this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId'; (options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
@@ -347,6 +357,8 @@ export default defineComponent({
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined; const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateFrom = this.searchersValues['search-date'].trim() || undefined; const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined; const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
let dateTo: string | undefined = undefined; let dateTo: string | undefined = undefined;
@@ -418,6 +430,10 @@ export default defineComponent({
queryParams['authorName'] = authorName; queryParams['authorName'] = authorName;
queryParams['dateFrom'] = dateFrom; queryParams['dateFrom'] = dateFrom;
queryParams['dateTo'] = dateTo; queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom;
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['issuedFrom'] = issuedFrom; queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] = queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined; this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
+15 -29
View File
@@ -22,8 +22,8 @@
v-for="(viewMode, i) in viewModes" v-for="(viewMode, i) in viewModes"
:key="i" :key="i"
class="btn btn--option" class="btn btn--option"
:class="{ checked: currentMode == viewMode.component }"
@click="setViewMode(viewMode.component)" @click="setViewMode(viewMode.component)"
:data-checked="currentMode == viewMode.component"
> >
{{ $t(viewMode.id) }} {{ $t(viewMode.id) }}
</button> </button>
@@ -121,10 +121,6 @@ export default defineComponent({
Status: Status.Data Status: Status.Data
}), }),
// activated() {
// this.loadSelectedCheckpoint();
// },
setup() { setup() {
const route = useRoute(); const route = useRoute();
@@ -215,11 +211,10 @@ button.back-btn {
position: relative; position: relative;
width: 100%;
max-width: var(--max-container-width); max-width: var(--max-container-width);
min-height: 100vh; width: 100%;
margin: 1rem 0; padding: 1rem 0;
text-align: center; text-align: center;
&[data-timetable-only='true'] { &[data-timetable-only='true'] {
@@ -228,30 +223,27 @@ button.back-btn {
} }
} }
.scenery-left { .scenery-left,
.scenery-right {
position: relative; position: relative;
overflow: auto;
background-color: #181818; background-color: #181818;
padding: 1em 0.5em; padding: 1em 0.5em;
height: 100vh; height: calc(100vh - 0.5em);
min-height: 750px; min-height: 800px;
max-height: 1000px; max-height: 2000px;
overflow: auto; }
.scenery-left {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.scenery-right { .scenery-right {
background: #181818;
padding: 1em 0.5em;
height: 100vh;
min-height: 750px;
max-height: 1000px;
display: grid; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr;
gap: 1em; gap: 1em;
} }
@@ -261,18 +253,12 @@ button.back-btn {
.info-actions { .info-actions {
display: flex; display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center;
gap: 0.75em; gap: 0.75em;
.btn { button {
padding: 0.5em; padding: 0.5em;
box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] {
color: var(--clr-primary);
}
} }
} }
+1 -1
View File
@@ -109,7 +109,7 @@ export default defineComponent({
this.$nextTick(() => { this.$nextTick(() => {
if (this.trainId) { if (this.trainId) {
this.selectModalTrain(this.trainId); this.selectModalTrainById(this.trainId);
} }
}); });
} }
+4 -2
View File
@@ -6,8 +6,10 @@ declare module '*.vue' {
export default component; export default component;
} }
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_APP_API_DEV: string; readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
readonly VITE_APP_WS_DEV: string; readonly VITE_API_VEHICLES_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_ACTIVE_DATA_MODE: 'production' | 'mocking' | 'development';
readonly VITE_UPDATE_TEST: 'test' | 'production';
} }
interface ImportMeta { interface ImportMeta {
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
["kowbojYT","matseb","peterminecraft333","MIlanSVK_SimRailCZ","kierownik_z_ulicy","luk31as","pppatryk123","Kryszakos","MilyPan","paweld","Isitkiwi","Krisoy007","zeswaq","robcioRK","Ugulele","Spanky","KapitanKoza","Kuba6396","BravuraLion","trichlor","jasieleczeq","trannelgamer","tommy001","Waffel","krytaqu","NadrazakHonza","zordem","Ludolog"]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -16
View File
@@ -24,29 +24,16 @@ export default defineConfig({
cleanupOutdatedCaches: true, cleanupOutdatedCaches: true,
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/getSceneries/i, urlPattern:
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
handler: 'StaleWhileRevalidate', handler: 'StaleWhileRevalidate',
options: { options: {
cacheName: 'spythere-sceneries-cache', cacheName: 'stacjownik-api-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200] statuses: [0, 200]
} }
} }
}, },
{
urlPattern: /^https:\/\/static.spythere.eu\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'spythere-static-cache',
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 8
}
}
}
] ]
}, },
devOptions: { devOptions: {
+518 -876
View File
File diff suppressed because it is too large Load Diff