Compare commits

...

192 Commits

Author SHA1 Message Date
Spythere b746843086 Merge pull request #115 from Spythere/development
v1.28.4
2024-11-15 19:00:00 +01:00
Spythere cbbd06fecd bump: v1.28.4 2024-11-15 18:52:21 +01:00
Spythere 11e99b6af0 hotfix: stations sorting by 'no limit' 2024-11-15 18:51:34 +01:00
Spythere 31f4a2e5b2 Merge pull request #114 from Spythere/development
v1.28.3
2024-10-22 21:30:53 +02:00
Spythere 22514c3263 bump: v.1.28.3 2024-10-22 21:22:37 +02:00
Spythere 0df673467c fix: image loading styles 2024-10-22 21:22:22 +02:00
Spythere 6377e13809 refactor: footer component 2024-10-22 21:13:23 +02:00
Spythere 13fa633db4 chore: updated api image source 2024-10-22 20:58:20 +02:00
Spythere dd9661551c fix: typo 2024-10-22 20:58:08 +02:00
Spythere 495012a5ca Merge pull request #113 from Spythere/development
hotfix: data fetching
2024-10-02 14:59:34 +02:00
Spythere 3cfccb1bb4 hotfix: data fetching 2024-10-02 14:58:58 +02:00
Spythere d2a8cdb2b0 Merge pull request #112 from Spythere/development
v1.28.2
2024-10-02 14:31:39 +02:00
Spythere c33b5ef8c1 refactor: journal dispatcher filters 2024-10-01 16:40:11 +02:00
Spythere 52d1771c21 chore: added tn/pn filters for trains & timetables 2024-10-01 15:53:59 +02:00
Spythere cac4345683 chore: added journal timetable comments 2024-10-01 15:28:22 +02:00
Spythere 6fd9e21213 bump: v1.28.2 2024-09-30 22:33:45 +02:00
Spythere 421add1ec1 chore: added TN/PN freight types 2024-09-30 22:32:29 +02:00
Spythere 4ac92198b7 chore: api mock configuration files 2024-09-30 14:23:20 +02:00
Spythere 5105229eef chore: api mock endpoints 2024-09-30 11:50:08 +02:00
Spythere 2ec02080c3 chore: added internal api mocking for tests 2024-09-29 13:15:46 +02:00
Spythere 95b2a696e1 Merge pull request #111 from Spythere/development
fix: vehicle thumbnail names
2024-09-19 16:00:02 +02:00
Spythere 091e94e396 fix: vehicle thumbnail names 2024-09-19 15:48:55 +02:00
Spythere 43c939bf01 Merge pull request #110 from Spythere/development
v1.28.1
2024-09-18 20:09:22 +02:00
Spythere 8285d5c579 fix: tooltips width 2024-09-18 20:04:53 +02:00
Spythere 547248b478 chore: updown arrow fix 2024-09-18 16:08:54 +02:00
Spythere e1b9b37ac8 bump: v1.28.1 2024-09-18 15:57:56 +02:00
Spythere c8ec28292b chore: removed obsolete console logs 2024-09-18 15:57:45 +02:00
Spythere 3daf800a89 chore: added SBL icon & route tooltips 2024-09-18 15:55:33 +02:00
Spythere 69d9be0bb3 chore: minor thumbnail loading changes 2024-09-18 15:26:14 +02:00
Spythere f53f3a18fe Merge pull request #109 from Spythere/development
v1.28.0
2024-09-09 14:26:32 +02:00
Spythere fac8fced3e chore: removed journal list status animations 2024-09-09 14:11:35 +02:00
Spythere a3d9e68c8a chore: tooltip placing 2024-09-08 16:32:11 +02:00
Spythere b09761de58 chore: TWR & SKR badges fixes 2024-09-07 21:07:41 +02:00
Spythere 8ac2c68660 bump: v1.28.0 2024-09-07 17:28:29 +02:00
Spythere 4177c6e5f4 chore: displaying warning notes in driver view & journal timetables 2024-09-07 17:28:05 +02:00
Spythere b8f135a454 chore: thumbnail loading optimization 2024-09-06 15:23:22 +02:00
Spythere f0863b2459 chore: added static data hourly refresh 2024-09-05 17:00:15 +02:00
Spythere 55b4732992 chore: cleanup 2024-09-05 15:34:11 +02:00
Spythere 7b3dcea89e fix: missing category translations 2024-09-03 15:25:10 +02:00
Spythere f4b0c39185 chore: entry details backwards comp. 2024-09-03 15:18:59 +02:00
Spythere 275d602f97 chore: hiding entry details on history change 2024-09-03 14:43:16 +02:00
Spythere c93514fdf0 refactor: journal timetable entries 2024-09-03 14:29:59 +02:00
Spythere 0861d92e4b hotfix: class name 2024-09-02 23:41:39 +02:00
Spythere bdfd73f4be chore: stops design 2024-09-02 22:56:00 +02:00
Spythere df86364c51 chore: journal timetable stop labels 2024-09-02 22:39:41 +02:00
Spythere 631bb20c61 hotfix: resolved merge conflicts 2024-09-01 16:09:16 +02:00
Spythere bed79ed2d0 chore: translations 2024-09-01 16:07:40 +02:00
Spythere 2a07471e12 chore: displaying other driver's trains in the driver view 2024-08-31 13:51:22 +02:00
Spythere cfe188d0dc bump: v1.27.1 2024-08-29 16:04:14 +02:00
Spythere d9865be83e fix: views viewport height 2024-08-29 16:03:26 +02:00
Spythere 9155fd9f8d chore: driver view return button 2024-08-29 15:51:53 +02:00
Spythere 4674bf886e fix: manifest theme color 2024-08-29 15:47:23 +02:00
Spythere 289fd310df chore: viewport & routing fixes 2024-08-29 15:42:44 +02:00
Spythere b04797052f chore: expanded containers width in views, adjusted dropdowns 2024-08-24 16:16:32 +02:00
Spythere 7079f20791 Merge pull request #107 from Spythere/development
hotfix: journal stats badge styles
2024-08-24 00:25:35 +02:00
Spythere c244275aee hotfix: journal stats badge styles 2024-08-24 00:24:55 +02:00
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
122 changed files with 12934 additions and 16956 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 name: Deploy to Firebase Hosting on merge
'on': 'on':
push: push:
@@ -11,10 +8,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: npm ci && npm run build - run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0 - uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}' firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_STACJOWNIK_TD2 }}'
channelId: live channelId: live
projectId: stacjownik-td2 projectId: stacjownik-td2
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- run: npm ci && npm run build - run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0 - uses: FirebaseExtended/action-hosting-deploy@v0
with: with:
repoToken: '${{ secrets.GITHUB_TOKEN }}' repoToken: '${{ secrets.GITHUB_TOKEN }}'
+5 -1
View File
@@ -33,6 +33,10 @@ node_modules
# Env # Env
.env .env
.env.*
.fake .fake
.ionide .ionide
# api-mock
/api-mock/endpoints/
+33
View File
@@ -0,0 +1,33 @@
import { existsSync } from 'fs';
import { mkdir, writeFile } from 'fs/promises';
async function fetchJSONEndpointData(url, fileName) {
try {
const res = await fetch(url);
const data = await res.json();
await writeFile(`./endpoints/${fileName}`, JSON.stringify(data));
return true;
} catch (error) {
console.error(error);
}
return false;
}
async function main() {
if (!existsSync('endpoints')) await mkdir('endpoints');
Promise.all(
['getActiveData', 'getDonators', 'getSceneries', 'getVehicles'].map((endpointName) =>
fetchJSONEndpointData(
`https://stacjownik.spythere.eu/api/${endpointName}`,
`${endpointName}.json`
)
)
).then(() => {
console.log('Endpoints downloaded!');
});
}
main();
+28
View File
@@ -0,0 +1,28 @@
import express from 'express';
import path from 'path';
import { cwd } from 'process';
import cors from 'cors';
const app = express();
app.use(cors());
app.get('/api/getActiveData', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getActiveData.json'));
});
app.get('/api/getSceneries', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
});
app.get('/api/getVehicles', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getVehicles.json'));
});
app.get('/api/getDonators', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getDonators.json'));
});
app.listen(3123, () => {
console.log('Mocking API server...');
});
+18
View File
@@ -0,0 +1,18 @@
{
"name": "api-mock",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"fetch": "node fetchEndpoints.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.3"
}
}
+481
View File
@@ -0,0 +1,481 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
dependencies:
mime-types "~2.1.34"
negotiator "0.6.3"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
body-parser@1.20.3:
version "1.20.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6"
integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==
dependencies:
bytes "3.1.2"
content-type "~1.0.5"
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
http-errors "2.0.0"
iconv-lite "0.4.24"
on-finished "2.4.1"
qs "6.13.0"
raw-body "2.5.2"
type-is "~1.6.18"
unpipe "1.0.0"
bytes@3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9"
integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
content-disposition@0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
dependencies:
safe-buffer "5.2.1"
content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cors@^2.8.5:
version "2.8.5"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
define-data-property@^1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e"
integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==
dependencies:
es-define-property "^1.0.0"
es-errors "^1.3.0"
gopd "^1.0.1"
depd@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
encodeurl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
es-define-property@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845"
integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==
dependencies:
get-intrinsic "^1.2.4"
es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
express@^4.18.3:
version "4.21.0"
resolved "https://registry.yarnpkg.com/express/-/express-4.21.0.tgz#d57cb706d49623d4ac27833f1cbc466b668eb915"
integrity sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.3"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.3.1"
fresh "0.5.2"
http-errors "2.0.0"
merge-descriptors "1.0.3"
methods "~1.1.2"
on-finished "2.4.1"
parseurl "~1.3.3"
path-to-regexp "0.1.10"
proxy-addr "~2.0.7"
qs "6.13.0"
range-parser "~1.2.1"
safe-buffer "5.2.1"
send "0.19.0"
serve-static "1.16.2"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "~1.6.18"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.1.tgz#0c575f1d1d324ddd1da35ad7ece3df7d19088019"
integrity sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==
dependencies:
debug "2.6.9"
encodeurl "~2.0.0"
escape-html "~1.0.3"
on-finished "2.4.1"
parseurl "~1.3.3"
statuses "2.0.1"
unpipe "~1.0.0"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
get-intrinsic@^1.1.3, get-intrinsic@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd"
integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
has-proto "^1.0.1"
has-symbols "^1.0.3"
hasown "^2.0.0"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==
dependencies:
get-intrinsic "^1.1.3"
has-property-descriptors@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854"
integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==
dependencies:
es-define-property "^1.0.0"
has-proto@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd"
integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==
has-symbols@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
dependencies:
depd "2.0.0"
inherits "2.0.4"
setprototypeof "1.2.0"
statuses "2.0.1"
toidentifier "1.0.1"
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
merge-descriptors@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
ms@2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.13.1:
version "1.13.2"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff"
integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
path-to-regexp@0.1.10:
version "0.1.10"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.10.tgz#67e9108c5c0551b9e5326064387de4763c4d5f8b"
integrity sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==
proxy-addr@~2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
dependencies:
forwarded "0.2.0"
ipaddr.js "1.9.1"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
dependencies:
side-channel "^1.0.6"
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.4.24"
unpipe "1.0.0"
safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.19.0.tgz#bbc5a388c8ea6c048967049dbeac0e4a3f09d7f8"
integrity sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==
dependencies:
debug "2.6.9"
depd "2.0.0"
destroy "1.2.0"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "2.0.0"
mime "1.6.0"
ms "2.1.3"
on-finished "2.4.1"
range-parser "~1.2.1"
statuses "2.0.1"
serve-static@1.16.2:
version "1.16.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.2.tgz#b6a5343da47f6bdd2673848bf45754941e803296"
integrity sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==
dependencies:
encodeurl "~2.0.0"
escape-html "~1.0.3"
parseurl "~1.3.3"
send "0.19.0"
set-function-length@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449"
integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==
dependencies:
define-data-property "^1.1.4"
es-errors "^1.3.0"
function-bind "^1.1.2"
get-intrinsic "^1.2.4"
gopd "^1.0.1"
has-property-descriptors "^1.0.2"
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2"
integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==
dependencies:
call-bind "^1.0.7"
es-errors "^1.3.0"
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
toidentifier@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
type-is@~1.6.18:
version "1.6.18"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.24"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
-1
View File
@@ -19,7 +19,6 @@
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#222222" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
+457 -1761
View File
File diff suppressed because it is too large Load Diff
+16 -17
View File
@@ -1,11 +1,15 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.24.1", "version": "1.28.4",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --mode staging",
"dev:mock": "vite --mode development & yarn --cwd ./api-mock start",
"dev:fetch": "yarn --cwd ./api-mock fetch",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"deploy": "yarn build && firebase deploy --only hosting", "deploy:prod": "yarn build && firebase deploy --only hosting",
"deploy:dev": "yarn build && firebase hosting:channel:deploy dev --expires 7d",
"preview": "yarn build && vite preview", "preview": "yarn build && vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
@@ -19,25 +23,20 @@
"showdown": "^2.1.0", "showdown": "^2.1.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1", "vue-i18n": "^9.4.1",
"vue-router": "^4.2.4" "vue-router": "^4.4.0"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3", "@types/node": "^20.14.12",
"@types/node": "^20.6.2",
"@types/showdown": "^2.0.6", "@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^0.2.4", "@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^5.1.0",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/tsconfig": "^0.5.1",
"@vue/eslint-config-typescript": "^12.0.0", "axios": "^1.7.2",
"@vue/tsconfig": "^0.4.0", "prettier": "^3.3.3",
"axios": "^1.5.0", "typescript": "^5.5.4",
"eslint": "^8.49.0", "vite": "^5.3.4",
"eslint-plugin-vue": "^9.17.0",
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.20.0", "vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11" "vue-tsc": "^2.0.28"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
+3
View File
@@ -0,0 +1,3 @@
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M82.7143 61.3284L118.429 7L22 74.9104H68.4286L36.2857 137L122 61.3284H82.7143Z" fill="#FFF500"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="144" height="144" viewBox="0 0 144 144" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9.20437" y="2.4661" width="36.5457" height="57.8287" rx="16.7449" transform="matrix(0.869001 -0.494811 0.505207 0.862998 50.006 87.4256)" stroke="white" stroke-width="13.3959"/>
<rect x="9.20437" y="2.4661" width="36.5457" height="57.8287" rx="16.7449" transform="matrix(0.869001 -0.494811 0.505207 0.862998 14.9599 29.6039)" stroke="white" stroke-width="13.3959"/>
<path d="M65.1133 58.145L79.8524 84.3103" stroke="white" stroke-width="10.0469" stroke-linecap="round" stroke-linejoin="bevel"/>
</svg>

After

Width:  |  Height:  |  Size: 611 B

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

+1 -1
View File
@@ -13,7 +13,7 @@
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffc014", "theme_color": "#4d4d4d",
"background_color": "#4d4d4d", "background_color": "#4d4d4d",
"display": "standalone", "display": "standalone",
"start_url": "." "start_url": "."
+27 -49
View File
@@ -1,18 +1,12 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal <UpdateCard
:update-modal-open="isUpdateModalOpen" :is-update-card-open="isUpdateCardOpen"
@toggle-modal="() => (isUpdateModalOpen = false)" @toggle-card="() => (isUpdateCardOpen = false)"
/> />
<Tooltip /> <Tooltip />
<transition name="modal-anim">
<keep-alive>
<TrainModal />
</keep-alive>
</transition>
<AppHeader :current-lang="currentLang" @change-lang="changeLang" /> <AppHeader :current-lang="currentLang" @change-lang="changeLang" />
<main class="app_main"> <main class="app_main">
@@ -23,21 +17,12 @@
</router-view> </router-view>
</main> </main>
<footer class="app_footer"> <AppFooter
&copy; :version="VERSION"
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> :is-on-production-host="isOnProductionHost"
{{ new Date().getUTCFullYear() }} | :is-update-card-open="isUpdateCardOpen"
<button class="btn--text" @click="() => (isUpdateModalOpen = true)"> @open-update-card="() => (isUpdateCardOpen = true)"
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }} />
</button>
<br />
<a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
</a>
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer>
</div> </div>
</template> </template>
@@ -45,7 +30,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { version } from '.././package.json'; import { version } from '../package.json';
import { Status } from './typings/common'; import { Status } from './typings/common';
import { useMainStore } from './store/mainStore'; import { useMainStore } from './store/mainStore';
import { useApiStore } from './store/apiStore'; import { useApiStore } from './store/apiStore';
@@ -54,11 +39,11 @@ import { useTooltipStore } from './store/tooltipStore';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.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'; import StorageManager from './managers/storageManager';
import AppFooter from './components/App/AppFooter.vue';
const STORAGE_VERSION_KEY = 'app_version'; const STORAGE_VERSION_KEY = 'app_version';
@@ -67,8 +52,8 @@ export default defineComponent({
Clock, Clock,
StatusIndicator, StatusIndicator,
AppHeader, AppHeader,
TrainModal, AppFooter,
UpdateModal, UpdateCard,
Tooltip Tooltip
}, },
@@ -78,12 +63,10 @@ export default defineComponent({
apiStore: useApiStore(), apiStore: useApiStore(),
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
isUpdateModalOpen: false, isUpdateCardOpen: false,
currentLang: 'pl', currentLang: 'pl',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
nextUpdateTime: 0
}), }),
created() { created() {
@@ -92,26 +75,18 @@ export default defineComponent({
async mounted() { async mounted() {
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e)); window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
window.addEventListener('mousedown', () => this.tooltipStore.hide());
}, },
methods: { methods: {
init() { init() {
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
this.loadLang(); this.loadLang();
this.setupOfflineHandling(); this.setupOfflineHandling();
this.checkAppVersion(); this.checkAppVersion();
this.apiStore.setupAPIData(); this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
update(t: number) {
if (t >= this.nextUpdateTime) {
this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.update);
}, },
async checkAppVersion() { async checkAppVersion() {
@@ -130,8 +105,9 @@ export default defineComponent({
releaseURL: releaseData.html_url releaseURL: releaseData.html_url
}; };
this.isUpdateModalOpen = this.isUpdateCardOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test'; (storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) { } catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`); console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
} }
@@ -157,6 +133,7 @@ export default defineComponent({
handleOnlineMode() { handleOnlineMode() {
this.store.isOffline = false; this.store.isOffline = false;
this.apiStore.dataStatuses.connection = Status.Data.Loading;
this.apiStore.connectToAPI(); this.apiStore.connectToAPI();
}, },
@@ -180,7 +157,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString(); const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) { if (naviLanguage.startsWith('en')) {
this.changeLang('en'); this.changeLang('en');
return; return;
} }
@@ -210,7 +187,7 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
@include smallScreen() { @include smallScreen() {
font-size: calc(0.65rem + 0.8vw); font-size: calc(0.65rem + 0.85vw);
} }
@include screenLandscape() { @include screenLandscape() {
@@ -226,6 +203,7 @@ export default defineComponent({
min-height: 100vh; min-height: 100vh;
overflow: hidden; overflow: hidden;
position: relative;
} }
.app_main { .app_main {
+41
View File
@@ -0,0 +1,41 @@
<template>
<footer class="app_footer">
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<button class="btn--text" @click="openUpdateCard">
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
<br />
<a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
</a>
<div style="display: none">&int; ukryta taktyczna całka do programowania w HTMLu</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['openUpdateCard'],
props: {
isUpdateCardOpen: {
type: Boolean,
required: true
},
version: String,
isOnProductionHost: Boolean
},
methods: {
openUpdateCard() {
this.$emit('openUpdateCard');
}
}
});
</script>
<style scoped></style>
@@ -1,12 +1,12 @@
<template> <template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)"> <Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="modal-content"> <div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1> <h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div> <div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</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') }} {{ $t('update.confirm') }}
</button> </button>
@@ -16,7 +16,7 @@
<span v-html="$t('update.info-2')"></span> <span v-html="$t('update.info-2')"></span>
</p> </p>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -25,21 +25,21 @@ import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { Converter } from 'showdown'; import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue'; import Card from '../Global/Card.vue';
const converter = new Converter(); const converter = new Converter();
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
updateModalOpen: { isUpdateCardOpen: {
type: Boolean, type: Boolean,
required: true required: true
} }
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
data() { data() {
return { return {
@@ -49,7 +49,7 @@ export default defineComponent({
}, },
watch: { watch: {
updateModalOpen(val: boolean) { isUpdateCardOpen(val: boolean) {
this.$nextTick(() => { this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus(); if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
}); });
@@ -60,37 +60,38 @@ export default defineComponent({
htmlChangelog() { htmlChangelog() {
if (this.mainStore.appUpdate == null) return ''; if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog); return converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
} }
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) { ::v-deep(h1) {
text-align: center; text-align: center;
color: $accentCol;
} }
::v-deep(h2) { ::v-deep(h2) {
padding: 0.25em 0; padding: 0.25em 0;
border-bottom: 1px solid #aaa;
} }
::v-deep(ul) { ::v-deep(ul) {
list-style: inside; list-style: initial;
padding: 0.5em; padding: 1em;
line-height: 1.5em; line-height: 1.5em;
} }
.modal-content { .content {
display: grid; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
gap: 0.5em; gap: 0.5em;
@@ -98,6 +99,7 @@ export default defineComponent({
min-height: 700px; min-height: 700px;
overflow: auto; overflow: auto;
text-align: justify; text-align: justify;
max-width: 700px;
} }
.no-features { .no-features {
@@ -1,11 +1,10 @@
<template> <template>
<transition name="modal-anim" tag="div"> <transition name="modal-anim" tag="div">
<div class="modal" v-if="isOpen"> <div class="card" v-if="isOpen">
<div class="modal-background" @click="toggleModal(false)"></div> <div class="card-background" @click="toggleCard(false)"></div>
<div class="modal-wrapper" ref="wrapper" tabindex="0"> <div class="card-body" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div> </div>
</transition> </transition>
</template> </template>
@@ -15,7 +14,7 @@ import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
export default defineComponent({ export default defineComponent({
emits: ['toggleModal'], emits: ['toggleCard'],
props: { props: {
isOpen: Boolean isOpen: Boolean
@@ -36,8 +35,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
} }
} }
}); });
@@ -46,17 +45,21 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal { .card {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100vh; height: 100%;
z-index: 200; z-index: 200;
display: flex;
justify-content: center;
align-items: center;
} }
.modal-background { .card-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -68,21 +71,23 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.modal-wrapper { .card-body {
position: absolute; position: relative;
top: 50%;
left: 50%; margin: 1em;
transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
max-height: 95vh; max-height: 95vh;
max-height: 95dvh;
& > :slotted(div) { background-color: #1a1a1a;
background-color: #1a1a1a; box-shadow: 0 0 15px 10px #0e0e0e;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw; overflow: auto;
max-width: 850px; }
@include smallScreen {
.card {
align-items: flex-start;
} }
} }
</style> </style>
@@ -1,12 +1,7 @@
<template> <template>
<AnimatedModal <Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
class="donation-modal" <div class="body">
:isOpen="isModalOpen" <div class="content">
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1> <h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0"> <div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span> <span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,9 +56,9 @@
</i> </i>
</div> </div>
<div class="modal_actions"> <div class="actions">
<a <a
class="modal-action a-button btn--image coffee" class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere" href="https://buycoffee.to/spythere"
target="_blank" target="_blank"
ref="action" ref="action"
@@ -73,7 +68,7 @@
</a> </a>
<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" href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank" target="_blank"
> >
@@ -81,30 +76,30 @@
{{ $t('donations.action-paypal') }} {{ $t('donations.action-paypal') }}
</a> </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" /> <img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }} {{ $t('donations.action-exit') }}
</button> </button>
</div> </div>
</div> </div>
</AnimatedModal> </Card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { Card },
props: { props: {
isModalOpen: Boolean isCardOpen: Boolean
}, },
emits: ['toggleModal'], emits: ['toggleCard'],
watch: { watch: {
isModalOpen(val: boolean) { isCardOpen(val: boolean) {
this.running = val; this.running = val;
this.lastUpdate = Date.now(); this.lastUpdate = Date.now();
@@ -138,8 +133,8 @@ export default defineComponent({
}, },
methods: { methods: {
toggleModal(value: boolean) { toggleCard(value: boolean) {
this.$emit('toggleModal', value); this.$emit('toggleCard', value);
}, },
runUpdate() { runUpdate() {
@@ -157,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.modal_content { .body {
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
gap: 1em; gap: 1em;
font-size: 1.1em; font-size: 1.1em;
& > div { max-width: 820px;
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
} }
.modal_main { .content {
overflow: auto; overflow: auto;
overflow-x: hidden; overflow-x: hidden;
padding: 1em;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
} }
.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; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em; gap: 0.5em;
padding: 1em;
form button { form button {
width: 100%; width: 100%;
} }
} }
.modal_actions > .modal-action { .actions > .action {
&.paypal { &.paypal {
$btnColor: #254069; $btnColor: #254069;
-1
View File
@@ -43,7 +43,6 @@ export default defineComponent({
width: 6em; width: 6em;
height: 1em; height: 1em;
margin: 0.5em 0;
.bar-fg, .bar-fg,
.bar-bg { .bar-bg {
+5 -4
View File
@@ -88,8 +88,9 @@ $unknown: #b93c3c;
.status-badge { .status-badge {
border-radius: 1em; border-radius: 1em;
font-weight: 500; font-weight: 500;
text-wrap: nowrap;
padding: 0.2em 0.55em; padding: 0.2rem 0.55rem;
background-color: $online; background-color: $online;
@@ -106,13 +107,13 @@ $unknown: #b93c3c;
&.no-limit { &.no-limit {
background-color: $no-limit; background-color: $no-limit;
font-size: 0.85em; font-size: 0.9em;
} }
&.not-signed, &.not-signed,
&.unavailable { &.unavailable {
background-color: $unav; background-color: $unav;
font-size: 0.85em; font-size: 0.9em;
} }
&.afk { &.afk {
@@ -125,7 +126,7 @@ $unknown: #b93c3c;
background-color: $no-space; background-color: $no-space;
border: 1px solid white; border: 1px solid white;
color: white; color: white;
font-size: 0.85em; font-size: 0.9em;
} }
&.unknown, &.unknown,
+113 -126
View File
@@ -1,84 +1,13 @@
<template> <template>
<div class="stock-list"> <div class="list-wrapper">
<div v-if="tractionOnly"> <ul class="stock-list">
<p> <li v-for="({ images, imagesFallbacks, vehicleString }, i) in thumbnailNames">
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }} <VehicleThumbnail
{{ computedStockList[0].split(':')[1] }} :key="i"
</p> :vehicle-string="vehicleString"
:images="images"
<img :image-fallbacks="imagesFallbacks"
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>
<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"
/>
<!-- /// 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> </li>
</ul> </ul>
</div> </div>
@@ -87,8 +16,11 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import VehicleThumbnail from './VehicleThumbnail.vue';
export default defineComponent({ export default defineComponent({
components: { VehicleThumbnail },
props: { props: {
trainStockList: { trainStockList: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
@@ -109,71 +41,126 @@ export default defineComponent({
computed: { computed: {
computedStockList() { computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList; return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
}
},
methods: {
getVehicleThumbnailURL(locoType: string, suffix?: string) {
return `https://static.spythere.eu/thumbnails/${locoType}${suffix}.png`;
}, },
onImageError(event: Event, stockName: string) { thumbnailNames() {
let fallbackName = ''; return (this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList)
.filter((v) => v.length != 0)
.map((vehicleString) => {
const [vehicleName] = vehicleString.split(':');
const isLoco = /.-\d{3}/.test(stockName); const vehicleThumbnailData = {
images: [] as string[],
imagesFallbacks: [] as string[],
vehicleName,
vehicleString
};
if (isLoco) { // Generowanie członów EN57
if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt'; if (vehicleName.startsWith('EN57')) {
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt'; vehicleThumbnailData['images'] = [
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e'; vehicleName + 'ra',
else fallbackName = 'loco-s'; vehicleName + 's',
} else { vehicleName + 'rb'
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName); ];
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('+');
fallbackName += 'car-'; vehicleThumbnailData['images'] = [
fallbackName += isCarPassenger ? 'passenger' : 'cargo'; `EN57-${firstVehicleNumber}ra`,
} `EN57-${firstVehicleNumber}s`,
`EN57-${firstVehicleNumber}rb`,
`EN57-${secondVehicleNumber}ra`,
`EN57-${secondVehicleNumber}s`,
`EN57-${secondVehicleNumber}rb`
];
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`; 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;
});
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.stock-list {
.list-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
.stock-list ul { .stock-list {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
overflow: auto; overflow: auto;
margin: 0 auto; margin: 0 auto;
padding: 1em 0;
}
ul > li > span {
display: flex;
align-items: flex-end;
cursor: crosshair;
}
img {
max-height: 60px;
width: auto;
height: auto;
}
img.traction-only {
max-width: 100%;
}
p {
text-align: center;
color: #aaa;
font-size: 0.95em;
margin-bottom: 1em;
} }
</style> </style>
@@ -0,0 +1,81 @@
<template>
<div class="vehicle-thumbnail" :data-load-status="imgStatus" ref="thumbRef">
<div class="stock-text">
<div>{{ vehicleName }}</div>
<small v-if="vehicleCargo">({{ vehicleCargo }})</small>
</div>
<div class="stock-images">
<img
v-for="(thumbnailImage, imageIndex) in images"
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
height="60"
loading="lazy"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="vehicleString"
@error="onImageError($event, imageFallbacks[imageIndex])"
@load="onImageLoad"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, PropType, Ref, ref } from 'vue';
const props = defineProps({
vehicleString: { type: String, required: true },
images: { type: Object as PropType<string[]>, required: true },
imageFallbacks: { type: Object as PropType<string[]>, required: true }
});
const thumbRef = ref(null) as Ref<HTMLElement | null>;
const imgStatus = ref('loading');
const vehicleName = computed(() => props.vehicleString.split(':')[0].replace(/_/g, ' '));
const vehicleCargo = computed(() => props.vehicleString.split(':')[1]);
function onImageError(event: Event, fallbackImage: string) {
(event.target as HTMLImageElement).src = `/images/${fallbackImage}.png`;
imgStatus.value = 'error';
}
function onImageLoad() {
if (imgStatus.value != 'error') {
imgStatus.value = 'loaded';
}
if (thumbRef.value) thumbRef.value.style.opacity = '1';
}
</script>
<style lang="scss" scoped>
.vehicle-thumbnail {
position: relative;
opacity: 0;
transition: opacity 100ms ease-in-out;
&[data-load-status='loading'] {
min-height: 60px;
min-width: 200px;
}
}
.stock-text {
text-align: center;
color: #aaa;
font-size: 0.9em;
margin-bottom: 0.25em;
padding: 0.25em 0;
}
.stock-images {
display: flex;
justify-content: center;
align-items: flex-end;
cursor: crosshair;
padding: 0.5em 0;
}
</style>
@@ -1,29 +1,28 @@
<template> <template>
<section class="daily-stats"> <section class="daily-stats">
<span :data-active="statsStatus"> <span :data-active="statsStatus">
<span class="stats-list"> <h3>
<h3> {{ $t('journal.daily-stats.title') }}
{{ $t('journal.daily-stats.title') }} <b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b> </h3>
</h3>
<hr class="header-separator" /> <hr class="header-separator" />
<b v-if="statsStatus == Status.Data.Loading"> <b v-if="statsStatus == Status.Data.Loading">
{{ $t('app.loading') }} {{ $t('app.loading') }}
</b> </b>
<b class="text--error" v-else-if="statsStatus == Status.Data.Error"> <b class="text--error" v-else-if="statsStatus == Status.Data.Error">
{{ $t('journal.stats-error') }} {{ $t('journal.stats-error') }}
</b> </b>
<b v-else-if="topDispatchers.length == 0"> <b v-else-if="topDispatchers.length == 0">
{{ $t('journal.daily-stats.info') }} {{ $t('journal.daily-stats.info') }}
</b> </b>
<div v-else> <div v-else>
<div v-if="stats.totalTimetables"> <ul class="stats-list">
&bull; <li v-if="stats.totalTimetables">
<i18n-t keypath="journal.daily-stats.total"> <i18n-t keypath="journal.daily-stats.total">
<template #count> <template #count>
<b class="text--primary"> <b class="text--primary">
@@ -36,10 +35,9 @@
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b> <b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
<div v-if="stats.maxTimetable"> <li v-if="stats.maxTimetable">
&bull;
<i18n-t keypath="journal.daily-stats.longest"> <i18n-t keypath="journal.daily-stats.longest">
<template #id> <template #id>
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.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> <b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
<div v-if="topDispatchers.length == 1"> <li v-if="topDispatchers.length == 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr"> <i18n-t keypath="journal.daily-stats.most-active-dr">
<template #dispatcher> <template #dispatcher>
<router-link <router-link
@@ -79,10 +76,9 @@
</b> </b>
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
<div v-if="topDispatchers.length > 1"> <li v-if="topDispatchers.length > 1">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-dr-many"> <i18n-t keypath="journal.daily-stats.most-active-dr-many">
<template #dispatchers> <template #dispatchers>
<span v-for="(disp, i) in topDispatchers" :key="i"> <span v-for="(disp, i) in topDispatchers" :key="i">
@@ -103,10 +99,9 @@
</b> </b>
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
<div v-if="stats.longestDuties.length > 0"> <li v-if="stats.longestDuties.length > 0">
&bull;
<i18n-t keypath="journal.daily-stats.longest-duties"> <i18n-t keypath="journal.daily-stats.longest-duties">
<template #dispatcher> <template #dispatcher>
<router-link <router-link
@@ -122,10 +117,9 @@
{{ calculateDuration(stats.longestDuties[0].duration) }} {{ calculateDuration(stats.longestDuties[0].duration) }}
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
<div v-if="stats.mostActiveDrivers.length > 0"> <li v-if="stats.mostActiveDrivers.length > 0">
&bull;
<i18n-t keypath="journal.daily-stats.most-active-driver"> <i18n-t keypath="journal.daily-stats.most-active-driver">
<template #driver> <template #driver>
<router-link <router-link
@@ -138,30 +132,30 @@
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b> <b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
</template> </template>
</i18n-t> </i18n-t>
</div> </li>
</ul>
<hr class="section-separator" /> <hr class="section-separator" />
<div class="stats-badges"> <div class="stats-badges">
<span <span
class="stat-badge" class="badge stat-badge"
v-for="key in [ v-for="key in [
'rippedSwitches', 'rippedSwitches',
'derailments', 'derailments',
'skippedStopSignals', 'skippedStopSignals',
'radioStops', 'radioStops',
'kills' 'kills'
]" ]"
:key="key" :key="key"
> >
<span>{{ $t(`journal.daily-stats.${key}`) }}</span> <span>{{ $t(`journal.daily-stats.${key}`) }}</span>
<span>{{ <span>
Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' {{ Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' }}
}}</span>
</span> </span>
</div> </span>
</div> </div>
</span> </div>
</span> </span>
</section> </section>
</template> </template>
@@ -178,7 +172,6 @@ export default defineComponent({
name: 'journal-daily-stats', name: 'journal-daily-stats',
mixins: [dateMixin], mixins: [dateMixin],
// emits: ['toggleStatsOpen'],
data() { data() {
return { return {
@@ -193,7 +186,6 @@ export default defineComponent({
activated() { activated() {
this.startFetchingDailyStats(); this.startFetchingDailyStats();
// this.$emit('toggleStatsOpen', true);
}, },
deactivated() { deactivated() {
@@ -249,14 +241,24 @@ export default defineComponent({
.daily-stats { .daily-stats {
text-align: left; text-align: left;
} }
.daily-stats > span[data-active='0'] { .daily-stats > span[data-active='0'] {
opacity: 0.75; opacity: 0.75;
} }
ul.stats-list {
list-style: disc;
padding: 0 1em;
}
.stats-list a { .stats-list a {
text-decoration: underline; text-decoration: underline;
} }
.stats-list > li {
margin: 0.25em 0;
}
.stats-badges { .stats-badges {
display: flex; display: flex;
flex-wrap: wrap; 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" /> <hr class="header-separator" />
<div class="info-stats"> <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>{{ $t('journal.dispatcher-stats.services-count') }}</span>
<span>{{ stats.services.count }}</span> <span>{{ stats.services.count }}</span>
</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>{{ $t('journal.dispatcher-stats.service-max') }}</span>
<span>{{ calculateDuration(stats.services.durationMax) }}</span> <span>{{ calculateDuration(stats.services.durationMax) }}</span>
</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>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
<span>{{ calculateDuration(stats.services.durationAvg) }}</span> <span>{{ calculateDuration(stats.services.durationAvg) }}</span>
</span> </span>
@@ -35,22 +35,22 @@
<hr class="section-separator" /> <hr class="section-separator" />
<div class="info-stats"> <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>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
<span>{{ stats.issuedTimetables.count }}</span> <span>{{ stats.issuedTimetables.count }}</span>
</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>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span> <span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
</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>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span> <span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
</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>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span> <span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
</span> </span>
@@ -1,140 +1,59 @@
<template> <template>
<transition name="status-anim" mode="out-in"> <div>
<div :key="dataStatus"> <div class="journal_warning" v-if="store.isOffline">
<div class="journal_warning" v-if="store.isOffline"> {{ $t('app.offline') }}
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $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>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
</div> </div>
</transition>
<Loading v-else-if="dataStatus == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }}
</div>
<div class="journal_warning" v-else-if="dispatcherHistory.length == 0">
{{ $t('app.no-result') }}
</div>
<div v-else>
<transition-group name="list-anim" class="journal-list" tag="ul">
<JournalDispatcherEntry
v-for="entry in dispatcherHistory"
:key="entry.id"
:entry="entry"
:onToggleShowExtraInfo="toggleExtraInfo"
:showExtraInfo="extraInfoIndexes.includes(entry.id)"
/>
</transition-group>
<AddDataButton
:list="dispatcherHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
<div class="journal_warning" v-if="scrollNoMoreData">
{{ $t('journal.no-further-data') }}
</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }}
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { regions } from '../../../data/options.json';
import { useMainStore } from '../../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
import { Status } from '../../../typings/common'; import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin'; import JournalDispatcherEntry from './JournalDispatcherEntry.vue';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
components: { Loading, AddDataButton }, components: { Loading, AddDataButton, JournalDispatcherEntry },
mixins: [dateMixin, styleMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
@@ -159,99 +78,32 @@ export default defineComponent({
return { return {
Status, Status,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(),
regions extraInfoIndexes: [] as number[]
}; };
}, },
computed: { watch: {
computedDispatcherHistory() { '$route.query': {
return this.dispatcherHistory.reduce( deep: true,
(acc, historyItem, i) => { handler() {
if (this.isAnotherDay(i - 1, i)) this.extraInfoIndexes.length = 0;
acc.push(new Date(historyItem.timestampFrom).toLocaleDateString('pl-PL')); }
acc.push(historyItem);
return acc;
},
[] as (API.DispatcherHistory.Data | string)[]
);
} }
}, },
methods: { methods: {
navigateToScenery(name: string, isOnline: boolean) { toggleExtraInfo(id: number) {
if (!isOnline) return; const existingIdx = this.extraInfoIndexes.indexOf(id);
this.$router.push(`/scenery?station=${name.trim().replace(/ /g, '_')}`); if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
}, else this.extraInfoIndexes.push(id);
isAnotherDay(prevIndex: number, currIndex: number) {
if (currIndex == 0) return true;
return (
new Date(this.dispatcherHistory[prevIndex].timestampFrom).getDate() !=
new Date(this.dispatcherHistory[currIndex].timestampFrom).getDate()
);
} }
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/animations.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
@import '../../../styles/variables.scss'; @import '../../../styles/variables.scss';
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection.scss';
table.dispatchers-table {
--_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> </style>
+10 -3
View File
@@ -30,7 +30,11 @@
</div> </div>
<transition name="dropdown-anim"> <transition name="dropdown-anim">
<div class="dropdown_wrapper" v-if="currentStatsTab !== null"> <div
class="dropdown_wrapper"
:class="{ 'dropdown-align-right': true }"
v-if="currentStatsTab !== null"
>
<keep-alive> <keep-alive>
<component :is="currentStatsTab" :key="currentStatsTab"></component> <component :is="currentStatsTab" :key="currentStatsTab"></component>
</keep-alive> </keep-alive>
@@ -79,7 +83,10 @@ export default defineComponent({
@import '../../styles/dropdown_filters.scss'; @import '../../styles/dropdown_filters.scss';
@import '../../styles/variables.scss'; @import '../../styles/variables.scss';
.dropdown_wrapper { .dropdown_wrapper.dropdown-align-right {
max-width: 100%; left: auto;
right: 0;
max-width: 700px;
// max-width: 100%;
} }
</style> </style>
@@ -0,0 +1,270 @@
<template>
<div>
<div class="details-actions">
<button class="btn--action" @click="toggleExtraInfo">
<b>{{ $t('journal.entry-details') }}</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="showExtraInfo">
<div class="g-separator"></div>
<EntryStops :timetable="timetable" />
<div class="g-separator"></div>
<div class="stock-specs">
<span class="badge" v-if="timetable.authorName">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
<span class="badge" v-if="timetable.maxSpeed">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge" v-if="timetable.stockLength">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge" v-if="timetable.stockMass">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<div class="stock-dangers" v-if="timetable.twr || timetable.skr">
<div class="g-separator"></div>
<b>{{ $t('journal.stock-dangers') }}:</b>
<ul>
<li v-if="timetable.twr">
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
</li>
<li v-if="timetable.skr">
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
</li>
<li v-if="timetable.hasDangerousCargo">
<b class="text--primary">{{ $t('warnings.TN') }}</b>
</li>
<li v-if="timetable.hasExtraDeliveries">
<b class="text--primary">{{ $t('warnings.PN') }}</b>
</li>
</ul>
<div class="dangers-notes" v-if="timetable.warningNotes">
<h4>{{ $t('warnings.header-title') }}</h4>
<p>
<i>{{ timetable.warningNotes }}</i>
</p>
</div>
</div>
<!-- Historia zmian w składzie -->
<div v-if="timetable.stockString || stockHistory.length != 0">
<div class="g-separator"></div>
<b>{{ $t('journal.stock-preview') }}:</b>
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<div v-if="timetable.stockString" style="margin-top: 1em">
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
import { RouteLocationRaw } from 'vue-router';
import EntryStops from './EntryStops.vue';
export default defineComponent({
components: { StockList, EntryStops },
emits: ['toggleExtraInfo'],
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
},
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);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.details-body {
margin-top: 0.5em;
}
.details-actions {
display: flex;
gap: 0.5em;
margin-top: 1em;
button img {
height: 1.25em;
}
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
hr {
margin: 0.5em 0;
}
.stock-dangers ul {
list-style: disc;
padding-left: 1em;
padding-top: 0.5em;
white-space: pre-wrap;
}
.dangers-notes {
margin-top: 0.5em;
white-space: pre-wrap;
p {
margin-top: 0.25em;
max-height: 200px;
max-width: 500px;
overflow: auto;
}
}
@include smallScreen() {
.stock-specs {
justify-content: center;
}
.details-actions {
justify-content: center;
}
}
</style>
@@ -3,13 +3,48 @@
<span class="general-train"> <span class="general-train">
<span class="text--grayed">#{{ timetable.id }}</span> <span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr"> <span
<span class="train-badge twr" v-if="timetable.twr" :title="$t('general.TWR')">TWR</span> class="train-badge twr"
<span class="train-badge skr" v-if="timetable.skr" :title="$t('general.SKR')">SKR</span> v-if="timetable.twr"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.TWR')"
>
TWR
</span>
<span
class="train-badge skr"
v-if="timetable.skr"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.SKR')"
>
SKR
</span>
<span
class="train-badge tn"
v-if="timetable.hasDangerousCargo"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.TN')"
>
TN
</span>
<span
class="train-badge pn"
v-if="timetable.hasExtraDeliveries"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.PN')"
>
PN
</span> </span>
<span> <span>
<strong class="text--primary"> <strong
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(timetable.trainCategoryCode)"
class="text--primary tooltip-help"
>
{{ timetable.trainCategoryCode }} {{ timetable.trainCategoryCode }}
</strong> </strong>
<strong>&nbsp;{{ timetable.trainNo }}</strong> <strong>&nbsp;{{ timetable.trainNo }}</strong>
@@ -23,17 +58,19 @@
{{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }} {{ timetable.driverLevel < 2 ? 'L' : `${timetable.driverLevel}` }}
</strong> </strong>
<strong <router-link
v-if="apiStore.donatorsData.includes(timetable.driverName)" v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator" class="text--donator"
:title="$t('donations.driver-message')" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
> >
{{ timetable.driverName }} <strong>{{ timetable.driverName }}</strong>
</strong> </router-link>
<strong v-else> <router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
{{ timetable.driverName }} <strong>{{ timetable.driverName }}</strong>
</strong> </router-link>
</span> </span>
<span class="general-time"> <span class="general-time">
@@ -61,15 +98,6 @@
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}` : `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}} }}
</b> </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> </span>
</div> </div>
</template> </template>
@@ -79,12 +107,12 @@ import { PropType, defineComponent } from 'vue';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin], mixins: [dateMixin, styleMixin, trainCategoryMixin],
data() { data() {
return { return {
@@ -97,14 +125,6 @@ export default defineComponent({
type: Object as PropType<API.TimetableHistory.Data>, type: Object as PropType<API.TimetableHistory.Data>,
required: true required: true
} }
},
methods: {
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
}
} }
}); });
</script> </script>
@@ -131,7 +151,6 @@ export default defineComponent({
gap: 0.25em; gap: 0.25em;
cursor: pointer; cursor: pointer;
line-height: 2;
} }
.general-time { .general-time {
@@ -174,6 +193,7 @@ export default defineComponent({
@include smallScreen { @include smallScreen {
.item-general { .item-general {
flex-direction: column;
justify-content: center; justify-content: center;
} }
} }
@@ -1,5 +1,5 @@
<template> <template>
<div class="item-status" style="margin: 0.5em 0"> <div class="entry-status" style="margin: 0.5em 0">
<ProgressBar <ProgressBar
:progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)" :progressPercent="~~((timetable.currentDistance / timetable.routeDistance) * 100)"
:progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''" :progressType="!timetable.fulfilled && timetable.terminated ? 'abandoned' : ''"
@@ -61,7 +61,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/responsive.scss'; @import '../../../styles/responsive.scss';
.item-status { .entry-status {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -0,0 +1,299 @@
<template>
<div class="entry-stops">
<ul class="stop-list">
<li v-for="(stop, i) in timetableStops" :key="stop.stopName">
<span class="stop-label" :data-confirmed="stop.isConfirmed">
<span v-if="i > 0">&gt;</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span
class="stop-date"
v-if="stop.scheduledArrivalTimestamp != 0"
:data-delayed="
stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp > 0
"
:data-preponed="
stop.isConfirmed &&
stop.arrivalTimestamp != 0 &&
stop.arrivalTimestamp - stop.scheduledArrivalTimestamp < 0
"
>
<span
v-if="stop.isConfirmed && stop.arrivalTimestamp - stop.scheduledArrivalTimestamp != 0"
>
p. <s>{{ timestampToString(stop.scheduledArrivalTimestamp) }}</s>
{{ timestampToString(stop.arrivalTimestamp) }}
</span>
<span v-else>p. {{ timestampToString(stop.scheduledArrivalTimestamp) }}</span>
</span>
<span
class="stop-time"
v-if="stop.stopTime > 0"
:data-stop-ph="stop.stopType.includes('ph')"
:data-stop-pt="stop.stopType.includes('pt')"
:data-stop-pm="stop.stopType.includes('pm')"
>
/<span>{{ stop.stopTime }} {{ stop.stopType }}</span
>/
</span>
<span
class="stop-date"
v-if="
stop.scheduledDepartureTimestamp != 0 &&
stop.scheduledArrivalTimestamp != stop.scheduledDepartureTimestamp
"
:data-delayed="
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp > 0
"
:data-preponed="
stop.isConfirmed &&
stop.departureTimestamp != 0 &&
stop.departureTimestamp - stop.scheduledDepartureTimestamp < 0
"
>
<span
v-if="
stop.isConfirmed && stop.departureTimestamp - stop.scheduledDepartureTimestamp != 0
"
>
o. <s>{{ timestampToString(stop.scheduledDepartureTimestamp) }}</s>
{{ timestampToString(stop.departureTimestamp) }}
</span>
<span v-else>o. {{ timestampToString(stop.scheduledDepartureTimestamp) }}</span>
</span>
</span>
</li>
</ul>
<ul class="timetable-path-list" v-if="timetablePathDetails">
<li
v-for="(pathData, i) in timetablePathDetails"
:data-visited="pathData.isVisited"
:data-next-visited="
i < timetablePathDetails.length - 1 && timetablePathDetails[i + 1].isVisited
"
>
<span v-if="i > 0" class="path-arrow">&gt;</span>
<span class="path-arrival" v-if="pathData.arrival">{{ pathData.arrival }}</span>
<b class="path-scenery">{{ pathData.sceneryName }}</b>
<span class="path-departure" v-if="pathData.departure">{{ pathData.departure }}</span>
</li>
</ul>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { API } from '../../../typings/api';
interface ITimetableStopDetails {
stopName: string;
stopComments: string | null;
stopTime: number;
stopType: string;
arrivalTimestamp: number;
scheduledArrivalTimestamp: number;
departureTimestamp: number;
scheduledDepartureTimestamp: number;
isConfirmed: boolean;
}
export default defineComponent({
mixins: [dateMixin],
props: {
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
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', '') ?? '';
const isVisited = this.timetable.visitedSceneries.includes(sceneryHash);
return {
arrival,
sceneryName,
sceneryHash,
departure,
isVisited,
isVisitedOffline:
!isVisited &&
this.timetable.visitedSceneries.includes(`${sceneryName} ${sceneryHash}.sc`)
};
});
},
timetableStops(): ITimetableStopDetails[] {
const timetable = this.timetable;
const stopNames = timetable.sceneriesString.split('%');
return stopNames.reduce<ITimetableStopDetails[]>((acc, stopName, i, arr) => {
const arrivalDate =
i == arr.length - 1
? (timetable.checkpointArrivals.at(i) ?? timetable.endDate)
: timetable.checkpointArrivals.at(i);
const scheduledArrivalDate =
i == arr.length - 1
? (timetable.checkpointArrivalsScheduled.at(i) ?? timetable.scheduledEndDate)
: timetable.checkpointArrivalsScheduled.at(i);
const departureDate =
i == 0
? (timetable.checkpointDepartures.at(i) ?? timetable.beginDate)
: timetable.checkpointDepartures.at(i);
const scheduledDepartureDate =
i == 0
? (timetable.checkpointDeparturesScheduled.at(i) ?? timetable.scheduledBeginDate)
: timetable.checkpointDeparturesScheduled.at(i);
const stopTime = Number(timetable.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
const stopType = timetable.checkpointStopTypes.at(i)?.split(',').slice(1).join(',') || 'pt';
const stopComments = timetable.checkpointComments.at(i) ?? null;
acc.push({
stopName,
stopTime,
stopType,
stopComments,
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
departureTimestamp: this.dateStringToTimestamp(departureDate),
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
isConfirmed: i < timetable.confirmedStopsCount
});
return acc;
}, []);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/badge.scss';
.entry-stops {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
}
.stop-list {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em 0;
}
.stop-label {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
align-items: center;
color: white;
&[data-confirmed='true'] > .stop-name {
color: lightgreen;
}
&[data-confirmed='true'] > .stop-date:not([data-preponed='true']):not([data-delayed='true']) {
color: lightgreen;
}
}
.stop-name {
font-weight: bold;
color: #ccc;
}
.stop-date {
color: #ccc;
s {
color: #aaa;
}
&[data-delayed='true'] {
color: salmon;
}
&[data-preponed='true'] {
color: mediumspringgreen;
}
}
.stop-time {
&[data-stop-pt='true'] span {
color: #999;
}
&[data-stop-ph='true'] span,
&[data-stop-pm='true'] span {
color: gold;
}
}
.timetable-path-list {
display: flex;
flex-wrap: wrap;
gap: 0.5em 0;
padding: 0.5em 0;
color: #ccc;
li > .path-scenery:first-child,
li > .path-arrival:nth-child(2) {
border-radius: 0.5em 0 0 0.5em;
}
li > :last-child {
border-radius: 0 0.5em 0.5em 0;
}
}
.path-scenery {
padding: 0.25em 0.5em;
background-color: #303030;
}
.path-arrival,
.path-departure {
padding: 0.25em;
display: inline-block;
background-color: #4e4e4e;
min-width: 25px;
text-align: center;
}
.path-arrow {
padding: 0 0.5em;
}
.timetable-path-list > li[data-visited='true'] {
.path-arrival,
.path-scenery,
.path-arrow {
color: lightgreen;
}
&[data-next-visited='true'] .path-departure {
color: lightgreen;
}
}
</style>
@@ -12,7 +12,7 @@
<hr class="header-separator" /> <hr class="header-separator" />
<div class="info-stats"> <div class="info-stats">
<span class="stat-badge"> <span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.timetables') }}</span> <span>{{ $t('journal.driver-stats.timetables') }}</span>
<span <span
>{{ store.driverStatsData._count.fulfilled }} / >{{ store.driverStatsData._count.fulfilled }} /
@@ -20,17 +20,17 @@
> >
</span> </span>
<span class="stat-badge"> <span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span> <span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span> <span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
</span> </span>
<span class="stat-badge"> <span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span> <span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span> <span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
</span> </span>
<span class="stat-badge"> <span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.distance') }}</span> <span>{{ $t('journal.driver-stats.distance') }}</span>
<span> <span>
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} / {{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
@@ -38,7 +38,7 @@
</span> </span>
</span> </span>
<span class="stat-badge"> <span class="badge stat-badge">
<span>{{ $t('journal.driver-stats.stations') }}</span> <span>{{ $t('journal.driver-stats.stations') }}</span>
<span> <span>
{{ store.driverStatsData._sum.confirmedStopsCount }} / {{ store.driverStatsData._sum.confirmedStopsCount }} /
@@ -0,0 +1,149 @@
<template>
<li class="timetable-history-entry">
<!-- General -->
<EntryGeneral :timetable="timetableEntry" />
<div @click="toggleExtraInfo" style="cursor: pointer">
<!-- Route -->
<div class="entry-route">
<b>{{ timetableEntry.route.replace('|', ' - ') }}</b>
</div>
<hr />
<!-- Status -->
<EntryStatus :timetable="timetableEntry" />
</div>
<!-- Extra -->
<EntryDetails
:timetable="timetableEntry"
:show-extra-info="showExtraInfo"
@toggle-extra-info="toggleExtraInfo"
/>
</li>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { API } from '../../../typings/api';
import { useApiStore } from '../../../store/apiStore';
import { Journal } from '../typings';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
import dateMixin from '../../../mixins/dateMixin';
import styleMixin from '../../../mixins/styleMixin';
import EntryGeneral from './EntryGeneral.vue';
import EntryStatus from './EntryStatus.vue';
import EntryDetails from './EntryDetails.vue';
export default defineComponent({
props: {
timetableEntry: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
},
showExtraInfo: {
type: Boolean,
required: true
}
},
components: { EntryDetails, EntryGeneral, EntryStatus },
mixins: [trainCategoryMixin, dateMixin, styleMixin],
emits: ['toggleShowExtraInfo'],
data() {
return {
apiStore: useApiStore()
};
},
computed: {
timetablePathDetails() {
if (!this.timetableEntry.path || this.timetableEntry.path == '') return null;
return this.timetableEntry.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.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false
};
});
},
timetableStops(): Journal.TimetableStopDetails[] {
const timetableEntry = this.timetableEntry;
const stopNames = timetableEntry.sceneriesString.split('%');
return stopNames.reduce<Journal.TimetableStopDetails[]>((acc, stopName, i, arr) => {
const arrivalDate =
i == arr.length - 1
? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate)
: timetableEntry.checkpointArrivals.at(i);
const scheduledArrivalDate =
i == arr.length - 1
? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate)
: timetableEntry.checkpointArrivalsScheduled.at(i);
const departureDate =
i == 0
? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate)
: timetableEntry.checkpointDepartures.at(i);
const scheduledDepartureDate =
i == 0
? (timetableEntry.checkpointDeparturesScheduled.at(i) ??
timetableEntry.scheduledBeginDate)
: timetableEntry.checkpointDeparturesScheduled.at(i);
const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || '';
acc.push({
stopName,
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
departureTimestamp: this.dateStringToTimestamp(departureDate),
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
stopTime,
stopType,
isConfirmed: i < timetableEntry.confirmedStopsCount
});
return acc;
}, []);
}
},
methods: {
toggleExtraInfo() {
this.$emit('toggleShowExtraInfo');
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
.timetable-history-entry {
background-color: #1a1a1a;
padding: 1em;
}
@include smallScreen {
.entry-route {
text-align: center;
}
}
</style>
@@ -1,62 +1,40 @@
<template> <template>
<div> <div>
<transition name="status-anim" mode="out-in"> <div class="journal_warning" v-if="store.isOffline">
<div :key="dataStatus"> {{ $t('app.offline') }}
<div class="journal_warning" v-if="store.isOffline"> </div>
{{ $t('app.offline') }}
</div>
<Loading v-else-if="dataStatus == Status.Data.Loading" /> <Loading v-else-if="dataStatus == Status.Data.Loading" />
<div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error"> <div v-else-if="dataStatus == Status.Data.Error" class="journal_warning error">
{{ $t('app.error') }} {{ $t('app.error') }}
</div> </div>
<div v-else-if="timetableHistory.length == 0" class="journal_warning"> <div v-else-if="timetableHistory.length == 0" class="journal_warning">
{{ $t('app.no-result') }} {{ $t('app.no-result') }}
</div> </div>
<div v-else> <div v-else>
<ul class="journal-list"> <transition-group name="list-anim" class="journal-list" tag="ul">
<transition-group name="list-anim"> <JournalTimetableEntry
<li v-for="(timetableEntry, i) in timetableHistory"
v-for="{ timetable, showExtraInfo } in computedTimetableHistory" :key="timetableEntry.id"
class="journal_item" :timetableEntry="timetableEntry"
:key="timetable.id" :onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)"
@click="showExtraInfo.value = !showExtraInfo.value" :showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
> />
<div class="journal_item-info"> </transition-group>
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr /> <AddDataButton
<!-- Stops --> :list="timetableHistory"
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" /> :scrollDataLoaded="scrollDataLoaded"
<!-- Status --> :scrollNoMoreData="scrollNoMoreData"
<TimetableStatus :timetable="timetable" /> @addHistoryData="addHistoryData"
/>
<!-- Extra --> </div>
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
<AddDataButton
:list="timetableHistory"
:scrollDataLoaded="scrollDataLoaded"
:scrollNoMoreData="scrollNoMoreData"
@addHistoryData="addHistoryData"
/>
</div>
</div>
</transition>
<div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div> <div class="journal_warning" v-if="scrollNoMoreData">{{ $t('journal.no-further-data') }}</div>
<div class="journal_warning" v-else-if="!scrollDataLoaded"> <div class="journal_warning" v-else-if="!scrollDataLoaded">
{{ $t('journal.loading-further-data') }} {{ $t('journal.loading-further-data') }}
</div> </div>
@@ -64,28 +42,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, ref } from 'vue'; import { defineComponent, PropType } from 'vue';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import JournalTimetableEntry from './JournalTimetableEntry.vue';
import { useMainStore } from '../../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common'; import { Status } from '../../../typings/common';
import { API } from '../../../typings/api'; import { API } from '../../../typings/api';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableDetails from './TimetableDetails.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
Loading, Loading,
AddDataButton, AddDataButton,
TimetableDetails, JournalTimetableEntry
TimetableGeneral,
TimetableStatus,
TimetableStops
}, },
props: { props: {
@@ -110,16 +81,26 @@ export default defineComponent({
data() { data() {
return { return {
Status, Status,
store: useMainStore() store: useMainStore(),
extraInfoIndexes: [] as number[]
}; };
}, },
computed: { watch: {
computedTimetableHistory() { '$route.query': {
return this.timetableHistory.map((timetable) => ({ deep: true,
timetable, handler() {
showExtraInfo: ref(false) this.extraInfoIndexes.length = 0;
})); }
}
},
methods: {
toggleExtraInfo(id: number) {
const existingIdx = this.extraInfoIndexes.indexOf(id);
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
else this.extraInfoIndexes.push(id);
} }
} }
}); });
@@ -1,195 +0,0 @@
<template>
<div>
<div class="details-actions">
<button class="btn--action">
<b>{{ $t('journal.stock-info') }}</b>
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
</button>
</div>
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { StockList },
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.details-body {
margin-top: 0.5em;
}
.details-actions {
display: flex;
button img {
height: 1.25em;
}
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
@include smallScreen() {
.stock-specs {
justify-content: center;
}
.details-actions {
justify-content: center;
}
}
</style>
@@ -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,112 +0,0 @@
<template>
<div class="stop-list" v-if="showExtraInfo == true">
<span
v-for="(stop, i) in timetableStops.filter((_, i) =>
!showExtraInfo ? i == 0 || i == timetableStops.length - 1 : true
)"
class="stop-list-item"
:key="stop.stopName"
:data-confirmed="stop.confirmed"
>
<span v-if="i > 0">
&gt;
<span v-if="!showExtraInfo && i == 1 && timetableStops.length > 2">
... (+{{ timetableStops.length - 2 }}) &gt;
</span>
</span>
<span class="stop-name">{{ stop.stopName }}</span>
<span v-html="stop.html"></span>
</span>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../../mixins/dateMixin';
import { API } from '../../../typings/api';
export default defineComponent({
mixins: [dateMixin],
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
computed: {
timetableStops() {
const timetable = this.timetable;
const stopNames = timetable.sceneriesString.split('%');
const beginDateHTML = ` (o. ${
timetable.beginDate != timetable.scheduledBeginDate
? `<s class="text--grayed">${this.localeTime(timetable.beginDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledBeginDate, this.$i18n.locale)}</span>)`;
const endDateHTML = ` (p. ${
timetable.endDate != timetable.scheduledEndDate && timetable.fulfilled
? `<s class="text--grayed">${this.localeTime(timetable.endDate, this.$i18n.locale)}</s>`
: ''
} <span>${this.localeTime(timetable.scheduledEndDate, this.$i18n.locale)}</span>)`;
return stopNames.map((stopName, i) => {
const confirmed = i < timetable.confirmedStopsCount;
if (i == 0) return { stopName, html: beginDateHTML, confirmed };
if (i == stopNames.length - 1) return { stopName, html: endDateHTML, confirmed };
const departureDateScheduled = this.stringToDate(
timetable.checkpointDeparturesScheduled?.at(i)
);
const departureDateReal = this.stringToDate(timetable.checkpointDepartures?.at(i));
const arrivalDateScheduled = this.stringToDate(
timetable.checkpointArrivalsScheduled?.at(i)
);
const arrivalDateReal = this.stringToDate(timetable.checkpointArrivals?.at(i));
const arrivalHTML =
(arrivalDateReal &&
arrivalDateScheduled &&
arrivalDateReal?.getTime() != arrivalDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(arrivalDateScheduled)}</s> `
: '') + this.parseDateToTimeString(arrivalDateReal || arrivalDateScheduled);
const departureHTML =
(departureDateReal &&
departureDateScheduled &&
departureDateReal?.getTime() != departureDateScheduled?.getTime()
? `<s class="text--grayed">${this.parseDateToTimeString(departureDateScheduled)}</s> `
: '') + this.parseDateToTimeString(departureDateReal || departureDateScheduled);
let html = `${arrivalHTML}${departureHTML ? ` / ${departureHTML}` : ''}`;
if (html) html = ` (${html})`;
return { stopName, html, confirmed };
});
}
}
});
</script>
<style lang="scss" scoped>
.stop-list {
word-wrap: break-word;
gap: 0.25em;
font-size: 0.95em;
color: #adadad;
&-item[data-confirmed='true'] {
color: lightgreen;
.stop-name {
font-weight: bold;
}
}
}
</style>
+18 -2
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train' | 'search-train'
| 'search-date' | 'search-date'
| 'search-dispatcher' | 'search-dispatcher'
| 'search-issuedFrom'; | 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = { export type TimetableSearchType = {
[key in TimetableSearchKey]: string; [key in TimetableSearchKey]: string;
@@ -17,7 +19,7 @@ export namespace Journal {
}; };
export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops'; export type TimetableSorterKey = 'timetableId' | 'beginDate' | 'distance' | 'total-stops';
export type DispatcherSorterKey = 'timestampFrom' | 'duration'; export type DispatcherSorterKey = 'timestampFrom' | 'currentDuration';
export interface DispatcherSorter { export interface DispatcherSorter {
id: DispatcherSorterKey; id: DispatcherSorterKey;
@@ -37,6 +39,8 @@ export namespace Journal {
ALL_SPECIALS = 'all-specials', ALL_SPECIALS = 'all-specials',
TWR = 'twr', TWR = 'twr',
SKR = 'skr', SKR = 'skr',
PN = 'pn',
TN = 'tn',
TWR_SKR = 'twr-skr' TWR_SKR = 'twr-skr'
} }
@@ -64,4 +68,16 @@ export namespace Journal {
iconName: string; iconName: string;
disabled: boolean; disabled: boolean;
} }
export interface TimetableStopDetails {
stopName: string;
arrivalTimestamp: number;
scheduledArrivalTimestamp: number;
departureTimestamp: number;
scheduledDepartureTimestamp: number;
stopTime: number;
stopType: string;
isConfirmed: boolean;
}
} }
@@ -1,31 +1,18 @@
<template> <template>
<section class="scenery-table-section"> <div class="scenery-dispatchers-history">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" /> <div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0"> <div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }} {{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div v-else class="journal-list">
<thead> <div v-for="historyItem in historyList" :key="historyItem.id">
<th>{{ $t('scenery.dispatchers-history-hash') }}</th> <span>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th> <span class="text--grayed" style="margin-right: 10px">
<th>{{ $t('scenery.dispatchers-history-level') }}</th> #{{ historyItem.stationHash }}
<th>{{ $t('scenery.dispatchers-history-rate') }}</th> </span>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<b <b
v-if="historyItem.dispatcherLevel !== null" v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher" class="level-badge dispatcher"
@@ -35,37 +22,52 @@
> >
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }} {{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b> </b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b> <div>
</td> <span>
<td class="text--primary"> {{ $t('scenery.dispatcher-rate') }}
<b>{{ historyItem.dispatcherRate }}</b> <b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</td> </span>
<td style="min-width: 300px"> |
<div v-if="historyItem.timestampTo"> <span>
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
</span>
</div>
</span>
<span>
<span v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b> <b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }} {{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{ - {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration) calculateDuration(historyItem.currentDuration)
}}) }})
</div> </span>
<div class="dispatcher-online" v-else> <span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }} {{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b> <b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }}) ({{ calculateDuration(historyItem.currentDuration) }})
</div> </span>
</td> </span>
</tr> </div>
</tbody> </div>
</table> </div>
</section>
<div class="bottom-info"> <div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory"> <button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }} {{ $t('scenery.bottom-info') }}
</button> </button>
</div>
</div> </div>
</template> </template>
@@ -149,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss'; @import '../../styles/sceneryViewTables.scss';
.scenery-dispatchers-history {
height: 100%;
overflow: auto;
display: grid;
gap: 0.5em;
grid-template-rows: auto 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.journal-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.journal-list > div {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.75em;
}
.level-badge { .level-badge {
margin: 0 auto; text-align: center;
display: inline-block;
line-height: 1.6em;
} }
.dispatcher-online { .dispatcher-online {
@@ -158,13 +195,10 @@ export default defineComponent({
} }
@include smallScreen { @include smallScreen {
.history-list { .journal-list > div {
font-size: 1.1em;
}
.list-item {
align-items: center;
flex-direction: column; flex-direction: column;
justify-content: center;
text-align: center;
} }
} }
</style> </style>
../../store/storeTypes
+1 -6
View File
@@ -72,7 +72,7 @@
<div class="info-lists"> <div class="info-lists">
<!-- user list --> <!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" /> <SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list --> <!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" /> <SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -124,11 +124,6 @@ h3.section-header {
align-items: center; align-items: center;
font-size: 1.2em; font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
} }
.info-lists { .info-lists {
@@ -13,16 +13,15 @@
</li> </li>
<li <li
v-for="train in onlineScenery?.stationTrains" v-for="{ train, status } in stationTrains"
class="badge user" class="badge user"
:class="train.stopStatus" :key="train.id"
:key="train.trainId" :data-status="status"
tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<span class="user_train">{{ train.trainNo }}</span> <router-link :to="train.driverRouteLocation" class="a-block">
<span class="user_name">{{ train.driverName }}</span> <span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span>
</router-link>
</li> </li>
</transition-group> </transition-group>
</section> </section>
@@ -30,17 +29,52 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin'; 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({ export default defineComponent({
mixins: [routerMixin, modalTrainMixin], mixins: [routerMixin],
props: { props: {
onlineScenery: { onlineScenery: {
type: Object as PropType<ActiveScenery>, type: Object as PropType<ActiveScenery>,
required: false 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 { .user {
cursor: pointer; &[data-status='no-timetable'] .user_train {
&_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 {
background-color: $no-timetable; background-color: $no-timetable;
} }
&.departed > &_train { &[data-status='departed'] .user_train {
background-color: $departed; background-color: $departed;
} }
&.stopped > &_train { &[data-status='stopped'] .user_train {
background-color: $stopped; background-color: $stopped;
} }
&.online > &_train { &[data-status='online'] .user_train {
background-color: $online; background-color: $online;
} }
&.terminated > &_train { &[data-status='terminated'] .user_train {
background-color: $terminated; background-color: $terminated;
} }
&.disconnected > &_train { &[data-status='disconnected'] .user_train {
background-color: $disconnected; background-color: $disconnected;
} }
&.offline { &[data-status='offline'] {
background: firebrick; background: firebrick;
pointer-events: none; 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 { .users-anim {
&-move, &-move,
&-enter-active, &-enter-active,
+150 -94
View File
@@ -14,6 +14,10 @@
</span> </span>
<span class="header_links" v-if="station"> <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')"> <a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" /> <img src="/images/icon-tablice.ico" alt="icon-tablice" />
</a> </a>
@@ -21,26 +25,23 @@
</h3> </h3>
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints"> <div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
<span v-for="(cp, i) in station.generalInfo.checkpoints" :key="i"> <template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
{{ (i > 0 && '&bull;') || '' }} <template v-if="i > 0">&bull;</template>
<router-link
<button class="checkpoint-item"
:key="cp" :class="{ current: chosenCheckpoint === ch }"
class="checkpoint_item" :to="`/scenery?station=${station.name}&checkpoint=${ch}`"
:class="{ current: chosenCheckpoint === cp }" >{{ ch }}</router-link
@click="setCheckpoint(cp)"
> >
{{ cp }} </template>
</button>
</span>
</div> </div>
</div> </div>
<div class="timetable-list"> <div class="timetable-list">
<transition-group name="list-anim"> <transition-group name="list-anim">
<div <div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em" style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading" key="list-loading"
> >
<Loading /> <Loading />
@@ -48,7 +49,7 @@
<span <span
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery" v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline" key="list-offline"
> >
{{ $t('scenery.offline') }} {{ $t('scenery.offline') }}
@@ -56,68 +57,72 @@
<div <div
class="timetable-item empty" class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0" v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables" key="list-no-timetables"
> >
{{ $t('scenery.no-timetables') }} {{ $t('scenery.no-timetables') }}
</div> </div>
<div <router-link
class="timetable-item" class="timetable-item a-block"
v-else v-else
v-for="scheduledTrain in computedScheduledTrains" v-for="(row, i) in sceneryTimetables"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp" :key="row.train.id + i"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" :to="row.train.driverRouteLocation"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
> >
<span class="timetable-general"> <span class="timetable-general">
<span class="general-info"> <span class="general-info">
<span class="info-number"> <div class="info-train">
<strong>{{ scheduledTrain.category }}</strong> <b
{{ scheduledTrain.trainNo }} 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 <span
v-if="scheduledTrain.stopInfo.comments" v-if="row.checkpointStop.comments"
:title="scheduledTrain.stopInfo.comments" data-tooltip-type="BaseTooltip"
:data-tooltip-content="row.checkpointStop.comments"
> >
<img src="/images/icon-warning.svg" /> <img src="/images/icon-warning.svg" />
</span> </span>
</span>
&nbsp;|&nbsp;
<span>
{{ scheduledTrain.driverName }}
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
</div> </div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" /> <div class="info-route">
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div>
<ScheduledTrainStatus :sceneryTimetableRow="row" />
</span> </span>
</span> </span>
<span class="timetable-schedule"> <span class="timetable-schedule">
<span class="schedule-arrival"> <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') }} {{ $t('timetables.begins') }}
</span> </span>
<span class="arrival-time" v-else> <span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0"> <div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }} {{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : '' ({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }}) }}{{ row.checkpointStop.arrivalDelay }})
</span> </span>
</div> </div>
</span> </span>
@@ -125,47 +130,45 @@
<span class="schedule-stop"> <span class="schedule-stop">
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.arrivingLine }} {{ row.arrivingLine }}
</span> </span>
<span class="stop-time"> <span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }} {{ row.checkpointStop.stopTime || '' }}
{{ {{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
</span> </span>
<span class="stop-connection"> <span class="stop-connection">
{{ scheduledTrain.departureLine }} {{ row.departureLine }}
</span> </span>
</span> </span>
<span class="schedule-departure"> <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') }} {{ $t('timetables.terminates') }}
</span> </span>
<span class="departure-time" v-else> <span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0"> <div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span> <span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div> </div>
<div v-else> <div v-else>
<div> <div>
<s style="margin-right: 0.2em" class="text--grayed">{{ <s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp) timestampToString(row.checkpointStop.departureTimestamp)
}}</s> }}</s>
</div> </div>
<span> <span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }} {{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : '' ({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }}) }}{{ row.checkpointStop.departureDelay }})
</span> </span>
</div> </div>
</span> </span>
</span> </span>
</span> </span>
</div> </router-link>
</transition-group> </transition-group>
</div> </div>
</section> </section>
@@ -178,18 +181,20 @@ import { useRoute } from 'vue-router';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { SceneryTimetableRow } from './typings';
import { ActiveScenery, Station } from '../../typings/common'; import { ActiveScenery, Station } from '../../typings/common';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
components: { Loading, ScheduledTrainStatus }, components: { Loading, ScheduledTrainStatus },
mixins: [dateMixin, routerMixin, modalTrainMixin], mixins: [dateMixin, routerMixin, trainCategoryMixin],
props: { props: {
station: { station: {
@@ -204,12 +209,14 @@ export default defineComponent({
listOpen: false listOpen: false
}), }),
mounted() { activated() {
this.loadSelectedOption(); this.loadSelectedOption();
}, },
activated() { watch: {
this.loadSelectedOption(); currentURL() {
this.loadSelectedOption();
}
}, },
setup(props) { setup(props) {
@@ -220,9 +227,10 @@ export default defineComponent({
const mainStore = useMainStore(); const mainStore = useMainStore();
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints[0] ??
? '' props.station?.name ??
: props.station?.generalInfo?.checkpoints[0] ?? null route.query['station']?.toString() ??
''
); );
return { return {
@@ -241,27 +249,57 @@ export default defineComponent({
return url; return url;
}, },
computedScheduledTrains() { pragotronHref() {
if (!this.station) return []; 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 ( return url;
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;
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1; sceneryTimetables(): SceneryTimetableRow[] {
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1; 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() { loadSelectedOption() {
if (!this.station) return; 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) { setCheckpoint(cp: string) {
@@ -339,7 +389,6 @@ export default defineComponent({
background: #353535; background: #353535;
cursor: pointer;
z-index: 10; z-index: 10;
&.empty { &.empty {
@@ -369,30 +418,35 @@ export default defineComponent({
} }
} }
.timetable-list {
position: relative;
}
.timetable-checkpoints { .timetable-checkpoints {
display: flex; display: flex;
justify-content: center; justify-content: center;
gap: 0.5em;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 1.1em; font-size: 1.1em;
margin-top: 0.5em; margin-top: 0.5em;
}
button.checkpoint_item { .checkpoint-item {
color: #aaa; color: #aaa;
display: inline; display: inline;
&:hover {
color: white;
} }
.checkpoint_item.current { &.current {
font-weight: bold; font-weight: bold;
color: $accentCol; color: $accentCol;
} }
} }
.timetable-list {
position: relative;
}
.general-info { .general-info {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -406,7 +460,9 @@ export default defineComponent({
} }
img { img {
width: 1.1em; height: 0.9em;
vertical-align: middle;
margin: 0 0.25em;
} }
} }
@@ -1,69 +1,97 @@
<template> <template>
<!-- WIP --> <div class="scenery-timetables-history">
<!-- <div class="top-filters"> <div class="history-modes">
<button class="btn btn--option">ROZPOCZYNA BIEG</button> <button
class="btn btn--option"
<button class="btn btn--option">PRZEZ</button> v-for="mode in historyModeList"
:key="mode"
<button class="btn btn--option">KOŃCZY BIEG</button> :class="{ checked: checkedHistoryMode == mode }"
</div> --> @click="checkHistoryMode(mode)"
>
<section class="scenery-table-section"> {{ $t(`scenery.timetable-${mode}`) }}
<Loading v-if="dataStatus != DataStatus.Loaded" /> </button>
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
</div> </div>
<table class="scenery-history-table" v-else> <div class="history-wrapper">
<thead> <Loading v-if="dataStatus != DataStatus.Loaded" />
<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>
<tbody> <div v-else-if="historyList.length == 0" class="no-history">
<tr v-for="historyItem in historyList" :key="historyItem.id"> {{ $t('scenery.history-list-empty') }}
<td> </div>
<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>
<td> <div v-else class="journal-list">
<router-link <div v-for="timetableHistory in historyList" :key="timetableHistory.id">
v-if="historyItem.authorName" <span>
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`" <div>
>{{ historyItem.authorName }} <span
</router-link> class="timetable-status-indicator"
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i> :data-terminated="timetableHistory.terminated"
</td> :data-fulfilled="timetableHistory.fulfilled"
<td> >
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b> &ofcir;
{{ localeTime(historyItem.beginDate, $i18n.locale) }} </span>
</td> #{{ timetableHistory.id }} |
</tr> <b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
</tbody> {{ timetableHistory.trainNo }}
</table> {{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</section> </div>
<div class="bottom-info"> <div class="text--grayed">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()"> <span>
{{ $t('scenery.bottom-info') }} {{ $t('scenery.timetable-issued-date') }}
</button> <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> </div>
</template> </template>
@@ -75,10 +103,15 @@ import Loading from '../Global/Loading.vue';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { ActiveScenery, Station, Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import routerMixin from '../../mixins/routerMixin';
import { useMainStore } from '../../store/mainStore';
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
type HistoryMode = (typeof historyModeList)[number];
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin], mixins: [dateMixin, routerMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -91,9 +124,14 @@ export default defineComponent({
data() { data() {
return { return {
historyList: [] as API.TimetableHistory.Response, historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(), apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
DataStatus: Status.Data DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
}; };
}, },
@@ -103,17 +141,22 @@ export default defineComponent({
methods: { methods: {
async fetchAPIData() { async fetchAPIData() {
if (!this.station && !this.onlineScenery) { const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
return; return;
} }
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try { try {
const response: API.TimetableHistory.Response = await ( const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: { params: requestFilters
issuedFrom: this.station?.name || this.onlineScenery?.name
}
}) })
).data; ).data;
@@ -125,11 +168,17 @@ export default defineComponent({
} }
}, },
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() { navigateToHistory() {
this.$router.push({ this.$router.push({
path: '/journal/timetables', path: '/journal/timetables',
query: { query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name [`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
} }
}); });
} }
@@ -142,13 +191,66 @@ export default defineComponent({
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss'; @import '../../styles/sceneryViewTables.scss';
.top-filters { .scenery-timetables-history {
height: 100%;
overflow: auto;
display: grid;
gap: 1em;
grid-template-rows: auto 1fr 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-modes {
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
padding: 0.25em;
button { button {
padding: 0.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> </style>
@@ -1,7 +1,7 @@
<template> <template>
<div class="general-status"> <div class="general-status">
<span <span
:class="computedScheduledTrain.stopStatus" :class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription" :title="computedScheduledTrain.stopStatusDescription"
> >
{{ computedScheduledTrain.stopStatusIndicator }} {{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../typings/common'; import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
export default defineComponent({ export default defineComponent({
props: { props: {
scheduledTrain: { sceneryTimetableRow: {
type: Object as PropType<ScheduledTrain>, type: Object as PropType<SceneryTimetableRow>,
required: true required: true
} }
}, },
computed: { computed: {
computedScheduledTrain(): ScheduledTrainComp { computedScheduledTrain() {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } = const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.scheduledTrain; this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}` ? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '', let stopStatusDescription = '',
stopStatusIndicator = ''; stopStatusIndicator = '';
switch (stopStatus) { switch (status) {
case StopStatus.ARRIVING: case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`; stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', { stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}` ? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`; : `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine }) ? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: ''; : '';
break; break;
@@ -85,7 +81,7 @@ export default defineComponent({
break; break;
} }
return { return {
...this.scheduledTrain, ...this.sceneryTimetableRow,
stopStatusDescription, stopStatusDescription,
stopStatusIndicator 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"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption { interface FilterOption {
id: string; id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'], emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: { watch: {
'option.value'() { '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) { handleDbClick(e: Event) {
e.preventDefault(); e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id; // this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true; // this.option.value = true;
this.$emit('update:optionValue', true); this.$emit('update:optionValue', true);
this.filterStore.inputs.options // this.filterStore.inputs.options
.filter((option) => { // .filter((option) => {
return option.section == this.option.section && option.id != this.option.id; // return option.section == this.option.section && option.id != this.option.id;
}) // })
.forEach((option) => { // .forEach((option) => {
option.value = !this.option.value; // option.value = !this.option.value;
}); // });
} }
} }
}); });
+222 -144
View File
@@ -4,58 +4,79 @@
<button class="card-button btn--filled btn--image" @click="toggleCard"> <button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" /> <img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
<p>[F] {{ $t('options.filters') }}</p> <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> </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> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters"> <div class="card" v-if="isVisible" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content" @scroll="onScroll" ref="cardContentRef"> <div class="card_content" tabindex="0" @scroll="onScroll" ref="cardContentRef">
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
<div class="changed-filters" :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"> <section class="card_options">
<div <div
class="option-section" class="option-section"
v-for="section in filterStore.inputs.optionSections" v-for="(sectionFilters, sectionKey) in filtersSections"
:key="section" :key="sectionKey"
> >
<h3 class="text--primary"> <h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }} <span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button> <button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3> </h3>
<hr /> <hr />
<div class="section-inputs"> <div class="section-filters">
<FilterOption <label
v-for="(option, i) in filterStore.inputs.options.filter( v-for="filterKey in sectionFilters"
(o) => o.section == section @dblclick="setSingleSectionFilter(sectionKey, filterKey)"
)" >
v-model:optionValue="option.value" <input
:option="option" type="checkbox"
:key="i" :checked="filters[filterKey]"
/> v-model="filters[filterKey]"
:class="sectionKey"
:name="filterKey"
:id="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div> </div>
</div> </div>
</section> </section>
@@ -68,7 +89,7 @@
<span>{{ <span>{{
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
: minimumHours < 8 : minimumHours < 7
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
@@ -76,43 +97,42 @@
</span> </span>
</section> </section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search"> <section class="card_authors-search">
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3> <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"> <form action="javascript:void(0);" @submit="handleAuthorsInput">
<input <input
type="text" type="text"
id="author" id="author"
list="authors" list="authors"
name="authors" name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')" :placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
/> />
<button class="btn--action">{{ $t('filters.authors-button-title') }}</button> <button class="btn--action">{{ $t('filters.search-button-title') }}</button>
</form> </form>
</section> </section>
<section class="card_sliders"> <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 <input
class="slider-input" class="slider-input"
type="range" type="range"
:name="slider.name" :name="slider.id"
:id="slider.id" :id="slider.id"
:min="slider.minRange" :min="slider.minRange"
:max="slider.maxRange" :max="slider.maxRange"
:step="slider.step" :step="slider.step"
v-model="slider.value" v-model="filters[slider.id]"
@change="handleInput"
/> />
<span class="slider-value">{{ slider.value }}</span> <span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content"> <div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }} {{ $t(`filters.sliders.${slider.id}`) }}
</div> </div>
@@ -133,9 +153,9 @@
<button <button
class="btn--action" class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters" @click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
> >
[R] {{ $t('filters.reset') }} [R] {{ $t('filters.reset') }}
</button> </button>
@@ -151,22 +171,36 @@
import { defineComponent, inject } from 'vue'; import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin'; import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue'; import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager'; 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({ export default defineComponent({
components: { FilterOption }, components: { FilterOption },
mixins: [keyMixin, routerMixin], mixins: [keyMixin, routerMixin],
data: () => ({ data: () => ({
saveOptions: false, saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '', filtersSections,
initSliders,
minimumHours: 0, minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
@@ -180,22 +214,33 @@ export default defineComponent({
setup() { setup() {
const isVisible = inject('isFilterCardVisible'); const isVisible = inject('isFilterCardVisible');
const store = useMainStore(); 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 { return {
isVisible, isVisible,
store, store,
filterStore filters,
changedFilters
}; };
}, },
mounted() { mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY); this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) { if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours'); this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
} }
this.currentRegion = this.store.region; this.currentRegion = this.store.region;
@@ -214,7 +259,7 @@ export default defineComponent({
return true; return true;
}, },
authors() { authorsHint() {
return this.store.stationList return this.store.stationList
.reduce((acc, station) => { .reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => { station.generalInfo?.authors?.forEach((author) => {
@@ -229,20 +274,11 @@ export default defineComponent({
}, },
watch: { 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) { isVisible(value: boolean) {
this.$nextTick(() => { this.$nextTick(() => {
if (value) { if (value) {
(this.$refs['cardRef'] as HTMLDivElement).focus();
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop; (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; 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() { handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue); this.filters['authors'] = this.authors;
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
}, },
changeNumericFilterValue(name: string, value: number, saveToStorage = false) { handleSceneriesInput() {
this.filterStore.changeFilterValue(name, value); const chosenStation = this.store.stationList.find(
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value); ({ name }) => name == this.chosenSearchScenery
);
if (chosenStation) {
this.$router.push(`/scenery?station=${chosenStation.name.replace(/ /g, '_')}`);
this.chosenSearchScenery = '';
this.isVisible = false;
}
}, },
subHour() { subHour() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1; this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
addHour() { addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1; this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
}, },
saveFilters() { saveFilters() {
this.saveOptions = !this.saveOptions; this.saveOptions = !this.saveOptions;
if (!this.saveOptions) { if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY); StorageManager.unregisterStorage(STORAGE_KEY);
return; return;
} }
StorageManager.registerStorage(this.STORAGE_KEY); StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) => Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setBooleanValue(option.name, !option.value) StorageManager.setValue(filterKey, this.filters[filterKey]);
); });
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
}, },
resetFilters() { resetFilters() {
this.authorsInputValue = ''; if (this.preventKeyDown) return;
// Reset local model values
this.minimumHours = 0; this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true); this.authors = '';
this.filterStore.resetFilters();
// 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() { closeCard() {
@@ -330,6 +382,7 @@ export default defineComponent({
@import '../../styles/responsive'; @import '../../styles/responsive';
@import '../../styles/card'; @import '../../styles/card';
@import '../../styles/animations'; @import '../../styles/animations';
@import '../../styles/variables';
h3.section-header { h3.section-header {
text-align: center; text-align: center;
@@ -339,6 +392,7 @@ h3.section-header {
.card { .card {
display: grid; display: grid;
grid-template-rows: 1fr auto; grid-template-rows: 1fr auto;
padding: 1px;
} }
.card_info { .card_info {
@@ -346,6 +400,15 @@ h3.section-header {
padding: 0.5em; padding: 0.5em;
} }
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls { .card_controls {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
@@ -374,28 +437,6 @@ h3.section-header {
text-align: center; 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 { .card_timestamp {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -421,8 +462,12 @@ h3.section-header {
} }
} }
.card_authors-search { .card_authors-search,
.card_sceneries-search {
margin: 1em 0; margin: 1em 0;
display: flex;
flex-direction: column;
align-items: center;
form { form {
display: flex; 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 { .card_actions {
padding: 0.5em; 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 { .slider {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25em;
margin-bottom: 1em; margin-bottom: 1em;
&-value { &-value {
color: $accentCol; color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em; padding: 0.1em 0.2em;
} }
&-content {
flex-grow: 2;
}
&-input { &-input {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
@@ -512,7 +586,6 @@ h3.section-header {
outline: none; outline: none;
min-width: 25%; min-width: 25%;
max-width: 120px;
&:focus-visible ~ * { &:focus-visible ~ * {
color: gold; color: gold;
@@ -584,8 +657,13 @@ h3.section-header {
} }
@include smallScreen { @include smallScreen {
.card_controls > button.card-button > p { .slider {
display: none; flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
} }
} }
</style> </style>
+103 -86
View File
@@ -1,56 +1,70 @@
<template> <template>
<div class="station-stats"> <div
<div class="separator" /> 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"> <button class="filter-button btn--filled btn--image" @click="toggleDropdown" ref="button">
<div> <img src="/images/icon-stats.svg" alt="Open filters icon" />
<span <!-- {{ $t('train-stats.stats-button') }} -->
>{{ $t('station-stats.u-factor') }} <span>STATYSTYKI</span>
<a </button>
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()"> <transition name="dropdown-anim">
{{ uFactor.toFixed(2) }} <div class="dropdown_wrapper" v-if="showDropdown">
</b> <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>
</transition>
<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>
</div> </div>
</template> </template>
@@ -61,11 +75,16 @@ import { useMainStore } from '../../store/mainStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
mainStore: useMainStore() mainStore: useMainStore(),
showDropdown: false
}; };
}, },
methods: { methods: {
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
calculateFactorStyle() { calculateFactorStyle() {
if (this.uFactor == 0) return ''; if (this.uFactor == 0) return '';
@@ -89,27 +108,19 @@ export default defineComponent({
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0; return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
}, },
medTimetableCount() { avgTimetableCount() {
const scheduledTrainsArr = this.mainStore.activeSceneryList const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
.reduce<number[]>((acc, sc) => { return sc.region == this.mainStore.region.id;
if (sc.region != this.mainStore.region.id) return acc; });
acc.push(sc.scheduledTrainCount.all); const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
return acc; if (regionSceneries.length == 0) return 0;
}, [])
.sort((a, b) => Math.sign(a - b));
if (scheduledTrainsArr.length == 0) return 0; return timetableCountSum / regionSceneries.length;
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)];
}, },
trackCount() { trackCount() {
@@ -179,25 +190,15 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.separator { @import '../../styles/dropdown.scss';
width: 100%; @import '../../styles/badge.scss';
height: 2px;
h1 img {
vertical-align: text-bottom;
}
h3 {
margin: 0.5em 0; 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 { .u-factor {
@@ -217,4 +218,20 @@ export default defineComponent({
color: rgb(22, 245, 22); 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> </style>
+317 -313
View File
@@ -1,383 +1,381 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<Loading <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 v-else-if="filteredStationList.length > 0">
<table> <thead>
<thead> <tr>
<tr> <th
<th v-for="headerName in headIds"
v-for="headerName in headIds" :key="headerName"
:key="headerName" @click="changeSorter(headerName)"
@click="changeSorter(headerName)" class="header-text"
class="header-text" :class="headerName"
: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"
> >
<td class="station-name" :class="station.generalInfo?.availability"> <span class="header_wrapper">
<b v-if="station.generalInfo?.project" style="color: salmon">{{ <div v-html="$t(`sceneries.headers.${headerName}`)"></div>
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level"> <img
<span v-if="station.generalInfo"> class="sort-icon"
<span v-if="activeSorter.headerName == headerName"
v-if=" :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
station.generalInfo.reqLevel > -1 && alt="sort icon"
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"
/> />
</td> </span>
</th>
<td class="station-dispatcher-name"> <th
<span v-if="station.onlineInfo?.dispatcherName"> v-for="headerName in headIconsIds"
<b :key="headerName"
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" @click="changeSorter(headerName)"
@click.stop="openDonationModal" class="header-image"
data-tooltip-type="DonatorTooltip" :class="headerName"
:data-tooltip-content="$t('donations.dispatcher-message')" >
> <span class="header_wrapper">
<img src="/images/icon-diamond.svg" alt="" /> <img
{{ station.onlineInfo.dispatcherName }} :src="`/images/icon-${headerName}.svg`"
</b> :alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else> <img
{{ station.onlineInfo.dispatcherName }} class="sort-icon"
</div> v-if="activeSorter.headerName == headerName"
</span> :src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
</td> 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 <span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1" v-if="
:style=" station.generalInfo.reqLevel > -1 &&
calculateExpStyle( station.generalInfo.availability != 'nonPublic' &&
station.onlineInfo.dispatcherExp, station.generalInfo.availability != 'unavailable'
station.onlineInfo.dispatcherIsSupporter
)
" "
:style="calculateExpStyle(station.generalInfo.reqLevel)"
> >
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }} {{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span> </span>
</td>
<td class="station-tracks"> <span v-else-if="station.generalInfo.availability == 'abandoned'">
<div v-if="station.generalInfo"> <img
<span src="/images/icon-abandoned.svg"
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0" alt="non-public"
class="track catenary" :title="$t('sceneries.info.abandoned')"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${ />
station.generalInfo.routes.singleElectrifiedNames.length </span>
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span <span v-else-if="station.generalInfo.availability == 'nonPublic'">
v-if="station.generalInfo.routes.singleOtherNames.length != 0" <img
class="track no-catenary" src="/images/icon-lock.svg"
:title="`${$t('sceneries.info.single-track-routes-other')}${ alt="non-public"
station.generalInfo.routes.singleOtherNames.length :title="$t('sceneries.info.non-public')"
}`" />
> </span>
{{ station.generalInfo.routes.singleOtherNames.length }}
</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> </div>
</td> </span>
</td>
<td class="station-tracks"> <td class="station-dispatcher-exp">
<div v-if="station.generalInfo"> <span
<span v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0" :style="
class="track catenary" calculateExpStyle(
:title="`${$t('sceneries.info.double-track-routes-catenary')}${ station.onlineInfo.dispatcherExp,
station.generalInfo.routes.doubleElectrifiedNames.length station.onlineInfo.dispatcherIsSupporter
}`" )
> "
{{ station.generalInfo.routes.doubleElectrifiedNames.length }} >
</span> {{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<span <td class="station-tracks">
v-if="station.generalInfo.routes.doubleOtherNames.length != 0" <div v-if="station.generalInfo">
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">
<span <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="scenery-icon icon-info" class="track catenary"
:class="station.generalInfo?.controlType.replace('+', '-')" :title="`${$t('sceneries.info.single-track-routes-catenary')}${
:title=" station.generalInfo.routes.singleElectrifiedNames.length
$t('sceneries.info.control-type') + }`"
$t(`controls.${station.generalInfo?.controlType}`)
"
> >
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }} {{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span> </span>
<img <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :title="`${$t('sceneries.info.single-track-routes-other')}${
:alt="station.generalInfo.signalType" station.generalInfo.routes.singleOtherNames.length
:title=" }`"
$t('sceneries.info.signals-type') + >
$t(`signals.${station.generalInfo.signalType}`) {{ station.generalInfo.routes.singleOtherNames.length }}
" </span>
/> </div>
</td>
<img <td class="station-tracks">
v-if="station.generalInfo?.SUP" <div v-if="station.generalInfo">
class="icon-info" <span
src="/images/icon-SUP.svg" v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
alt="SUP (RASP-UZK)" class="track catenary"
:title="$t('sceneries.info.SUP')" :title="`${$t('sceneries.info.double-track-routes-catenary')}${
/> station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<img <span
v-if="station.generalInfo?.ASDEK" v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
src="/images/icon-ASDEK.svg" :title="`${$t('sceneries.info.double-track-routes-other')}${
alt="dSAT ASDEK" station.generalInfo.routes.doubleOtherNames.length
:title="$t('sceneries.info.ASDEK')" }`"
/> >
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<img <td class="station-info">
v-if="!station.generalInfo" <span
class="icon-info" v-if="station.generalInfo?.signalType"
src="/images/icon-unknown.svg" class="scenery-icon icon-info"
alt="icon-unknown" :class="station.generalInfo?.controlType.replace('+', '-')"
:title="$t('sceneries.info.unknown')" :title="
/> $t('sceneries.info.control-type') +
</td> $t(`controls.${station.generalInfo?.controlType}`)
"
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
> >
<span class="text--primary">{{ {{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
station.onlineInfo?.stationTrains?.length ?? '-' </span>
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }"> <img
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span> v-if="station.generalInfo?.signalType"
</td> 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 <img
class="station-spawns" v-if="station.generalInfo?.SUP"
:class="{ inactive: !station.onlineInfo }" class="icon-info"
data-tooltip-type="SpawnsTooltip" src="/images/icon-SUP.svg"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])" alt="SUP (RASP-UZK)"
> :title="$t('sceneries.info.SUP')"
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span> />
</td>
<td <img
class="station-schedules all" v-if="station.generalInfo?.ASDEK"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" src="/images/icon-ASDEK.svg"
> alt="dSAT ASDEK"
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }} :title="$t('sceneries.info.ASDEK')"
</td> />
<td <img
class="station-schedules unconfirmed" v-if="!station.generalInfo"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" src="/images/icon-unknown.svg"
> alt="icon-unknown"
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }} :title="$t('sceneries.info.unknown')"
</td> />
</td>
<td <td
class="station-schedules confirmed" class="station-users"
style="width: 30px" :class="{ inactive: !station.onlineInfo }"
:class="{ inactive: !station.onlineInfo }" data-tooltip-type="UsersTooltip"
> :data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }} >
</td> <span class="text--primary">{{
</tr> station.onlineInfo?.stationTrains?.length ?? '-'
</tbody> }}</span>
</table> /
</div> <span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</router-link>
</tbody>
</table>
<div class="no-stations" v-else> <div 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> </div>
</section> </section>
</template> </template>
<script lang="ts"> <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 dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin'; 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 { useApiStore } from '../../store/apiStore';
import { useMainStore } from '../../store/mainStore';
import { Station, Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore'; 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({ export default defineComponent({
emits: ['toggleDonationModal'], emits: ['toggleDonationCard'],
components: { Loading, StationStatusBadge }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin], mixins: [styleMixin, dateMixin],
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
headIds, headIds,
lastSelectedStationName: '' getChangedFilters
}), }),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
}
},
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
const tooltipStore = useTooltipStore(); 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 { return {
Status: Status.Data, Status: Status.Data,
stationFiltersStore,
mainStore, mainStore,
apiStore, apiStore,
tooltipStore tooltipStore,
filters,
filteredStationList,
activeSorter
}; };
}, },
methods: { methods: {
setScenery(name: string) { getSceneryRoute(station: Station) {
const station = this.displayedStations.find((station) => station.name === name); // TODO: Hide tooltips when navigating away
if (!station) return; return {
this.lastSelectedStationName = station.name;
this.tooltipStore.hide();
this.$router.push({
name: 'SceneryView', name: 'SceneryView',
query: { query: {
station: station.name.replaceAll(' ', '_'), station: station.name,
region: this.$route.query.region || undefined region: this.$route.query.region || undefined
} }
}); };
}, },
openDonationModal(e: Event) { openDonationCard(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide(); this.tooltipStore.hide();
}, },
@@ -388,10 +386,14 @@ export default defineComponent({
window.open(url, '_blank'); window.open(url, '_blank');
}, },
changeSorter(headerName: HeadIdsTypes) { changeSorter(headerName: HeadIdsType) {
if (headerName == 'general') return; 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,19 +407,19 @@ export default defineComponent({
$rowCol: #424242; $rowCol: #424242;
.station_table { .station_table {
height: 80vh; height: calc(100vh - 11em);
min-height: 700px; max-height: 2000px;
min-height: 500px;
overflow: auto; overflow: auto;
font-weight: 500; font-weight: 500;
} }
.no-stations { .no-stations {
text-align: center; text-align: center;
font-size: 1.5em; font-size: 1.25em;
padding: 1em; padding: 1em;
background: #1a1a1a; background: #1a1a1a;
line-height: 1.5em;
} }
table { table {
@@ -494,8 +496,10 @@ table {
} }
} }
tr { tr,
.a-row {
background-color: $rowCol; background-color: $rowCol;
vertical-align: middle;
&:nth-child(even) { &:nth-child(even) {
background-color: lighten($rowCol, 5); background-color: lighten($rowCol, 5);
+25 -52
View File
@@ -5,56 +5,29 @@ export interface FilterOption {
defaultValue: boolean; defaultValue: boolean;
} }
export interface Filter { export const headIds = [
[key: string]: boolean | number | string; 'station',
default: boolean; 'min-lvl',
notDefault: boolean; 'status',
real: boolean; 'dispatcher',
fictional: boolean; 'dispatcher-lvl',
SPK: boolean; 'routes-single',
SCS: boolean; 'routes-double',
SPE: boolean; 'general'
SUP: boolean; ] as const;
noSUP: boolean;
ASDEK: boolean; export const headIconsIds = [
noASDEK: boolean; 'user',
ręczne: boolean; 'like',
'ręczne+SPK': boolean; 'spawn',
'ręczne+SCS': boolean; 'timetableAll',
mechaniczne: boolean; 'timetableUnconfirmed',
'mechaniczne+SPK': boolean; 'timetableConfirmed'
'mechaniczne+SCS': boolean; ] as const;
SBL: boolean;
PBL: boolean; export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
współczesna: boolean;
kształtowa: boolean; export interface ActiveSorter {
historyczna: boolean; headerName: HeadIdsType;
mieszana: boolean; dir: number;
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;
} }
+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;
};
+2 -1
View File
@@ -23,12 +23,13 @@ export default defineComponent({
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 0.5em; gap: 0.5em;
white-space: pre-line;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
border-radius: 0.25em; border-radius: 0.25em;
width: 100%; width: 100%;
background-color: #333; background-color: #1f1f1f;
box-shadow: 0 0 5px 2px #aaa; box-shadow: 0 0 5px 2px #aaa;
} }
+1 -2
View File
@@ -32,12 +32,11 @@ export default defineComponent({
<style scoped> <style scoped>
.tooltip-content { .tooltip-content {
width: 300px; width: 200px;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
border-radius: 0.25em; border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b; background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa; box-shadow: 0 0 5px 2px #aaa;
} }
+11 -12
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview"> <div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" /> <component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div> </div>
</template> </template>
@@ -13,6 +13,8 @@ import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue'; import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue'; import UsersTooltip from './UsersTooltip.vue';
const BOX_PADDING_PX = 20;
export default defineComponent({ export default defineComponent({
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip }, components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
@@ -33,17 +35,14 @@ export default defineComponent({
const boxWidth = previewEl.getBoundingClientRect().width; const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0', let translateX = '0',
translateY = '30px'; translateY = `calc(-100% - ${BOX_PADDING_PX}px)`;
if (clientWidth < 500) { if (val[0] <= boxWidth / 2 + BOX_PADDING_PX) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0'; previewEl.style.left = '0';
translateX = '0px'; translateX = BOX_PADDING_PX + 'px';
} else if (val[0] >= clientWidth - boxWidth / 2) { } else if (val[0] >= clientWidth - boxWidth / 2 - BOX_PADDING_PX) {
previewEl.style.left = '100%'; previewEl.style.left = '100%';
translateX = '-100%'; translateX = `calc(-100% - ${BOX_PADDING_PX}px)`;
} else { } else {
previewEl.style.left = `${val[0]}px`; previewEl.style.left = `${val[0]}px`;
translateX = '-50%'; translateX = '-50%';
@@ -52,10 +51,10 @@ export default defineComponent({
previewEl.style.top = `${val[1]}px`; previewEl.style.top = `${val[1]}px`;
const isOutside = const isOutside =
val[1] + previewEl.getBoundingClientRect().height + 30 >= val[1] - previewEl.getBoundingClientRect().height <=
window.innerHeight + window.scrollY; window.scrollY + BOX_PADDING_PX * 2;
if (isOutside) translateY = 'calc(-100% - 30px)'; if (isOutside) translateY = BOX_PADDING_PX + 'px';
previewEl.style.transform = `translate(${translateX}, ${translateY})`; previewEl.style.transform = `translate(${translateX}, ${translateY})`;
}); });
} }
+3 -4
View File
@@ -10,7 +10,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common'; import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
data() { data() {
@@ -23,7 +23,7 @@ export default defineComponent({
trains() { trains() {
if (this.tooltipStore.content == '') return []; 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); return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
} }
} }
@@ -32,12 +32,11 @@ export default defineComponent({
<style scoped> <style scoped>
.tooltip-content { .tooltip-content {
width: 300px; width: 200px;
padding: 0.25em 0.5em; padding: 0.25em 0.5em;
border-radius: 0.25em; border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b; background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa; box-shadow: 0 0 5px 2px #aaa;
} }
@@ -1,25 +1,31 @@
<template> <template>
<div class="tooltip-content"> <div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info"> <div class="image-box">
{{ $t('vehicle-preview.loading') }} <div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
<div v-if="imageState == 'error'" class="loading-info">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="tooltipStore.type"
@load="onImageLoad"
@error="onImageError"
width="300"
height="176"
:src="`https://stacjownik.spythere.eu/static/images/${vehicleName}--300px.jpg`"
/>
</div> </div>
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="tooltipStore.type"
@load="onImageLoad"
@error="onImageError"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
/>
<div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name"> <div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }} {{ vehicleName.replace(/_/g, ' ') }}
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
</div>
<div class="vehicle-props" v-if="vehicleData">
{{ vehicleData.group.speed }}km/h &bull; {{ vehicleData.group.length }}m &bull;
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
</div> </div>
</div> </div>
</template> </template>
@@ -27,11 +33,13 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
tooltipStore: useTooltipStore(), tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading' imageState: 'loading'
}; };
}, },
@@ -41,7 +49,7 @@ export default defineComponent({
}, },
watch: { watch: {
'tooltipStore.type'(prev, val) { vehicleName(prev, val) {
if (prev != val) this.imageState = 'loading'; if (prev != val) this.imageState = 'loading';
} }
}, },
@@ -56,6 +64,24 @@ export default defineComponent({
(e.target as HTMLElement).style.display = 'none'; (e.target as HTMLElement).style.display = 'none';
} }
},
computed: {
vehicleName() {
return this.tooltipStore.content.split(':')[0];
},
vehicleData() {
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
},
vehicleCargo() {
const x = this.vehicleData?.group.cargoTypes?.find(
(c) => c.id == this.tooltipStore.content.split(':')[1]
);
return x;
}
} }
}); });
</script> </script>
@@ -64,17 +90,24 @@ export default defineComponent({
.tooltip-content { .tooltip-content {
width: 300px; width: 300px;
min-height: 200px; min-height: 200px;
background-color: #333; background-color: #1f1f1f;
box-shadow: 0 0 10px 2px #aaa; box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em; padding: 0.5em;
border-radius: 0.5em; border-radius: 0.5em;
} }
.image-box {
position: relative;
min-height: 150px;
}
.loading-info { .loading-info {
position: absolute; position: absolute;
left: 50%; left: 50%;
transform: translateX(-50%); top: 50%;
font-size: 1.15em;
transform: translate(-50%, -50%);
} }
img { img {
@@ -85,10 +118,13 @@ img {
.vehicle-name { .vehicle-name {
text-align: center; text-align: center;
margin-top: 0.5em; margin-top: 0.5em;
color: #ccc;
text-wrap: wrap; text-wrap: wrap;
} }
.vehicle-props {
color: #ccc;
}
.error-placeholder { .error-placeholder {
height: 176px; height: 176px;
} }
+91 -61
View File
@@ -1,20 +1,22 @@
<template> <template>
<span class="stop-label" :data-sbl="stop.isSBL"> <span
<span class="name" v-html="stop.nameHtml"></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="stop-name" v-html="stop.nameHtml"></span>
</router-link>
<span v-else class="stop-name" v-html="stop.nameHtml"></span>
<span <span
v-if="stop.position != 'begin'" v-if="stop.position != 'begin'"
class="date arrival" class="date arrival"
:data-status=" :data-status-delayed="stop.arrivalDelay > 0"
stop.arrivalDelay > 0 && stop.status != 'unconfirmed' :data-status-preponed="stop.arrivalDelay < 0"
? 'delayed' :data-status="stop.status"
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
> >
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'"> <span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s> <s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }} {{ timestampToString(stop.arrivalReal) }}
@@ -27,19 +29,16 @@
</span> </span>
<span <span
v-if=" v-if="stop.duration"
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
"
class="date stop" class="date stop"
:data-stop-types="stop.type.replace(', ', '-')" :data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status=" :data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
> >
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }} {{
stop.duration == 0 && stop.departureDelay > 0
? stop.departureDelay - stop.arrivalDelay
: stop.duration
}}
{{ stop.type == '' ? 'pt' : stop.type }} {{ stop.type == '' ? 'pt' : stop.type }}
</span> </span>
@@ -49,16 +48,11 @@
(stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay) (stop.duration != 0 || stop.status == 'stopped' || stop.departureDelay != stop.arrivalDelay)
" "
class="date departure" class="date departure"
:data-status=" :data-status-delayed="stop.departureDelay > 0"
stop.departureDelay > 0 && stop.status == 'confirmed' :data-status-preponed="stop.departureDelay < 0"
? 'delayed' :data-status-confirmed="stop.status == 'confirmed'"
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
> >
o.
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'"> <span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
<s>{{ timestampToString(stop.departureScheduled) }}</s> <s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }} {{ timestampToString(stop.departureReal) }}
@@ -76,7 +70,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import { TrainScheduleStop } from './TrainSchedule.vue'; import { TrainScheduleStop } from './typings';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin], mixins: [dateMixin],
@@ -86,6 +80,12 @@ export default defineComponent({
type: Object as PropType<TrainScheduleStop>, type: Object as PropType<TrainScheduleStop>,
required: true required: true
} }
},
computed: {
sceneryHref() {
return `/scenery?station=${this.stop.sceneryName}&checkpoint=${this.stop.nameRaw}`;
}
} }
}); });
</script> </script>
@@ -96,27 +96,20 @@ $delayedClr: salmon;
$dateClr: #525151; $dateClr: #525151;
$stopExchangeClr: #db8e29; $stopExchangeClr: #db8e29;
$stopDefaultClr: #252525; $stopDefaultClr: #252525;
$stopNameClr: #22a8d1; $stopNameClr: #303030;
s {
color: #ccc;
}
.stop-label { .stop-label {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
&[data-sbl='true'] { .stop-name {
.date {
display: none;
}
.name {
background: none;
color: #aaa;
padding: 0;
}
}
.name {
background: $stopNameClr; background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
display: flex; display: flex;
@@ -130,6 +123,22 @@ $stopNameClr: #22a8d1;
.date { .date {
background: $dateClr; background: $dateClr;
padding: 0.3em 0.5em; padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
}
&[data-minor='true'] {
.date {
display: none;
}
.stop-name {
background: none;
color: #aaa;
padding: 0;
}
} }
.stop { .stop {
@@ -145,27 +154,48 @@ $stopNameClr: #22a8d1;
color: $delayedClr; color: $delayedClr;
} }
} }
}
.arrival, .stop-label > a {
.departure { z-index: 0;
&[data-status='delayed'] { }
s {
color: #999;
}
span { .stop .arrival {
color: $delayedClr; &[data-status='confirmed'][data-status-delayed='true'] {
} span {
color: $delayedClr;
} }
}
&[data-status='preponed'] { &[data-status='confirmed'][data-status-preponed='true'] {
s { span {
color: #999; color: $preponedClr;
} }
}
span { &[data-status='stopped'][data-status-preponed='true'] {
color: $preponedClr; 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;
} }
} }
} }
+142 -74
View File
@@ -2,47 +2,60 @@
<div class="train-info" :data-extended="extended"> <div class="train-info" :data-extended="extended">
<section class="train-general"> <section class="train-general">
<div class="general-top-bar"> <div class="general-top-bar">
<div> <div class="top-bar-header">
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b> <b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData"> <span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }} #{{ train.timetableData.timetableId }}
</span> </span>
<span <span
class="timetable-warnings" class="train-badge twr"
v-if="train.timetableData?.TWR || train.timetableData?.SKR" v-if="train.timetableData?.TWR"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.TWR')"
> >
<span TWR
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> </span>
<strong> <span
<span v-if="train.timetableData" class="text--primary" class="train-badge tn"
>{{ train.timetableData.category }}&nbsp;</span v-if="train.timetableData?.hasDangerousCargo"
> data-tooltip-type="BaseTooltip"
<span class="train-number">{{ train.trainNo }}</span> :data-tooltip-content="$t('warnings.TN')"
</strong>
<span>&bull;</span>
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
> >
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }} TN
</span>
<span
class="train-badge pn"
v-if="train.timetableData?.hasExtraDeliveries"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('warnings.PN')"
>
PN
</span>
<b
v-if="train.timetableData"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="getCategoryExplanation(train.timetableData.category)"
class="text--primary tooltip-help"
>
{{ train.timetableData.category }}
</b> </b>
<b class="train-number">{{ train.trainNo }}</b>
<span>&bull;</span>
<div class="train-driver"> <div class="train-driver">
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<b <b
v-if="apiStore.donatorsData.includes(train.driverName)" v-if="apiStore.donatorsData.includes(train.driverName)"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
@@ -55,19 +68,6 @@
<span v-else>{{ train.driverName }}</span> <span v-else>{{ train.driverName }}</span>
</div> </div>
</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>
<div class="general-timetable" v-if="train.timetableData"> <div class="general-timetable" v-if="train.timetableData">
@@ -88,7 +88,7 @@
<div class="general-stops" v-if="train.timetableData"> <div class="general-stops" v-if="train.timetableData">
<span v-if="train.timetableData.followingStops.length > 2"> <span v-if="train.timetableData.followingStops.length > 2">
{{ $t('trains.via-title') }} {{ $t('trains.via-title') }}
<span v-html="displayStopList(train.timetableData.followingStops)"></span> <span v-html="getTrainStopsHtml(train.timetableData.followingStops)"></span>
</span> </span>
</div> </div>
@@ -97,21 +97,22 @@
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" /> <ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="progress-distance"> <span class="progress-distance">
&nbsp; {{ currentDistance(train.timetableData.followingStops) }} km / <span>{{ currentDistance(train.timetableData.followingStops) }} km</span>
<span class="text--primary"> {{ train.timetableData.routeDistance }} km </span> <span>/</span>
| <span class="text--primary">{{ train.timetableData.routeDistance }} km </span>
<span>|</span>
<span v-html="currentDelay(train.timetableData.followingStops)"></span> <span v-html="currentDelay(train.timetableData.followingStops)"></span>
</span> </span>
</div> </div>
<div class="status-badges"> <div class="status-badges">
<div v-if="!train.currentStationHash" class="train-badge offline"> <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') }} {{ $t('trains.scenery-offline') }}
</div> </div>
<div v-if="!train.online" class="train-badge offline"> <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) }} Offline {{ lastSeenMessage(train.lastSeen) }}
</div> </div>
</div> </div>
@@ -131,12 +132,53 @@
<div> <div>
<img src="/images/icon-speed.svg" alt="speed icon" /> <img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h {{ train.speed }} km/h
<span v-if="stockSpeedLimit != Infinity">
&bull;
<em
class="text--grayed"
style="text-decoration: underline dotted"
tabindex="0"
:data-tooltip="$t('trains.vmax-tooltip')"
>
{{ stockSpeedLimit }} km/h
</em>
</span>
</div> </div>
</div> </div>
<div class="text--grayed" style="margin-top: 0.25em"> <div class="text--grayed" style="margin-top: 0.25em">
{{ displayTrainPosition(train) }} {{ displayTrainPosition(train) }}
</div> </div>
<div
class="train-dangers"
v-if="extended && train.timetableData && train.timetableData.warningNotes"
>
<div class="dangers-badges">
<div v-if="train.timetableData?.TWR">
<div class="train-badge twr">TWR</div>
- {{ $t('warnings.TWR') }}
</div>
<div v-if="train.timetableData?.hasDangerousCargo">
<div class="train-badge tn">TN</div>
- {{ $t('warnings.TN') }}
</div>
<div v-if="train.timetableData?.hasExtraDeliveries">
<div class="train-badge pn">PN</div>
- {{ $t('warnings.PN') }}
</div>
</div>
<div class="dangers-notes">
<h4>{{ $t('warnings.header-title') }}</h4>
<p>
<i>{{ train.timetableData?.warningNotes }}</i>
</p>
</div>
</div>
</section> </section>
<section class="train-stats" v-if="!extended"> <section class="train-stats" v-if="!extended">
@@ -167,11 +209,11 @@ import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common'; import { Train } from '../../typings/common';
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin], mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
components: { ProgressBar, StockList }, components: { ProgressBar, StockList },
props: { props: {
@@ -191,16 +233,23 @@ export default defineComponent({
}; };
}, },
methods: { computed: {
navigateToJournal() { stockSpeedLimit() {
this.$router.push({ 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', path: '/journal/timetables',
query: { query: {
'search-driver': this.train.driverName 'search-driver': this.train.driverName
} }
}); };
this.closeModal();
} }
} }
}); });
@@ -227,6 +276,28 @@ export default defineComponent({
line-height: 1.5em; line-height: 1.5em;
} }
.train-dangers {
margin-top: 0.5em;
}
.dangers-badges {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.dangers-notes {
margin-top: 0.5em;
white-space: pre-wrap;
p {
margin-top: 0.25em;
max-height: 200px;
max-width: 500px;
overflow: auto;
}
}
.train-info { .train-info {
display: grid; display: grid;
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
@@ -242,6 +313,12 @@ export default defineComponent({
gap: 0.5em; gap: 0.5em;
} }
.train-driver {
display: flex;
align-items: center;
gap: 0.25em;
}
.train-driver img { .train-driver img {
max-height: 20px; max-height: 20px;
vertical-align: text-bottom; vertical-align: text-bottom;
@@ -273,24 +350,15 @@ export default defineComponent({
.general-top-bar { .general-top-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
& > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
}
} }
.btn-timetable { .top-bar-header {
padding: 0.25em; display: flex;
} align-items: center;
flex-wrap: wrap;
.btn-exit { gap: 0.25em;
padding: 0.25em;
} }
.general-status { .general-status {
@@ -337,10 +405,14 @@ export default defineComponent({
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em 0;
} }
.progress-distance { .progress-distance {
margin-right: 0.25em; display: flex;
flex-wrap: wrap;
gap: 0.25em;
} }
.timetable-warnings { .timetable-warnings {
@@ -353,9 +425,5 @@ export default defineComponent({
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1em 0; gap: 1em 0;
} }
.btn-timetable > span {
display: none;
}
} }
</style> </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_content">
<div class="search-box"> <div class="search-box">
<input <input
v-model="searchedTrain"
class="search-input" class="search-input"
ref="initFocusedElement" ref="initFocusedElement"
id="train-search"
name="train-search"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
:placeholder="$t(`options.search-train`)" :placeholder="$t(`options.search-train`)"
v-model="searchedTrain"
/> />
<button class="search-exit"> <button class="search-exit">
<img <img
@@ -33,11 +35,13 @@
<div class="search-box"> <div class="search-box">
<input <input
v-model="searchedDriver"
class="search-input" class="search-input"
id="driver-search"
name="driver-search"
@focus="preventKeyDown = true" @focus="preventKeyDown = true"
@blur="preventKeyDown = false" @blur="preventKeyDown = false"
:placeholder="$t(`options.search-driver`)" :placeholder="$t(`options.search-driver`)"
v-model="searchedDriver"
/> />
<button class="search-exit"> <button class="search-exit">
<img <img
+126 -77
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type" :data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive" :data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed" :data-last-confirmed="stop.isLastConfirmed"
x
> >
<span class="stop_info"> <span class="stop_info">
<span class="distance"> <span class="distance">
@@ -48,26 +47,78 @@
<span <span
v-if=" v-if="
stop.departureLine && stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine && scheduleStops[i + 1] != undefined &&
!/sbl/gi.test(stop.departureLine) !/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
" "
> >
{{ stop.departureLine }} <div class="scenery-route">
</span> <span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
| {{ stop.departureLineInfo.routeSpeed }}
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)"> <img
<div>{{ stop.departureLine }}</div> :src="
<div stop.departureLineInfo.isElectric
class="scenery-change-name" ? '/images/icon-catenary.svg'
v-if=" : '/images/icon-we4a.png'
i < scheduleStops.length - 1 && "
stop.sceneryName != scheduleStops[i + 1].sceneryName width="10"
" data-tooltip-type="BaseTooltip"
> :data-tooltip-content="
{{ scheduleStops[i + 1].sceneryName }} $t(
`trains.${!stop.departureLineInfo.isElectric ? 'no-' : ''}catenary-tooltip`
)
"
/>
<img
v-if="stop.departureLineInfo.isRouteSBL"
src="/images/icon-sbl-transparent.svg"
width="10"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('trains.sbl-tooltip')"
/>
</span>
</div> </div>
<div>
{{ scheduleStops[i + 1].arrivalLine }} <div
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
class="scenery-change-name"
>
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
<span v-if="stop.departureLineInfo?.routeTracks == 1"> &UpDownArrow;</span>
<span v-else> &DownArrowUpArrow;</span>
</div>
<div class="scenery-route">
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
<img
:src="
scheduleStops[i + 1].arrivalLineInfo!.isElectric
? '/images/icon-catenary.svg'
: '/images/icon-we4a.png'
"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="
$t(
`trains.${!scheduleStops[i + 1].arrivalLineInfo!.isElectric ? 'no-' : ''}catenary-tooltip`
)
"
width="10"
/>
<img
v-if="scheduleStops[i + 1].arrivalLineInfo!.isRouteSBL"
src="/images/icon-sbl-transparent.svg"
width="10"
data-tooltip-type="BaseTooltip"
:data-tooltip-content="$t('trains.sbl-tooltip')"
/>
</span>
</div> </div>
</span> </span>
</div> </div>
@@ -85,40 +136,8 @@ import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common'; import { StationRoutesInfo, Train } from '../../typings/common';
import { TrainScheduleStop } from './typings';
export interface TrainScheduleStop {
nameHtml: string;
nameRaw: string;
status: 'confirmed' | 'unconfirmed' | 'stopped';
type: string;
position: 'begin' | 'end' | 'en-route';
arrivalScheduled: number;
arrivalReal: number;
departureScheduled: number;
departureReal: number;
departureDelay: number;
arrivalDelay: number;
duration: number | null;
isActive: boolean;
isLastConfirmed: boolean;
isSBL: boolean;
sceneryName: string | null;
sceneryHash: string;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
comments: string | null;
}
export default defineComponent({ export default defineComponent({
components: { StopLabel, StockList }, components: { StopLabel, StockList },
@@ -142,17 +161,29 @@ export default defineComponent({
computed: { computed: {
scheduleStops(): TrainScheduleStop[] { scheduleStops(): TrainScheduleStop[] {
let currentSceneryIndex = 0; if (!this.train.timetableData) return [];
const { timetablePath } = this.train.timetableData;
let currentPathIndex = 0;
return ( return (
this.train.timetableData?.followingStops.map((stop, i, arr) => { this.train.timetableData?.followingStops.map((stop, i, arr) => {
if ( const isExternal =
i > 0 && i < arr.length - 1 &&
stop.arrivalLine && stop.departureLine === timetablePath[currentPathIndex].departureRouteExt;
stop.arrivalLine != arr[i - 1].departureLine &&
!/sbl/gi.test(stop.arrivalLine) const sceneryName = timetablePath[currentPathIndex].stationName;
) const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
currentSceneryIndex++;
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.arrivalLine
);
const departureLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.departureLine
);
if (isExternal) currentPathIndex++;
return { return {
nameHtml: stop.stopName, nameHtml: stop.stopName,
@@ -174,14 +205,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine, arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine, departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType, type: stop.stopType,
distance: stop.stopDistance, distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i), isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere, isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName), isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route', position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '', sceneryName,
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed' status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
}; };
}) ?? [] }) ?? []
@@ -210,7 +245,7 @@ export default defineComponent({
i < this.train.timetableData!.followingStops.length; i < this.train.timetableData!.followingStops.length;
i++ 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); activeMinorStopList.push(i);
else break; else break;
} }
@@ -247,7 +282,7 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
} }
.train-schedule { .train-schedule {
padding: 0 1em; padding: 1em;
} }
.schedule-wrapper { .schedule-wrapper {
@@ -483,22 +518,36 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
} }
} }
.bottom-line-info { .scenery-route {
.scenery-change-name { display: flex;
position: relative; gap: 0.25em;
margin: 0.25em 0;
&::before { span:nth-child(2) {
content: ''; display: flex;
position: absolute; gap: 0.25em;
height: 2px; align-items: center;
width: 30px; }
background-color: #aaa;
top: 50%; img {
right: calc(100% + 5px); width: 1em;
transform: translate(0, -50%); cursor: help;
} }
}
.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> </style>
+13 -38
View File
@@ -1,5 +1,9 @@
<template> <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> <div class="bg" v-if="showOptions" @click="showOptions = false"></div>
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button"> <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 v-if="apiStore.dataStatuses.connection == Status.Loaded && regionTrains.length > 0">
<div class="top-list general"> <div class="top-list general">
<transition-group tag="ul" name="stats-anim"> <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>{{ $t('train-stats.timetable-count') }}</span>
<span> <span>
<b>{{ regionTrainsWithTT.length }}</b> <b>{{ regionTrainsWithTT.length }}</b>
</span> </span>
</li> </li>
<li class="badge" key="avg-speed"> <li class="badge stat-badge" key="avg-speed">
<span>{{ $t('train-stats.avg-speed') }}</span> <span>{{ $t('train-stats.avg-speed') }}</span>
<span> <span>
<b>{{ stats.avgSpeed.toFixed(1) }} km/h</b> <b>{{ stats.avgSpeed.toFixed(1) }} km/h</b>
</span> </span>
</li> </li>
<li class="badge" key="avg-distance"> <li class="badge stat-badge" key="avg-distance">
<span>{{ $t('train-stats.avg-timetable') }}</span> <span>{{ $t('train-stats.avg-timetable') }}</span>
<span> <span>
<b>{{ stats.avgDistance.toFixed(1) }} km</b> <b>{{ stats.avgDistance.toFixed(1) }} km</b>
@@ -46,7 +50,7 @@
<h3>{{ $t('train-stats.top-categories') }}</h3> <h3>{{ $t('train-stats.top-categories') }}</h3>
<transition-group tag="ul" name="stats-anim"> <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.name }}</span>
<span>{{ top.count }}</span> <span>{{ top.count }}</span>
</li> </li>
@@ -61,7 +65,7 @@
<h3>{{ $t('train-stats.top-vehicles') }}</h3> <h3>{{ $t('train-stats.top-vehicles') }}</h3>
<transition-group tag="ul" name="stats-anim"> <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.name }}</span>
<span>{{ top.count }}</span> <span>{{ top.count }}</span>
</li> </li>
@@ -76,7 +80,7 @@
<h3>{{ $t('train-stats.top-units') }}</h3> <h3>{{ $t('train-stats.top-units') }}</h3>
<transition-group tag="ul" name="stats-anim"> <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.name }}</span>
<span>{{ top.count }}</span> <span>{{ top.count }}</span>
</li> </li>
@@ -95,6 +99,8 @@
<div class="no-data" v-else> <div class="no-data" v-else>
{{ $t('train-stats.no-stats') }} {{ $t('train-stats.no-stats') }}
</div> </div>
<div tabindex="0" @focus="() => (showOptions = false)"></div>
</div> </div>
</transition> </transition>
</div> </div>
@@ -236,43 +242,12 @@ h3 {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
// @include smallScreen {
// justify-content: center;
// }
}
.badge {
margin: 0;
& > span:first-child {
background-color: $accentCol;
color: black;
}
} }
.dropdown_wrapper { .dropdown_wrapper {
max-width: 600px; 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 { @include smallScreen {
h1, h1,
.no-data { .no-data {
+10 -27
View File
@@ -8,19 +8,19 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" /> <Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0"> <div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }} {{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
>)
</div> </div>
<transition-group name="list-anim" tag="ul"> <transition-group name="list-anim" tag="ul">
<li <li
class="train-row" class="train-row"
v-for="train in trains" v-for="train in trains"
:key="train.trainId" :key="train.id"
tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<TrainInfo :train="train" :extended="false" /> <router-link class="a-block" :to="train.driverRouteLocation">
<TrainInfo :train="train" :extended="false" />
</router-link>
</li> </li>
</transition-group> </transition-group>
</div> </div>
@@ -29,7 +29,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, PropType, Ref } from 'vue'; import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
@@ -46,8 +45,6 @@ export default defineComponent({
} }
}, },
mixins: [modalTrainMixin],
setup() { setup() {
const store = useMainStore(); const store = useMainStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
@@ -76,17 +73,6 @@ export default defineComponent({
return Status.Data.Loaded; return Status.Data.Loaded;
} }
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
} }
}); });
</script> </script>
@@ -96,10 +82,10 @@ export default defineComponent({
@import '../../styles/animations.scss'; @import '../../styles/animations.scss';
.train-table { .train-table {
position: relative; height: calc(100vh - 11em);
min-height: 500px;
height: 90vh; position: relative;
min-height: 550px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
} }
@@ -108,8 +94,7 @@ export default defineComponent({
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
font-size: 1.25em;
font-size: 1.5em;
background: #1a1a1a; background: #1a1a1a;
} }
@@ -118,7 +103,5 @@ li.train-row {
background-color: var(--clr-secondary); background-color: var(--clr-secondary);
margin-bottom: 1em; margin-bottom: 1em;
width: 100%; width: 100%;
cursor: pointer;
} }
</style> </style>
+48 -2
View File
@@ -1,3 +1,5 @@
import { StationRoutesInfo } from "../../typings/common";
export enum TrainFilterSection { export enum TrainFilterSection {
TRAIN_TYPE = 'TRAIN_TYPE', TRAIN_TYPE = 'TRAIN_TYPE',
TIMETABLE_TYPE = 'TIMETABLE_TYPE', TIMETABLE_TYPE = 'TIMETABLE_TYPE',
@@ -10,12 +12,14 @@ export const enum TrainFilterId {
withComments = 'withComments', withComments = 'withComments',
twr = 'twr', twr = 'twr',
skr = 'skr', tn = 'tn',
pn = 'pn',
common = 'common', common = 'common',
passenger = 'passenger', passenger = 'passenger',
freight = 'freight', freight = 'freight',
other = 'other', other = 'other',
noTimetable = 'noTimetable', noTimetable = 'noTimetable',
withTimetable = 'withTimetable' withTimetable = 'withTimetable'
} }
@@ -38,7 +42,12 @@ export const trainFilters: TrainFilter[] = [
isActive: true isActive: true
}, },
{ {
id: TrainFilterId.skr, id: TrainFilterId.tn,
section: TrainFilterSection.TRAIN_TYPE,
isActive: true
},
{
id: TrainFilterId.pn,
section: TrainFilterSection.TRAIN_TYPE, section: TrainFilterSection.TRAIN_TYPE,
isActive: true isActive: true
}, },
@@ -117,3 +126,40 @@ export const sorterOptions: TrainSorter[] = [
value: 'długość' value: 'długość'
} }
]; ];
export interface TrainScheduleStop {
nameHtml: string;
nameRaw: string;
status: 'confirmed' | 'unconfirmed' | 'stopped';
type: string;
position: 'begin' | 'end' | 'en-route';
arrivalScheduled: number;
arrivalReal: number;
departureScheduled: number;
departureReal: number;
departureDelay: number;
arrivalDelay: number;
duration: number | null;
isActive: boolean;
isLastConfirmed: boolean;
isSBL: boolean;
sceneryName: string | null;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null;
}
File diff suppressed because it is too large Load Diff
-365
View File
@@ -1,369 +1,4 @@
{ {
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"station-type",
"access",
"control",
"blockades",
"signals",
"addons"
],
"options": [
{
"id": "real",
"name": "real",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "fictional",
"name": "fictional",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "non-public",
"name": "nonPublic",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "unavailable",
"name": "unavailable",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "abandoned",
"name": "abandoned",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "junction",
"name": "junction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "nonJunction",
"name": "nonJunction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "SPK",
"name": "SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS",
"name": "SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPE",
"name": "SPE",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-M",
"name": "mechaniczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "mechanical",
"name": "mechaniczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SUP",
"name": "SUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "ASDEK",
"name": "ASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noASDEK",
"name": "noASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "SBL",
"name": "SBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "modern",
"name": "współczesna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "semaphores",
"name": "kształtowa",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "mixed",
"name": "mieszana",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "historical",
"name": "historyczna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "free",
"name": "free",
"section": "status",
"value": false,
"defaultValue": false
},
{
"id": "occupied",
"name": "occupied",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "endingStatus",
"name": "endingStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "afkStatus",
"name": "afkStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "noSpaceStatus",
"name": "noSpaceStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "unavailableStatus",
"name": "unavailableStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
}
],
"sliders": [
{
"id": "min-lvl",
"name": "minLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "max-lvl",
"name": "maxLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 20,
"defaultValue": 20
},
{
"id": "min-vmax",
"name": "minVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 0,
"defaultValue": 0
},
{
"id": "max-vmax",
"name": "maxVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 200,
"defaultValue": 200
},
{
"id": "routes-1t-cat",
"name": "minOneWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-1t-other",
"name": "minOneWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-cat",
"name": "minTwoWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-other",
"name": "minTwoWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
}
],
"modes": [
{
"id": "include-selected",
"name": "include-selected",
"section": "mode",
"value": true,
"defaultValue": true
},
{
"id": "save",
"name": "save",
"section": "mode",
"value": true,
"defaultValue": true
}
],
"regions": [ "regions": [
{ {
"id": "eu", "id": "eu",
+115 -43
View File
@@ -20,11 +20,16 @@
"dispatcher-message": "Dispatcher supporting the Stacjownik project!", "dispatcher-message": "Dispatcher supporting the Stacjownik project!",
"driver-message": "Driver supporting the Stacjownik project!" "driver-message": "Driver supporting the Stacjownik project!"
}, },
"warnings": {
"TWR": "Train with high risk cargo",
"SKR": "Train with exceeded gauge",
"PN": "Train with extra deliveries",
"TN": "Train with dangerous cargo",
"header-title": "Freight notes:"
},
"general": { "general": {
"and": " and ", "and": " and ",
"refresh": "REFRESH", "refresh": "REFRESH"
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
}, },
"update": { "update": {
"title": "Stacjownik update!", "title": "Stacjownik update!",
@@ -48,7 +53,50 @@
"footer": { "footer": {
"discord": "Stacjownik Discord server" "discord": "Stacjownik Discord server"
}, },
"categories": {
"EI": "domestic express",
"EC": "international express",
"EN": "domestic night express",
"MP": "intervoivodeship bullet",
"MO": "intervoivodeship regio",
"MM": "international bullet",
"MH": "intervoivodeship night bullet",
"RP": "voivodeship bullet",
"RM": "international voivodeship regio",
"RO": "voivodeship regio",
"RA": "voivodeship regio (urban)",
"PW": "empty passenger",
"PX": "empty passenger test drive",
"TC": "international freight (intermodal)",
"TG": "international freight (organized cargo)",
"TR": "international freight (unorganized cargo)",
"TD": "domestic freight (intermodal)",
"TM": "domestic freight (organized cargo)",
"TN": "domestic freight (unorganized cargo)",
"TK": "freight (for stations & sidings)",
"TS": "empty freight test drive",
"TH": "locomotive rolling stock (over 3 vehicles)",
"LT": "freight locomotive only",
"LP": "passenger locomotive only",
"LS": "shunting locomotive only",
"LZ": "shunting locomotive only",
"ZN": "inspection / diagnostic type",
"ZU": "other maintenance type",
"ZG": "emergency (deprecated)",
"AP": "voivodeship regio (deprecated)",
"E": "electric loco",
"J": "EMU",
"S": "diesel loco",
"M": "DMU"
},
"vehicle-preview": { "vehicle-preview": {
"loading": "Loading preview...", "loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/" "error": "Oops! The vehicle preview seems to be missing! :/"
@@ -112,8 +160,8 @@
"filters": "FILTERS", "filters": "FILTERS",
"donate": "DONATE", "donate": "DONATE",
"search-button": "Search", "search-button": "SEARCH",
"reset-button": "Reset", "reset-button": "RESET",
"sort-title": "SORT BY:", "sort-title": "SORT BY:",
"filter-title": "FILTER BY:", "filter-title": "FILTER BY:",
@@ -123,9 +171,11 @@
"search-train": "Train no.", "search-train": "Train no.",
"search-driver": "Driver name", "search-driver": "Driver name",
"search-dispatcher": "Dispatcher name", "search-dispatcher": "Dispatcher name",
"search-station": "Scenery name", "search-station": "Scenery name / #",
"search-author": "Timetable author name", "search-author": "Timetable author name",
"search-issuedFrom": "Origin scenery name", "search-issuedFrom": "Issuing scenery name",
"search-via": "Via scenery name",
"search-terminatingAt": "Terminating scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)", "search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)", "search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)", "search-date": "Date (UTC+2 / CEST)",
@@ -143,15 +193,17 @@
"sort-beginDate": "date", "sort-beginDate": "date",
"sort-timetableId": "timetable ID", "sort-timetableId": "timetable ID",
"sort-timestampFrom": "date", "sort-timestampFrom": "date",
"sort-duration": "duration", "sort-currentDuration": "duration",
"filter-noComments": "NO COMMENTS", "filter-noComments": "NO COMMENTS",
"filter-withComments": "COMMENTS", "filter-withComments": "COMMENTS",
"filter-twr": "HIGH RISK CARGO", "filter-twr": "TWR",
"filter-skr": "EXCEEDED GAUGE", "filter-skr": "SKR",
"filter-tn": "TN",
"filter-pn": "PN",
"filter-twr-skr": "BOTH TYPES", "filter-twr-skr": "BOTH TYPES",
"filter-all-specials": "ALL", "filter-all-specials": "ALL",
"filter-common": "NO WARNINGS", "filter-common": "COMMON",
"filter-passenger": "PASSENGER", "filter-passenger": "PASSENGER",
"filter-freight": "FREIGHT", "filter-freight": "FREIGHT",
"filter-other": "OTHER", "filter-other": "OTHER",
@@ -174,9 +226,9 @@
"sections": { "sections": {
"quick": "QUICK FILTERS", "quick": "QUICK FILTERS",
"station-type": "STATION TYPE", "stationType": "STATION TYPE",
"reality": "SCENERY REALITY", "reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY", "packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY", "access": "GENERAL AVAILABILITY",
"control": "CONTROLS", "control": "CONTROLS",
"signals": "SIGNALLING", "signals": "SIGNALLING",
@@ -187,6 +239,9 @@
"spawns": "OPEN SPAWNS" "spawns": "OPEN SPAWNS"
}, },
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE", "all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE", "all-free": "CURRENTLY FREE",
@@ -197,11 +252,11 @@
"title": "STATION FILTERS", "title": "STATION FILTERS",
"default": "IN-GAME", "default": "IN-GAME",
"not-default": "ADDITIONAL", "notDefault": "ADDITIONAL",
"real": "REAL", "real": "REAL",
"fictional": "FICTIONAL", "fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED", "unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC", "nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED", "abandoned": "ABANDONED",
"SPK": "SPK", "SPK": "SPK",
@@ -211,7 +266,6 @@
"SCS-R": "SCS + MANUAL", "SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.", "SCS-M": "SCS + MECH.",
"SPE": "SPE", "SPE": "SPE",
"manual": "MANUAL", "manual": "MANUAL",
"mechanical": "MECHANICAL", "mechanical": "MECHANICAL",
@@ -238,19 +292,21 @@
"nonJunction": "OTHER", "nonJunction": "OTHER",
"sliders": { "sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL", "minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL", "maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED", "minVmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED", "maxVmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES", "minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES", "minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES", "minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER 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-search": "SEARCH BY AUTHOR NAME (other filters apply):",
"authors-placeholder": "Enter the author nickname...", "authors-placeholder": "Enter the author nickname...",
"authors-button-title": "Search", "search-button-title": "SEARCH",
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:", "minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
"now": "NOW", "now": "NOW",
@@ -298,12 +354,13 @@
"single-track-routes-other": "Not electrified single-track routes count: " "single-track-routes-other": "Not electrified single-track routes count: "
}, },
"no-stations": "No stations to show here!", "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": { "station-stats": {
"u-factor": "U-factor", "u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)", "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:", "single-track-count": "Single track routes:",
"double-track-count": "Double track routes:", "double-track-count": "Double track routes:",
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)", "cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
@@ -326,6 +383,12 @@
"current-signal": "at signal", "current-signal": "at signal",
"current-track": "on track", "current-track": "on track",
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
"catenary-tooltip": "Electrified route",
"no-catenary-tooltip": "Non-electrified route",
"sbl-tooltip": "Route with SBL\n(automatic block signalling)",
"delayed": "Delayed: ", "delayed": "Delayed: ",
"preponed": "Ahead of schedule: ", "preponed": "Ahead of schedule: ",
"on-time": "On time", "on-time": "On time",
@@ -352,7 +415,15 @@
"scenery-offline": "Offline ride", "scenery-offline": "Offline ride",
"timeout": "An error occured while trying to refresh SWDR timetable data!", "timeout": "An error occured while trying to refresh SWDR timetable data!",
"journal-button": "DRIVER'S JOURNAL" "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, changed its number or is offline.",
"driver-not-found-desc-2": "You can browse timetable history in the",
"driver-not-found-journal": "TIMETABLES JOURNAL",
"driver-not-found-others": "Player {driver} is online as:",
"driver-not-found-return": "GO BACK TO THE MAIN SITE"
}, },
"train-stats": { "train-stats": {
"stats-button": "STATISTICS", "stats-button": "STATISTICS",
@@ -380,6 +451,7 @@
"no-further-data": "No further data for current parameters", "no-further-data": "No further data for current parameters",
"loading-further-data": "Loading...", "loading-further-data": "Loading...",
"route-length": "Route length:", "route-length": "Route length:",
"station-count": "Stations:", "station-count": "Stations:",
@@ -397,11 +469,16 @@
"minutes": "{value} min | {value} mins", "minutes": "{value} min | {value} mins",
"seconds": "{value} s", "seconds": "{value} s",
"stock-info": "DETAILS", "entry-details": "DETAILS",
"no-entry-details": "NO DETAILS AVAILABLE",
"stock-length": "Length", "stock-length": "Length",
"stock-mass": "Mass", "stock-mass": "Mass",
"stock-max-speed": "Max. speed", "stock-max-speed": "Max. speed",
"stock-dangers": "ADDITIONAL NOTES",
"stock-preview": "STOCK PREVIEW",
"load-data": "Load further data...", "load-data": "Load further data...",
"last-seen-at": "Last seen at", "last-seen-at": "Last seen at",
@@ -488,28 +565,23 @@
"option-timetables-history": "Timetables history PL1", "option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1", "option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by", "timetable-via": "ALL TIMETABLES",
"timetable-author-unknown": "Author unknown", "timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID", "timetable-issued-date": "Issued",
"timetables-history-number": "Number", "timetable-issued-by": " by:",
"timetables-history-route": "Route", "timetable-issued-for": " for driver:",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Rate:",
"dispatchers-history-dispatcher": "Dispatcher", "dispatcher-status-changes": "Status changes:",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No recorded scenery history!",
"forum-topic": "Official {name} forum topic", "forum-topic": "Official {name} forum topic",
"pragotron-link": "Timetable pallet board (beta)", "pragotron-link": "Timetable pallet board",
"tablice-link": "Timetable summary board (by Thundo)", "tablice-link": "Timetable summary board (by Thundo)",
"bottom-info": "Show full history in the Journal tab" "bottom-info": "Show full history in the Journal tab"
+112 -39
View File
@@ -20,11 +20,16 @@
"dispatcher-message": "Dyżurny wspierający projekt Stacjownika!", "dispatcher-message": "Dyżurny wspierający projekt Stacjownika!",
"driver-message": "Maszynista wspierający projekt Stacjownika!" "driver-message": "Maszynista wspierający projekt Stacjownika!"
}, },
"warnings": {
"TWR": "Pociąg z towarami niebezpiecznie wysokiego ryzyka",
"SKR": "Pociąg z przekroczoną skrajnią",
"PN": "Pociąg z przesyłkami nadzwyczajnymi",
"TN": "Pociąg z towarami niebezpiecznymi",
"header-title": "Uwagi przewozowe:"
},
"general": { "general": {
"and": " oraz ", "and": " oraz ",
"refresh": "ODŚWIEŻ", "refresh": "ODŚWIEŻ"
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
}, },
"update": { "update": {
"title": "Aktualizacja Stacjownika!", "title": "Aktualizacja Stacjownika!",
@@ -45,6 +50,50 @@
"footer": { "footer": {
"discord": "Serwer Discord Stacjownika" "discord": "Serwer Discord Stacjownika"
}, },
"categories": {
"EI": "ekspres krajowy",
"EC": "ekspres międzynarodowy",
"EN": "ekspres krajowy nocny",
"MP": "międzywojewódzki pospieszny",
"MO": "międzywojewódzki osobowy",
"MM": "międzynarodowy pospieszny",
"MH": "międzywojewódzki pospieszny (nocny)",
"RP": "wojewódzki pospieszny",
"RO": "wojewódzki osobowy",
"RM": "wojewódzki osobowy międzynarodowy",
"RA": "wojewódzki osobowy aglomeracyjny",
"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)",
"AP": "wojewódzki osobowy (kat. wycofana)",
"E": "elektrowóz",
"J": "EZT",
"S": "spalinowóz",
"M": "SZT"
},
"vehicle-preview": { "vehicle-preview": {
"loading": "Ładowanie podglądu...", "loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/" "error": "Ups! Nie znaleziono podglądu pojazdu! :/"
@@ -119,9 +168,11 @@
"search-train": "Nr pociągu / #", "search-train": "Nr pociągu / #",
"search-driver": "Nick maszynisty", "search-driver": "Nick maszynisty",
"search-dispatcher": "Nick dyżurnego", "search-dispatcher": "Nick dyżurnego",
"search-station": "Nazwa scenerii", "search-station": "Nazwa scenerii / #",
"search-author": "Nick autora rozkładu jazdy", "search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa", "search-issuedFrom": "Sceneria początkowa",
"search-via": "Przez scenerię",
"search-terminatingAt": "Sceneria końcowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)", "search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)", "search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)", "search-date": "Data (UTC+2 / CEST)",
@@ -131,7 +182,7 @@
"sort-beginDate": "data", "sort-beginDate": "data",
"sort-timetableId": "ID rozkładu", "sort-timetableId": "ID rozkładu",
"sort-timestampFrom": "data", "sort-timestampFrom": "data",
"sort-duration": "czas dyżuru", "sort-currentDuration": "czas dyżuru",
"sort-id": "id rozkładu", "sort-id": "id rozkładu",
"sort-mass": "masa", "sort-mass": "masa",
@@ -144,8 +195,10 @@
"filter-withComments": "UWAGI EKSPLOATACYJNE", "filter-withComments": "UWAGI EKSPLOATACYJNE",
"filter-noComments": "BEZ UWAG", "filter-noComments": "BEZ UWAG",
"filter-twr": "WYS. RYZYKA", "filter-twr": "TWR",
"filter-skr": "SKRAJNIA", "filter-skr": "SKR",
"filter-tn": "TN",
"filter-pn": "PN",
"filter-twr-skr": "TWR/SKR", "filter-twr-skr": "TWR/SKR",
"filter-all-statuses": "WSZYSTKIE", "filter-all-statuses": "WSZYSTKIE",
"filter-common": "ZWYKŁE", "filter-common": "ZWYKŁE",
@@ -171,9 +224,9 @@
"sections": { "sections": {
"quick": "SZYBKIE FILTRY", "quick": "SZYBKIE FILTRY",
"station-type": "RODZAJ STACJI", "stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII", "reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE", "packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA", "control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI", "signals": "TYP SYGNALIZACJI",
@@ -184,6 +237,9 @@
"spawns": "OTWARTE SPAWNY" "spawns": "OTWARTE SPAWNY"
}, },
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE", "all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE", "all-free": "WSZYSTKIE WOLNE",
@@ -194,11 +250,11 @@
"title": "FILTRUJ STACJE", "title": "FILTRUJ STACJE",
"default": "DOMYŚLNA", "default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ", "notDefault": "POZA PACZKĄ",
"real": "REALNA", "real": "REALNA",
"fictional": "FIKCYJNA", "fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA", "unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA", "nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA", "abandoned": "WYCOFANA",
"SPK": "SPK", "SPK": "SPK",
@@ -234,19 +290,21 @@
"nonJunction": "INNE", "nonJunction": "INNE",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA", "minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA", "maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)", "minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)", "minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (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-placeholder": "Wpisz nick autora...",
"authors-button-title": "Szukaj", "search-button-title": "SZUKAJ",
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:", "minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
"now": "TERAZ", "now": "TERAZ",
"hour": " godz.", "hour": " godz.",
@@ -291,12 +349,13 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: " "single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
}, },
"no-stations": "Brak stacji do wyświetlenia!", "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..." "scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
}, },
"station-stats": { "station-stats": {
"u-factor": "Współczynnik Ugla", "u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)", "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:", "single-track-count": "Szlaki jednotorowe:",
"double-track-count": "Szlaki dwutorowe:", "double-track-count": "Szlaki dwutorowe:",
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):", "cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
@@ -311,6 +370,12 @@
"current-signal": "przy semaforze", "current-signal": "przy semaforze",
"current-track": "na szlaku", "current-track": "na szlaku",
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
"catenary-tooltip": "Szlak zelektryfikowany",
"no-catenary-tooltip": "Szlak niezelektryfikowany",
"sbl-tooltip": "Szlak posiadający\nsamoczynną blokadę liniową",
"delayed": "Opóźniony: ", "delayed": "Opóźniony: ",
"preponed": "Przed czasem: ", "preponed": "Przed czasem: ",
"on-time": "Planowo", "on-time": "Planowo",
@@ -336,7 +401,15 @@
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR", "timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
"journal-button": "DZIENNIK MASZYNISTY" "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, zmienił numer 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-others": "Gracz {driver} jest online jako:",
"driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ"
}, },
"train-stats": { "train-stats": {
"stats-button": "STATYSTYKI", "stats-button": "STATYSTYKI",
@@ -380,11 +453,16 @@
"timetable-abandoned": "PORZUCONY", "timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE", "timetable-online-button": "RJ ONLINE",
"stock-info": "SZCZEGÓŁY", "entry-details": "SZCZEGÓŁY",
"no-entry-details": "BRAK DOSTĘPNYCH SZCZEGÓŁÓW",
"stock-length": "Długość", "stock-length": "Długość",
"stock-mass": "Masa", "stock-mass": "Masa",
"stock-max-speed": "Prędkość maks.", "stock-max-speed": "Prędkość maks.",
"stock-dangers": "DODATKOWE UWAGI",
"stock-preview": "PODGLĄD SKŁADU",
"load-data": "Pobierz dalszą historię...", "load-data": "Pobierz dalszą historię...",
"last-seen-at": "Ostatnio widziany na: ", "last-seen-at": "Ostatnio widziany na: ",
@@ -470,28 +548,23 @@
"option-timetables-history": "Historia rozkładów PL1", "option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1", "option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez", "timetable-via": "WSZYSTKIE RJ",
"timetable-author-unknown": "Autor nieznany", "timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID", "timetable-issued-date": "Wystawiony",
"timetables-history-number": "Numer", "timetable-issued-by": " przez:",
"timetables-history-route": "Trasa", "timetable-issued-for": " dla maszynisty:",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"dispatchers-history-hash": "Hash", "dispatcher-rate": "Ocena:",
"dispatchers-history-dispatcher": "Dyżurny", "dispatcher-status-changes": "Zmiany statusów:",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego", "req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!", "history-list-empty": "Brak historii dla tej scenerii!",
"forum-topic": "Oficjalny wątek scenerii {name}", "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)", "tablice-link": "Tablica informacyjna zbiorcza (autorstwa Thundo)",
"bottom-info": "Pokaż pełną historię w zakładce Dziennika" "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); window.localStorage.removeItem(key);
} }
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean { static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false; return window.localStorage.getItem(key) === 'true' ? true : false;
} }
+5 -2
View File
@@ -45,8 +45,11 @@ function filterTrainList(
case TrainFilterId.twr: case TrainFilterId.twr:
return !train.timetableData?.TWR; return !train.timetableData?.TWR;
case TrainFilterId.skr: case TrainFilterId.pn:
return !train.timetableData?.SKR; return !train.timetableData?.hasExtraDeliveries;
case TrainFilterId.tn:
return !train.timetableData?.hasDangerousCargo;
case TrainFilterId.common: case TrainFilterId.common:
return train.timetableData?.SKR || train.timetableData?.TWR; return train.timetableData?.SKR || train.timetableData?.TWR;
+6 -2
View File
@@ -57,6 +57,10 @@ export default defineComponent({
: ''; : '';
}, },
dateStringToTimestamp(dateString?: string) {
return dateString ? new Date(dateString).getTime() : 0;
},
calculateDuration(timestampMs: number, showSeconds = false) { calculateDuration(timestampMs: number, showSeconds = false) {
const secondsTotal = Math.floor(timestampMs / 1000); const secondsTotal = Math.floor(timestampMs / 1000);
const minsTotal = Math.round(timestampMs / 60000); const minsTotal = Math.round(timestampMs / 60000);
@@ -70,8 +74,8 @@ export default defineComponent({
minsInHour minsInHour
)}` )}`
: showSeconds && secondsTotal <= 60 : showSeconds && secondsTotal <= 60
? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal) ? this.$t('journal.seconds', { value: secondsTotal }, secondsTotal)
: this.$t('journal.minutes', { value: minsTotal }, minsTotal); : this.$t('journal.minutes', { value: minsTotal }, minsTotal);
} }
} }
}); });
-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); return positionString.charAt(0).toUpperCase() + positionString.slice(1);
}, },
displayStopList(stops: TrainStop[]): string | undefined { getTrainStopsHtml(stops: TrainStop[]): string {
if (!stops) return ''; if (!stops) return '';
return stops return stops
.reduce((acc: string[], stop: TrainStop, i: number) => { .reduce((acc: string[], stop: TrainStop, i: number) => {
if (stop.stopType.includes('ph') && !stop.stopNameRAW.includes('po.')) if (stop.stopType.includes('ph'))
acc.push( acc.push(
`<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${ `<strong style='color:${stop.confirmed ? 'springgreen' : 'white'}'>${
stop.stopName stop.stopName
}</strong>` }</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( acc.push(
`<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${ `<span style='color:${stop.confirmed ? 'springgreen' : 'lightgray'}'>${
stop.stopName stop.stopName
@@ -108,16 +108,19 @@ export default defineComponent({
}, },
currentDelay(stops: TrainStop[]) { currentDelay(stops: TrainStop[]) {
const delay = const lastConfirmedStop = stops.find(
stops.find( (stop, i) =>
(stop, i) => (i == 0 && !stop.confirmed) ||
(i == 0 && !stop.confirmed) || (i > 0 && stops[i - 1].confirmed && !stop.confirmed) (i > 0 && stops[i - 1].confirmed && !stop.confirmed) ||
)?.departureDelay || 0; (stops[i + 1] == undefined && stop.confirmed)
);
if (delay > 0) const lastDelay = lastConfirmedStop?.departureDelay ?? lastConfirmedStop?.arrivalDelay ?? 0;
return `<span style='color: salmon'>${this.$t('trains.delayed')} ${delay} min</span>`;
else if (delay < 0) if (lastDelay > 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${delay} min</span>`; return `<span style='color: salmon'>${this.$t('trains.delayed')} ${lastDelay} min</span>`;
else if (lastDelay < 0)
return `<span style='color: lightgreen'>${this.$t('trains.preponed')} ${lastDelay} min</span>`;
else return this.$t('trains.on-time'); else return this.$t('trains.on-time');
}, },
+16 -2
View File
@@ -20,6 +20,15 @@ const routes: Array<RouteRecordRaw> = [
region: route.query.region 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', path: '/scenery',
name: 'SceneryView', name: 'SceneryView',
@@ -57,8 +66,13 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({ const router = createRouter({
scrollBehavior(to, from, savedPosition) { scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined) if (
return { el: `.app_main` }; (to.name == 'SceneryView' || to.name == 'DriverView') &&
from.name !== to.name &&
from.query['view'] === undefined &&
!savedPosition
)
return { el: `.app_main`, top: -15 };
if (savedPosition) return savedPosition; if (savedPosition) return savedPosition;
}, },
-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;
};
+47 -21
View File
@@ -4,24 +4,22 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings'; import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', { export const useApiStore = defineStore('apiStore', {
state: () => ({ state: () => ({
dataStatuses: { dataStatuses: {
connection: Status.Data.Loading, connection: Status.Data.Loading,
sceneries: Status.Data.Loading sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
}, },
activeData: undefined as API.ActiveData.Response | undefined, activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response, donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[], sceneryData: [] as StationJSONData[],
lastFetchData: new Date(), nextUpdateTime: 0,
nextDataCheckTime: 0,
client: undefined as AxiosInstance | undefined, client: undefined as AxiosInstance | undefined,
@@ -51,21 +49,37 @@ export const useApiStore = defineStore('apiStore', {
}, },
async connectToAPI() { async connectToAPI() {
// Static data window.requestAnimationFrame(this.updateTick);
this.fetchDonatorsData(); },
this.fetchStationsGeneralInfo();
updateTick(t: number) {
if (this.dataStatuses.connection == Status.Data.Offline) return;
// Static data refresh
if (t >= this.nextDataCheckTime) {
this.fetchDonatorsData();
this.fetchVehiclesInfo();
this.fetchStationsGeneralInfo();
this.nextDataCheckTime = t + 3600000;
}
// Active data fefresh
if (t >= this.nextUpdateTime) {
this.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.updateTick);
}, },
async fetchActiveData() { async fetchActiveData() {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response.data;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded; this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) { } catch (error) {
this.dataStatuses.connection = Status.Data.Error; this.dataStatuses.connection = Status.Data.Error;
@@ -84,17 +98,29 @@ export const useApiStore = defineStore('apiStore', {
}, },
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = ( try {
await this.client!.get<StationJSONData[]>('api/getSceneries') const sceneryData: StationJSONData[] = (
).data; 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; 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; async fetchVehiclesInfo() {
this.sceneryData = sceneryData; 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);
}
} }
} }
}); });
+135 -52
View File
@@ -1,9 +1,9 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns } from './utils';
import { import {
ActiveScenery, ActiveScenery,
ScheduledTrain, CheckpointTrain,
Station, Station,
StationRoutes, StationRoutes,
Status, Status,
@@ -12,6 +12,9 @@ import {
import { useApiStore } from './apiStore'; import { useApiStore } from './apiStore';
import { MainStoreState } from './typings'; import { MainStoreState } from './typings';
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', { export const useMainStore = defineStore('mainStore', {
state: () => state: () =>
({ ({
@@ -36,6 +39,9 @@ export const useMainStore = defineStore('mainStore', {
trainList(): Train[] { trainList(): Train[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? []) return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online) .filter((train) => train.timetable || train.online)
.map((train) => { .map((train) => {
@@ -44,17 +50,9 @@ export const useMainStore = defineStore('mainStore', {
const timetable = train.timetable; const timetable = train.timetable;
const sceneryNames = const trainObj = {
train.timetable?.sceneries?.map( id: train.id,
(sceneryHash) => modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
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(),
trainNo: train.trainNo, trainNo: train.trainNo,
mass: train.mass, mass: train.mass,
@@ -79,20 +77,85 @@ export const useMainStore = defineStore('mainStore', {
isSupporter: train.driverIsSupporter, isSupporter: train.driverIsSupporter,
driverLevel: train.driverLevel, driverLevel: train.driverLevel,
driverRouteLocation: {
name: 'DriverView',
query: {
trainId: train.id
}
},
timetableData: timetable timetableData: timetable
? { ? {
timetableId: timetable.timetableId, timetableId: timetable.timetableId,
SKR: timetable.SKR,
TWR: timetable.TWR,
route: timetable.route, route: timetable.route,
category: timetable.category, category: timetable.category,
followingStops: timetable.stopList, followingStops: timetable.stopList,
routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance, routeDistance: timetable.stopList[timetable.stopList.length - 1].stopDistance,
sceneries: timetable.sceneries, sceneries: timetable.sceneries,
sceneryNames: sceneryNames.reverse() TWR: timetable.TWR,
SKR: timetable.SKR,
warningNotes: timetable.warningNotes,
hasDangerousCargo: timetable.hasDangerousCargo,
hasExtraDeliveries: timetable.hasExtraDeliveries,
timetablePath: timetable.path.split(';').map((pathElementString) => {
const [arrival, station, departure] = pathElementString.split(',');
return {
arrivalRouteExt: arrival,
departureRouteExt: departure,
stationName: station.split(' ').slice(0, -1).join(' '),
stationHash: station.split(' ').slice(-1).join(' ').replace('.sc', '')
};
})
} }
: undefined : undefined
} as Train; } as Train;
// 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 +170,15 @@ export const useMainStore = defineStore('mainStore', {
const offlineActiveSceneries = this.trainList.reduce((acc, train) => { const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
train.timetableData.sceneryNames.forEach((name) => { train.timetableData.timetablePath.forEach((p) => {
if ( 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( apiStore.activeData?.activeSceneries?.findIndex(
(sc) => (sc) =>
sc.stationName === name && (sc.stationName == p.stationName || sc.stationHash == p.stationHash) &&
sc.region == train.region && sc.region == train.region &&
Date.now() - sc.lastSeen < 1000 * 60 * 2 Date.now() - sc.lastSeen < 1000 * 60 * 2
) != -1 ) != -1
@@ -120,7 +186,7 @@ export const useMainStore = defineStore('mainStore', {
return acc; return acc;
acc.push({ acc.push({
name: name, name: p.stationName,
hash: '', hash: '',
region: train.region, region: train.region,
maxUsers: 0, maxUsers: 0,
@@ -131,13 +197,14 @@ export const useMainStore = defineStore('mainStore', {
dispatcherId: -1, dispatcherId: -1,
dispatcherExp: -1, dispatcherExp: -1,
dispatcherIsSupporter: false, dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE, dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1, dispatcherTimestamp: -1,
isOnline: false, isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -149,16 +216,20 @@ export const useMainStore = defineStore('mainStore', {
return acc; return acc;
}, [] as ActiveScenery[]); }, [] as ActiveScenery[]);
const referenceTimestamp = Date.now();
const onlineActiveSceneries = apiStore.activeData?.activeSceneries.reduce((list, scenery) => { const onlineActiveSceneries = apiStore.activeData?.activeSceneries.reduce((list, scenery) => {
if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return list; if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return list;
if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return list; if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return list;
const dispatcherTimestamp = const dispatcherTimestamp =
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
? Date.now() + 25500000 ? referenceTimestamp + 25500000
: scenery.dispatcherStatus > 5 : scenery.dispatcherStatus > 5
? scenery.dispatcherStatus ? scenery.dispatcherStatus
: null; : null;
list.push({ list.push({
name: scenery.stationName, name: scenery.stationName,
@@ -177,8 +248,9 @@ export const useMainStore = defineStore('mainStore', {
isOnline: scenery.isOnline == 1, isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [], stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: 0, all: 0,
confirmed: 0, confirmed: 0,
@@ -196,37 +268,45 @@ export const useMainStore = defineStore('mainStore', {
const station = this.stationList.find((s) => s.name === scenery.name); const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains( let checkpointsSet: Set<string> = new Set();
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
const stationTrains = getStationTrains( // Add checkpoints to active scenery data
this.trainList, checkpointsSet.add(scenery.name.toLowerCase());
scheduledTrains,
this.region.id,
scenery.name
);
// Remove checkpoint duplicates station?.generalInfo?.checkpoints.forEach((cpName) => {
const uniqueScheduledTrains = scheduledTrains.reduce( checkpointsSet.add(cpName.toLowerCase());
(uniqueList, sTrain) => });
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
scenery.scheduledTrains = scheduledTrains; const checkpoints = Array.from(checkpointsSet);
scenery.stationTrains = stationTrains;
scenery.scheduledTrainCount = { scenery.stationTrains =
all: uniqueScheduledTrains.length, sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length 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; return allActiveSceneries;
@@ -281,7 +361,10 @@ export const useMainStore = defineStore('mainStore', {
...scenery, ...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()), authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes, 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 { API } from '../typings/api';
import { Availability, StationRoutesInfo, Status } from '../typings/common'; import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export interface MainStoreState { export interface MainStoreState {
region: { id: string; value: string; name: string }; region: { id: string; value: string; name: string };
+1 -183
View File
@@ -1,13 +1,4 @@
import { import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
TrainStop,
StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
export function getStatusTimestamp(stationStatus: any): number { export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2; if (!stationStatus) return -2;
@@ -57,176 +48,3 @@ export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
export function getTimestamp(date: string | null): number { export function getTimestamp(date: string | null): number {
return date ? new Date(date).getTime() : 0; 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'
}));
}

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