Compare commits

..

136 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 44df685606 Merge pull request #92 from Spythere/development
v1.24.4
2024-05-30 14:38:04 +02:00
Spythere 785a42b849 hotfix: detecting user timetable status at checkpoints 2024-05-30 14:29:09 +02:00
Spythere ccfcca8728 hotfix: scenery timetable duplicating 2024-05-30 14:24:18 +02:00
Spythere d9a7ba122c Merge pull request #91 from Spythere/development
v1.24.3
2024-05-26 01:44:45 +02:00
Spythere bf8d4a9ef4 chore: global font sizing; chore: train modal dvh 2024-05-25 18:06:01 +02:00
Spythere 6ea1e91d1d hotfix: card positioning 2024-05-25 17:57:25 +02:00
Spythere 813b557455 chore: improved card positioning 2024-05-25 17:55:18 +02:00
Spythere 834b14da69 fix: card dvh 2024-05-25 17:26:27 +02:00
Spythere c809b2146d chore: locale update 2024-05-25 17:12:19 +02:00
Spythere 33b98ca313 chore: added text color for active filters info 2024-05-25 17:11:28 +02:00
Spythere bcb9c63cb0 chore: reactive hiding body scroll on modal 2024-05-25 17:05:41 +02:00
Spythere 17d77a80d8 bump: 1.24.3 2024-05-25 16:02:40 +02:00
Spythere 65b159f8fd fix: scenery timetable duplicates; fix: not opening train modal for queries 2024-05-25 16:02:20 +02:00
Spythere 063d5283e4 Merge pull request #90 from Spythere/development
v1.24.2
2024-05-24 13:56:39 +02:00
Spythere 29de1b3c4b chore: scenery view layout 2024-05-24 13:52:42 +02:00
Spythere f0c02bf12e chore: pwa adjustments 2024-05-24 13:43:29 +02:00
Spythere 8aa23468b3 chore: changed station stats median to avg 2024-05-23 15:53:18 +02:00
Spythere 4c1fcf710b refactor: global modals to cards 2024-05-23 15:01:30 +02:00
Spythere a529d6e9eb chore: changed no stations message 2024-05-23 14:08:42 +02:00
Spythere 9fc602e08f chore: filters improvements 2024-05-22 15:41:33 +02:00
Spythere 56e40bd84b bump: version (1.24.2) 2024-05-21 16:17:41 +02:00
Spythere a5b5df7452 refactor: restructured station filters 2024-05-21 16:17:23 +02:00
Spythere 1a8da02ced chore: checkpoints detection fix 2024-05-19 23:42:06 +02:00
Spythere 7e75fa2516 chore: checkpoints hotfix 2024-05-19 23:12:07 +02:00
Spythere 3ed2c09184 chore: checkpoints filtering 2024-05-19 23:05:57 +02:00
Spythere 6901c3d2b4 chore: hotfix 2024-05-19 22:30:21 +02:00
Spythere 8417754403 refactor: optimization of train schedules 2024-05-19 19:50:01 +02:00
Spythere 44f6cf4232 Merge branch 'development' 2024-05-12 15:22:28 +02:00
102 changed files with 11163 additions and 16376 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.1",
"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

+17 -34
View File
@@ -1,18 +1,11 @@
<template>
<div class="app_container">
<UpdateModal
:update-modal-open="isUpdateModalOpen"
@toggle-modal="() => (isUpdateModalOpen = false)"
<UpdateCard
:is-update-card-open="isUpdateCardOpen"
@toggle-card="() => (isUpdateCardOpen = false)"
/>
<Tooltip />
<transition name="modal-anim">
<keep-alive>
<TrainModal />
</keep-alive>
</transition>
<AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<main class="app_main">
@@ -27,7 +20,7 @@
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<button class="btn--text" @click="() => (isUpdateModalOpen = true)">
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
@@ -54,9 +47,8 @@ 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 UpdateModal from './components/App/UpdateModal.vue';
import UpdateCard from './components/App/UpdateCard.vue';
import StorageManager from './managers/storageManager';
@@ -67,8 +59,7 @@ export default defineComponent({
Clock,
StatusIndicator,
AppHeader,
TrainModal,
UpdateModal,
UpdateCard,
Tooltip
},
@@ -78,12 +69,10 @@ export default defineComponent({
apiStore: useApiStore(),
tooltipStore: useTooltipStore(),
isUpdateModalOpen: false,
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() {
@@ -130,8 +110,9 @@ export default defineComponent({
releaseURL: releaseData.html_url
};
this.isUpdateModalOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
this.isUpdateCardOpen =
(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;
}
@@ -210,7 +192,7 @@ export default defineComponent({
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
font-size: calc(0.65rem + 0.85vw);
}
@include screenLandscape() {
@@ -226,6 +208,7 @@ export default defineComponent({
min-height: 100vh;
overflow: hidden;
position: relative;
}
.app_main {
@@ -1,12 +1,12 @@
<template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
<div class="modal-content">
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)">
<button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
{{ $t('update.confirm') }}
</button>
@@ -16,7 +16,7 @@
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</AnimatedModal>
</Card>
</template>
<script lang="ts">
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue';
import Card from '../Global/Card.vue';
const converter = new Converter();
export default defineComponent({
components: { AnimatedModal },
components: { Card },
props: {
updateModalOpen: {
isUpdateCardOpen: {
type: Boolean,
required: true
}
},
emits: ['toggleModal'],
emits: ['toggleCard'],
data() {
return {
@@ -49,7 +49,7 @@ export default defineComponent({
},
watch: {
updateModalOpen(val: boolean) {
isUpdateCardOpen(val: boolean) {
this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
});
@@ -60,37 +60,38 @@ export default defineComponent({
htmlChangelog() {
if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
return converter.makeHtml(this.mainStore.appUpdate.changelog);
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) {
text-align: center;
color: $accentCol;
}
::v-deep(h2) {
padding: 0.25em 0;
border-bottom: 1px solid #aaa;
}
::v-deep(ul) {
list-style: inside;
padding: 0.5em;
list-style: initial;
padding: 1em;
line-height: 1.5em;
}
.modal-content {
.content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
@@ -98,6 +99,7 @@ export default defineComponent({
min-height: 700px;
overflow: auto;
text-align: justify;
max-width: 700px;
}
.no-features {
@@ -1,11 +1,10 @@
<template>
<transition name="modal-anim" tag="div">
<div class="modal" v-if="isOpen">
<div class="modal-background" @click="toggleModal(false)"></div>
<div class="modal-wrapper" ref="wrapper" tabindex="0">
<div class="card" v-if="isOpen">
<div class="card-background" @click="toggleCard(false)"></div>
<div class="card-body" tabindex="0">
<slot></slot>
</div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div>
</transition>
</template>
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
emits: ['toggleModal'],
emits: ['toggleCard'],
props: {
isOpen: Boolean
@@ -36,8 +35,8 @@ export default defineComponent({
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
@@ -46,17 +45,21 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal {
.card {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
height: 100%;
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
}
.modal-background {
.card-background {
position: absolute;
top: 0;
left: 0;
@@ -68,21 +71,23 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55);
}
.modal-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
.card-body {
position: relative;
margin: 1em;
max-height: 95vh;
max-height: 95dvh;
& > :slotted(div) {
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw;
max-width: 850px;
overflow: auto;
}
@include smallScreen {
.card {
align-items: flex-start;
}
}
</style>
@@ -1,12 +1,7 @@
<template>
<AnimatedModal
class="donation-modal"
:isOpen="isModalOpen"
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
<div class="body">
<div class="content">
<h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,9 +56,9 @@
</i>
</div>
<div class="modal_actions">
<div class="actions">
<a
class="modal-action a-button btn--image coffee"
class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
ref="action"
@@ -73,7 +68,7 @@
</a>
<a
class="modal-action a-button btn--image paypal"
class="action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank"
>
@@ -81,30 +76,30 @@
{{ $t('donations.action-paypal') }}
</a>
<button class="modal-action btn--image exit" @click="toggleModal(false)">
<button class="action btn--image exit" @click="toggleCard(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({
components: { AnimatedModal },
components: { Card },
props: {
isModalOpen: Boolean
isCardOpen: Boolean
},
emits: ['toggleModal'],
emits: ['toggleCard'],
watch: {
isModalOpen(val: boolean) {
isCardOpen(val: boolean) {
this.running = val;
this.lastUpdate = Date.now();
@@ -138,8 +133,8 @@ export default defineComponent({
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
},
runUpdate() {
@@ -157,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal_content {
.body {
display: grid;
grid-template-rows: 1fr auto;
gap: 1em;
font-size: 1.1em;
& > div {
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
max-width: 820px;
}
.modal_main {
.content {
overflow: auto;
overflow-x: hidden;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
padding: 1em;
}
.modal_actions {
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em;
padding: 1em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
.actions > .action {
&.paypal {
$btnColor: #254069;
-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.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), 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
+1 -6
View File
@@ -72,7 +72,7 @@
<div class="info-lists">
<!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" />
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center;
font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
}
.info-lists {
@@ -13,16 +13,15 @@
</li>
<li
v-for="train in onlineScenery?.stationTrains"
v-for="{ train, status } in stationTrains"
class="badge user"
:class="train.stopStatus"
:key="train.trainId"
tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
:key="train.id"
:data-status="status"
>
<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,17 +29,52 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery } 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: {
type: Object as PropType<ActiveScenery>,
required: false
},
station: {
type: Object as PropType<Station>
}
},
data() {
return {
mainStore: useMainStore()
};
},
computed: {
stationTrains() {
if (!this.onlineScenery) return;
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
return this.onlineScenery.stationTrains.map((train) => {
const stop = train.timetableData?.followingStops.find(
(stop) =>
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
);
const status = stop
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
: 'no-timetable';
return {
train,
status
};
});
}
}
});
@@ -63,46 +97,45 @@ 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;
}
&.no-timetable .user_train {
&[data-status='no-timetable'] .user_train {
background-color: $no-timetable;
}
&.departed > &_train {
&[data-status='departed'] .user_train {
background-color: $departed;
}
&.stopped > &_train {
&[data-status='stopped'] .user_train {
background-color: $stopped;
}
&.online > &_train {
&[data-status='online'] .user_train {
background-color: $online;
}
&.terminated > &_train {
&[data-status='terminated'] .user_train {
background-color: $terminated;
}
&.disconnected > &_train {
&[data-status='disconnected'] .user_train {
background-color: $disconnected;
}
&.offline {
&[data-status='offline'] {
background: firebrick;
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,
+150 -94
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,26 +25,23 @@
</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>
<div class="timetable-list">
<transition-group name="list-anim">
<div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading"
>
<Loading />
@@ -48,7 +49,7 @@
<span
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery"
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline"
>
{{ $t('scenery.offline') }}
@@ -56,68 +57,72 @@
<div
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0"
v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables"
>
{{ $t('scenery.no-timetables') }}
</div>
<div
class="timetable-item"
<router-link
class="timetable-item a-block"
v-else
v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
v-for="(row, i) in sceneryTimetables"
:key="row.train.id + i"
tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
:to="row.train.driverRouteLocation"
>
<span class="timetable-general">
<span class="general-info">
<span class="info-number">
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<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="scheduledTrain.stopInfo.comments"
:title="scheduledTrain.stopInfo.comments"
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>
{{ scheduledTrain.driverName }}
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<div class="info-route">
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div>
<ScheduledTrainStatus :sceneryTimetableRow="row" />
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
{{ $t('timetables.begins') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
<div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.arrivalDelay }})
</span>
</div>
</span>
@@ -125,47 +130,45 @@
<span class="schedule-stop">
<span class="stop-connection">
{{ scheduledTrain.arrivingLine }}
{{ row.arrivingLine }}
</span>
<span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
{{ row.checkpointStop.stopTime || '' }}
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
</span>
<span class="stop-connection">
{{ scheduledTrain.departureLine }}
{{ row.departureLine }}
</span>
</span>
<span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
{{ $t('timetables.terminates') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
<div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
timestampToString(row.checkpointStop.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.departureDelay }})
</span>
</div>
</span>
</span>
</span>
</div>
</router-link>
</transition-group>
</div>
</section>
@@ -178,18 +181,20 @@ 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 ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { SceneryTimetableRow } from './typings';
import { ActiveScenery, Station } from '../../typings/common';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({
name: 'SceneryTimetable',
components: { Loading, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, modalTrainMixin],
mixins: [dateMixin, routerMixin, trainCategoryMixin],
props: {
station: {
@@ -204,12 +209,14 @@ export default defineComponent({
listOpen: false
}),
mounted() {
activated() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
watch: {
currentURL() {
this.loadSelectedOption();
}
},
setup(props) {
@@ -220,9 +227,10 @@ export default defineComponent({
const mainStore = useMainStore();
const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0
? ''
: props.station?.generalInfo?.checkpoints[0] ?? null
props.station?.generalInfo?.checkpoints[0] ??
props.station?.name ??
route.query['station']?.toString() ??
''
);
return {
@@ -241,27 +249,57 @@ export default defineComponent({
return url;
},
computedScheduledTrains() {
if (!this.station) return [];
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 (
this.onlineScenery?.scheduledTrains
?.filter(
(train) =>
train.checkpointName.toLocaleLowerCase() ==
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
train.region == this.mainStore.region.id
)
.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
if (a.stopStatusID < b.stopStatusID) return -1;
return url;
},
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1;
sceneryTimetables(): SceneryTimetableRow[] {
if (!this.onlineScenery) return [];
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1;
}) || []
);
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()
)
.map((ct) => {
const trainStopStatus = getTrainStopStatus(
ct.checkpointStop,
ct.train.currentStationName,
sceneryName
);
return {
checkpointStop: ct.checkpointStop,
train: ct.train,
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
};
})
.sort((a, b) => {
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
return -1;
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
return 1;
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
});
}
},
@@ -269,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) {
@@ -339,7 +389,6 @@ export default defineComponent({
background: #353535;
cursor: pointer;
z-index: 10;
&.empty {
@@ -369,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;
@@ -406,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>
@@ -1,7 +1,7 @@
<template>
<div class="general-status">
<span
:class="computedScheduledTrain.stopStatus"
:class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription"
>
{{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../typings/common';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
sceneryTimetableRow: {
type: Object as PropType<SceneryTimetableRow>,
required: true
}
},
computed: {
computedScheduledTrain(): ScheduledTrainComp {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } =
this.scheduledTrain;
computedScheduledTrain() {
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '',
stopStatusIndicator = '';
switch (stopStatus) {
switch (status) {
case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: '';
break;
@@ -85,7 +81,7 @@ export default defineComponent({
break;
}
return {
...this.scheduledTrain,
...this.sceneryTimetableRow,
stopStatusDescription,
stopStatusIndicator
};
+13
View File
@@ -0,0 +1,13 @@
import { StopStatus, Train, TrainStop } from '../../typings/common';
export interface SceneryTimetableRow {
checkpointStop: TrainStop;
train: Train;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
departureLine: string | null;
arrivingLine: string | null;
prevStationName: string | null;
nextStationName: string | null;
status: StopStatus;
}
+42
View File
@@ -0,0 +1,42 @@
import { StopStatus, TrainStop } from '../../typings/common';
export const stopStatusPriority = [
StopStatus.ONLINE,
StopStatus.STOPPED,
StopStatus.DEPARTED,
StopStatus.ARRIVING,
StopStatus.DEPARTED_AWAY,
StopStatus.TERMINATED
];
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
if (stopInfo.terminatesHere && stopInfo.confirmed) {
return StopStatus.TERMINATED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
return StopStatus.DEPARTED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
return StopStatus.DEPARTED_AWAY;
}
if (currentStationName == sceneryName && !stopInfo.stopped) {
return StopStatus.ONLINE;
}
if (currentStationName == sceneryName && stopInfo.stopped) {
return StopStatus.STOPPED;
}
if (currentStationName != sceneryName) {
return StopStatus.ARRIVING;
}
return StopStatus.ONLINE;
}
+9 -16
View File
@@ -15,7 +15,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption {
id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: {
'option.value'() {
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
// this.filterStore.changeFilterValue(this.option.name, !this.option.value);
}
},
@@ -56,17 +49,17 @@ export default defineComponent({
handleDbClick(e: Event) {
e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id;
// this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true;
this.$emit('update:optionValue', true);
this.filterStore.inputs.options
.filter((option) => {
return option.section == this.option.section && option.id != this.option.id;
})
.forEach((option) => {
option.value = !this.option.value;
});
// this.filterStore.inputs.options
// .filter((option) => {
// return option.section == this.option.section && option.id != this.option.id;
// })
// .forEach((option) => {
// option.value = !this.option.value;
// });
}
}
});
+222 -144
View File
@@ -4,58 +4,79 @@
<button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
<p>[F] {{ $t('options.filters') }}</p>
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
<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>
<div class="changed-filters" :data-active="changedFilters.length > 0">
<template v-if="changedFilters.length > 0">
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
</template>
<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"
v-for="section in filterStore.inputs.optionSections"
:key="section"
v-for="(sectionFilters, sectionKey) in filtersSections"
:key="sectionKey"
>
<h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3>
<hr />
<div class="section-inputs">
<FilterOption
v-for="(option, i) in filterStore.inputs.options.filter(
(o) => o.section == section
)"
v-model:optionValue="option.value"
:option="option"
:key="i"
/>
<div class="section-filters">
<label
v-for="filterKey in sectionFilters"
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
>
<input
type="checkbox"
:checked="filters[filterKey]"
v-model="filters[filterKey]"
:class="sectionKey"
:name="filterKey"
:id="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div>
</div>
</section>
@@ -68,7 +89,7 @@
<span>{{
minimumHours == 0
? $t('filters.now')
: minimumHours < 8
: minimumHours < 7
? minimumHours + $t('filters.hour')
: $t('filters.no-limit')
}}</span>
@@ -76,43 +97,42 @@
</span>
</section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search">
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
<datalist id="authors" name="authors">
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
</datalist>
<form action="javascript:void(0);" @submit="handleAuthorsInput">
<input
type="text"
id="author"
list="authors"
name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true"
@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>
<section class="card_sliders">
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
<div class="slider" v-for="(slider, i) in initSliders" :key="i">
<input
class="slider-input"
type="range"
:name="slider.name"
:name="slider.id"
:id="slider.id"
:min="slider.minRange"
:max="slider.maxRange"
:step="slider.step"
v-model="slider.value"
@change="handleInput"
v-model="filters[slider.id]"
/>
<span class="slider-value">{{ slider.value }}</span>
<span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }}
</div>
@@ -133,9 +153,9 @@
<button
class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
>
[R] {{ $t('filters.reset') }}
</button>
@@ -151,22 +171,36 @@
import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager';
import {
filtersSections,
initSliders,
initFilters,
getChangedFilters
} from '../../managers/stationFilterManager';
import { StationFilterSection } from '../../managers/stationFilterManager';
import { computed } from 'vue';
import { watch } from 'vue';
const STORAGE_KEY = 'options_saved';
export default defineComponent({
components: { FilterOption },
mixins: [keyMixin, routerMixin],
data: () => ({
saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '',
filtersSections,
initSliders,
minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' },
@@ -180,22 +214,33 @@ export default defineComponent({
setup() {
const isVisible = inject('isFilterCardVisible');
const store = useMainStore();
const filterStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const changedFilters = computed(() => getChangedFilters(filters));
// Save filters to persistent storage
watch(filters, (value) => {
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
Object.keys(value).forEach((filterKey) => {
StorageManager.setValue(filterKey, filters[filterKey]);
});
});
return {
isVisible,
store,
filterStore
filters,
changedFilters
};
},
mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
}
this.currentRegion = this.store.region;
@@ -214,7 +259,7 @@ export default defineComponent({
return true;
},
authors() {
authorsHint() {
return this.store.stationList
.reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => {
@@ -229,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();
}
});
}
@@ -258,61 +294,77 @@ export default defineComponent({
this.scrollTop = (e.target as HTMLElement).scrollTop;
},
handleInput(e: Event) {
const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue(target.name, target.value);
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
},
handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
this.filters['authors'] = this.authors;
},
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue(name, value);
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, 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() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
saveFilters() {
this.saveOptions = !this.saveOptions;
if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY);
StorageManager.unregisterStorage(STORAGE_KEY);
return;
}
StorageManager.registerStorage(this.STORAGE_KEY);
StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) =>
StorageManager.setBooleanValue(option.name, !option.value)
);
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setValue(filterKey, this.filters[filterKey]);
});
},
resetFilters() {
this.authorsInputValue = '';
if (this.preventKeyDown) return;
// Reset local model values
this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.authors = '';
// Reset global filters
Object.keys(this.filters).forEach((filterKey) => {
this.filters[filterKey] = (initFilters as any)[filterKey];
});
},
areSectionFiltersDefault(sectionKey: StationFilterSection) {
return filtersSections[sectionKey].every((filterKey) => {
return this.filters[filterKey] == initFilters[filterKey];
});
},
resetSectionFilters(sectionKey: StationFilterSection) {
filtersSections[sectionKey].forEach((filterKey) => {
this.filters[filterKey] = initFilters[filterKey];
});
},
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
filtersSections[sectionKey].forEach((filterKey) => {
if (typeof this.filters[filterKey] === 'boolean')
this.filters[filterKey] = filterKey != chosenKey;
});
},
closeCard() {
@@ -330,6 +382,7 @@ export default defineComponent({
@import '../../styles/responsive';
@import '../../styles/card';
@import '../../styles/animations';
@import '../../styles/variables';
h3.section-header {
text-align: center;
@@ -339,6 +392,7 @@ h3.section-header {
.card {
display: grid;
grid-template-rows: 1fr auto;
padding: 1px;
}
.card_info {
@@ -346,6 +400,15 @@ h3.section-header {
padding: 0.5em;
}
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls {
display: flex;
gap: 0.5em;
@@ -374,28 +437,6 @@ h3.section-header {
text-align: center;
}
.card_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
.card_timestamp {
display: flex;
flex-direction: column;
@@ -421,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;
@@ -441,6 +486,52 @@ h3.section-header {
}
}
.section-filters {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.section-filters > label {
position: relative;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
span {
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
font-weight: bold;
background-color: forestgreen;
}
span:hover {
background-color: #22aa22;
}
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: #444;
&:hover {
background-color: #555;
}
}
&:focus-visible + span {
outline: 1px solid $accentCol;
}
}
}
.card_actions {
padding: 0.5em;
@@ -475,35 +566,18 @@ h3.section-header {
}
}
.section-inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider {
display: flex;
align-items: center;
gap: 0.25em;
margin-bottom: 1em;
&-value {
color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em;
}
&-content {
flex-grow: 2;
}
&-input {
-webkit-appearance: none;
appearance: none;
@@ -512,7 +586,6 @@ h3.section-header {
outline: none;
min-width: 25%;
max-width: 120px;
&:focus-visible ~ * {
color: gold;
@@ -584,8 +657,13 @@ h3.section-header {
}
@include smallScreen {
.card_controls > button.card-button > p {
display: none;
.slider {
flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
}
}
</style>
+103 -86
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.med-timetable-count') }}
<b>{{ medTimetableCount }}</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 '';
@@ -89,27 +108,19 @@ export default defineComponent({
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
},
medTimetableCount() {
const scheduledTrainsArr = this.mainStore.activeSceneryList
.reduce<number[]>((acc, sc) => {
if (sc.region != this.mainStore.region.id) return acc;
avgTimetableCount() {
const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
return sc.region == this.mainStore.region.id;
});
acc.push(sc.scheduledTrainCount.all);
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
return acc;
}, [])
.sort((a, b) => Math.sign(a - b));
if (regionSceneries.length == 0) return 0;
if (scheduledTrainsArr.length == 0) return 0;
if (scheduledTrainsArr.length % 2 == 0) {
let v1 = scheduledTrainsArr[scheduledTrainsArr.length / 2];
let v2 = scheduledTrainsArr[scheduledTrainsArr.length / 2 - 1];
return (v1 + v2) / 2;
}
return scheduledTrainsArr[~~(scheduledTrainsArr.length / 2)];
return timetableCountSum / regionSceneries.length;
},
trackCount() {
@@ -179,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 {
@@ -217,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>
+316 -312
View File
@@ -1,383 +1,381 @@
<template>
<section class="station_table">
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0"
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/>
<div class="table_wrapper" v-else-if="displayedStations.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="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.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="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="station in displayedStations"
: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="openDonationModal"
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>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
>)
<div>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
>)
</div>
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
⚠ {{ $t('sceneries.active-filters') }}
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, inject, computed } from 'vue';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import { useMainStore } from '../../store/mainStore';
import { Station, Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore';
import { getChangedFilters } from '../../managers/stationFilterManager';
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils';
export default defineComponent({
emits: ['toggleDonationModal'],
emits: ['toggleDonationCard'],
components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin],
data: () => ({
headIconsIds,
headIds,
lastSelectedStationName: ''
getChangedFilters
}),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
}
},
setup() {
const mainStore = useMainStore();
const apiStore = useApiStore();
const tooltipStore = useTooltipStore();
const stationFiltersStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
const filteredStationList = computed(() =>
mainStore.allStationInfo
.filter((station) => filterStations(station, filters))
.sort((a, b) => sortStations(a, b, activeSorter))
);
return {
Status: Status.Data,
stationFiltersStore,
mainStore,
apiStore,
tooltipStore
tooltipStore,
filters,
filteredStationList,
activeSorter
};
},
methods: {
setScenery(name: string) {
const station = this.displayedStations.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
}
});
};
},
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
openDonationCard(e: Event) {
this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide();
},
@@ -388,10 +386,14 @@ export default defineComponent({
window.open(url, '_blank');
},
changeSorter(headerName: HeadIdsTypes) {
changeSorter(headerName: HeadIdsType) {
if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName);
if (headerName == this.activeSorter.headerName)
this.activeSorter.dir = -1 * this.activeSorter.dir;
else this.activeSorter.dir = 1;
this.activeSorter.headerName = headerName;
}
}
});
@@ -405,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;
@@ -413,11 +416,10 @@ $rowCol: #424242;
.no-stations {
text-align: center;
font-size: 1.5em;
font-size: 1.25em;
padding: 1em;
background: #1a1a1a;
line-height: 1.5em;
}
table {
@@ -494,8 +496,10 @@ table {
}
}
tr {
tr,
.a-row {
background-color: $rowCol;
vertical-align: middle;
&:nth-child(even) {
background-color: lighten($rowCol, 5);
+25 -52
View File
@@ -5,56 +5,29 @@ export interface FilterOption {
defaultValue: boolean;
}
export interface Filter {
[key: string]: boolean | number | string;
default: boolean;
notDefault: boolean;
real: boolean;
fictional: boolean;
SPK: boolean;
SCS: boolean;
SPE: boolean;
SUP: boolean;
noSUP: boolean;
ASDEK: boolean;
noASDEK: boolean;
ręczne: boolean;
'ręczne+SPK': boolean;
'ręczne+SCS': boolean;
mechaniczne: boolean;
'mechaniczne+SPK': boolean;
'mechaniczne+SCS': boolean;
SBL: boolean;
PBL: boolean;
współczesna: boolean;
kształtowa: boolean;
historyczna: boolean;
mieszana: boolean;
minLevel: number;
maxLevel: number;
minOneWayCatenary: number;
minOneWay: number;
minTwoWayCatenary: number;
minTwoWay: number;
minVmax: number;
maxVmax: number;
'no-1track': boolean;
'no-2track': boolean;
'include-selected': boolean;
free: boolean;
occupied: boolean;
nonPublic: boolean;
unavailable: boolean;
abandoned: boolean;
endingStatus: boolean;
afkStatus: boolean;
noSpaceStatus: boolean;
unavailableStatus: boolean;
unsignedStatus: boolean;
authors: string;
onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
junction: boolean;
nonJunction: boolean;
export const headIds = [
'station',
'min-lvl',
'status',
'dispatcher',
'dispatcher-lvl',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
'timetableConfirmed'
] as const;
export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
export interface ActiveSorter {
headerName: HeadIdsType;
dir: number;
}
+275
View File
@@ -0,0 +1,275 @@
import { ActiveSorter } from '../../components/StationsView/typings';
import { ActiveScenery, StationGeneralInfo, Status } from '../../typings/common';
import { Station } from '../../typings/common';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
const filtersAssociations: Record<string, string> = {
mechaniczne: 'mechanical',
ręczne: 'manual',
'mechaniczne+SPK': 'SPK-M',
'ręczne+SPK': 'SPK-R',
'mechaniczne+SCS': 'SCS-M',
'ręczne+SCS': 'SCS-R',
współczesna: 'modern',
historyczna: 'historical',
kształtowa: 'semaphores',
mieszana: 'mixed'
};
function filterStatusSection(
filters: Record<string, any>,
{ dispatcherStatus, dispatcherTimestamp }: ActiveScenery
) {
return (
(filters['endingStatus'] && dispatcherStatus == Status.ActiveDispatcher.ENDING) ||
(filters['unavailableStatus'] &&
(dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE ||
dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN)) ||
(filters['afkStatus'] && dispatcherStatus == Status.ActiveDispatcher.AFK) ||
(filters['noSpaceStatus'] && dispatcherStatus == Status.ActiveDispatcher.NO_SPACE) ||
(filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE) ||
(filters['onlineFromHours'] > 0 &&
(dispatcherTimestamp ?? 0) <= Date.now() + filters['onlineFromHours'] * 3600000)
);
}
function filterTimetablesSection(filters: Record<string, any>, station: Station) {
return (
(filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0)) ||
(filters['withActiveTimetables'] &&
station.onlineInfo &&
(station.onlineInfo.scheduledTrainCount.all != 0 ||
station.onlineInfo.dispatcherStatus == Status.ActiveDispatcher.FREE))
);
}
function filterAccessibilitySection(filters: Record<string, any>, station: Station) {
if (
filters['nonPublic'] &&
(!station.generalInfo || station.generalInfo.availability == 'nonPublic')
)
return true;
if (!station.generalInfo) return false;
const { availability } = station.generalInfo;
return (
(filters['unavailable'] && availability == 'unavailable' && !station.onlineInfo) ||
(filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) ||
(filters['default'] && availability == 'default') ||
(filters['notDefault'] &&
availability != 'default' &&
availability != 'abandoned' &&
availability != 'unavailable')
);
}
function filterRealitySection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (filters['real'] && generalInfo.lines) || (filters['fictional'] && !generalInfo.lines);
}
function filterProgramsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
(filters['SUP'] && generalInfo.SUP) ||
(filters['noSUP'] && !generalInfo.SUP) ||
(filters['ASDEK'] && generalInfo.ASDEK) ||
(filters['noASDEK'] && !generalInfo.ASDEK)
);
}
function filterControlsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.controlType] == true ||
filters[filtersAssociations[generalInfo.controlType]] == true
);
}
function filterSignalsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.signalType] == true ||
filters[filtersAssociations[generalInfo.signalType]] == true ||
(filters['SBL'] && generalInfo.routes.sblNames.length > 0) ||
(filters['PBL'] && generalInfo.routes.sblNames.length == 0)
);
}
function filterStationType(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const singleTracks = generalInfo.routes.single.filter((r) => !r.isInternal);
const doubleTracks = generalInfo.routes.double.filter((r) => !r.isInternal);
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
return (filters['junction'] && isJunction) || (filters['nonJunction'] && !isJunction);
}
function filterSliderValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const { availability, reqLevel, routes } = generalInfo;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
return (
filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0) ||
filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0) ||
filters['minVmax'] > routes.maxRouteSpeed ||
filters['maxVmax'] < routes.minRouteSpeed ||
(filters['no-1track'] && routes.single.length != 0) ||
(filters['no-2track'] && routes.double.length != 0) ||
filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length ||
filters['minOneWay'] > routes.singleOtherNames.length ||
filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length ||
filters['minTwoWay'] > routes.doubleOtherNames.length
);
}
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters['authors'].length > 3 &&
!generalInfo.authors
?.map((a) => a.toLocaleLowerCase())
.includes(filters['authors'].toLocaleLowerCase())
);
}
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Record<string, any>) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
// Scenery Timetables section
if (filterTimetablesSection(filters, station)) return false;
// Scenery Accessibility section
if (filterAccessibilitySection(filters, station)) return false;
// Scenery Status section
if (station.onlineInfo && filterStatusSection(filters, station.onlineInfo)) return false;
if (station.generalInfo) {
// Scenery Reality section
if (filterRealitySection(filters, station.generalInfo)) return false;
// Scenery Additional Programs section
if (filterProgramsSection(filters, station.generalInfo)) return false;
// Scenery Controls section
if (filterControlsSection(filters, station.generalInfo)) return false;
// Scenery Signalling section(s)
if (filterSignalsSection(filters, station.generalInfo)) return false;
// Scenery Station Type section
if (filterStationType(filters, station.generalInfo)) return false;
// Scenery sliders
if (filterSliderValues(filters, station.generalInfo)) return false;
// Scenery Authors section
if (filterInputValues(filters, station.generalInfo)) return false;
}
return true;
};
+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) {
+2 -2
View File
@@ -10,7 +10,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common';
import { Train } from '../../typings/common';
export default defineComponent({
data() {
@@ -23,7 +23,7 @@ export default defineComponent({
trains() {
if (this.tooltipStore.content == '') return [];
const parsedTrains = JSON.parse(this.tooltipStore.content) as StationTrain[];
const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
}
}
@@ -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>
-114
View File
@@ -1,114 +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.trainId == this.store.chosenModalTrainId);
}
},
watch: {
chosenTrain(train: Train | undefined) {
this.$nextTick(() => {
if (train) {
const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus();
}
});
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.train-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
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;
margin-top: 1em;
width: 95vw;
max-height: 95vh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
}
@include midScreen {
.exit {
margin: 0.5em;
img {
width: 1.75rem;
}
}
}
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</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 {
+7 -24
View File
@@ -8,19 +8,19 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }}
{{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
>)
</div>
<transition-group name="list-anim" tag="ul">
<li
class="train-row"
v-for="train in trains"
:key="train.trainId"
tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
:key="train.id"
>
<TrainInfo :train="train" :extended="false" />
<router-link class="a-block" :to="train.driverRouteLocation">
<TrainInfo :train="train" :extended="false" />
</router-link>
</li>
</transition-group>
</div>
@@ -29,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';
@@ -46,8 +45,6 @@ export default defineComponent({
}
},
mixins: [modalTrainMixin],
setup() {
const store = useMainStore();
const apiStore = useApiStore();
@@ -76,17 +73,6 @@ export default defineComponent({
return Status.Data.Loaded;
}
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
}
});
</script>
@@ -108,8 +94,7 @@ export default defineComponent({
text-align: center;
padding: 1em 0;
font-size: 1.5em;
font-size: 1.25em;
background: #1a1a1a;
}
@@ -118,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",
+88 -35
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! :/"
@@ -112,8 +153,8 @@
"filters": "FILTERS",
"donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"search-button": "SEARCH",
"reset-button": "RESET",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
@@ -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)",
@@ -174,9 +217,9 @@
"sections": {
"quick": "QUICK FILTERS",
"station-type": "STATION TYPE",
"stationType": "STATION TYPE",
"reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY",
"packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY",
"control": "CONTROLS",
"signals": "SIGNALLING",
@@ -187,6 +230,9 @@
"spawns": "OPEN SPAWNS"
},
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
@@ -197,11 +243,11 @@
"title": "STATION FILTERS",
"default": "IN-GAME",
"not-default": "ADDITIONAL",
"notDefault": "ADDITIONAL",
"real": "REAL",
"fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC",
"nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED",
"SPK": "SPK",
@@ -211,7 +257,6 @@
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
@@ -238,19 +283,21 @@
"nonJunction": "OTHER",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
"minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"minVmax": "MIN. SCENERY ROUTE SPEED",
"maxVmax": "MAX. SCENERY ROUTE SPEED",
"minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"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",
@@ -298,12 +345,13 @@
"single-track-routes-other": "Not electrified single-track routes count: "
},
"no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
"scenery-search": "Search for scenery...",
"active-filters": "Attention! You got active filters!"
},
"station-stats": {
"u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
"med-timetable-count": "Median of scenery timetables:",
"avg-timetable-count": "Average count of scenery timetables:",
"single-track-count": "Single track routes:",
"double-track-count": "Double track routes:",
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
@@ -326,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",
@@ -352,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",
@@ -488,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"
+87 -32
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)",
@@ -171,9 +215,9 @@
"sections": {
"quick": "SZYBKIE FILTRY",
"station-type": "RODZAJ STACJI",
"stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE",
"packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI",
@@ -184,6 +228,9 @@
"spawns": "OTWARTE SPAWNY"
},
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
@@ -194,11 +241,11 @@
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"notDefault": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA",
"nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA",
"SPK": "SPK",
@@ -234,19 +281,21 @@
"nonJunction": "INNE",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
"minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"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.",
@@ -291,12 +340,13 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
},
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
"scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
},
"station-stats": {
"u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
"med-timetable-count": "Mediana rozkładów jazdy na sceneriach:",
"avg-timetable-count": "Średnia liczba rozkładów jazdy na sceneriach:",
"single-track-count": "Szlaki jednotorowe:",
"double-track-count": "Szlaki dwutorowe:",
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
@@ -311,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",
@@ -336,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",
@@ -470,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"
+119
View File
@@ -0,0 +1,119 @@
import StorageManager from './storageManager';
export const sections = [
'status',
'timetables',
'reality',
'packageAccess',
'stationType',
'access',
'control',
'blockades',
'signals',
'addons'
] as const;
export const initFilters = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
manual: false,
'SPK-R': false,
'SCS-R': false,
mechanical: false,
'SPK-M': false,
'SCS-M': false,
modern: false,
semaphores: false,
historical: false,
mixed: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
authors: ''
};
export const initSliders = [
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 }
];
export type StationFilter = keyof typeof initFilters;
export type StationFilterSection = (typeof sections)[number];
export const filtersSections: Record<StationFilterSection, StationFilter[]> = {
status: ['free', 'occupied', 'endingStatus', 'afkStatus', 'noSpaceStatus', 'unavailableStatus'],
timetables: ['withActiveTimetables', 'withoutActiveTimetables'],
reality: ['real', 'fictional'],
packageAccess: ['default', 'notDefault'],
stationType: ['junction', 'nonJunction'],
access: ['nonPublic', 'unavailable', 'abandoned'],
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
control: ['SPK', 'SCS', 'SPE', 'SPK-M', 'SCS-M', 'mechanical', 'SPK-R', 'SCS-R', 'manual'],
blockades: ['SBL', 'PBL'],
signals: ['modern', 'semaphores', 'mixed', 'historical']
};
export function setupFilters(currentFilters: Record<string, any>) {
if (!StorageManager.isRegistered('options_saved')) return;
Object.keys(currentFilters).forEach((filterKey) => {
const savedValue = StorageManager.getValue(filterKey);
if (savedValue != null) {
if (typeof currentFilters[filterKey] == 'boolean')
currentFilters[filterKey] = savedValue === 'true';
else if (typeof currentFilters[filterKey] == 'number')
currentFilters[filterKey] = Number(savedValue);
else currentFilters[filterKey] = savedValue.toString();
}
});
}
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
return (
Object.keys(currentFilters).filter(
(filterKey) =>
currentFilters[filterKey] !== initFilters[filterKey as keyof typeof initFilters]
) ?? []
);
}
+4
View File
@@ -34,6 +34,10 @@ export default class StorageManager {
window.localStorage.removeItem(key);
}
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false;
}
-30
View File
@@ -1,30 +0,0 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore(),
tooltipStore: useTooltipStore()
};
},
methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
if (target) this.store.modalLastClickedTarget = target;
},
closeModal() {
this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
}
}
});
+12
View File
@@ -0,0 +1,12 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getCategoryExplanation(categoryCode: string) {
const categoryKey = categoryCode.slice(0, 2);
const vehicleTypeKey = categoryCode.slice(-1);
return `${this.$t('categories.' + categoryKey)}\n(${this.$t('categories.' + vehicleTypeKey)})`;
}
}
});
+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;
-21
View File
@@ -1,21 +0,0 @@
export const headIds = [
'station',
'min-lvl',
'status',
'dispatcher',
'dispatcher-lvl',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
'timetableConfirmed'
] as const;
export type HeadIdsTypes = (typeof headIds)[number] | (typeof headIconsIds)[number];
-242
View File
@@ -1,242 +0,0 @@
import { Filter } from '../../components/StationsView/typings';
import { Status } from '../../typings/common';
import { HeadIdsTypes } from '../data/stationHeaderNames';
import { Station } from '../../typings/common';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
export const sortStations = (
a: Station,
b: Station,
sorter: { headerName: HeadIdsTypes; dir: number }
) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Filter) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
if (station.onlineInfo) {
const { dispatcherStatus } = station.onlineInfo;
const excludeEnding =
dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const excludeNotSigned =
(dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN ||
dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) &&
filters['unavailableStatus'];
const excludeAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const excludeNoSpace =
dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus'];
const excludeOccupied = filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE;
const excludeActiveTTs =
(dispatcherStatus == Status.ActiveDispatcher.FREE ||
station.onlineInfo.scheduledTrainCount.all != 0) &&
filters['withActiveTimetables'];
if (
excludeEnding ||
excludeAFK ||
excludeNoSpace ||
excludeNotSigned ||
excludeOccupied ||
excludeActiveTTs
)
return false;
if (
filters['onlineFromHours'] > 0 &&
dispatcherStatus <= Date.now() + filters['onlineFromHours'] * 3600000
)
return false;
}
const excludeNoActiveTTs =
filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0);
if (excludeNoActiveTTs) return false;
if (
(station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) &&
filters['nonPublic']
)
return false;
if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, ASDEK, authors } =
station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return false;
if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false;
if (availability == 'default' && filters['default']) return false;
if (
availability != 'default' &&
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable')
)
return false;
if (filters['real'] && lines) return false;
if (filters['fictional'] && !lines) return false;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false;
if (filters['minVmax'] > station.generalInfo.routes.maxRouteSpeed) return false;
if (filters['maxVmax'] < station.generalInfo.routes.minRouteSpeed) return false;
if (
filters['no-1track'] &&
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
)
return false;
if (
filters['no-2track'] &&
(routes.doubleElectrifiedNames.length != 0 || routes.doubleOtherNames.length != 0)
)
return false;
if (routes.singleElectrifiedNames.length < filters['minOneWayCatenary']) return false;
if (routes.singleOtherNames.length < filters['minOneWay']) return false;
if (routes.doubleElectrifiedNames.length < filters['minTwoWayCatenary']) return false;
if (routes.doubleOtherNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false;
if (filters[signalType]) return false;
if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false;
if (filters['ASDEK'] && ASDEK) return false;
if (filters['noASDEK'] && !ASDEK) return false;
if (filters['SBL'] && routes.sblNames.length > 0) return false;
if (filters['PBL'] && routes.sblNames.length == 0) return false;
if (
filters['authors'].length > 3 &&
!authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
return false;
const singleTracks = routes.single.filter((r) => !r.isInternal);
const doubleTracks = routes.double.filter((r) => !r.isInternal);
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
if (filters['junction'] && isJunction) return false;
if (filters['nonJunction'] && !isJunction) return false;
}
return true;
};
+59 -18
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,18 +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 {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
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;
@@ -84,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);
}
}
}
});
+125 -49
View File
@@ -1,9 +1,9 @@
import { defineStore } from 'pinia';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { parseSpawns } from './utils';
import {
ActiveScenery,
ScheduledTrain,
CheckpointTrain,
Station,
StationRoutes,
Status,
@@ -12,6 +12,9 @@ import {
import { useApiStore } from './apiStore';
import { MainStoreState } from './typings';
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', {
state: () =>
({
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
trainList(): Train[] {
const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online)
.map((train) => {
@@ -44,17 +50,9 @@ 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
) ?? [];
return {
trainId: train.driverName + train.trainNo.toString(),
const trainObj = {
id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo,
mass: train.mass,
@@ -79,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,
@@ -89,10 +94,65 @@ 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;
// Sceneries trains map
if (sceneriesTrains.has(train.currentStationName)) {
sceneriesTrains.set(train.currentStationName, [
...sceneriesTrains.get(train.currentStationName)!,
trainObj
]);
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
// Checkpoints trains map
if (trainObj.timetableData) {
let currentSceneryIndex = 0;
const timetablePath = trainObj.timetableData.timetablePath;
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;
});
},
@@ -107,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
@@ -120,7 +183,7 @@ export const useMainStore = defineStore('mainStore', {
return acc;
acc.push({
name: name,
name: p.stationName,
hash: '',
region: train.region,
maxUsers: 0,
@@ -131,13 +194,14 @@ export const useMainStore = defineStore('mainStore', {
dispatcherId: -1,
dispatcherExp: -1,
dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1,
isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -177,8 +241,9 @@ export const useMainStore = defineStore('mainStore', {
isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -196,37 +261,45 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains(
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
let checkpointsSet: Set<string> = new Set();
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery.name
);
// Add checkpoints to active scenery data
checkpointsSet.add(scenery.name.toLowerCase());
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
station?.generalInfo?.checkpoints.forEach((cpName) => {
checkpointsSet.add(cpName.toLowerCase());
});
scenery.scheduledTrains = scheduledTrains;
scenery.stationTrains = stationTrains;
const checkpoints = Array.from(checkpointsSet);
scenery.scheduledTrainCount = {
all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
};
scenery.stationTrains =
sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
const uniqueTrainIds: string[] = [];
checkpoints.forEach((cp) => {
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
if (!scheduledTrains) return;
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;
scenery.scheduledTrainCount.all += 1;
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
else scenery.scheduledTrainCount.unconfirmed++;
uniqueTrainIds.push(train.id);
});
});
}
return allActiveSceneries;
@@ -281,7 +354,10 @@ export const useMainStore = defineStore('mainStore', {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? []
checkpoints:
scenery.checkpoints && scenery.checkpoints.trim().length > 0
? scenery.checkpoints.split(';')
: []
}
};
});
-146
View File
@@ -1,146 +0,0 @@
import { defineStore } from 'pinia';
import inputData from '../data/options.json';
import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/stationFilterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager';
import { Filter } from '../components/StationsView/typings';
const filterInitStates: Filter = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
ręczne: false,
'ręczne+SPK': false,
'ręczne+SCS': false,
mechaniczne: false,
'mechaniczne+SPK': false,
'mechaniczne+SCS': false,
współczesna: false,
kształtowa: false,
historyczna: false,
mieszana: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
ending: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
authors: '',
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
lastClickedFilterId: ''
};
},
getters: {
areFiltersAtDefault: (state) => {
return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
},
filteredStationList: (state) => {
const store = useMainStore();
return store.allStationInfo
.filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive));
}
},
actions: {
setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name);
this.filters[option.name] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(name: string, value: any) {
this.filters[name] = value;
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
},
resetFilters() {
this.filters = { ...filterInitStates };
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
resetSectionOptions(section: string) {
this.inputs.options
.filter((option) => option.section == section)
.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == this.sorterActive.headerName)
this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.headerName = headerName;
}
}
});
+1 -1
View File
@@ -1,5 +1,5 @@
import { API } from '../typings/api';
import { Availability, StationRoutesInfo, Status } from '../typings/common';
import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export interface MainStoreState {
region: { id: string; value: string; name: string };
+1 -183
View File
@@ -1,13 +1,4 @@
import {
TrainStop,
StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2;
@@ -57,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
export function getTimestamp(date: string | null): number {
return date ? new Date(date).getTime() : 0;
}
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
let stopStatus = StopStatus.ARRIVING,
stopLabel = '',
stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = StopStatus.TERMINATED;
stopLabel = 'Skończył bieg';
stopStatusID = 5;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
stopStatus = StopStatus.DEPARTED;
stopLabel = 'Odprawiony';
stopStatusID = 2;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
stopStatus = StopStatus.DEPARTED_AWAY;
stopLabel = 'Odjechał';
stopStatusID = 4;
} else if (currentStationName == sceneryName && !stopInfo.stopped) {
stopStatus = StopStatus.ONLINE;
stopLabel = 'Na stacji';
stopStatusID = 0;
} else if (currentStationName == sceneryName && stopInfo.stopped) {
stopStatus = StopStatus.STOPPED;
stopLabel = 'Postój';
stopStatusID = 1;
} else if (currentStationName != sceneryName) {
stopStatus = StopStatus.ARRIVING;
stopLabel = 'W drodze';
stopStatusID = 3;
}
return { stopStatus, stopLabel, stopStatusID };
}
export function getCheckpointTrain(
train: Train,
trainStopIndex: number,
sceneryName: string
): ScheduledTrain {
const timetable = train.timetableData!;
const followingStops = timetable.followingStops;
const trainStop = followingStops[trainStopIndex];
const trainStopStatus = getTrainStopStatus(trainStop, train.currentStationName, sceneryName);
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex; i >= 0; i--) {
const stop = 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 = followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < followingStops.length; i++) {
const stop = 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 = followingStops[i + 1]?.arrivalLine || null;
}
}
return {
checkpointName: trainStop.stopNameRAW,
trainNo: train.trainNo,
trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nextStationName,
prevStationName,
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID,
region: train.region,
arrivingLine: arrivingLine,
departureLine: departureLine,
nextArrivalLine,
prevDepartureLine
};
}
export function getScheduledTrains(
trainList: Train[],
stationGeneralInfo: Station['generalInfo'],
stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] {
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
if (train.region != region) return acc;
const timetable = train.timetableData;
if (!timetable.sceneryNames.includes(stationName)) return acc;
const checkpoints = [stationName];
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
const checkpointScheduledTrains: ScheduledTrain[] = [];
for (let i = 0; i < timetable.followingStops.length; i++) {
if (
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
timetable.followingStops[i].stopNameRAW
)
) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
}
}
acc.push(...checkpointScheduledTrains);
return acc;
}, []) as ScheduledTrain[];
}
export function getStationTrains(
trainList: Train[],
scheduledTrainList: ScheduledTrain[],
region: string,
stationName: string
): StationTrain[] {
return trainList
.filter(
(train) =>
train?.region === region && train.online && train.currentStationName === stationName
)
.map((train) => ({
driverName: train.driverName,
driverId: train.driverId,
trainNo: train.trainNo,
trainId: train.trainId,
stopStatus:
scheduledTrainList.find((st) => st.trainNo === train.trainNo)?.stopStatus || 'no-timetable'
}));
}
+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 -7
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[];
}
}
@@ -161,7 +170,6 @@ export namespace API {
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
@@ -194,6 +202,8 @@ export namespace API {
TWR: boolean;
SKR: boolean;
sceneries: string[];
path: string;
}
}
@@ -248,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[];
@@ -317,6 +325,10 @@ export namespace API {
export namespace Donators {
export type Response = string[];
}
export namespace Vehicles {
export type Response = VehicleData[];
}
}
export namespace GithubAPI {
+75 -75
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,9 +41,16 @@ export interface RegionCounters {
timetablesCount: number;
}
export interface TimetablePathElement {
arrivalRouteExt?: string;
departureRouteExt?: string;
stationName: string;
stationHash: string;
}
export interface Train {
id: string;
trainId: string;
modalId: string;
mass: number;
length: number;
speed: number;
@@ -63,6 +72,8 @@ export interface Train {
isTimeout: boolean;
isSupporter: boolean;
driverRouteLocation: RouteLocationRaw,
timetableData?: {
timetableId: number;
category: string;
@@ -72,42 +83,37 @@ export interface Train {
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
timetablePath: TimetablePathElement[];
};
}
export interface Station {
name: string;
generalInfo?: {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
generalInfo?: StationGeneralInfo;
onlineInfo?: ActiveScenery;
}
export interface StationGeneralInfo {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
}
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
@@ -148,8 +154,8 @@ export interface ActiveScenery {
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
stationTrains: Train[];
scheduledTrains: CheckpointTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
@@ -164,49 +170,6 @@ export interface ScenerySpawn {
spawnType: ScenerySpawnType;
}
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
@@ -223,13 +186,50 @@ export interface TrainStop {
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
confirmed: number;
stopped: number;
stopTime: number | null;
}
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
: '';
+20 -32
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();
@@ -200,7 +197,7 @@ button.back-btn {
display: inline-block;
font-size: 1.5em;
font-size: 1.25em;
button {
margin: 1em auto;
@@ -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: 95vh;
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: 95vh;
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);
}
}
}
+33 -16
View File
@@ -8,20 +8,21 @@
ref="filterCardRef"
/>
<StationStats />
<button
class="btn-donation btn--image"
ref="btn"
@click="isDonationModalOpen = true"
@focus="isDonationModalOpen = false"
@click="isDonationCardOpen = true"
@focus="isDonationCardOpen = false"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
</div>
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
<StationTable @toggleDonationModal="toggleDonationModal" />
<StationStats />
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggle-donation-card="toggleDonationCard" />
</div>
</section>
</template>
@@ -30,34 +31,47 @@
import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore';
import DonationModal from '../components/Global/DonationModal.vue';
import DonationCard from '../components/Global/DonationCard.vue';
import StationStats from '../components/StationsView/StationStats.vue';
import { initFilters, setupFilters } from '../managers/stationFilterManager';
import { reactive } from 'vue';
import { provide } from 'vue';
import { ActiveSorter } from '../components/StationsView/typings';
import { onMounted } from 'vue';
const filterInitStates = { ...initFilters };
export default defineComponent({
components: {
StationTable,
StationFilterCard,
StationStats,
DonationModal
DonationCard
},
data: () => ({
filterCardOpen: false,
isDonationModalOpen: false,
isDonationCardOpen: false,
filterStore: useStationFiltersStore(),
store: useMainStore()
mainStore: useMainStore()
}),
mounted() {
this.filterStore.setupFilters();
setup() {
const filters = reactive(filterInitStates);
const activeSorter = reactive({ headerName: 'station', dir: 1 }) as ActiveSorter;
provide('StationsView_filters', filters);
provide('StationsView_activeSorter', activeSorter);
onMounted(() => {
setupFilters(filters);
});
},
methods: {
toggleDonationModal(value: boolean) {
this.isDonationModalOpen = value;
toggleDonationCard(value: boolean) {
this.isDonationCardOpen = value;
}
}
});
@@ -83,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.selectModalTrain(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

Some files were not shown because too many files have changed in this diff Show More