Compare commits

...

109 Commits

Author SHA1 Message Date
Spythere fbc9785341 Merge pull request #105 from Spythere/development
v1.27.0
2024-08-24 00:20:08 +02:00
Spythere 9fd02c2336 chore: scenery view border radius 2024-08-23 16:24:21 +02:00
Spythere c031dd55c1 chore: removed return button 2024-08-23 16:22:54 +02:00
Spythere b0870699a4 hotfix: anchor style 2024-08-22 23:20:09 +02:00
Spythere a8da634b0e hotfix: TrainsView watcher causing routing problems 2024-08-22 23:19:42 +02:00
Spythere 8920b1e5e8 hotfix: view styles 2024-08-22 17:00:40 +02:00
Spythere 4fa1c05831 fix: input attrs 2024-08-22 16:44:51 +02:00
Spythere ecef2d5ee4 chore: backwards compatibility with train modal for ext. links 2024-08-22 16:37:47 +02:00
Spythere 1749871d08 fix: filters double click 2024-08-22 16:37:12 +02:00
Spythere 5545616706 chore: styling hotfixes and improvements 2024-08-22 02:28:40 +02:00
Spythere b35bb03868 merge: 'dominik-korsa-links' into development 2024-08-22 02:27:53 +02:00
Spythere b9521918cb hotfix: translation fix 2024-08-21 21:38:21 +02:00
Spythere 7ad17fc2c5 chore: style improvements & finishing touches 2024-08-21 21:33:46 +02:00
Spythere a80144cb1c chore: missing translations 2024-08-21 18:30:17 +02:00
Spythere 1227cdb94a chore: driver view links 2024-08-21 18:12:35 +02:00
Spythere b33594fd6f fix: journal list styles 2024-08-21 17:17:52 +02:00
Spythere 9f8656e590 fix: progress indicator 2024-08-21 17:06:50 +02:00
dominik-korsa 81cd165fe7 Use <router-link> instead of <tr> with click handler in StationTable 2024-08-21 14:42:03 +02:00
dominik-korsa 41e4b45599 Use <router-link> for driver journal button in TrainInfo 2024-08-21 13:54:48 +02:00
dominik-korsa 462dd7dd7a Replace all remaining uses of driverViewMixin with <router-link> 2024-08-21 13:49:31 +02:00
dominik-korsa 9837ae97e1 Use <router-link> instead of a @click handler in SceneryTimetable 2024-08-21 13:29:53 +02:00
dominik-korsa a818cd980b Use <router-link> instead of a @click handler in TrainTable 2024-08-21 12:58:47 +02:00
Spythere 573ebc233b chore: removed registering train modal 2024-08-21 02:06:38 +02:00
Spythere aae47c6abd fix: journal stats badges 2024-08-21 02:06:23 +02:00
Spythere 24c9b62162 feat: driver train view 2024-08-21 02:02:35 +02:00
Spythere 481d43b6d8 chore: selecting station checkpoint from url 2024-08-20 14:31:52 +02:00
Spythere 4969a433cc fix: train modal responsiveness & icons 2024-08-20 13:33:24 +02:00
Spythere 8a2b453dc6 chore: journal daily stats styling 2024-08-20 13:16:37 +02:00
Spythere 86d178ef56 chore: restored Pragotron link 2024-08-20 13:16:02 +02:00
Spythere 7769477508 chore: station stats styling 2024-08-20 00:14:44 +02:00
Spythere 551b60c733 fix: omitting "po" stops in timetable progress bar 2024-08-19 23:56:15 +02:00
Spythere 80a5b56785 feat: router links embeded into timetable stop names 2024-08-18 23:45:42 +02:00
Spythere 6bd62f13a1 chore: stats button responsiveness 2024-08-18 23:14:18 +02:00
Spythere 42591f6e76 chore: journal timetables styling improvements 2024-08-18 23:03:00 +02:00
Spythere 4ca0f09e75 chore: added ZG category 2024-08-18 22:58:22 +02:00
Spythere 02c3629c00 bump: v1.27.0 2024-08-18 01:43:51 +02:00
Spythere 9c4c806f0e chore: moved station stats to a dropdown 2024-08-18 01:43:27 +02:00
Spythere 58d6a97762 fix: twr/skr badges sizing 2024-08-17 15:51:36 +02:00
Spythere cd71c78eb4 chore: apicache typings 2024-08-17 15:51:25 +02:00
Spythere 300e70dcfe chore: journal timetables buttons alignment 2024-08-15 15:01:36 +02:00
Spythere 09b31f7914 Merge pull request #104 from Spythere/development
hotfix: warning icon placement
2024-08-10 23:41:34 +02:00
Spythere c29c3c6abe hotfix: warning icon placement 2024-08-10 23:41:00 +02:00
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 44f6cf4232 Merge branch 'development' 2024-05-12 15:22:28 +02:00
85 changed files with 10081 additions and 15295 deletions
+2 -5
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
'on':
push:
@@ -11,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
channelId: live
projectId: stacjownik-td2
projectId: stacjownik-td2
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci && npm run build
- run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
-1
View File
@@ -19,7 +19,6 @@
<link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#222222" />
<link rel="icon" href="favicon.ico" />
+457 -1761
View File
File diff suppressed because it is too large Load Diff
+13 -16
View File
@@ -1,11 +1,13 @@
{
"name": "stacjownik",
"version": "1.24.4",
"version": "1.27.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting",
"deploy:prod": "yarn build && firebase deploy --only hosting",
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
"preview": "yarn build && vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -19,25 +21,20 @@
"showdown": "^2.1.0",
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.4"
"vue-router": "^4.4.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2",
"@types/node": "^20.14.12",
"@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"axios": "^1.5.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"@vitejs/plugin-vue": "^5.1.0",
"@vue/tsconfig": "^0.5.1",
"axios": "^1.7.2",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^5.3.4",
"vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11"
"vue-tsc": "^2.0.28"
},
"browserslist": [
"> 1%",
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

+8 -25
View File
@@ -6,13 +6,6 @@
/>
<Tooltip />
<transition name="modal-anim">
<keep-alive>
<TrainModal />
</keep-alive>
</transition>
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<main class="app_main">
@@ -54,7 +47,6 @@ import { useTooltipStore } from './store/tooltipStore';
import Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateCard from './components/App/UpdateCard.vue';
@@ -67,7 +59,6 @@ export default defineComponent({
Clock,
StatusIndicator,
AppHeader,
TrainModal,
UpdateCard,
Tooltip
},
@@ -81,9 +72,7 @@ export default defineComponent({
isUpdateCardOpen: false,
currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
nextUpdateTime: 0
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
}),
created() {
@@ -96,22 +85,13 @@ export default defineComponent({
methods: {
init() {
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
this.loadLang();
this.setupOfflineHandling();
this.checkAppVersion();
this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
update(t: number) {
if (t >= this.nextUpdateTime) {
this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.update);
},
async checkAppVersion() {
@@ -131,7 +111,8 @@ export default defineComponent({
};
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) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
@@ -157,6 +138,7 @@ export default defineComponent({
handleOnlineMode() {
this.store.isOffline = false;
this.apiStore.dataStatuses.connection = Status.Data.Loading;
this.apiStore.connectToAPI();
},
@@ -180,7 +162,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) {
if (naviLanguage.startsWith('en')) {
this.changeLang('en');
return;
}
@@ -226,6 +208,7 @@ export default defineComponent({
min-height: 100vh;
overflow: hidden;
position: relative;
}
.app_main {
-1
View File
@@ -43,7 +43,6 @@ export default defineComponent({
width: 6em;
height: 1em;
margin: 0.5em 0;
.bar-fg,
.bar-bg {
+5 -4
View File
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
.status-badge {
border-radius: 1em;
font-weight: 500;
text-wrap: nowrap;
padding: 0.2em 0.55em;
padding: 0.2rem 0.55rem;
background-color: $online;
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
&.no-limit {
background-color: $no-limit;
font-size: 0.85em;
font-size: 0.9em;
}
&.not-signed,
&.unavailable {
background-color: $unav;
font-size: 0.85em;
font-size: 0.9em;
}
&.afk {
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
background-color: $no-space;
border: 1px solid white;
color: white;
font-size: 0.85em;
font-size: 0.9em;
}
&.unknown,
+129 -100
View File
@@ -1,83 +1,24 @@
<template>
<div class="stock-list">
<div v-if="tractionOnly">
<p>
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ computedStockList[0].split(':')[1] }}
</p>
<img
class="traction-only"
:src="
getVehicleThumbnailURL(
computedStockList[0].split(':')[0],
/^EN/.test(computedStockList[0]) ? 'rb' : ''
)
"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
/>
</div>
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
</p>
<ul>
<li
v-for="(
{ vehicleName, vehicleCargo, images, imagesFallbacks, vehicleString }, i
) in thumbnailNames"
:key="i"
>
<div class="stock-text">
<p>{{ vehicleName.replace(/_/g, ' ') }}</p>
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
</div>
<span>
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
:src="
getVehicleThumbnailURL(stockName.split(':')[0], /^EN/.test(stockName) ? 'rb' : '')
"
@error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400"
height="60"
<VehicleThumbnail
v-for="(thumbnailImage, imageIndex) in images"
:vehicle-name="vehicleString"
:img-name="thumbnailImage"
:fallback-name="imagesFallbacks[imageIndex]"
/>
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="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>
</li>
</ul>
@@ -87,8 +28,11 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { useApiStore } from '../../store/apiStore';
import VehicleThumbnail from './VehicleThumbnail.vue';
export default defineComponent({
components: { VehicleThumbnail },
props: {
trainStockList: {
type: Array as PropType<string[]>,
@@ -109,32 +53,117 @@ export default defineComponent({
computed: {
computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
},
thumbnailNames() {
return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
.filter((v) => v.length != 0)
.map((vehicleString) => {
const [vehicleName, vehicleCargo] = vehicleString.split(':');
const vehicleThumbnailData = {
images: [] as string[],
imagesFallbacks: [] as string[],
vehicleName,
vehicleCargo,
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: {
getVehicleThumbnailURL(locoType: string, suffix?: string) {
return `https://static.spythere.eu/thumbnails/${locoType}${suffix}.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`;
onImageError(event: Event, fallbackImage: string) {
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
}
}
});
@@ -170,10 +199,10 @@ img.traction-only {
max-width: 100%;
}
p {
.stock-text {
text-align: center;
color: #aaa;
font-size: 0.95em;
margin-bottom: 1em;
font-size: 0.9em;
margin-bottom: 0.25em;
}
</style>
@@ -0,0 +1,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>
@@ -1,29 +1,28 @@
<template>
<section class="daily-stats">
<span :data-active="statsStatus">
<span class="stats-list">
<h3>
{{ $t('journal.daily-stats.title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<h3>
{{ $t('journal.daily-stats.title') }}
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
</h3>
<hr class="header-separator" />
<hr class="header-separator" />
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }}
</b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }}
</b>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats.info') }}
</b>
<b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats.info') }}
</b>
<div v-else>
<div v-if="stats.totalTimetables">
&bull;
<div v-else>
<ul class="stats-list">
<li v-if="stats.totalTimetables">
<i18n-t keypath="journal.daily-stats.total">
<template #count>
<b class="text--primary">
@@ -36,10 +35,9 @@
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</li>
<div v-if="stats.maxTimetable">
&bull;
<li v-if="stats.maxTimetable">
<i18n-t keypath="journal.daily-stats.longest">
<template #id>
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
@@ -60,10 +58,9 @@
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template>
</i18n-t>
</div>
</li>
<div v-if="topDispatchers.length == 1">
&bull;
<li v-if="topDispatchers.length == 1">
<i18n-t keypath="journal.daily-stats.most-active-dr">
<template #dispatcher>
<router-link
@@ -79,10 +76,9 @@
</b>
</template>
</i18n-t>
</div>
</li>
<div v-if="topDispatchers.length > 1">
&bull;
<li v-if="topDispatchers.length > 1">
<i18n-t keypath="journal.daily-stats.most-active-dr-many">
<template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i">
@@ -103,10 +99,9 @@
</b>
</template>
</i18n-t>
</div>
</li>
<div v-if="stats.longestDuties.length > 0">
&bull;
<li v-if="stats.longestDuties.length > 0">
<i18n-t keypath="journal.daily-stats.longest-duties">
<template #dispatcher>
<router-link
@@ -122,10 +117,9 @@
{{ calculateDuration(stats.longestDuties[0].duration) }}
</template>
</i18n-t>
</div>
</li>
<div v-if="stats.mostActiveDrivers.length > 0">
&bull;
<li v-if="stats.mostActiveDrivers.length > 0">
<i18n-t keypath="journal.daily-stats.most-active-driver">
<template #driver>
<router-link
@@ -138,30 +132,30 @@
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template>
</i18n-t>
</div>
</li>
</ul>
<hr class="section-separator" />
<hr class="section-separator" />
<div class="stats-badges">
<span
class="stat-badge"
v-for="key in [
'rippedSwitches',
'derailments',
'skippedStopSignals',
'radioStops',
'kills'
]"
:key="key"
>
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
<span>{{
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--'
}}</span>
<div class="stats-badges">
<span
class="badge stat-badge"
v-for="key in [
'rippedSwitches',
'derailments',
'skippedStopSignals',
'radioStops',
'kills'
]"
:key="key"
>
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
<span>
{{ Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' }}
</span>
</div>
</span>
</div>
</span>
</div>
</span>
</section>
</template>
@@ -178,7 +172,6 @@ export default defineComponent({
name: 'journal-daily-stats',
mixins: [dateMixin],
// emits: ['toggleStatsOpen'],
data() {
return {
@@ -193,7 +186,6 @@ export default defineComponent({
activated() {
this.startFetchingDailyStats();
// this.$emit('toggleStatsOpen', true);
},
deactivated() {
@@ -249,14 +241,24 @@ export default defineComponent({
.daily-stats {
text-align: left;
}
.daily-stats > span[data-active='0'] {
opacity: 0.75;
}
ul.stats-list {
list-style: disc;
padding: 0 1em;
}
.stats-list a {
text-decoration: underline;
}
.stats-list > li {
margin: 0.25em 0;
}
.stats-badges {
display: flex;
flex-wrap: wrap;
@@ -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>
@@ -16,17 +16,17 @@
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.services">
<span class="badge stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
<span>{{ stats.services.count }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span class="badge stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
</span>
<span class="stat-badge" v-if="stats.services">
<span class="badge stat-badge" v-if="stats.services">
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
</span>
@@ -35,22 +35,22 @@
<hr class="section-separator" />
<div class="info-stats">
<span class="stat-badge" v-if="stats.issuedTimetables">
<span class="badge stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
<span>{{ stats.issuedTimetables.count }}</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span class="badge stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span class="badge stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
</span>
<span class="stat-badge" v-if="stats.issuedTimetables">
<span class="badge stat-badge" v-if="stats.issuedTimetables">
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
</span>
@@ -15,90 +15,16 @@
{{ $t('app.no-result') }}
</div>
<div v-else>
<table class="dispatchers-table">
<thead>
<th>{{ $t('journal.history-name') }}</th>
<th>{{ $t('journal.history-hash') }}</th>
<th>{{ $t('journal.history-dispatcher') }}</th>
<th>{{ $t('journal.history-level') }}</th>
<th>{{ $t('journal.history-rate') }}</th>
<th>{{ $t('journal.history-region') }}</th>
<th>{{ $t('journal.history-date') }}</th>
</thead>
<tbody>
<transition-group name="list-anim">
<tr v-for="historyItem in dispatcherHistory" :key="historyItem.id">
<td>
<router-link
:to="`/journal/dispatchers?search-station=${historyItem.stationName}`"
>
<b>{{ historyItem.stationName }}</b>
</router-link>
</td>
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
{{ historyItem.dispatcherName }}
</b>
<b v-else>
{{ historyItem.dispatcherName }}
</b>
</router-link>
</td>
<td>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
:style="
calculateExpStyle(
historyItem.dispatcherLevel,
historyItem.dispatcherIsSupporter
)
"
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td>
<b class="region-badge" :aria-describedby="historyItem.region">{{
regions.find((r) => r.id == historyItem.region)?.value || '???'
}}</b>
</td>
<td style="min-width: 200px" class="time">
<span v-if="historyItem.timestampTo" class="text--offline">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</span>
<span class="dispatcher-online" v-else>
<b class="text--online">
<router-link :to="`/scenery?station=${historyItem.stationName}`">{{
$t('journal.online-since')
}}</router-link>
{{ timestampToString(historyItem.timestampFrom) }}
</b>
({{ calculateDuration(historyItem.currentDuration) }})
</span>
</td>
</tr>
</transition-group>
</tbody>
</table>
<ul v-else class="journal-list">
<transition-group name="list-anim">
<JournalDispatcherEntry
v-for="entry in dispatcherHistory"
:key="entry.id"
:entry="entry"
:onToggleShowExtraInfo="toggleExtraInfo"
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
/>
</transition-group>
<AddDataButton
:list="dispatcherHistory"
@@ -106,7 +32,7 @@
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
</ul>
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
@@ -121,20 +47,15 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { useMainStore } from '../../../store/mainStore';
import { API } from '../../../typings/api';
import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin],
components: { Loading, AddDataButton, JournalDispatcherEntry },
props: {
dispatcherHistory: {
@@ -159,99 +80,23 @@ export default defineComponent({
return {
Status,
store: useMainStore(),
apiStore: useApiStore(),
regions
extraInfoIndexes: [] as number[]
};
},
computed: {
computedDispatcherHistory() {
return this.dispatcherHistory.reduce(
(acc, historyItem, i) => {
if (this.isAnotherDay(i - 1, i))
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL'));
acc.push(historyItem);
return acc;
},
[] as (API.DispatcherHistory.Data | string)[]
);
}
},
methods: {
navigateToScenery(name: string, isOnline: boolean) {
if (!isOnline) return;
toggleExtraInfo(id: number) {
const existingIdx = this.extraInfoIndexes.indexOf(id);
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`);
},
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
else this.extraInfoIndexes.push(id);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/animations.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
@import '../../../styles/variables.scss';
@import '../../../styles/JournalSection.scss';
table.dispatchers-table {
--_bg-table: #111;
--_bg-head: #101010;
--_bg-row: #2f2f2f;
width: 100%;
border-collapse: collapse;
position: relative;
text-align: center;
margin-bottom: 1em;
thead {
position: sticky;
top: 0;
background-color: var(--_bg-head);
}
th {
padding: 0.5em;
}
tr {
background-color: var(--_bg-row);
border-bottom: 2px solid black;
&:last-child {
border: none;
}
}
td {
padding: 0.75em;
.level-badge {
margin: 0 auto;
}
}
}
.text {
&--online {
color: springgreen;
}
&--offline {
color: #ddd;
}
}
</style>
@@ -12,7 +12,7 @@
<hr class="header-separator" />
<div class="info-stats">
<span class="stat-badge">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.timetables') }}</span>
<span
>{{ store.driverStatsData._count.fulfilled }} /
@@ -20,17 +20,17 @@
>
</span>
<span class="stat-badge">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span>
<span class="stat-badge">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.distance') }}</span>
<span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
@@ -38,7 +38,7 @@
</span>
</span>
<span class="stat-badge">
<span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.stations') }}</span>
<span>
{{ store.driverStatsData._sum.confirmedStopsCount }} /
@@ -17,17 +17,13 @@
</div>
<div v-else>
<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" />
<transition-group name="list-anim" tag="ul" class="journal-list">
<li v-for="timetable in timetableHistory" class="journal_item" :key="timetable.id">
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<div @click="toggleExtraInfo(timetable.id)" style="cursor: pointer">
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
@@ -35,16 +31,23 @@
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<TimetableStops
:timetable="timetable"
:showExtraInfo="extraInfoIndexes.includes(timetable.id)"
/>
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<!-- Extra -->
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
<!-- Extra -->
<TimetableDetails
:timetable="timetable"
:showExtraInfo="extraInfoIndexes.includes(timetable.id)"
@toggle-extra-info="toggleExtraInfo"
/>
</div>
</li>
</transition-group>
<AddDataButton
:list="timetableHistory"
@@ -57,6 +60,7 @@
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
@@ -64,7 +68,7 @@
</template>
<script lang="ts">
import { defineComponent, PropType, ref } from 'vue';
import { defineComponent, Prop, PropType, ref } from 'vue';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
@@ -110,16 +114,17 @@ export default defineComponent({
data() {
return {
Status,
store: useMainStore()
store: useMainStore(),
extraInfoIndexes: [] as number[]
};
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
methods: {
toggleExtraInfo(id: number) {
const existingIdx = this.extraInfoIndexes.indexOf(id);
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
else this.extraInfoIndexes.push(id);
}
}
});
@@ -1,10 +1,19 @@
<template>
<div>
<div class="details-actions">
<button class="btn--action">
<button class="btn--action" @click="toggleExtraInfo">
<b>{{ $t('journal.stock-info') }}</b>
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
</button>
<router-link
v-if="driverRouteLocation !== null"
class="a-button btn--action btn-timetable"
:to="driverRouteLocation"
>
<img src="/images/icon-train.svg" alt="train icon" />
<b>{{ $t('journal.timetable-online-button') }}</b>
</router-link>
</div>
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
@@ -77,9 +86,13 @@
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
import { RouteLocationRaw } from 'vue-router';
export default defineComponent({
components: { StockList },
emits: ['toggleExtraInfo'],
props: {
showExtraInfo: {
type: Boolean,
@@ -112,12 +125,25 @@ export default defineComponent({
stockLength: Number(historyData[3]) || undefined
};
});
},
driverRouteLocation(): RouteLocationRaw | null {
if (this.timetable.terminated) return null;
return {
name: 'DriverView',
query: {
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
}
}
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
},
toggleExtraInfo() {
this.$emit('toggleExtraInfo', this.timetable.id);
}
}
});
@@ -134,6 +160,7 @@ export default defineComponent({
.details-actions {
display: flex;
gap: 0.5em;
button img {
height: 1.25em;
@@ -3,13 +3,15 @@
<span class="general-train">
<span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr">
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
</span>
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span>
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span>
<span>
<strong class="text--primary">
<strong
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(timetable.trainCategoryCode)"
class="text--primary tooltip-help"
>
{{ timetable.trainCategoryCode }}
</strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong>
@@ -23,17 +25,19 @@
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong>
<strong
<router-link
v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator"
:title="$t('donations.driver-message')"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
>
{{ timetable.driverName }}
</strong>
<strong>{{ timetable.driverName }}</strong>
</router-link>
<strong v-else>
{{ timetable.driverName }}
</strong>
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
<strong>{{ timetable.driverName }}</strong>
</router-link>
</span>
<span class="general-time">
@@ -61,15 +65,6 @@
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
<button
v-if="timetable.terminated == false"
class="btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="train icon" />
<b>{{ $t('journal.timetable-online-button') }}</b>
</button>
</span>
</div>
</template>
@@ -79,12 +74,12 @@ import { PropType, defineComponent } from 'vue';
import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin],
mixins: [dateMixin, styleMixin, trainCategoryMixin],
data() {
return {
@@ -97,14 +92,6 @@ export default defineComponent({
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
methods: {
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
}
}
});
</script>
@@ -131,7 +118,6 @@ export default defineComponent({
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.general-time {
@@ -174,6 +160,7 @@ export default defineComponent({
@include smallScreen {
.item-general {
flex-direction: column;
justify-content: center;
}
}
@@ -1,101 +0,0 @@
<template>
<ul class="journal-list">
<transition-group name="list-anim">
<li
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<button class="btn--action btn--show">
{{ $t('journal.stock-info') }}
<img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
alt="Arrow icon"
/>
</button>
<!-- Extra -->
<TimetableExtra :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
</template>
<script lang="ts">
import { PropType, defineComponent, ref } from 'vue';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableExtra from './TimetableExtra.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { TimetableGeneral, TimetableStops, TimetableStatus, TimetableExtra },
props: {
timetableHistory: {
type: Array as PropType<API.TimetableHistory.Response>,
required: true
}
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables';
@import '../../../styles/responsive';
@import '../../../styles/JournalSection';
.btn--show {
display: flex;
font-weight: bold;
padding: 0.2em 0.45em;
img {
height: 1.3em;
}
}
hr {
margin: 0.25em 0;
}
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
.btn--show {
margin: 1em auto 0 auto;
}
}
</style>
@@ -1,23 +1,41 @@
<template>
<div class="stop-list" v-if="showExtraInfo == true">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
<div class="timetable-stops">
<div class="stop-list">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
<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>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
</div>
</template>
@@ -42,6 +60,24 @@ export default defineComponent({
},
computed: {
timetablePathDetails() {
if (!this.timetable.path || this.timetable.path == '') return null;
return this.timetable.path.split(';').map((pathEl, i) => {
const [arrival, name, departure] = pathEl.split(',');
const sceneryName = name.split(' ').slice(0, -1).join(' ');
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
return {
arrival,
sceneryName,
sceneryHash,
departure,
isVisited: this.timetable.visitedSceneries?.includes(sceneryHash) ?? false
};
});
},
timetableStops() {
const timetable = this.timetable;
@@ -94,13 +130,14 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.stop-list {
.timetable-stops {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
}
.stop-list {
&-item[data-confirmed='true'] {
color: lightgreen;
@@ -109,4 +146,19 @@ export default defineComponent({
}
}
}
.path-details {
margin-top: 0.5em;
}
.path-details > span[data-visited='true'] {
.path-arrival,
.path-scenery {
color: lightgreen;
}
&[data-next-visited='true'] .path-departure {
color: lightgreen;
}
}
</style>
+3 -1
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
| 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = {
[key in TimetableSearchKey]: string;
@@ -1,31 +1,18 @@
<template>
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="scenery-dispatchers-history">
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
</div>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<div v-else class="journal-list">
<div v-for="historyItem in historyList" :key="historyItem.id">
<span>
<span class="text--grayed" style="margin-right: 10px">
#{{ historyItem.stationHash }}
</span>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
@@ -35,37 +22,52 @@
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<div>
<span>
{{ $t('scenery.dispatcher-rate') }}
<b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</span>
|
<span>
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
</span>
</div>
</span>
<span>
<span v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</div>
</span>
<div class="dispatcher-online" v-else>
<span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
</div>
</td>
</tr>
</tbody>
</table>
</section>
</span>
</span>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
@@ -149,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.scenery-dispatchers-history {
height: 100%;
overflow: auto;
display: grid;
gap: 0.5em;
grid-template-rows: auto 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.journal-list > div {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.75em;
}
.level-badge {
margin: 0 auto;
text-align: center;
display: inline-block;
line-height: 1.6em;
}
.dispatcher-online {
@@ -158,13 +195,10 @@ export default defineComponent({
}
@include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item {
align-items: center;
.journal-list > div {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
</style>
../../store/storeTypes
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center;
font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
}
.info-lists {
@@ -15,14 +15,13 @@
<li
v-for="{ train, status } in stationTrains"
class="badge user"
tabindex="0"
:key="train.id"
:data-status="status"
@click.prevent="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
>
<span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span>
<router-link :to="train.driverRouteLocation" class="a-block">
<span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span>
</router-link>
</li>
</transition-group>
</section>
@@ -30,14 +29,13 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
import { ActiveScenery, Station } from '../../../typings/common';
import { getTrainStopStatus } from '../utils';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({
mixins: [routerMixin, modalTrainMixin],
mixins: [routerMixin],
props: {
onlineScenery: {
@@ -99,38 +97,27 @@ ul {
}
.user {
cursor: pointer;
&_train {
color: black;
background-color: $no-timetable;
transition: background-color 200ms;
-ms-transition: background-color 200ms;
-webkit-transition: background-color 200ms;
}
&[data-status='no-timetable'] .user_train {
background-color: $no-timetable;
}
&[data-status='departed'] > &_train {
&[data-status='departed'] .user_train {
background-color: $departed;
}
&[data-status='stopped'] > &_train {
&[data-status='stopped'] .user_train {
background-color: $stopped;
}
&[data-status='online'] > &_train {
&[data-status='online'] .user_train {
background-color: $online;
}
&[data-status='terminated'] > &_train {
&[data-status='terminated'] .user_train {
background-color: $terminated;
}
&[data-status='disconnected'] > &_train {
&[data-status='disconnected'] .user_train {
background-color: $disconnected;
}
@@ -139,6 +126,16 @@ ul {
pointer-events: none;
}
}
.user_train {
color: black;
background-color: $no-timetable;
transition: background-color 200ms;
-ms-transition: background-color 200ms;
-webkit-transition: background-color 200ms;
}
.users-anim {
&-move,
&-enter-active,
+94 -107
View File
@@ -14,6 +14,10 @@
</span>
<span class="header_links" v-if="station">
<a :href="pragotronHref" target="_blank" :title="$t('scenery.pragotron-link')">
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
</a>
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
</a>
@@ -21,18 +25,15 @@
</h3>
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i">
{{ (i > 0 && '&bull;') || '' }}
<button
:key="cp"
class="checkpoint_item"
:class="{ current: chosenCheckpoint === cp }"
@click="setCheckpoint(cp)"
<template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
<template v-if="i > 0">&bull;</template>
<router-link
class="checkpoint-item"
:class="{ current: chosenCheckpoint === ch }"
:to="`/scenery?station=${station.name}&checkpoint=${ch}`"
>{{ ch }}</router-link
>
{{ cp }}
</button>
</span>
</template>
</div>
</div>
@@ -62,29 +63,36 @@
{{ $t('scenery.no-timetables') }}
</div>
<div
class="timetable-item"
<router-link
class="timetable-item a-block"
v-else
v-for="(row, i) in sceneryTimetables"
:key="row.train.id + i"
tabindex="0"
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
:to="row.train.driverRouteLocation"
>
<span class="timetable-general">
<span class="general-info">
<span class="info-number">
<strong>{{ row.train.timetableData!.category }}</strong>
{{ row.train.trainNo }}
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
<div class="info-train">
<b
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(row.train.timetableData!.category)"
class="text--primary tooltip-help"
>
{{ row.train.timetableData!.category }}
</b>
<span>&nbsp;</span>
<b>{{ row.train.trainNo }}</b>
<span>&nbsp;&bull;&nbsp;</span>
<span>{{ row.train.driverName }}</span>
<span
v-if="row.checkpointStop.comments"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="row.checkpointStop.comments"
>
<img src="/images/icon-warning.svg" />
</span>
</span>
&nbsp;|&nbsp;
<span>
{{ row.train.driverName }}
</span>
</div>
<div class="info-route">
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
@@ -160,7 +168,7 @@
</span>
</span>
</span>
</div>
</router-link>
</transition-group>
</div>
</section>
@@ -173,12 +181,12 @@ import { useRoute } from 'vue-router';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { SceneryTimetableRow } from './typings';
import { ActiveScenery, Station } from '../../typings/common';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({
@@ -186,7 +194,7 @@ export default defineComponent({
components: { Loading, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, modalTrainMixin],
mixins: [dateMixin, routerMixin, trainCategoryMixin],
props: {
station: {
@@ -205,6 +213,12 @@ export default defineComponent({
this.loadSelectedOption();
},
watch: {
currentURL() {
this.loadSelectedOption();
}
},
setup(props) {
const route = useRoute();
const currentURL = computed(() => `${location.origin}${route.fullPath}`);
@@ -213,7 +227,10 @@ export default defineComponent({
const mainStore = useMainStore();
const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints[0] ?? props.station?.name ?? ''
props.station?.generalInfo?.checkpoints[0] ??
props.station?.name ??
route.query['station']?.toString() ??
''
);
return {
@@ -232,13 +249,22 @@ export default defineComponent({
return url;
},
pragotronHref() {
let url = `https://pragotron-td2.web.app/board?name=${this.station!.name}&region=${this.mainStore.region.id}`;
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
return url;
},
sceneryTimetables(): SceneryTimetableRow[] {
if (!this.station) return [];
if (!this.onlineScenery) return [];
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
return this.onlineScenery.scheduledTrains
.filter(
(ct) =>
// ct.timetablePathElement.stationName == sceneryName &&
ct.train.region == this.mainStore.region.id &&
this.chosenCheckpoint &&
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
@@ -247,75 +273,18 @@ export default defineComponent({
const trainStopStatus = getTrainStopStatus(
ct.checkpointStop,
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 {
checkpointStop: ct.checkpointStop,
train: ct.train,
prevDepartureLine,
nextArrivalLine,
departureLine,
arrivingLine,
prevStationName,
nextStationName,
prevDepartureLine: ct.previousSceneryElement?.departureRouteExt ?? null,
nextArrivalLine: ct.nextSceneryElement?.arrivalRouteExt ?? null,
departureLine: ct.timetablePathElement.departureRouteExt ?? null,
arrivingLine: ct.timetablePathElement.arrivalRouteExt ?? null,
prevStationName: ct.previousSceneryElement?.stationName ?? null,
nextStationName: ct.nextSceneryElement?.stationName ?? null,
status: trainStopStatus
};
})
@@ -338,7 +307,19 @@ export default defineComponent({
loadSelectedOption() {
if (!this.station) return;
this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
if (!this.station.generalInfo) {
this.chosenCheckpoint = this.station.name;
return;
}
const queryCheckpoint = this.$route.query['checkpoint']?.toString();
this.chosenCheckpoint =
this.station.generalInfo.checkpoints.find(
(ch) => ch.toLocaleLowerCase() === queryCheckpoint?.toLocaleLowerCase()
) ??
this.station.generalInfo.checkpoints[0] ??
this.station.name;
},
setCheckpoint(cp: string) {
@@ -408,7 +389,6 @@ export default defineComponent({
background: #353535;
cursor: pointer;
z-index: 10;
&.empty {
@@ -438,30 +418,35 @@ export default defineComponent({
}
}
.timetable-list {
position: relative;
}
.timetable-checkpoints {
display: flex;
justify-content: center;
gap: 0.5em;
flex-wrap: wrap;
font-size: 1.1em;
margin-top: 0.5em;
}
button.checkpoint_item {
color: #aaa;
display: inline;
.checkpoint-item {
color: #aaa;
display: inline;
&:hover {
color: white;
}
.checkpoint_item.current {
&.current {
font-weight: bold;
color: $accentCol;
}
}
.timetable-list {
position: relative;
}
.general-info {
display: flex;
flex-wrap: wrap;
@@ -475,7 +460,9 @@ export default defineComponent({
}
img {
width: 1.1em;
height: 0.9em;
vertical-align: middle;
margin: 0 0.25em;
}
}
@@ -1,69 +1,97 @@
<template>
<!-- WIP -->
<!-- <div class="top-filters">
<button class="btn btn--option">ROZPOCZYNA BIEG</button>
<button class="btn btn--option">PRZEZ</button>
<button class="btn btn--option">KOŃCZY BIEG</button>
</div> -->
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
<div class="scenery-timetables-history">
<div class="history-modes">
<button
class="btn btn--option"
v-for="mode in historyModeList"
:key="mode"
:class="{ checked: checkedHistoryMode == mode }"
@click="checkHistoryMode(mode)"
>
{{ $t(`scenery.timetable-${mode}`) }}
</button>
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.timetables-history-id') }}</th>
<th>{{ $t('scenery.timetables-history-number') }}</th>
<th>{{ $t('scenery.timetables-history-route') }}</th>
<th>{{ $t('scenery.timetables-history-driver') }}</th>
<th>{{ $t('scenery.timetables-history-author') }}</th>
<th>{{ $t('scenery.timetables-history-date') }}</th>
</thead>
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
#{{ historyItem.id }}
</router-link>
</td>
<td>
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
{{ historyItem.trainNo }}
</td>
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<td>
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
{{ historyItem.driverName }}
</router-link>
</td>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<td>
<router-link
v-if="historyItem.authorName"
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
>{{ historyItem.authorName }}
</router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</td>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</td>
</tr>
</tbody>
</table>
</section>
<div v-else class="journal-list">
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
<span>
<div>
<span
class="timetable-status-indicator"
:data-terminated="timetableHistory.terminated"
:data-fulfilled="timetableHistory.fulfilled"
>
&ofcir;
</span>
#{{ timetableHistory.id }} |
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
{{ timetableHistory.trainNo }}
{{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
<div class="text--grayed">
<span>
{{ $t('scenery.timetable-issued-date') }}
<b>
{{
localeDateTime(
timetableHistory.createdAt > timetableHistory.beginDate
? timetableHistory.beginDate
: timetableHistory.createdAt,
$i18n.locale
)
}}
</b></span
>
<span v-if="timetableHistory.authorName">
{{ $t('scenery.timetable-issued-by') }}
<b>
<router-link
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
>
{{ timetableHistory.authorName }}
</router-link>
</b>
</span>
<span>
{{ $t('scenery.timetable-issued-for') }}
<b>
<router-link
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
>
{{ timetableHistory.driverName }}
</router-link>
</b>
</span>
</div>
</span>
<button
@click="
navigateTo(`/journal/timetables`, {
'search-train': `#${timetableHistory.id}`
})
"
>
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
</button>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api';
import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import routerMixin from '../../mixins/routerMixin';
import { useMainStore } from '../../store/mainStore';
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
type HistoryMode = (typeof historyModeList)[number];
export default defineComponent({
name: 'SceneryTimetablesHistory',
mixins: [dateMixin],
mixins: [dateMixin, routerMixin],
props: {
station: {
type: Object as PropType<Station>
@@ -91,9 +124,14 @@ export default defineComponent({
data() {
return {
historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading,
DataStatus: Status.Data
DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
};
},
@@ -103,17 +141,22 @@ export default defineComponent({
methods: {
async fetchAPIData() {
if (!this.station && !this.onlineScenery) {
const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded;
return;
}
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try {
const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', {
params: {
issuedFrom: this.station?.name || this.onlineScenery?.name
}
params: requestFilters
})
).data;
@@ -125,11 +168,17 @@ export default defineComponent({
}
},
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
}
});
}
@@ -142,13 +191,66 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.top-filters {
.scenery-timetables-history {
height: 100%;
overflow: auto;
display: grid;
gap: 1em;
grid-template-rows: auto 1fr 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-modes {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.25em;
button {
padding: 0.5em;
padding: 0.35em;
min-width: 120px;
}
}
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.journal-list > div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.5em;
}
.journal-list > div > button > img {
width: 2em;
transform: rotate(180deg);
}
.timetable-status-indicator {
&[data-fulfilled='true'] {
color: lightgreen;
}
&[data-terminated='false'] {
color: lightblue;
}
&[data-terminated='true'][data-fulfilled='false'] {
color: lightcoral;
}
}
</style>
@@ -6,30 +6,11 @@
<p>[F] {{ $t('options.filters') }}</p>
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
</button>
<label for="scenery-search">
<input
id="scenery-search"
list="sceneries"
:placeholder="$t('sceneries.scenery-search')"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
v-model="chosenSearchScenery"
/>
<datalist id="sceneries">
<option
v-for="scenery in sortedStationList"
:key="scenery.name"
:value="scenery.name"
></option>
</datalist>
</label>
</div>
<transition name="card-anim">
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content" @scroll="onScroll" ref="cardContentRef">
<div class="card" v-if="isVisible" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content" tabindex="0" @scroll="onScroll" ref="cardContentRef">
<div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p>
@@ -40,6 +21,31 @@
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
</div>
<section class="card_sceneries-search">
<h3 class="section-header">{{ $t('filters.sceneries-search') }}</h3>
<datalist id="sceneries">
<option
v-for="scenery in sortedStationList"
:key="scenery.name"
:value="scenery.name"
></option>
</datalist>
<form action="javascript:void(0);" @submit="handleSceneriesInput">
<input
v-model="chosenSearchScenery"
id="scenery-search"
list="sceneries"
:placeholder="$t('filters.sceneries-placeholder')"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/>
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
</form>
</section>
<section class="card_options">
<div
class="option-section"
@@ -57,16 +63,15 @@
<div class="section-filters">
<label
v-for="filterKey in sectionFilters"
@click="() => (filters[filterKey] = !filters[filterKey])"
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
:for="filterKey"
>
<input
type="checkbox"
:checked="filters[filterKey]"
v-model="filters[filterKey]"
type="checkbox"
:class="sectionKey"
:name="filterKey"
:id="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
@@ -111,7 +116,7 @@
@blur="preventKeyDown = false"
/>
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button>
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
</form>
</section>
@@ -269,20 +274,11 @@ export default defineComponent({
},
watch: {
chosenSearchScenery(value: string) {
const chosenStation = this.store.stationList.find(({ name }) => name == value);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
}
},
isVisible(value: boolean) {
this.$nextTick(() => {
if (value) {
(this.$refs['cardRef'] as HTMLDivElement).focus();
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
(this.$refs['cardContentRef'] as HTMLDivElement).focus();
}
});
}
@@ -300,7 +296,18 @@ export default defineComponent({
handleAuthorsInput() {
this.filters['authors'] = this.authors;
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
},
handleSceneriesInput() {
const chosenStation = this.store.stationList.find(
({ name }) => name == this.chosenSearchScenery
);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
this.isVisible = false;
}
},
subHour() {
@@ -329,6 +336,8 @@ export default defineComponent({
},
resetFilters() {
if (this.preventKeyDown) return;
// Reset local model values
this.minimumHours = 0;
this.authors = '';
@@ -353,7 +362,8 @@ export default defineComponent({
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
filtersSections[sectionKey].forEach((filterKey) => {
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
if (typeof this.filters[filterKey] === 'boolean')
this.filters[filterKey] = filterKey != chosenKey;
});
},
@@ -382,6 +392,7 @@ h3.section-header {
.card {
display: grid;
grid-template-rows: 1fr auto;
padding: 1px;
}
.card_info {
@@ -451,8 +462,12 @@ h3.section-header {
}
}
.card_authors-search {
.card_authors-search,
.card_sceneries-search {
margin: 1em 0;
display: flex;
flex-direction: column;
align-items: center;
form {
display: flex;
@@ -642,10 +657,6 @@ h3.section-header {
}
@include smallScreen {
.card_controls > button.card-button > p {
display: none;
}
.slider {
flex-wrap: wrap;
justify-content: center;
+93 -68
View File
@@ -1,56 +1,70 @@
<template>
<div class="station-stats">
<div class="separator" />
<div
class="dropdown"
@keydown.esc="showDropdown = false"
v-click-outside="() => (showDropdown = false)"
>
<div class="bg" v-if="showDropdown" @click="showDropdown = false"></div>
<div class="stats-row">
<div>
<span
>{{ $t('station-stats.u-factor') }}
<a
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
target="_blank"
:data-tooltip="$t('station-stats.u-factor-tooltip')"
>(?)</a
>:
</span>
<button class="filter-button btn--filled btn--image" @click="toggleDropdown" ref="button">
<img src="/images/icon-stats.svg" alt="Open filters icon" />
<!-- {{ $t('train-stats.stats-button') }} -->
<span>STATYSTYKI</span>
</button>
<b class="u-factor" :style="calculateFactorStyle()">
{{ uFactor.toFixed(2) }}
</b>
<transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="showDropdown">
<div>
<h1 class="text--primary">
<img src="/images/icon-stats.svg" alt="Open filters icon" />
{{ $t('train-stats.title') }}
</h1>
<hr style="margin: 0.5em 0" />
<ul class="stats-list">
<li>
<span>
{{ $t('station-stats.u-factor') }}
<a
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
target="_blank"
:data-tooltip="$t('station-stats.u-factor-tooltip')"
>(?)</a
>:
</span>
<b class="u-factor" :style="calculateFactorStyle()">
{{ uFactor.toFixed(2) }}
</b>
</li>
<li>
{{ $t('station-stats.avg-timetable-count') }}
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</li>
<li>
{{ $t('station-stats.single-track-count') }}
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} </b>)
</li>
<li>
{{ $t('station-stats.double-track-count') }}
<b>{{ trackCount.twoWay }}</b>
(<b>{{ trackCount.twoWayElectric }} </b>)
</li>
<li>
{{ $t('station-stats.cross-sceneries') }}
<b>{{ trackCount.crossTrack }}</b> (<b>{{ trackCount.crossTrackElectric }} </b>)
</li>
<li>
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
<b>{{ spawnCount.all }}</b> - ALL
</li>
</ul>
</div>
<div tabindex="0" @focus="() => (showDropdown = false)"></div>
</div>
<div>
&bull;
{{ $t('station-stats.avg-timetable-count') }}
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</div>
<div>
&bull;
{{ $t('station-stats.single-track-count') }}
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} </b>)
</div>
<div>
&bull;
{{ $t('station-stats.double-track-count') }}
<b>{{ trackCount.twoWay }}</b>
(<b>{{ trackCount.twoWayElectric }} </b>)
</div>
<div>
&bull; {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
>{{ trackCount.crossTrackElectric }} </b
>)
</div>
<div>
&bull;
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
<b>{{ spawnCount.all }}</b> - ALL
</div>
</div>
</transition>
</div>
</template>
@@ -61,11 +75,16 @@ import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
mainStore: useMainStore()
mainStore: useMainStore(),
showDropdown: false
};
},
methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
calculateFactorStyle() {
if (this.uFactor == 0) return '';
@@ -171,25 +190,15 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.separator {
width: 100%;
height: 2px;
@import '../../styles/dropdown.scss';
@import '../../styles/badge.scss';
h1 img {
vertical-align: text-bottom;
}
h3 {
margin: 0.5em 0;
background-color: #aaa;
}
.station-stats {
text-align: center;
color: #ddd;
}
.stats-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
text-wrap: pretty;
gap: 0.25em;
margin-top: 0.25em;
}
.u-factor {
@@ -209,4 +218,20 @@ export default defineComponent({
color: rgb(22, 245, 22);
}
}
ul.stats-list {
list-style: disc;
padding-left: 1em;
margin-top: 1em;
& > li {
margin: 0.25em 0;
}
}
@include smallScreen {
.filter-button span {
display: none;
}
}
</style>
+275 -282
View File
@@ -4,302 +4,298 @@
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/>
<div class="table_wrapper" v-else-if="filteredStationList.length > 0">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="station in filteredStationList"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
<table v-else-if="filteredStationList.length > 0">
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</td>
</span>
</th>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationCard"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<td class="station-dispatcher-exp">
<tbody>
<router-link
v-for="station in filteredStationList"
class="a-row"
role="row"
:key="station.name"
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
@keydown.space.prevent="openForumSite($event, station.generalInfo?.url)"
:to="getSceneryRoute(station)"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</td>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.prevent="openDonationCard"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</td>
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<td class="station-dispatcher-exp">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<td class="station-info">
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/>
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
</div>
</td>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
"
/>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
</div>
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</router-link>
</tbody>
</table>
<div class="no-stations" v-else>
<div>
@@ -322,7 +318,7 @@ import dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin';
import { useApiStore } from '../../store/apiStore';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { Station, Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore';
import { getChangedFilters } from '../../managers/stationFilterManager';
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
@@ -337,7 +333,6 @@ export default defineComponent({
data: () => ({
headIconsIds,
headIds,
lastSelectedStationName: '',
getChangedFilters
}),
@@ -367,21 +362,16 @@ export default defineComponent({
},
methods: {
setScenery(name: string) {
const station = this.filteredStationList.find((station) => station.name === name);
getSceneryRoute(station: Station) {
// TODO: Hide tooltips when navigating away
if (!station) return;
this.lastSelectedStationName = station.name;
this.tooltipStore.hide();
this.$router.push({
return {
name: 'SceneryView',
query: {
station: station.name.replaceAll(' ', '_'),
station: station.name,
region: this.$route.query.region || undefined
}
});
};
},
openDonationCard(e: Event) {
@@ -417,7 +407,8 @@ export default defineComponent({
$rowCol: #424242;
.station_table {
height: 80vh;
height: 90vh;
max-height: 2000px;
min-height: 700px;
overflow: auto;
font-weight: 500;
@@ -505,8 +496,10 @@ table {
}
}
tr {
tr,
.a-row {
background-color: $rowCol;
vertical-align: middle;
&:nth-child(even) {
background-color: lighten($rowCol, 5);
+1
View File
@@ -23,6 +23,7 @@ export default defineComponent({
justify-content: center;
align-items: center;
gap: 0.5em;
white-space: pre-line;
padding: 0.25em 0.5em;
border-radius: 0.25em;
+2 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview">
<div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div>
</template>
@@ -35,10 +35,7 @@ export default defineComponent({
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) {
@@ -13,13 +13,20 @@
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
:src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
/>
<div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }}
{{ vehicleName.replace(/_/g, ' ') }}
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
</div>
<div class="vehicle-props" v-if="vehicleData">
{{ vehicleData.group.speed }}km/h &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>
</template>
@@ -27,11 +34,13 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading'
};
},
@@ -41,7 +50,7 @@ export default defineComponent({
},
watch: {
'tooltipStore.type'(prev, val) {
vehicleName(prev, val) {
if (prev != val) this.imageState = 'loading';
}
},
@@ -56,6 +65,22 @@ export default defineComponent({
(e.target as HTMLElement).style.display = 'none';
}
},
computed: {
vehicleName() {
return this.tooltipStore.content.split(':')[0];
},
vehicleData() {
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
},
vehicleCargo() {
return this.vehicleData?.group.cargoTypes?.find(
(c) => c.id == this.tooltipStore.content.split(':')[1]
);
}
}
});
</script>
@@ -85,10 +110,13 @@ img {
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
.vehicle-props {
color: #ccc;
}
.error-placeholder {
height: 176px;
}
+89 -59
View File
@@ -1,20 +1,22 @@
<template>
<span class="stop-label" :data-sbl="stop.isSBL">
<span class="name" v-html="stop.nameHtml"></span>
<span
class="stop-label"
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po') && !stop.duration)"
>
<router-link v-if="/(, podg$|<strong>)/.test(stop.nameHtml)" :to="sceneryHref">
<span class="name" v-html="stop.nameHtml"></span>
</router-link>
<span v-else class="name" v-html="stop.nameHtml"></span>
<span
v-if="stop.position != 'begin'"
class="date arrival"
:data-status="
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
? 'delayed'
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
:data-status-delayed="stop.arrivalDelay > 0"
:data-status-preponed="stop.arrivalDelay < 0"
:data-status="stop.status"
>
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }}
@@ -27,19 +29,16 @@
</span>
<span
v-if="
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
"
v-if="stop.duration"
class="date stop"
:data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status="
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
>
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }}
{{
stop.duration == 0 && stop.departureDelay > 0
? stop.departureDelay - stop.arrivalDelay
: stop.duration
}}
{{ stop.type == '' ? 'pt' : stop.type }}
</span>
@@ -49,16 +48,11 @@
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
"
class="date departure"
:data-status="
stop.departureDelay > 0 && stop.status == 'confirmed'
? 'delayed'
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
:data-status-delayed="stop.departureDelay > 0"
:data-status-preponed="stop.departureDelay < 0"
:data-status-confirmed="stop.status == 'confirmed'"
>
o.
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
<s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }}
@@ -86,6 +80,12 @@ export default defineComponent({
type: Object as PropType<TrainScheduleStop>,
required: true
}
},
computed: {
sceneryHref() {
return `/scenery?station=${this.stop.sceneryName}&checkpoint=${this.stop.nameRaw}`;
}
}
});
</script>
@@ -96,27 +96,20 @@ $delayedClr: salmon;
$dateClr: #525151;
$stopExchangeClr: #db8e29;
$stopDefaultClr: #252525;
$stopNameClr: #22a8d1;
$stopNameClr: #303030;
s {
color: #ccc;
}
.stop-label {
display: flex;
flex-wrap: wrap;
align-items: center;
&[data-sbl='true'] {
.date {
display: none;
}
.name {
background: none;
color: #aaa;
padding: 0;
}
}
.name {
background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em;
display: flex;
@@ -130,6 +123,22 @@ $stopNameClr: #22a8d1;
.date {
background: $dateClr;
padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
}
&[data-minor='true'] {
.date {
display: none;
}
.name {
background: none;
color: #aaa;
padding: 0;
}
}
.stop {
@@ -145,27 +154,48 @@ $stopNameClr: #22a8d1;
color: $delayedClr;
}
}
}
.arrival,
.departure {
&[data-status='delayed'] {
s {
color: #999;
}
.stop-label > a {
z-index: 0;
}
span {
color: $delayedClr;
}
.stop .arrival {
&[data-status='confirmed'][data-status-delayed='true'] {
span {
color: $delayedClr;
}
}
&[data-status='preponed'] {
s {
color: #999;
}
&[data-status='confirmed'][data-status-preponed='true'] {
span {
color: $preponedClr;
}
}
span {
color: $preponedClr;
}
&[data-status='stopped'][data-status-preponed='true'] {
span {
color: $preponedClr;
}
}
&[data-status='stopped'][data-status-delayed='true'] {
span {
color: $delayedClr;
}
}
}
.stop .departure[data-status-confirmed='true'] {
&[data-status-delayed='true'] {
span {
color: $delayedClr;
}
}
&[data-status-preponed='true'] {
span {
color: $preponedClr;
}
}
}
+71 -75
View File
@@ -2,47 +2,39 @@
<div class="train-info" :data-extended="extended">
<section class="train-general">
<div class="general-top-bar">
<div>
<div class="top-bar-header">
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }}
</span>
<span
class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
>
<span
class="train-badge twr"
v-if="train.timetableData?.TWR"
:title="$t('general.TWR')"
>
TWR
</span>
<span
class="train-badge skr"
v-if="train.timetableData?.SKR"
:title="$t('general.SKR')"
>
SKR
</span>
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">
TWR
</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">
SKR
</span>
<strong>
<span v-if="train.timetableData" class="text--primary"
>{{ train.timetableData.category }}&nbsp;</span
>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
v-if="train.timetableData"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(train.timetableData.category)"
class="text--primary tooltip-help"
>
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
{{ train.timetableData.category }}
</b>
<b class="train-number">{{ train.trainNo }}</b>
<span>&bull;</span>
<div class="train-driver">
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-tooltip-type="DonatorTooltip"
@@ -55,19 +47,6 @@
<span v-else>{{ train.driverName }}</span>
</div>
</div>
<div v-if="extended">
<button class="btn-timetable btn--image btn--action" @click="navigateToJournal">
<img src="/images/icon-train.svg" alt="train icon" />
<span>
{{ $t('trains.journal-button') }}
</span>
</button>
<button class="btn-exit btn--image btn--action" @click="closeModal">
<img src="/images/icon-exit.svg" alt="modal exit icon" />
</button>
</div>
</div>
<div class="general-timetable" v-if="train.timetableData">
@@ -88,7 +67,7 @@
<div class="general-stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2">
{{ $t('trains.via-title') }}
<span v-html="displayStopList(train.timetableData.followingStops)"></span>
<span v-html="getTrainStopsHtml(train.timetableData.followingStops)"></span>
</span>
</div>
@@ -97,21 +76,22 @@
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km /
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span>
|
<span>{{ currentDistance(train.timetableData.followingStops) }} km</span>
<span>/</span>
<span class="text--primary">{{ train.timetableData.routeDistance }} km </span>
<span>|</span>
<span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span>
</div>
<div class="status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline">
<img src="/images/icon-offline.svg" alt="" />
<img src="/images/icon-offline.svg" alt="offline train icon" />
{{ $t('trains.scenery-offline') }}
</div>
<div v-if="!train.online" class="train-badge offline">
<img src="/images/icon-offline.svg" alt="" />
<img src="/images/icon-offline.svg" alt="offline train icon" />
Offline {{ lastSeenMessage(train.lastSeen) }}
</div>
</div>
@@ -131,6 +111,18 @@
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ 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>
@@ -167,11 +159,11 @@ import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
components: { ProgressBar, StockList },
props: {
@@ -191,16 +183,23 @@ export default defineComponent({
};
},
methods: {
navigateToJournal() {
this.$router.push({
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);
},
journalRouteLocation() {
return {
path: '/journal/timetables',
query: {
'search-driver': this.train.driverName
}
});
this.closeModal();
}
}
}
});
@@ -242,6 +241,12 @@ export default defineComponent({
gap: 0.5em;
}
.train-driver {
display: flex;
align-items: center;
gap: 0.25em;
}
.train-driver img {
max-height: 20px;
vertical-align: text-bottom;
@@ -273,24 +278,15 @@ export default defineComponent({
.general-top-bar {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
& > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
}
}
.btn-timetable {
padding: 0.25em;
}
.top-bar-header {
display: flex;
align-items: center;
flex-wrap: wrap;
.btn-exit {
padding: 0.25em;
gap: 0.25em;
}
.general-status {
@@ -337,10 +333,14 @@ export default defineComponent({
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em 0;
}
.progress-distance {
margin-right: 0.25em;
display: flex;
flex-wrap: wrap;
gap: 0.25em;
}
.timetable-warnings {
@@ -353,9 +353,5 @@ export default defineComponent({
grid-template-columns: 1fr;
gap: 1em 0;
}
.btn-timetable > span {
display: none;
}
}
</style>
-103
View File
@@ -1,103 +0,0 @@
<template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-content" ref="content" tabindex="0">
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
import { Train } from '../../typings/common';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [modalTrainMixin],
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
}
},
watch: {
chosenTrain(train: Train | undefined) {
this.$nextTick(() => {
if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus();
} else {
(this.store.modalLastClickedTarget as any)?.focus();
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 90);
}
});
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.train-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
align-items: flex-start;
text-align: left;
}
.modal-background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.modal-content {
position: relative;
overflow-y: scroll;
width: 95vw;
max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
}
@include midScreen {
.exit {
margin: 0.5em;
img {
width: 1.75rem;
}
}
}
</style>
+6 -2
View File
@@ -15,12 +15,14 @@
<div class="search_content">
<div class="search-box">
<input
v-model="searchedTrain"
class="search-input"
ref="initFocusedElement"
id="train-search"
name="train-search"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/>
<button class="search-exit">
<img
@@ -33,11 +35,13 @@
<div class="search-box">
<input
v-model="searchedDriver"
class="search-input"
id="driver-search"
name="driver-search"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
:placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/>
<button class="search-exit">
<img
+98 -46
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed"
x
>
<span class="stop_info">
<span class="distance">
@@ -48,26 +47,46 @@
<span
v-if="
stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine &&
!/sbl/gi.test(stop.departureLine)
scheduleStops[i + 1] != undefined &&
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
"
>
{{ stop.departureLine }}
</span>
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
<div>{{ stop.departureLine }}</div>
<div
class="scenery-change-name"
v-if="
i < scheduleStops.length - 1 &&
stop.sceneryName != scheduleStops[i + 1].sceneryName
"
>
{{ scheduleStops[i + 1].sceneryName }}
<div class="scenery-route">
<span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
| {{ stop.departureLineInfo.routeSpeed }}
<span v-if="stop.departureLineInfo.isElectric">⚡</span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="10"
/>
</span>
</div>
<div>
{{ scheduleStops[i + 1].arrivalLine }}
<div
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
class="scenery-change-name"
>
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
<span v-if="stop.departureLineInfo?.routeTracks == 1"> &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="10"
/>
</span>
</div>
</span>
</div>
@@ -85,7 +104,7 @@ import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common';
import { StationRoutesInfo, Train } from '../../typings/common';
export interface TrainScheduleStop {
nameHtml: string;
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
isSBL: boolean;
sceneryName: string | null;
sceneryHash: string;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null;
}
@@ -142,17 +165,29 @@ export default defineComponent({
computed: {
scheduleStops(): TrainScheduleStop[] {
let currentSceneryIndex = 0;
if (!this.train.timetableData) return [];
const { timetablePath } = this.train.timetableData;
let currentPathIndex = 0;
return (
this.train.timetableData?.followingStops.map((stop, i, arr) => {
if (
i > 0 &&
stop.arrivalLine &&
stop.arrivalLine != arr[i - 1].departureLine &&
!/sbl/gi.test(stop.arrivalLine)
)
currentSceneryIndex++;
const isExternal =
i < arr.length - 1 &&
stop.departureLine === timetablePath[currentPathIndex].departureRouteExt;
const sceneryName = timetablePath[currentPathIndex].stationName;
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
);
if (isExternal) currentPathIndex++;
return {
nameHtml: stop.stopName,
@@ -174,14 +209,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType,
distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '',
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
sceneryName,
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
};
}) ?? []
@@ -210,7 +249,7 @@ export default defineComponent({
i < this.train.timetableData!.followingStops.length;
i++
) {
if (/po\.|sbl/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
if (/(, po$|sbl|, pe$)/gi.test(this.train.timetableData!.followingStops[i].stopNameRAW))
activeMinorStopList.push(i);
else break;
}
@@ -247,7 +286,7 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
}
.train-schedule {
padding: 0 1em;
padding: 1em;
}
.schedule-wrapper {
@@ -483,22 +522,35 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
}
}
.bottom-line-info {
.scenery-change-name {
position: relative;
margin: 0.25em 0;
.scenery-route {
display: flex;
gap: 0.25em;
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
span:nth-child(2) {
display: flex;
gap: 0.25em;
align-items: center;
}
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
img {
width: 1em;
}
}
.scenery-change-name {
position: relative;
margin: 0.25em 0;
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
}
</style>
+13 -38
View File
@@ -1,5 +1,9 @@
<template>
<div class="dropdown" @keydown.esc="showOptions = false" @focusout="showOptions = false">
<div
class="dropdown"
@keydown.esc="showOptions = false"
v-click-outside="() => (showOptions = false)"
>
<div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
@@ -19,21 +23,21 @@
<div v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
<div class="top-list general">
<transition-group tag="ul" name="stats-anim">
<li class="badge" key="timetable-count">
<li class="badge stat-badge" key="timetable-count">
<span>{{ $t('train-stats.timetable-count') }}</span>
<span>
<b>{{ regionTrainsWithTT.length }}</b>
</span>
</li>
<li class="badge" key="avg-speed">
<li class="badge stat-badge" key="avg-speed">
<span>{{ $t('train-stats.avg-speed') }}</span>
<span>
<b>{{ stats.avgSpeed.toFixed(1) }} km/h</b>
</span>
</li>
<li class="badge" key="avg-distance">
<li class="badge stat-badge" key="avg-distance">
<span>{{ $t('train-stats.avg-timetable') }}</span>
<span>
<b>{{ stats.avgDistance.toFixed(1) }} km</b>
@@ -46,7 +50,7 @@
<h3>{{ $t('train-stats.top-categories') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topCategories" :key="top.name">
<li class="badge stat-badge" v-for="top in stats.topCategories" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
@@ -61,7 +65,7 @@
<h3>{{ $t('train-stats.top-vehicles') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topVehicles" :key="top.name">
<li class="badge stat-badge" v-for="top in stats.topVehicles" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
@@ -76,7 +80,7 @@
<h3>{{ $t('train-stats.top-units') }}</h3>
<transition-group tag="ul" name="stats-anim">
<li class="badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
<li class="badge stat-badge" v-for="top in stats.topUnits.slice(0, 7)" :key="top.name">
<span>{{ top.name }}</span>
<span>{{ top.count }}</span>
</li>
@@ -95,6 +99,8 @@
<div class="no-data" v-else>
{{ $t('train-stats.no-stats') }}
</div>
<div tabindex="0" @focus="() => (showOptions = false)"></div>
</div>
</transition>
</div>
@@ -236,43 +242,12 @@ h3 {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
// @include smallScreen {
// justify-content: center;
// }
}
.badge {
margin: 0;
& > span:first-child {
background-color: $accentCol;
color: black;
}
}
.dropdown_wrapper {
max-width: 600px;
}
.stats-anim {
&-move,
&-enter-active,
&-leave-active {
transition: all 250ms ease;
}
&-enter-from,
&-leave-to {
opacity: 0;
transform: translateX(5px);
}
&-leave-active {
position: absolute;
}
}
@include smallScreen {
h1,
.no-data {
+3 -9
View File
@@ -17,11 +17,10 @@
class="train-row"
v-for="train in trains"
:key="train.id"
tabindex="0"
@click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
>
<TrainInfo :train="train" :extended="false" />
<router-link class="a-block" :to="train.driverRouteLocation">
<TrainInfo :train="train" :extended="false" />
</router-link>
</li>
</transition-group>
</div>
@@ -30,7 +29,6 @@
<script lang="ts">
import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue';
@@ -47,8 +45,6 @@ export default defineComponent({
}
},
mixins: [modalTrainMixin],
setup() {
const store = useMainStore();
const apiStore = useApiStore();
@@ -107,7 +103,5 @@ li.train-row {
background-color: var(--clr-secondary);
margin-bottom: 1em;
width: 100%;
cursor: pointer;
}
</style>
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": [
{
"id": "eu",
+68 -18
View File
@@ -48,7 +48,48 @@
"footer": {
"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",
"ZG": "emergency (deprecated category)",
"E": "electric loco",
"J": "EMU",
"S": "diesel loco",
"M": "DMU"
},
"vehicle-preview": {
"loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/"
@@ -123,9 +164,11 @@
"search-train": "Train no.",
"search-driver": "Driver name",
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-station": "Scenery name / #",
"search-author": "Timetable author name",
"search-issuedFrom": "Origin scenery name",
"search-issuedFrom": "Issuing scenery name",
"search-via": "Via scenery name",
"search-terminatingAt": "Terminating scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
@@ -250,9 +293,11 @@
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
},
"sceneries-search": "SCENERY SEARCH:",
"sceneries-placeholder": "Enter scenery name...",
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
"authors-placeholder": "Enter the author nickname...",
"authors-button-title": "Search",
"search-button-title": "SEARCH",
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
"now": "NOW",
@@ -329,6 +374,9 @@
"current-signal": "at signal",
"current-track": "on track",
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
"we4a-tooltip": "Non-electrified track",
"delayed": "Delayed: ",
"preponed": "Ahead of schedule: ",
"on-time": "On time",
@@ -355,7 +403,14 @@
"scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!",
"journal-button": "DRIVER'S JOURNAL"
"driver-journal-link": "DRIVER JOURNAL",
"driver-return-link": "GO BACK",
"driver-not-found-header": "Train not found! :/",
"driver-not-found-desc-1": "This train has already been terminated or it's offline.",
"driver-not-found-desc-2": "You can browse timetable history in the",
"driver-not-found-journal": "TIMETABLES JOURNAL",
"driver-not-found-return": "GO BACK TO THE MAIN SITE"
},
"train-stats": {
"stats-button": "STATISTICS",
@@ -491,28 +546,23 @@
"option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown",
"timetable-via": "ALL TIMETABLES",
"timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID",
"timetables-history-number": "Number",
"timetables-history-route": "Route",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"timetable-issued-date": "Issued",
"timetable-issued-by": " by:",
"timetable-issued-for": " for driver:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"dispatcher-rate": "Rate:",
"dispatcher-status-changes": "Status changes:",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!",
"forum-topic": "Official {name} forum topic",
"pragotron-link": "Timetable pallet board (beta)",
"pragotron-link": "Timetable pallet board",
"tablice-link": "Timetable summary board (by Thundo)",
"bottom-info": "Show full history in the Journal tab"
+69 -18
View File
@@ -45,6 +45,48 @@
"footer": {
"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",
"ZG": "ratunkowy (kat. wycofana)",
"E": "elektrowóz",
"J": "EZT",
"S": "spalinowóz",
"M": "SZT"
},
"vehicle-preview": {
"loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
@@ -119,9 +161,11 @@
"search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii",
"search-station": "Nazwa scenerii / #",
"search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa",
"search-via": "Przez scenerię",
"search-terminatingAt": "Sceneria końcowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
@@ -247,9 +291,11 @@
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
},
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
"sceneries-search": "WYSZUKAJ SCENERIĘ:",
"sceneries-placeholder": "Wpisz nazwę scenerii...",
"authors-search": "WYSZUKAJ AUTORA (uwzględnia inne filtry):",
"authors-placeholder": "Wpisz nick autora...",
"authors-button-title": "Szukaj",
"search-button-title": "SZUKAJ",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ",
"hour": " godz.",
@@ -315,6 +361,9 @@
"current-signal": "przy semaforze",
"current-track": "na szlaku",
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
"we4a-tooltip": "Szlak niezelektryfikowany",
"delayed": "Opóźniony: ",
"preponed": "Przed czasem: ",
"on-time": "Planowo",
@@ -340,7 +389,14 @@
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
"journal-button": "DZIENNIK MASZYNISTY"
"driver-journal-link": "DZIENNIK MASZYNISTY",
"driver-return-link": "POWRÓT",
"driver-not-found-header": "Nie znaleziono pociągu! :/",
"driver-not-found-desc-1": "Ten pociąg prawdopodobnie zakończył już swój bieg lub jest offline.",
"driver-not-found-desc-2": "Historię rozkładów jazdy możesz przejrzeć w",
"driver-not-found-journal": "DZIENNIKU RJ",
"driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ"
},
"train-stats": {
"stats-button": "STATYSTYKI",
@@ -474,28 +530,23 @@
"option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany",
"timetable-via": "WSZYSTKIE RJ",
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID",
"timetables-history-number": "Numer",
"timetables-history-route": "Trasa",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"timetable-issued-date": "Wystawiony",
"timetable-issued-by": " przez:",
"timetable-issued-for": " dla maszynisty:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"dispatcher-rate": "Ocena:",
"dispatcher-status-changes": "Zmiany statusów:",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}",
"pragotron-link": "Paletowa tablica informacyjna (beta)",
"pragotron-link": "Paletowa tablica informacyjna",
"tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika"
-30
View File
@@ -1,30 +0,0 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({
data() {
return {
store: useMainStore(),
tooltipStore: useTooltipStore()
};
},
methods: {
selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = train.modalId;
if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target;
},
closeModal() {
this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide();
}
}
});
+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)})`;
}
}
});
+15 -12
View File
@@ -75,18 +75,18 @@ export default defineComponent({
return positionString.charAt(0).toUpperCase() + positionString.slice(1);
},
displayStopList(stops: TrainStop[]): string | undefined {
getTrainStopsHtml(stops: TrainStop[]): string {
if (!stops) return '';
return stops
.reduce((acc: string[], stop: TrainStop, i: number) => {
if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.'))
if (stop.stopType.includes('ph'))
acc.push(
`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${
stop.stopName
}</strong>`
);
else if (i > 0 && i < stops.length - 1 && !/po\.|sbl/gi.test(stop.stopNameRAW))
else if (i > 0 && i < stops.length - 1 && !/(, po$|sbl)/gi.test(stop.stopNameRAW))
acc.push(
`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${
stop.stopName
@@ -108,16 +108,19 @@ export default defineComponent({
},
currentDelay(stops: TrainStop[]) {
const delay =
stops.find(
(stop, i) =>
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed)
)?.departureDelay || 0;
const lastConfirmedStop = stops.find(
(stop, i) =>
(i == 0 && !stop.confirmed) ||
(i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
(stops[i + 1] == undefined && stop.confirmed)
);
if (delay > 0)
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
else if (delay < 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`;
const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
if (lastDelay > 0)
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
else if (lastDelay < 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
else return this.$t('trains.on-time');
},
+21 -2
View File
@@ -20,6 +20,15 @@ const routes: Array<RouteRecordRaw> = [
region: route.query.region
})
},
{
path: '/driver',
name: 'DriverView',
component: () => import('../views/DriverView.vue'),
props: (route) => ({
trainId: route.query.trainId,
modalId: route.query.modalId
})
},
{
path: '/scenery',
name: 'SceneryView',
@@ -57,8 +66,12 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` };
if (
(to.name == 'SceneryView' || to.name == 'DriverView') &&
from.name !== to.name &&
from.query['view'] === undefined
)
return { el: `.app_main`, top: -15 };
if (savedPosition) return savedPosition;
},
@@ -66,4 +79,10 @@ const router = createRouter({
routes
});
router.beforeEach((to, from, next) => {
next((vm) => {
(vm as any)['xd'] = 'xd';
});
});
export default router;
+59 -16
View File
@@ -4,24 +4,21 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', {
state: () => ({
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading
sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
},
activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[],
lastFetchData: new Date(),
nextUpdateTime: 0,
client: undefined as AxiosInstance | undefined,
@@ -54,16 +51,40 @@ export const useApiStore = defineStore('apiStore', {
// Static data
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
this.fetchVehiclesInfo();
window.requestAnimationFrame(this.updateTick);
},
updateTick(t: number) {
if (this.dataStatuses.connection == Status.Data.Offline) return;
if (t >= this.nextUpdateTime) {
this.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.updateTick);
},
async fetchActiveData() {
// if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
// import('../../tests/data/getActiveData.json').then((data) => {
// console.warn('activeData: mocking mode');
// this.activeData = data.default as API.ActiveData.Response;
// this.dataStatuses.connection = Status.Data.Loaded;
// });
// return;
// }
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try {
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
@@ -82,17 +103,39 @@ export const useApiStore = defineStore('apiStore', {
},
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
try {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
} catch (error) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o sceneriach:', error);
}
},
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
async fetchVehiclesInfo() {
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
// import('../../tests/data/vehicles.json').then((data) => {
// console.warn('vehicles.json: mocking mode');
// this.vehiclesData = data.default;
// this.dataStatuses.vehicles = Status.Data.Loaded;
// });
// return;
// }
try {
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
this.vehiclesData = response.data;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) {
this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
}
}
}
});
+65 -30
View File
@@ -50,15 +50,6 @@ export const useMainStore = defineStore('mainStore', {
const timetable = train.timetable;
const sceneryNames =
train.timetable?.sceneries?.map(
(sceneryHash) =>
apiStore.activeData?.activeSceneries?.find((st) => st.stationHash === sceneryHash)
?.stationName ??
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
sceneryHash
) ?? [];
const trainObj = {
id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
@@ -86,6 +77,13 @@ export const useMainStore = defineStore('mainStore', {
isSupporter: train.driverIsSupporter,
driverLevel: train.driverLevel,
driverRouteLocation: {
name: 'DriverView',
query: {
trainId: train.id
}
},
timetableData: timetable
? {
timetableId: timetable.timetableId,
@@ -96,7 +94,17 @@ export const useMainStore = defineStore('mainStore', {
followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
sceneries: timetable.sceneries,
sceneryNames: sceneryNames.reverse()
// sceneryNames: sceneryNames.reverse(),
timetablePath: timetable.path.split(';').map((pathElementString) => {
const [arrival, station, departure] = pathElementString.split(',');
return {
arrivalRouteExt: arrival,
departureRouteExt: departure,
stationName: station.split(' ').slice(0, -1).join(' '),
stationHash: station.split(' ').slice(-1).join(' ').replace('.sc', '')
};
})
}
: undefined
} as Train;
@@ -110,21 +118,39 @@ export const useMainStore = defineStore('mainStore', {
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
// Checkpoints trains map
timetable?.stopList.forEach((stop, i) => {
if (/strong|podg\.|pe\./.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = {
train: trainObj,
checkpointStop: stop
};
if (trainObj.timetableData) {
let currentSceneryIndex = 0;
const timetablePath = trainObj.timetableData.timetablePath;
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
});
trainObj.timetableData.followingStops.forEach((stop, i) => {
if (/strong|podg|pe/.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = {
train: trainObj,
checkpointStop: stop,
previousSceneryElement:
currentSceneryIndex > 0 ? timetablePath[currentSceneryIndex - 1] : null,
nextSceneryElement:
currentSceneryIndex < timetablePath.length - 1
? timetablePath[currentSceneryIndex + 1]
: null,
timetablePathElement: timetablePath[currentSceneryIndex]
};
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
if (timetablePath[currentSceneryIndex].departureRouteExt == stop.departureLine)
currentSceneryIndex++;
});
}
return trainObj;
});
@@ -141,12 +167,15 @@ export const useMainStore = defineStore('mainStore', {
const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
if (!train.timetableData) return acc;
train.timetableData.sceneryNames.forEach((name) => {
train.timetableData.timetablePath.forEach((p) => {
if (
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
acc.findIndex(
(v) =>
(v.name == p.stationName || v.hash == p.stationHash) && v.region == train.region
) != -1 ||
apiStore.activeData?.activeSceneries?.findIndex(
(sc) =>
sc.stationName === name &&
(sc.stationName == p.stationName || sc.stationHash == p.stationHash) &&
sc.region == train.region &&
Date.now() - sc.lastSeen < 1000 * 60 * 2
) != -1
@@ -154,7 +183,7 @@ export const useMainStore = defineStore('mainStore', {
return acc;
acc.push({
name: name,
name: p.stationName,
hash: '',
region: train.region,
maxUsers: 0,
@@ -252,8 +281,14 @@ export const useMainStore = defineStore('mainStore', {
if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop }) => {
scenery.scheduledTrains.push({ train, checkpointStop });
scheduledTrains.forEach(({ train, checkpointStop, timetablePathElement, ...v }) => {
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;
+9 -7
View File
@@ -1,10 +1,18 @@
@import 'responsive.scss';
@import 'animations.scss';
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
margin-bottom: 0.5em;
}
.list_wrapper {
overflow-y: auto;
height: 90vh;
min-height: 550px;
min-height: 650px;
margin-top: 0.5em;
position: relative;
@@ -38,16 +46,10 @@
}
}
.journal_item {
cursor: pointer;
}
.journal_item,
.journal_warning {
background-color: #1a1a1a;
padding: 1em;
margin-bottom: 1em;
cursor: pointer;
}
.journal_top-bar {
-16
View File
@@ -33,22 +33,6 @@ hr.section-separator {
gap: 0.5em;
}
.stat-badge {
display: flex;
span {
background-color: $accentCol;
color: black;
font-weight: bold;
padding: 0.2em 0.5em;
}
span:first-child {
background-color: #333;
color: white;
}
}
@include smallScreen {
.journal-stats {
text-align: center;
+13 -3
View File
@@ -1,3 +1,5 @@
@import 'variables.scss';
.badge {
font-weight: 600;
@@ -78,14 +80,12 @@
.train-badge {
display: flex;
align-items: center;
gap: 0.5em;
gap: 0.3em;
padding: 0.1em 0.3em;
border-radius: 0.2em;
font-weight: bold;
font-size: 0.9em;
&.twr {
background-color: var(--clr-twr);
box-shadow: 0 0 5px 1px var(--clr-twr);
@@ -114,3 +114,13 @@
background-color: #007599;
}
}
.stat-badge {
margin: 0;
color: white;
& > span:first-child {
background-color: $accentCol;
color: black;
}
}
+51 -23
View File
@@ -124,10 +124,12 @@ input {
}
a {
display: inline-block;
color: white;
text-decoration: none;
color: inherit;
}
a:not(.a-block):not(.a-button):not(.a-row) {
display: inline-block;
transition: color 0.3s;
@@ -138,6 +140,14 @@ a {
}
}
a.a-block {
display: block;
}
a.a-row {
display: table-row;
}
ul {
padding: 0;
list-style: none;
@@ -184,6 +194,7 @@ a.a-button {
color: white;
background: none;
border-radius: 0.25em;
text-decoration: none;
display: flex;
align-items: center;
@@ -214,7 +225,7 @@ a.a-button {
font-weight: bold;
&:hover {
background-color: #555;
background-color: #424242;
}
}
@@ -228,6 +239,10 @@ a.a-button {
background-color: #3c3c3c;
}
&:hover {
background-color: #555;
}
}
&.btn--image {
@@ -283,6 +298,32 @@ a.a-button {
}
}
// Basic tooltip
[data-tooltip] {
cursor: help;
line-height: initial;
}
[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 {
::-webkit-scrollbar {
width: 0.5em;
@@ -296,24 +337,11 @@ a.a-button {
background-color: #777;
}
}
}
// Basic tooltip
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(10px, -50%);
content: attr(data-tooltip);
color: white;
background-color: #171717;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.25em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: help;
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
transform: translate(-50%, 2em);
left: 50%;
width: 100%;
}
}
+3 -7
View File
@@ -1,11 +1,7 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table {
width: 100%;
table-layout: fixed;
min-width: 900px;
border-collapse: collapse;
thead {
@@ -25,7 +21,7 @@ table.scenery-history-table {
td {
padding: 0.75em;
border-bottom: solid 5px #111;
border-bottom: solid 5px #181818;
}
}
+19 -6
View File
@@ -1,9 +1,15 @@
import { Status } from './common';
import { Status, VehicleData } from './common';
export enum APIDataStatus {
OK = 'OK',
WARNING = 'WARNING'
}
export interface APICache {
lastModified: string;
name: string;
}
export namespace API {
export namespace ActiveData {
export interface APIStatuses {
@@ -11,6 +17,7 @@ export namespace API {
trainsAPI: APIDataStatus;
dispatchersAPI: APIDataStatus;
sceneryRequirementsAPI: APIDataStatus;
caches: APICache[];
}
export interface Response {
@@ -19,11 +26,12 @@ export namespace API {
apiStatuses?: APIStatuses;
}
}
export namespace DispatcherHistory {
export type Response = Data[];
export interface Data {
id: string;
id: number;
currentDuration: number;
dispatcherId: number;
dispatcherName: string;
@@ -38,6 +46,7 @@ export namespace API {
stationName: string;
timestampFrom: number;
timestampTo?: number;
statusHistory: string[];
}
}
@@ -193,6 +202,8 @@ export namespace API {
TWR: boolean;
SKR: boolean;
sceneries: string[];
path: string;
}
}
@@ -247,16 +258,14 @@ export namespace API {
hashesString?: string;
currentSceneryName?: string;
currentSceneryHash?: string;
routeSceneries?: string;
checkpointArrivals?: string[];
checkpointDepartures?: string[];
checkpointArrivalsScheduled?: string[];
checkpointDeparturesScheduled?: string[];
checkpointStopTypes?: string[];
visitedSceneries?: string[];
path: string;
}
export type Response = Data[];
@@ -316,6 +325,10 @@ export namespace API {
export namespace Donators {
export type Response = string[];
}
export namespace Vehicles {
export type Response = VehicleData[];
}
}
export namespace GithubAPI {
+45 -1
View File
@@ -1,3 +1,5 @@
import { RouteLocationRaw } from 'vue-router';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
@@ -39,6 +41,13 @@ export interface RegionCounters {
timetablesCount: number;
}
export interface TimetablePathElement {
arrivalRouteExt?: string;
departureRouteExt?: string;
stationName: string;
stationHash: string;
}
export interface Train {
id: string;
modalId: string;
@@ -63,6 +72,8 @@ export interface Train {
isTimeout: boolean;
isSupporter: boolean;
driverRouteLocation: RouteLocationRaw,
timetableData?: {
timetableId: number;
category: string;
@@ -72,7 +83,7 @@ export interface Train {
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
timetablePath: TimetablePathElement[];
};
}
@@ -188,4 +199,37 @@ export interface TrainStop {
export interface CheckpointTrain {
checkpointStop: TrainStop;
train: Train;
timetablePathElement: TimetablePathElement;
previousSceneryElement: TimetablePathElement | null;
nextSceneryElement: TimetablePathElement | null;
}
// Vehicles Data
export interface VehicleData {
id: number;
name: string;
type: string;
cabinName: string | null;
restrictions: Record<string, any> | null;
vehicleGroupsId: number;
group: VehiclesGroup;
}
export interface VehiclesGroup {
id: number;
name: string;
speed: number;
length: number;
weight: number;
cargoTypes: VehicleCargo[] | null;
locoProps: {
coldStart: boolean;
doubleManned: boolean;
} | null;
}
export interface VehicleCargo {
id: string;
weight: number;
}
+128
View File
@@ -0,0 +1,128 @@
<template>
<section class="driver-view">
<div class="view-wrapper">
<div v-if="chosenTrain">
<div class="actions">
<router-link
:to="`/journal/timetables?search-driver=${chosenTrain.driverName}`"
class="a-button btn--image"
>
<span class="hidable">
{{ $t('trains.driver-journal-link') }}
</span>
<img src="/images/icon-train.svg" alt="train icon" />
</router-link>
</div>
<div class="train-card">
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" />
</div>
</div>
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Data.Loading" />
<div v-else class="driver-not-found">
<h2>&olcross; {{ $t('trains.driver-not-found-header') }}</h2>
<p>
{{ $t('trains.driver-not-found-desc-1') }} <br />
{{ $t('trains.driver-not-found-desc-2') }}
<router-link to="/journal/timetables"
>{{ $t('trains.driver-not-found-journal') }} </router-link
>!
</p>
<router-link to="/">&lt;&lt; {{ $t('trains.driver-not-found-return') }}</router-link>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, onActivated, onMounted, useAttrs } from 'vue';
import TrainInfo from '../components/TrainsView/TrainInfo.vue';
import TrainSchedule from '../components/TrainsView/TrainSchedule.vue';
import Loading from '../components/Global/Loading.vue';
import { useMainStore } from '../store/mainStore';
import { useApiStore } from '../store/apiStore';
import { Status } from '../typings/common';
const props = defineProps({
trainId: {
type: String,
required: true
},
modalId: {
type: String
}
});
const mainStore = useMainStore();
const apiStore = useApiStore();
const chosenTrain = computed(() =>
mainStore.trainList.find((train) => train.id == props.trainId || train.modalId == props.modalId)
);
onActivated(() => {
console.log();
});
</script>
<style lang="scss" scoped>
@import '../styles/responsive';
$viewBgCol: #1a1a1a;
.driver-view {
margin: 0 auto;
padding: 1em 0;
max-width: 2000px;
min-height: 95vh;
}
.actions {
display: flex;
align-items: flex-end;
justify-content: flex-end;
gap: 0.5em;
}
.actions > a {
background-color: $viewBgCol;
padding: 0.5em;
border-radius: 0.5em 0.5em 0 0;
&:hover {
background-color: lighten($viewBgCol, 10);
}
}
.train-card {
background-color: $viewBgCol;
border-radius: 0 0 0.5em 0.5em;
}
.driver-not-found {
background-color: $viewBgCol;
text-align: center;
padding: 1em;
p {
padding: 1em 0;
color: #aaa;
}
a {
text-decoration: underline;
color: white;
}
}
@include smallScreen {
.actions > a > span.hidable {
display: none;
}
}
</style>
+4 -1
View File
@@ -267,7 +267,10 @@ export default defineComponent({
const timestampTo = timestampFrom ? timestampFrom + 86400000 : undefined;
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)
queries.push(`timestampFrom=${timestampFrom}`, `timestampTo=${timestampTo}`);
+23 -8
View File
@@ -40,7 +40,6 @@ import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import dateMixin from '../mixins/dateMixin';
import routerMixin from '../mixins/routerMixin';
import modalTrainMixin from '../mixins/modalTrainMixin';
import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalStats from '../components/JournalView/JournalStats.vue';
@@ -126,6 +125,8 @@ interface TimetablesQueryParams {
dateTo?: string;
issuedFrom?: string;
terminatingAt?: string;
via?: string;
countFrom?: number;
countLimit?: number;
@@ -146,7 +147,7 @@ export default defineComponent({
JournalHeader,
JournalTimetablesList
},
mixins: [dateMixin, routerMixin, modalTrainMixin],
mixins: [dateMixin, routerMixin],
name: 'JournalTimetables',
@@ -206,6 +207,8 @@ export default defineComponent({
'search-driver': '',
'search-dispatcher': '',
'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-date': ''
} as Journal.TimetableSearchType);
@@ -299,11 +302,17 @@ export default defineComponent({
},
setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? '';
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? '';
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
});
// this.searchersValues['search-date'] = options['search-date'] ?? '';
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
// this.searchersValues['search-train'] = options['search-train'] ?? '';
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
// this.searchersValues['search-via'] = options['search-via'] ?? '';
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
@@ -347,6 +356,8 @@ export default defineComponent({
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
let dateTo: string | undefined = undefined;
@@ -418,6 +429,10 @@ export default defineComponent({
queryParams['authorName'] = authorName;
queryParams['dateFrom'] = dateFrom;
queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom;
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
@@ -446,7 +461,7 @@ export default defineComponent({
this.timetableHistory = responseData;
// Stats display
this.store.driverStatsName =
this.mainStore.driverStatsName =
this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim()
? this.timetableHistory[0].driverName
: '';
+19 -31
View File
@@ -13,6 +13,7 @@
:station="stationInfo"
:onlineScenery="onlineSceneryInfo"
/>
<SceneryInfo :station="stationInfo" :onlineScenery="onlineSceneryInfo" />
</div>
@@ -22,8 +23,8 @@
v-for="(viewMode, i) in viewModes"
:key="i"
class="btn btn--option"
:class="{ checked: currentMode == viewMode.component }"
@click="setViewMode(viewMode.component)"
:data-checked="currentMode == viewMode.component"
>
{{ $t(viewMode.id) }}
</button>
@@ -121,10 +122,6 @@ export default defineComponent({
Status: Status.Data
}),
// activated() {
// this.loadSelectedCheckpoint();
// },
setup() {
const route = useRoute();
@@ -215,11 +212,10 @@ button.back-btn {
position: relative;
width: 100%;
max-width: var(--max-container-width);
min-height: 100vh;
width: 100%;
margin: 1rem 0;
padding: 1rem 0;
text-align: center;
&[data-timetable-only='true'] {
@@ -228,30 +224,28 @@ button.back-btn {
}
}
.scenery-left {
.scenery-left,
.scenery-right {
position: relative;
background-color: #181818;
padding: 1em 0.5em;
height: 100vh;
min-height: 750px;
max-height: 1000px;
overflow: auto;
background-color: #181818;
border-radius: 0.5em;
padding: 1em 0.5em;
height: calc(100vh - 0.5em);
min-height: 800px;
max-height: 2000px;
}
.scenery-left {
display: flex;
flex-direction: column;
}
.scenery-right {
background: #181818;
padding: 1em 0.5em;
height: 100vh;
min-height: 750px;
max-height: 1000px;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-rows: auto 1fr;
gap: 1em;
}
@@ -261,18 +255,12 @@ button.back-btn {
.info-actions {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 0.75em;
.btn {
button {
padding: 0.5em;
box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] {
color: var(--clr-primary);
}
}
}
+6 -2
View File
@@ -8,6 +8,8 @@
ref="filterCardRef"
/>
<StationStats />
<button
class="btn-donation btn--image"
ref="btn"
@@ -21,7 +23,6 @@
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggle-donation-card="toggleDonationCard" />
<StationStats />
</div>
</section>
</template>
@@ -96,14 +97,17 @@ export default defineComponent({
.stations-options {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
position: relative;
margin-bottom: 0.5em;
}
button.btn-donation {
margin-left: auto;
$btnColor: #254069;
background-color: $btnColor;
+6 -9
View File
@@ -19,7 +19,6 @@
import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch } from 'vue';
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin';
import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager';
@@ -33,8 +32,6 @@ export default defineComponent({
TrainStats
},
mixins: [modalTrainMixin],
props: {
train: {
type: String,
@@ -102,16 +99,16 @@ export default defineComponent({
},
activated() {
// Backwards compatibility with external links leading to train modal
if (this.trainId) {
this.$router.replace(`/driver?modalId=${this.trainId}`);
return;
}
if (this.train) {
this.searchedTrain = this.train;
this.searchedDriver = this.driver || '';
}
this.$nextTick(() => {
if (this.trainId) {
this.selectModalTrainById(this.trainId);
}
});
}
});
</script>
+4 -2
View File
@@ -6,8 +6,10 @@ declare module '*.vue' {
export default component;
}
interface ImportMetaEnv {
readonly VITE_APP_API_DEV: string;
readonly VITE_APP_WS_DEV: string;
readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_VEHICLES_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_ACTIVE_DATA_MODE: 'production' | 'mocking' | 'development';
readonly VITE_UPDATE_TEST: 'test' | 'production';
}
interface ImportMeta {
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,
runtimeCaching: [
{
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/getSceneries/i,
urlPattern:
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'spythere-sceneries-cache',
cacheName: 'stacjownik-api-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/static.spythere.eu\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'spythere-static-cache',
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 8
}
}
}
]
},
devOptions: {
+518 -876
View File
File diff suppressed because it is too large Load Diff