Compare commits

..

112 Commits

Author SHA1 Message Date
Spythere 1f457d6389 Merge pull request #88 from Spythere/development
hotfix: minor adjustments for new simulator version (2024.1.1)
2024-05-13 15:05:28 +02:00
Spythere eb5b94c9f6 chore: vehicle images hotfixes 2024-05-13 15:02:15 +02:00
Spythere 328e8c0573 chore: fixed stock fallback thumbnnail 2024-05-13 14:54:21 +02:00
Spythere 9f58ae5428 Merge pull request #87 from Spythere/development
hotfix: modal positioning
2024-05-12 15:23:30 +02:00
Spythere ebd0eeb8c4 hotfix: modal positioning 2024-05-12 15:22:03 +02:00
Spythere fa656c2f26 Merge pull request #86 from Spythere/development
v1.24.0
2024-05-12 15:14:22 +02:00
Spythere 0cc3a12d1d fix: modal responsiveness 2024-05-12 14:55:35 +02:00
Spythere 392a6437f8 feature: current users tooltip 2024-05-09 17:19:22 +02:00
Spythere 122532f0ed chore: general fixes 2024-05-09 16:40:53 +02:00
Spythere 366ff91f60 hotfix: update modal 2024-05-08 20:12:07 +02:00
Spythere a0496736dd chore: modals update 2024-05-08 20:04:41 +02:00
Spythere f974120e87 fix: lock files 2024-05-08 18:42:33 +02:00
Spythere abd8b8178b chore: vue deep selector 2024-05-08 16:42:04 +02:00
Spythere f1fcde8459 feat: update modal 2024-05-08 16:41:14 +02:00
Spythere b3289d6aab chore: region dropdown fixes 2024-05-08 15:16:20 +02:00
Spythere 6481a4a3b0 chore: design improvements 2024-05-08 15:10:40 +02:00
Spythere 05dc268526 fix: spawns detection 2024-05-06 18:18:15 +02:00
Spythere 669acc98d2 chore: station stats translation 2024-05-06 18:16:30 +02:00
Spythere 3371b661c2 fix: ufactor calc 2024-05-06 17:53:07 +02:00
Spythere 871b2c0221 feature: open spawns tooltip 2024-05-06 17:36:23 +02:00
Spythere d366a877a4 refactor: popups -> tooltips 2024-05-06 16:37:56 +02:00
Spythere 405aab96bd feature: stations stats 2024-05-05 13:34:43 +02:00
Spythere f29c160000 fix: lock files 2024-05-04 14:47:30 +02:00
Spythere a2de0e2030 refactor: types & performance 2024-05-04 14:43:34 +02:00
Spythere 7dd1c06f3f chore: accessibility of filters 2024-05-03 19:29:10 +02:00
Spythere ff041b9aaf bump(version): 1.24.0 2024-05-03 19:02:49 +02:00
Spythere 4782dba444 feat(app): added min route speed & max route speed station filters 2024-05-03 19:02:16 +02:00
Spythere d6b8d032d6 fix(app): improved data fetching scheduler 2024-05-03 19:02:13 +02:00
Spythere c16616330c chore(packages): update & cleanup 2024-05-03 18:01:54 +02:00
Spythere 57cec8bfe7 chore: pwa adjustments 2024-05-03 17:49:54 +02:00
Spythere 6bea340e19 chore(pwa): changed sceneries cache to cachefirst 2024-05-01 19:37:51 +02:00
Spythere c181cf7e64 fix(workflows): release color 2024-04-27 01:11:38 +02:00
Spythere 8e4ae64cd3 chore(workflows): added release discord webhook notification 2024-04-15 15:13:22 +02:00
Spythere 5750490f01 refactor: journals 2024-04-08 23:21:50 +02:00
Spythere 3ef27e1d69 Merge pull request #85 from Spythere/development
Wersja 1.23.1
2024-04-01 13:00:28 +02:00
Spythere f53993c717 hotfix 2024-03-31 21:55:33 +02:00
Spythere 235c16e30f train modal 2024-03-31 21:37:14 +02:00
Spythere c3533f07ad literówka 2024-03-30 17:48:34 +01:00
Spythere d05579c5ee popupy 2024-03-30 13:24:39 +01:00
Spythere c8f53c2f06 hotfixy designu 2024-03-30 00:18:54 +01:00
Spythere b44f88ebcd src miniaturek 2024-03-29 23:37:26 +01:00
Spythere 7805d1350c responsywność 2024-03-29 23:35:56 +01:00
Spythere b17bd19433 zmiana położenia przycisku RJ ONLINE w dzienniku 2024-03-29 23:23:14 +01:00
Spythere c12a6cbacd zmiana rozłożenia elementów w modalu aktywnego pociągu 2024-03-29 23:21:15 +01:00
Spythere ba650238db poprawki rozmieszczenia popupu 2024-03-29 23:04:08 +01:00
Spythere d5ec9919e2 update modal (wip) 2024-03-29 20:34:56 +01:00
Spythere 20cd393e05 Merge pull request #84 from Spythere/development
Wersja 1.23.0
2024-03-24 01:30:03 +01:00
Spythere 31e65c09d6 hotfix: podgląd pojazdów 2024-03-24 00:05:39 +01:00
Spythere fb2348e774 hotfixy designu 2024-03-23 23:55:18 +01:00
Spythere 1ec75bda70 poprawki do popupów 2024-03-23 16:47:57 +01:00
Spythere 6b6b837dde bump: v1.23.0 2024-03-23 00:01:15 +01:00
Spythere 66a02d76bd dodano odnośnik do dziennika RJ maszynisty 2024-03-23 00:00:52 +01:00
Spythere c7162dbd14 dodano dymki kontekstowe oraz podgląd pojazdu 2024-03-22 23:41:43 +01:00
Spythere 1cfe073bab Merge pull request #83 from Spythere/development
Wersja 1.22.3
2024-03-17 18:38:27 +01:00
Spythere e3b72c81ea bump: 1.22.3 2024-03-17 18:37:58 +01:00
Spythere 5552995564 fix: duplikujące się aktywne RJ scenerii 2024-03-17 18:37:45 +01:00
Spythere 623d5dd2ce fix: RJ scenerii offline 2024-03-17 17:33:19 +01:00
Spythere 6992b998a8 Merge pull request #82 from Spythere/development
Wersja 1.22.2
2024-03-17 16:47:52 +01:00
Spythere 669975c68e hotfixy 2024-03-17 16:42:35 +01:00
Spythere 084823de44 fix pobierania danych 2024-03-16 22:13:38 +01:00
Spythere f62d6982e5 Merge pull request #81 from Spythere/development
Wersja 1.22.1
2024-03-12 16:51:26 +01:00
Spythere 1c9b54b578 bump: 1.22.1 2024-03-11 23:50:00 +01:00
Spythere 0f4e5ee5f3 fix: niepoprawne miniaturki pojazdów 2024-03-11 23:49:45 +01:00
Spythere 29b5e715fa hotfixy wyglądu 2024-03-11 23:43:10 +01:00
Spythere 91a18b51a0 Merge pull request #80 from Spythere/development
Wersja 1.22.0
2024-03-06 22:49:46 +01:00
Spythere 241648ec49 asdek: filtry 2024-03-06 18:33:30 +01:00
Spythere ed7d93e7fc fixy filtrowania; ogólne 2024-03-06 18:12:20 +01:00
Spythere 436e3e63f9 hotfixy 2024-03-05 15:27:42 +01:00
Spythere 17ebdace82 fix filtrowania ocenami 2024-03-04 21:13:12 +01:00
Spythere 20826d905d poprawki tłumaczeń 2024-03-04 21:10:43 +01:00
Spythere 41b1ab398c bump: 1.22.0 2024-03-04 20:32:05 +01:00
Spythere 03465a1487 poprawki 2024-03-04 20:31:54 +01:00
Spythere a19fdbc19d hotfix 2024-03-04 18:18:37 +01:00
Spythere 032f82acd2 animacje statusów listy scenerii; fixy tłumaczeń 2024-03-04 18:06:47 +01:00
Spythere b4a9d4ca3b brakujące tłumaczenia 2024-03-04 17:50:45 +01:00
Spythere 986c7ba95e asdek 2024-03-04 17:46:09 +01:00
Spythere 17f6f9c8ef poprawki designu scenerii 2024-03-03 22:17:15 +01:00
Spythere 40bbdbe4fa sortowanie po liczbie szlaków i ocenie dyżurnego 2024-03-03 21:44:39 +01:00
Spythere 9f5d882119 poprawki tabelki scenerii 2024-03-03 20:30:05 +01:00
Spythere 6f45663c6c design stock listy 2024-03-03 19:04:17 +01:00
Spythere a7861b361d design statystyk pociągu 2024-03-03 15:10:29 +01:00
Spythere 5f8d7480d1 fix designu 2024-03-03 14:46:37 +01:00
Spythere e222dc63eb scroller sponsorów projektu w modalu 2024-03-03 14:31:04 +01:00
Spythere 9c2f0ac797 bump: 1.21.1 2024-03-02 23:21:13 +01:00
Spythere e33ba4af90 design szlaków na liście scenerii 2024-03-02 23:16:17 +01:00
Spythere 7b868a9f28 brakujące tłumaczenia 2024-03-02 22:38:53 +01:00
Spythere 2a18ba4368 poprawki szerokości 2024-03-02 22:19:41 +01:00
Spythere fcbd6d0883 migracja http clienta do apiStore 2024-03-02 16:13:33 +01:00
Spythere 20fc4aba5b padding scrollbaru 2024-03-01 19:12:23 +01:00
Spythere 76ca0d1786 cleanup kodu 2024-03-01 19:07:21 +01:00
Spythere 7e3c150815 usprawnienia miniaturek pojazdów 2024-03-01 19:07:00 +01:00
Spythere c8d56ec442 vite config dev 2024-02-29 13:31:48 +01:00
Spythere 5c4c486643 Wersja 1.21.0
Wersja 1.21.0
2024-02-12 15:14:50 +01:00
Spythere 755c729a9b brakujące tłumaczenia; poprawki 2024-02-12 14:58:59 +01:00
Spythere 3ac8d60c5c filtry aktywnych RJ 2024-02-11 15:30:19 +01:00
Spythere dcff3b088f poprawki filtrowania statusów 2024-02-10 23:11:13 +01:00
Spythere 90b2099955 checkpointy; hotfixy 2024-02-10 22:42:35 +01:00
Spythere fc0c04ec9d bump: 1.21.0 2024-02-10 01:31:08 +01:00
Spythere 41b335555a wyświetlanie RJ dla scenerii offline 2024-02-10 01:30:43 +01:00
Spythere 60f7b3bbb5 Wersja 1.20.5
Wersja 1.20.5
2024-02-03 14:36:57 +01:00
Spythere eaefe955a7 bump: 1.20.5 2024-02-02 21:14:29 +01:00
Spythere edaa4f2684 cleanup 2024-02-02 21:14:16 +01:00
Spythere 30fce3787b usprawnienia pobierania danych; statusy SWDR na semaforze 2024-02-02 21:13:21 +01:00
Spythere 4716f1c7a4 Wersja 1.20.4
Wersja 1.20.4
2024-01-30 16:44:55 +01:00
Spythere bb7ccf98fd bump wersji 2024-01-30 16:41:27 +01:00
Spythere c06d75b981 lock fix; linting 2024-01-30 14:00:38 +01:00
Spythere c7da8477fa przywrócenie komunikacji po WS (test) 2024-01-30 13:58:47 +01:00
Spythere e43f1e0819 Wersja 1.20.3
Wersja 1.20.3
2024-01-26 13:59:23 +01:00
Spythere f130e6900b hotfix: brak komentarzy dla ostatniej stacji w RJ 2024-01-26 13:54:47 +01:00
Spythere db205915be bump wersji 2024-01-26 13:39:04 +01:00
Spythere 05c38e10e3 hotfix postojów pt w SRJP 2024-01-26 13:38:45 +01:00
Spythere a8f683a585 mock data 2024-01-26 13:38:28 +01:00
92 changed files with 8049 additions and 16434 deletions
@@ -0,0 +1,17 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "15844367"
footer_title: "Changelog - Stacjownik"
footer_timestamp: true
+2829 -9385
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.20.2", "version": "1.24.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -14,10 +14,9 @@
"dependencies": { "dependencies": {
"core-js": "^3.32.2", "core-js": "^3.32.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"firebase": "^10.4.0",
"howler": "^2.2.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.67.0", "sass": "^1.67.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.2.4"
@@ -25,7 +24,8 @@
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3", "@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2", "@types/node": "^20.6.2",
"@vite-pwa/assets-generator": "^0.0.10", "@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-vue": "^4.3.4", "@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
@@ -36,7 +36,7 @@
"prettier": "^3.0.3", "prettier": "^3.0.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11" "vue-tsc": "^1.8.11"
}, },
"browserslist": [ "browserslist": [
+4
View File
@@ -0,0 +1,4 @@
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="60" fill="#898989"/>
<path d="M10.5573 31.3793L9.43741 28.0694C9.35445 27.8592 9.26596 27.6131 9.17195 27.3311C9.07793 27.0435 8.98391 26.7338 8.8899 26.402C8.80694 26.7393 8.71845 27.0518 8.62444 27.3394C8.53042 27.6269 8.44193 27.8758 8.35898 28.086L7.24736 31.3793H10.5573ZM15.0121 36H12.8386C12.5953 36 12.3989 35.9447 12.2496 35.8341C12.1003 35.7179 11.9869 35.5714 11.9095 35.3944L11.1961 33.2873H6.6003L5.88688 35.3944C5.82604 35.5493 5.71544 35.6903 5.55505 35.8175C5.4002 35.9392 5.20664 36 4.97436 36H2.78431L7.46305 23.9133H10.3333L15.0121 36ZM22.643 26.3688C22.5601 26.5015 22.4716 26.6011 22.3775 26.6674C22.2891 26.7338 22.1729 26.767 22.0291 26.767C21.9019 26.767 21.7637 26.7283 21.6143 26.6508C21.4706 26.5679 21.3046 26.4766 21.1166 26.3771C20.9341 26.2775 20.724 26.189 20.4861 26.1116C20.2483 26.0287 19.9773 25.9872 19.6732 25.9872C19.1478 25.9872 18.7551 26.1006 18.4952 26.3273C18.2408 26.5485 18.1136 26.8499 18.1136 27.2315C18.1136 27.4749 18.191 27.6767 18.3459 27.8371C18.5007 27.9975 18.7026 28.1357 18.9515 28.2519C19.2059 28.368 19.4934 28.4759 19.8142 28.5754C20.1405 28.6694 20.4723 28.7773 20.8097 28.8989C21.147 29.0151 21.4761 29.1533 21.7969 29.3137C22.1231 29.4741 22.4107 29.6787 22.6596 29.9276C22.914 30.1765 23.1186 30.4806 23.2735 30.8401C23.4283 31.1941 23.5058 31.6227 23.5058 32.1259C23.5058 32.6845 23.409 33.2071 23.2154 33.6938C23.0218 34.1805 22.7398 34.6063 22.3693 34.9713C22.0042 35.3308 21.5508 35.6156 21.0088 35.8258C20.4723 36.0304 19.8612 36.1327 19.1754 36.1327C18.7994 36.1327 18.415 36.094 18.0223 36.0166C17.6352 35.9392 17.2591 35.8313 16.8941 35.6931C16.5291 35.5493 16.1862 35.3806 15.8655 35.187C15.5447 34.9935 15.2654 34.7778 15.0276 34.54L15.8572 33.2293C15.9235 33.1352 16.0093 33.0578 16.1143 32.997C16.225 32.9306 16.3439 32.8974 16.4711 32.8974C16.637 32.8974 16.8029 32.95 16.9688 33.0551C17.1402 33.1601 17.331 33.2763 17.5412 33.4035C17.7569 33.5307 18.003 33.6468 18.2795 33.7519C18.556 33.857 18.8823 33.9095 19.2584 33.9095C19.7672 33.9095 20.1626 33.7989 20.4447 33.5777C20.7267 33.3509 20.8677 32.9942 20.8677 32.5075C20.8677 32.2255 20.7903 31.996 20.6355 31.819C20.4806 31.642 20.276 31.4955 20.0216 31.3793C19.7727 31.2632 19.4879 31.1609 19.1671 31.0724C18.8464 30.9839 18.5173 30.8871 18.18 30.7821C17.8426 30.6714 17.5135 30.5387 17.1928 30.3839C16.872 30.2235 16.5844 30.0161 16.33 29.7617C16.0812 29.5018 15.8793 29.181 15.7245 28.7994C15.5696 28.4123 15.4922 27.9367 15.4922 27.3725C15.4922 26.9191 15.5834 26.4766 15.7659 26.0452C15.9484 25.6139 16.2167 25.2295 16.5706 24.8922C16.9246 24.5548 17.3587 24.2866 17.873 24.0875C18.3874 23.8829 18.9763 23.7805 19.64 23.7805C20.0105 23.7805 20.37 23.811 20.7184 23.8718C21.0724 23.9271 21.407 24.0128 21.7222 24.129C22.0374 24.2396 22.3305 24.3751 22.6015 24.5354C22.8781 24.6903 23.1242 24.8673 23.3398 25.0664L22.643 26.3688ZM36.1186 29.9525C36.1186 30.8263 35.9665 31.6337 35.6623 32.3748C35.3637 33.1104 34.9406 33.7491 34.3931 34.2911C33.8456 34.8276 33.1847 35.2479 32.4105 35.552C31.6417 35.8507 30.7873 36 29.8471 36H25.1518V23.9133H29.8471C30.7873 23.9133 31.6417 24.0654 32.4105 24.3695C33.1847 24.6737 33.8456 25.094 34.3931 25.6305C34.9406 26.1669 35.3637 26.8057 35.6623 27.5468C35.9665 28.2823 36.1186 29.0842 36.1186 29.9525ZM33.2483 29.9525C33.2483 29.3552 33.1709 28.816 33.016 28.3348C32.8612 27.8537 32.6372 27.4472 32.3441 27.1154C32.0565 26.778 31.7026 26.5209 31.2823 26.3439C30.8619 26.1614 30.3836 26.0701 29.8471 26.0701H27.9723V33.8431H29.8471C30.3836 33.8431 30.8619 33.7547 31.2823 33.5777C31.7026 33.3952 32.0565 33.138 32.3441 32.8062C32.6372 32.4688 32.8612 32.0596 33.016 31.5784C33.1709 31.0973 33.2483 30.5553 33.2483 29.9525ZM40.594 26.0701V28.8906H44.3934V30.9646H40.594V33.8431H45.5547V36H37.7735V23.9133H45.5547V26.0701H40.594ZM50.0882 28.8077H50.5361C50.9509 28.8077 51.2496 28.6777 51.4321 28.4178L54.153 24.4691C54.3134 24.2589 54.4849 24.1151 54.6674 24.0377C54.8554 23.9547 55.0877 23.9133 55.3642 23.9133H57.8031L54.2194 28.7994C53.965 29.1368 53.6912 29.3801 53.3981 29.5294C53.6083 29.6068 53.7991 29.7147 53.9705 29.8529C54.142 29.9912 54.3024 30.1709 54.4517 30.3922L58.1018 36H55.5965C55.4361 36 55.2978 35.9889 55.1817 35.9668C55.0711 35.9447 54.9743 35.9115 54.8913 35.8673C54.8084 35.823 54.7365 35.7705 54.6757 35.7097C54.6148 35.6433 54.5568 35.5686 54.5015 35.4857L51.7639 31.2798C51.6643 31.1249 51.5371 31.0171 51.3823 30.9563C51.233 30.8899 51.0284 30.8567 50.7684 30.8567H50.0882V36H47.2843V23.9133H50.0882V28.8077Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

-71
View File
@@ -1,71 +0,0 @@
@import './styles/responsive.scss';
@import './styles/variables.scss';
@import './styles/global.scss';
@import './styles/animations.scss';
.route {
margin: 0 0.2em;
&-active,
&[data-active='true'] {
color: $accentCol;
font-weight: bold;
}
}
// APP
#app {
color: white;
font-size: 1rem;
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
}
}
// CONTAINER
.app_container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 100%;
min-height: 100vh;
}
.app_main {
padding: 0 0.5em;
}
.warning {
background-color: firebrick;
text-align: center;
padding: 0.5em 0.4em;
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 1em 1em;
}
// FOOTER
footer.app_footer {
max-width: 100%;
padding: 0.5em;
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
+150 -41
View File
@@ -1,8 +1,15 @@
<template> <template>
<div class="app_container"> <div class="app_container">
<UpdateModal
:update-modal-open="isUpdateModalOpen"
@toggle-modal="() => (isUpdateModalOpen = false)"
/>
<Tooltip />
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
<TrainModal v-if="store.chosenModalTrainId" /> <TrainModal />
</keep-alive> </keep-alive>
</transition> </transition>
@@ -20,7 +27,10 @@
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | {{ new Date().getUTCFullYear() }} |
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a> <button class="btn--text" @click="() => (isUpdateModalOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
<br /> <br />
<a href="https://discord.gg/x2mpNN3svk"> <a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b> <img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
@@ -32,38 +42,48 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, watch } from 'vue'; import { defineComponent } from 'vue';
import axios from 'axios'; import axios from 'axios';
import packageInfo from '.././package.json';
import { version } from '.././package.json';
import { Status } from './typings/common';
import { useMainStore } from './store/mainStore';
import { useApiStore } from './store/apiStore';
import { useTooltipStore } from './store/tooltipStore';
import Clock from './components/App/Clock.vue'; import Clock from './components/App/Clock.vue';
import { useMainStore } from './store/mainStore';
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 TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateModal from './components/App/UpdateModal.vue';
import StorageManager from './managers/storageManager'; import StorageManager from './managers/storageManager';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common'; const STORAGE_VERSION_KEY = 'app_version';
export default defineComponent({ export default defineComponent({
components: { components: {
Clock, Clock,
StatusIndicator, StatusIndicator,
AppHeader, AppHeader,
TrainModal TrainModal,
UpdateModal,
Tooltip
}, },
data: () => ({ data: () => ({
VERSION: packageInfo.version, VERSION: version,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(), apiStore: useApiStore(),
tooltipStore: useTooltipStore(),
isUpdateModalOpen: false,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
nextUpdateTime: 0
}), }),
created() { created() {
@@ -71,22 +91,52 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
watch( window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
() => this.store.blockScroll,
(value) => {
if (value) document.body.classList.add('no-scroll');
else document.body.classList.remove('no-scroll');
}
);
}, },
methods: { methods: {
init() { init() {
this.loadLang(); this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling(); this.setupOfflineHandling();
this.checkAppVersion();
this.apiStore.setupAPI(); 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() {
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
if (!releaseData) return;
this.store.appUpdate = {
version,
changelog: releaseData.body,
releaseURL: releaseData.html_url
};
this.isUpdateModalOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
}, },
setupOfflineHandling() { setupOfflineHandling() {
@@ -101,16 +151,14 @@ export default defineComponent({
handleOfflineMode() { handleOfflineMode() {
this.store.isOffline = true; this.store.isOffline = true;
this.apiStore.stopActiveDataScheduler();
this.apiStore.activeData = undefined; this.apiStore.activeData = undefined;
this.apiStore.dataStatuses.connection = Status.Data.Offline; this.apiStore.dataStatuses.connection = Status.Data.Offline;
}, },
handleOnlineMode() { handleOnlineMode() {
this.store.isOffline = false; this.store.isOffline = false;
this.apiStore.setupAPI(); this.apiStore.connectToAPI();
}, },
changeLang(lang: string) { changeLang(lang: string) {
@@ -120,21 +168,6 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); StorageManager.setStringValue('lang', lang);
}, },
async setReleaseURL() {
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
if (!releaseData) return;
this.releaseURL = releaseData.html_url;
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
return;
}
},
loadLang() { loadLang() {
const storageLang = StorageManager.getStringValue('lang'); const storageLang = StorageManager.getStringValue('lang');
@@ -156,4 +189,80 @@ export default defineComponent({
}); });
</script> </script>
<style lang="scss" src="./App.scss"></style> <style lang="scss">
@import './styles/global';
@import './styles/animations';
.route {
margin: 0 0.2em;
&-active,
&[data-active='true'] {
color: $accentCol;
font-weight: bold;
}
}
// APP
#app {
color: white;
font-size: 1rem;
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
}
@include screenLandscape() {
font-size: calc(0.45rem + 0.8vw);
}
}
// CONTAINER
.app_container {
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-columns: 100%;
min-height: 100vh;
overflow: hidden;
}
.app_main {
padding: 0 0.5em;
}
.warning {
background-color: firebrick;
text-align: center;
padding: 0.5em 0.4em;
max-width: 1100px;
margin: 0 auto;
border-radius: 0 0 1em 1em;
}
// FOOTER
.app_footer {
max-width: 100%;
padding: 0.5em;
button {
display: inline-block;
padding: 0.1em;
}
img {
width: 1.1em;
vertical-align: text-bottom;
}
z-index: 10;
background: #111;
color: white;
text-align: center;
vertical-align: middle;
}
</style>
+2 -13
View File
@@ -29,11 +29,6 @@
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" /> <img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span> <span class="text--primary">{{ onlineDispatchersCount }}</span>
<!-- <span class="g-tooltip">
<b class="text--primary">{{ factorU }}U</b>
<div class="content">Test</div>
</span> -->
<span class="text--grayed"> / </span> <span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span> <span class="text--primary">{{ onlineTrainsCount }}</span>
<img src="/images/icon-train.svg" alt="icon train" /> <img src="/images/icon-train.svg" alt="icon train" />
@@ -100,15 +95,9 @@ export default defineComponent({
}, },
onlineDispatchersCount() { onlineDispatchersCount() {
return this.store.onlineSceneryList.filter( return this.store.activeSceneryList.filter(
(scenery) => scenery.region == this.store.region.id (scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
).length; ).length;
},
factorU() {
return this.onlineDispatchersCount == 0
? '-'
: (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
} }
}, },
components: { StatusIndicator, Clock, RegionDropdown } components: { StatusIndicator, Clock, RegionDropdown }
+95 -104
View File
@@ -36,11 +36,11 @@
<circle id="Ellipse 18" cx="15" cy="17" r="7" fill="#393838" /> <circle id="Ellipse 18" cx="15" cy="17" r="7" fill="#393838" />
</g> </g>
<g v-if="greenLight" filter="url(#filter0_d_843_28)"> <g v-if="indicator.lights.greenLight" filter="url(#filter0_d_843_28)">
<circle cx="15" cy="17" r="7" fill="#00FF0A" /> <circle cx="15" cy="17" r="7" fill="#00FF0A" />
</g> </g>
<g v-if="greenBlinkLight" filter="url(#filter0_d_843_28)"> <g v-if="indicator.lights.greenBlinkLight" filter="url(#filter0_d_843_28)">
<circle cx="15" cy="17" r="7" fill="#00FF0A" /> <circle cx="15" cy="17" r="7" fill="#00FF0A" />
<animate <animate
@@ -52,14 +52,14 @@
/> />
</g> </g>
<g v-if="redTopLight" filter="url(#filter1_d_843_28)"> <g v-if="indicator.lights.redTopLight" filter="url(#filter1_d_843_28)">
<circle cx="15" cy="36" r="7" fill="#F40000" /> <circle cx="15" cy="36" r="7" fill="#F40000" />
</g> </g>
<g v-if="orangeLight" filter="url(#filter2_d_843_28)"> <g v-if="indicator.lights.orangeLight" filter="url(#filter2_d_843_28)">
<circle cx="15" cy="55" r="7" fill="#FFB800" /> <circle cx="15" cy="55" r="7" fill="#FFB800" />
</g> </g>
<g v-if="redBottomLight" filter="url(#filter3_d_843_28)"> <g v-if="indicator.lights.redBottomLight" filter="url(#filter3_d_843_28)">
<circle cx="15" cy="74" r="7" fill="#F40000" /> <circle cx="15" cy="74" r="7" fill="#F40000" />
<animate <animate
@@ -186,7 +186,11 @@
</svg> </svg>
<transition name="tooltip-anim"> <transition name="tooltip-anim">
<div v-html="$t(indicator.message)" class="indicator-tooltip" v-if="tooltipActive"></div> <div
v-html="$t('data-status.' + indicator.message)"
class="indicator-tooltip"
v-if="tooltipActive"
></div>
</transition> </transition>
</div> </div>
</div> </div>
@@ -194,125 +198,112 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { APIDataStatus } from '../../typings/api';
interface Indicator {
// status: Status.Data;
message: string;
lights: {
greenLight: boolean;
greenBlinkLight: boolean;
redTopLight: boolean;
orangeLight: boolean;
redBottomLight: boolean;
};
}
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
tooltipActive: false, tooltipActive: false,
indicator: { apiStore: useApiStore()
offline: false,
status: Status.Data.Loading,
message: 'data-status.S3'
},
greenLight: false,
greenBlinkLight: false,
redTopLight: false,
orangeLight: false,
redBottomLight: false
}; };
}, },
mounted() {
this.setSignalStatus(Status.Data.Loading);
},
setup() {
const store = useMainStore();
const apiStore = useApiStore();
return {
dataStatus: apiStore.dataStatuses,
store
};
},
watch: {
dataStatus: {
deep: true,
handler(statuses: any) {
const connectionStatus = statuses.connection;
const sceneryDataStatus = statuses.sceneries;
const trainsDataStatus = statuses.trains;
const dispatcherDataStatus = statuses.dispatchers;
if (connectionStatus == Status.Data.Offline) {
this.setSignalStatus(Status.Data.Offline);
this.indicator.status = Status.Data.Offline;
this.indicator.message = 'data-status.S1-offline';
return;
}
if (connectionStatus == Status.Data.Error) {
this.setSignalStatus(connectionStatus);
this.indicator.status = connectionStatus;
this.indicator.message = 'data-status.S1a-connection';
return;
}
if (sceneryDataStatus == Status.Data.Error) {
this.setSignalStatus(sceneryDataStatus);
this.indicator.status = sceneryDataStatus;
this.indicator.message = 'data-status.S1a-sceneries';
return;
}
if (trainsDataStatus == Status.Data.Warning) {
this.setSignalStatus(trainsDataStatus);
this.indicator.status = trainsDataStatus;
this.indicator.message = 'data-status.S5-trains';
return;
}
if (dispatcherDataStatus == Status.Data.Warning) {
this.setSignalStatus(dispatcherDataStatus);
this.indicator.status = dispatcherDataStatus;
this.indicator.message = 'data-status.S5-dispatchers';
return;
}
if (sceneryDataStatus == Status.Data.Loaded) {
this.setSignalStatus(Status.Data.Loaded);
this.indicator.status = Status.Data.Loaded;
this.indicator.message = 'data-status.S2';
}
}
}
},
methods: { methods: {
setSignalStatus(status: Status.Data) { setLights(message: string) {
this.greenLight = false; let lights = {
this.greenBlinkLight = false; greenBlinkLight: false,
this.redTopLight = false; greenLight: false,
this.orangeLight = false; orangeLight: false,
this.redBottomLight = false; redBottomLight: false,
redTopLight: false
};
if (status == Status.Data.Initialized || status == Status.Data.Offline) { switch (message) {
this.redTopLight = true; case 'S3':
lights.greenBlinkLight = true;
break;
case 'S2':
lights.greenLight = true;
break;
case 'S1-offline':
lights.redTopLight = true;
break;
case 'S1a-connection':
case 'S1a-sceneries':
lights.redTopLight = true;
lights.redBottomLight = true;
break;
case 'S5-dispatchers':
case 'S5-trains':
lights.orangeLight = true;
break;
default:
break;
} }
if (status == Status.Data.Loaded) { return lights;
this.greenLight = true; }
},
computed: {
indicator(): Indicator {
const dataStatuses = this.apiStore.dataStatuses;
const swdrStatuses = this.apiStore.activeData?.apiStatuses;
let message = 'S3';
switch (dataStatuses.connection) {
case Status.Data.Loading:
message = 'S3';
break;
case Status.Data.Loaded:
message = 'S2';
break;
case Status.Data.Offline:
message = 'S1-offline';
break;
case Status.Data.Error:
message = 'S1a-connection';
break;
default:
break;
} }
if (status == Status.Data.Warning) { if (swdrStatuses?.dispatchersAPI == APIDataStatus.WARNING) {
this.orangeLight = true; message = 'S5-dispatchers';
} }
if (status == Status.Data.Error) { if (swdrStatuses?.trainsAPI == APIDataStatus.WARNING) {
this.redTopLight = true; message = 'S5-trains';
this.redBottomLight = true;
} }
if (status == Status.Data.Loading) { if (swdrStatuses?.stationsAPI == APIDataStatus.WARNING) {
this.greenBlinkLight = true; message = 'S1a-sceneries';
} }
return {
lights: this.setLights(message),
message
};
} }
} }
}); });
+121
View File
@@ -0,0 +1,121 @@
<template>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
<div class="modal-content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)">
{{ $t('update.confirm') }}
</button>
<p class="bottom-info">
{{ $t('update.info-1') }}
<br />
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</AnimatedModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue';
const converter = new Converter();
export default defineComponent({
components: { AnimatedModal },
props: {
updateModalOpen: {
type: Boolean,
required: true
}
},
emits: ['toggleModal'],
data() {
return {
mainStore: useMainStore(),
version: version
};
},
watch: {
updateModalOpen(val: boolean) {
this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
});
}
},
computed: {
htmlChangelog() {
if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
}
});
</script>
<style lang="scss" scoped>
::v-deep(h1) {
text-align: center;
}
::v-deep(h2) {
padding: 0.25em 0;
}
::v-deep(ul) {
list-style: inside;
padding: 0.5em;
line-height: 1.5em;
}
.modal-content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
padding: 1em;
min-height: 700px;
overflow: auto;
text-align: justify;
}
.no-features {
text-align: center;
}
button {
margin: 0 auto;
padding: 0.5em 0.75em;
font-size: 1.1em;
}
p.bottom-info {
text-align: center;
color: #ccc;
}
a {
text-decoration: underline;
}
</style>
+17 -30
View File
@@ -1,8 +1,8 @@
<template> <template>
<transition name="modal-anim" tag="div" class="modal"> <transition name="modal-anim" tag="div">
<div class="body" v-if="isOpen"> <div class="modal" v-if="isOpen">
<div class="background" @click="toggleModal(false)"></div> <div class="modal-background" @click="toggleModal(false)"></div>
<div class="wrapper" ref="wrapper" tabindex="0"> <div class="modal-wrapper" ref="wrapper" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div> <div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
@@ -30,8 +30,7 @@ export default defineComponent({
watch: { watch: {
isOpen(v) { isOpen(v) {
this.$nextTick(() => { this.$nextTick(() => {
if (v) (this.$refs['wrapper'] as HTMLElement).focus(); if (v == false) (this.store.modalLastClickedTarget as HTMLElement)?.focus();
else (this.store.modalLastClickedTarget as HTMLElement)?.focus();
}); });
} }
}, },
@@ -47,17 +46,17 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.body { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 200;
width: 100vw; width: 100%;
height: 100vh; height: 100vh;
z-index: 200;
} }
.background { .modal-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -69,33 +68,21 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.wrapper { .modal-wrapper {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 210;
overflow: auto;
max-height: 95vh;
& > :slotted(div) {
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #333333; box-shadow: 0 0 15px 10px #0e0e0e;
width: 95%; width: 95vw;
max-width: 800px; max-width: 850px;
max-height: 95vh;
& > :slotted(div) {
max-height: 95vh;
}
}
@include smallScreen {
.wrapper {
top: 0;
transform: translate(-50%, 1em);
max-height: 90vh;
& > :slotted(div) {
max-height: 90vh;
}
} }
} }
</style> </style>
-201
View File
@@ -1,201 +0,0 @@
<template>
<div class="donation-modal" @keydown.esc="toggleModal(false)">
<button
class="btn-toggle btn--image"
ref="btn"
@click="toggleModal(true)"
@focus="toggleModal(false)"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
<AnimatedModal :isOpen="isModalOpen" @toggleModal="toggleModal">
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1>
<br />
<p v-html="$t('donations.p1')"></p>
<br />
<i18n-t keypath="donations.p2" tag="p">
<template v-slot:b1>
<b>{{ $t('donations.p2-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p2-b2') }}</b>
</template>
<template v-slot:b3>
<b>{{ $t('donations.p2-b3') }}</b>
</template>
<template v-slot:link>
<a class="discord" href="https://discord.gg/x2mpNN3svk" target="_blank">
{{ $t('donations.p2-a1') }}
</a>
</template>
</i18n-t>
<br />
<p v-html="$t('donations.p3')"></p>
<br />
<i18n-t keypath="donations.p4" tag="p">
<template v-slot:img>
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</template>
<template v-slot:b1>
<b>{{ $t('donations.p4-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p4-b2') }}</b>
</template>
</i18n-t>
<br />
<i
v-html="$t('donations.p5')"
style="display: flex; justify-content: flex-end; text-align: right"
>
</i>
</div>
<div class="modal_actions">
<a
class="modal-action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
>
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
{{ $t('donations.action-buycoffee') }}
</a>
<a
class="modal-action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank"
>
<img src="/images/icon-dollar.svg" alt="paypal donation" />
{{ $t('donations.action-paypal') }}
</a>
<button class="modal-action btn--image exit" @click="toggleModal(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
export default defineComponent({
props: {
isModalOpen: Boolean
},
emits: ['toggleModal'],
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
},
components: { AnimatedModal }
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
button.btn-toggle {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
@include smallScreen {
span {
display: none;
}
}
}
.modal_content {
display: grid;
grid-template-rows: 1fr auto;
gap: 1em;
font-size: 1.1em;
& > div {
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
}
.modal_main {
overflow: auto;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
}
.modal_actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.5em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
&.paypal {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.coffee {
$btnColor: #009255;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.exit {
$btnColor: #686868;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
}
</style>
+261
View File
@@ -0,0 +1,261 @@
<template>
<AnimatedModal
class="donation-modal"
:isOpen="isModalOpen"
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
<transition mode="out-in" name="slider-anim" class="current-name">
<span :key="displayingName">
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
{{ displayingName }}
</span>
</transition>
</div>
<br />
<p v-html="$t('donations.p1')"></p>
<br />
<i18n-t keypath="donations.p2" tag="p">
<template v-slot:b1>
<b>{{ $t('donations.p2-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p2-b2') }}</b>
</template>
<template v-slot:b3>
<b>{{ $t('donations.p2-b3') }}</b>
</template>
<template v-slot:link>
<a class="discord" href="https://discord.gg/x2mpNN3svk" target="_blank">
{{ $t('donations.p2-a1') }}
</a>
</template>
</i18n-t>
<br />
<p v-html="$t('donations.p3')"></p>
<br />
<i18n-t keypath="donations.p4" tag="p">
<template v-slot:img>
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</template>
<template v-slot:b1>
<b>{{ $t('donations.p4-b1') }}</b>
</template>
<template v-slot:b2>
<b>{{ $t('donations.p4-b2') }}</b>
</template>
</i18n-t>
<br />
<i
v-html="$t('donations.p5')"
style="display: flex; justify-content: flex-end; text-align: right"
>
</i>
</div>
<div class="modal_actions">
<a
class="modal-action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
ref="action"
>
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
{{ $t('donations.action-buycoffee') }}
</a>
<a
class="modal-action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank"
>
<img src="/images/icon-dollar.svg" alt="paypal donation" />
{{ $t('donations.action-paypal') }}
</a>
<button class="modal-action btn--image exit" @click="toggleModal(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
components: { AnimatedModal },
props: {
isModalOpen: Boolean
},
emits: ['toggleModal'],
watch: {
isModalOpen(val: boolean) {
this.running = val;
this.lastUpdate = Date.now();
this.$nextTick(() => {
if (val) (this.$refs['action'] as HTMLElement).focus();
});
}
},
created() {
this.runUpdate();
},
data() {
return {
apiStore: useApiStore(),
displayingIndex: 0,
lastUpdate: 0,
running: false
};
},
computed: {
displayingName() {
return this.donatorList[this.displayingIndex];
},
donatorList() {
return this.apiStore.donatorsData.slice().sort(() => Math.sign(Math.random() * -2 + 1));
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
},
runUpdate() {
if (Date.now() >= this.lastUpdate + 2000 && this.running) {
this.displayingIndex = (this.displayingIndex + 1) % this.donatorList.length;
this.lastUpdate = Date.now();
}
window.requestAnimationFrame(this.runUpdate);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal_content {
display: grid;
grid-template-rows: 1fr auto;
gap: 1em;
font-size: 1.1em;
& > div {
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
}
.modal_main {
overflow: auto;
overflow-x: hidden;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
}
.modal_actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.5em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
&.paypal {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.coffee {
$btnColor: #009255;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
&.exit {
$btnColor: #686868;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
}
}
.donators-slider {
text-align: center;
line-height: 30px;
.current-name {
backface-visibility: hidden;
display: block;
font-weight: bold;
word-wrap: break-word;
color: var(--clr-donator);
}
}
.slider-anim {
&-move,
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
opacity: 0;
}
}
</style>
+6 -15
View File
@@ -59,20 +59,18 @@ export default defineComponent({
'store.region.id': { 'store.region.id': {
handler(regionId) { handler(regionId) {
this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId); this.selectedItemIndex = this.regionList.findIndex((reg) => reg.id == regionId);
console.log('region id', regionId);
} }
}, },
'$route.query.region': { '$route.query.region': {
immediate: true, immediate: true,
handler(regionQuery: string) { handler(regionQuery: string) {
if (regionQuery) { if (regionQuery) {
this.store.region.id = this.store.region =
regionsJSON.find( regionsJSON.find(
(reg) => (reg) =>
reg.id == regionQuery.toLocaleLowerCase() || reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase() reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu'; ) ?? regionsJSON[0];
} }
} }
} }
@@ -85,8 +83,8 @@ export default defineComponent({
regionList() { regionList() {
return regionsJSON.map((region) => { return regionsJSON.map((region) => {
const regionStationCount = this.store.onlineSceneryList.filter( const regionStationCount = this.store.activeSceneryList.filter(
(scenery) => scenery.region == region.id (scenery) => scenery.region == region.id && scenery.dispatcherId != -1
).length; ).length;
const regionTrainCount = const regionTrainCount =
@@ -141,15 +139,10 @@ button.selected-region {
color: paleturquoise; color: paleturquoise;
font-weight: bold; font-weight: bold;
padding: 0.1em 0.5em;
&:focus { &:focus {
background-color: #262626; background-color: #262626;
} }
span {
margin-right: 10px;
}
} }
.content { .content {
@@ -199,6 +192,8 @@ li.option {
} }
label { label {
width: 100%;
padding: 0.5em 0;
position: relative; position: relative;
display: inline-block; display: inline-block;
@@ -209,10 +204,6 @@ li.option {
background-color: #333333f2; background-color: #333333f2;
} }
padding: 0.5em 0;
width: 100%;
cursor: pointer; cursor: pointer;
} }
} }
+4 -1
View File
@@ -61,6 +61,9 @@ export default defineComponent({
case Status.ActiveDispatcher.UNKNOWN: case Status.ActiveDispatcher.UNKNOWN:
return 'unknown'; return 'unknown';
case Status.ActiveDispatcher.FREE:
return 'free';
default: default:
if (this.dispatcherTimestamp != null && this.dispatcherStatus >= Date.now() + 25500000) if (this.dispatcherTimestamp != null && this.dispatcherStatus >= Date.now() + 25500000)
return 'no-limit'; return 'no-limit';
@@ -83,7 +86,7 @@ $online: #09a116;
$unknown: #b93c3c; $unknown: #b93c3c;
.status-badge { .status-badge {
border-radius: 1rem; border-radius: 1em;
font-weight: 500; font-weight: 500;
padding: 0.2em 0.55em; padding: 0.2em 0.55em;
+68 -12
View File
@@ -1,7 +1,24 @@
<template> <template>
<div class="stock-list"> <div class="stock-list">
<ul> <div v-if="tractionOnly">
<li v-for="(stockName, i) in trainStockList" :key="i"> <p>
{{ computedStockList[0].split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ computedStockList[0].split(':')[1] }}
</p>
<img
class="traction-only"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
/^EN/.test(computedStockList[0]) ? 'rb' : ''
}.png`"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
/>
</div>
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p> <p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }} {{ stockName.split(':')[1] }}
@@ -9,39 +26,55 @@
<span> <span>
<img <img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${ :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : '' /^EN/.test(stockName) ? 'rb' : ''
}.png`" }.png`"
@error="onImageError($event, stockName)" @error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400" width="400"
height="60" height="60"
/> />
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img <img
:data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error=" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png') (event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
" "
@click.stop="() => {}"
/> />
<img <img
class="train-thumbnail" :data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)" v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error=" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png') (event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
" "
@click.stop="() => {}"
/> />
<img <img
class="train-thumbnail" :data-mouseover="stockName"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error=" @error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png') (event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
" "
@click.stop="() => {}"
/> />
<!-- /// -->
</span> </span>
</li> </li>
</ul> </ul>
@@ -50,7 +83,6 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { API } from '../../typings/api';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
@@ -58,6 +90,10 @@ export default defineComponent({
trainStockList: { trainStockList: {
type: Array as PropType<string[]>, type: Array as PropType<string[]>,
required: true required: true
},
tractionOnly: {
type: Boolean,
required: false
} }
}, },
@@ -67,14 +103,29 @@ export default defineComponent({
}; };
}, },
computed: {
computedStockList() {
return this.tractionOnly ? this.trainStockList.slice(0, 1) : this.trainStockList;
}
},
methods: { methods: {
onImageError(event: Event, stockName: string) { onImageError(event: Event, stockName: string) {
const fallbackName = let fallbackName = '';
Object.keys(this.apiStore.rollingStockData!.info).find((type) => {
return this.apiStore.rollingStockData!.info[type as keyof API.RollingStock.Info].find( const isLoco = /.-\d{3}/.test(stockName);
(v) => v[0] === stockName.split(':')[0]
); if (isLoco) {
}) || 'vehicle-unknown'; if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
else fallbackName = 'loco-s';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
fallbackName += 'car-';
fallbackName += isCarPassenger ? 'passenger' : 'cargo';
}
(event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`; (event.target as HTMLImageElement).src = `/images/icon-${fallbackName}.png`;
} }
@@ -99,6 +150,7 @@ export default defineComponent({
ul > li > span { ul > li > span {
display: flex; display: flex;
align-items: flex-end; align-items: flex-end;
cursor: crosshair;
} }
img { img {
@@ -107,10 +159,14 @@ img {
height: auto; height: auto;
} }
img.traction-only {
max-width: 100%;
}
p { p {
text-align: center; text-align: center;
color: #aaa; color: #aaa;
font-size: 0.9em; font-size: 0.95em;
margin-bottom: 1em; margin-bottom: 1em;
} }
</style> </style>
-85
View File
@@ -1,85 +0,0 @@
<template>
<img class="train-thumbnail" :src="placeholderUrl" v-if="isNotFound" />
<img
class="train-thumbnail"
v-else
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${name.split(':')[0]}${
stockType == 'loco-ezt' ? 'rb' : ''
}.png`"
@error="onImageError"
@load="onImageLoad"
width="220"
height="60"
/>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { API } from '../../typings/api';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
props: {
name: {
type: String,
required: true
},
onlyFirstSegment: {
type: Boolean,
default: false
}
},
data() {
return {
apiStore: useApiStore(),
isNotFound: false,
isLoaded: false
};
},
computed: {
url() {
return `https://rj.td2.info.pl/dist/img/thumbnails/${this.name.split(':')[0]}.png`;
},
placeholderUrl() {
return `/images/icon-${this.stockType}.png`;
},
stockType() {
if (!this.apiStore.rollingStockData) return 'vehicle-unknown';
return (
Object.keys(this.apiStore.rollingStockData.info).find((type) => {
return this.apiStore.rollingStockData?.info[type as keyof API.RollingStock.Info].find(
(v) => v[0] === this.name.split(':')[0]
);
}) || 'vehicle-unknown'
);
}
},
methods: {
onImageError() {
this.isNotFound = true;
this.isLoaded = false;
},
onImageLoad() {
this.isNotFound = false;
this.isLoaded = true;
}
}
});
</script>
<style lang="scss" scoped>
.train-thumbnail {
width: auto;
height: auto;
max-height: 60px;
}
</style>
@@ -172,7 +172,7 @@ import dateMixin from '../../mixins/dateMixin';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import http from '../../http'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'journal-daily-stats', name: 'journal-daily-stats',
@@ -186,7 +186,8 @@ export default defineComponent({
statsStatus: Status.Data.Loading, statsStatus: Status.Data.Loading,
intervalId: -1, intervalId: -1,
stats: {} as API.DailyStats.Response stats: {} as API.DailyStats.Response,
apiStore: useApiStore()
}; };
}, },
@@ -211,7 +212,9 @@ export default defineComponent({
methods: { methods: {
async fetchDailyTimetableStats() { async fetchDailyTimetableStats() {
try { try {
const res: API.DailyStats.Response = await (await http.get('api/getDailyStats')).data; const res: API.DailyStats.Response = await (
await this.apiStore.client!.get('api/getDailyStats')
).data;
this.stats = res; this.stats = res;
@@ -43,7 +43,7 @@
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`" :to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
> >
<b <b
v-if="isDonator(historyItem.dispatcherName)" v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator" class="text--donator"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
> >
@@ -128,13 +128,13 @@ 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 dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
components: { Loading, AddDataButton }, components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin], mixins: [dateMixin, styleMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
@@ -159,6 +159,7 @@ export default defineComponent({
return { return {
Status, Status,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(),
regions regions
}; };
}, },
@@ -9,7 +9,7 @@
ref="button" ref="button"
> >
<img src="/images/icon-filter2.svg" alt="Open filters" /> <img src="/images/icon-filter2.svg" alt="Open filters" />
{{ $t('options.filters') }} [F] [F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span> <span class="active-indicator" v-if="currentOptionsActive"></span>
</button> </button>
@@ -116,7 +116,7 @@ import keyMixin from '../../mixins/keyMixin';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { Journal } from './typings'; import { Journal } from './typings';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import http from '../../http'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'], emits: ['onSearchConfirm', 'onOptionsReset', 'onRefreshData'],
@@ -158,6 +158,7 @@ export default defineComponent({
searchTimeout: 0, searchTimeout: 0,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(),
JournalFilterSection: Journal.FilterSection JournalFilterSection: Journal.FilterSection
}; };
@@ -241,7 +242,7 @@ export default defineComponent({
this.searchTimeout = window.setTimeout(async () => { this.searchTimeout = window.setTimeout(async () => {
try { try {
const suggestions: string[] = await ( const suggestions: string[] = await (
await http.get(`api/get${type}Suggestions?name=${value}`) await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`)
).data; ).data;
this[`${type}Suggestions`] = suggestions; this[`${type}Suggestions`] = suggestions;
@@ -17,7 +17,34 @@
</div> </div>
<div v-else> <div v-else>
<TimetableHistoryList :timetableHistory="timetableHistory" /> <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" />
<!-- Extra -->
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
<AddDataButton <AddDataButton
:list="timetableHistory" :list="timetableHistory"
@@ -37,17 +64,29 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType, ref } 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 TimetableHistoryList from './TimetableHistoryList.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: { Loading, AddDataButton, TimetableHistoryList }, components: {
Loading,
AddDataButton,
TimetableDetails,
TimetableGeneral,
TimetableStatus,
TimetableStops
},
props: { props: {
timetableHistory: { timetableHistory: {
@@ -73,6 +112,15 @@ export default defineComponent({
Status, Status,
store: useMainStore() store: useMainStore()
}; };
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
} }
}); });
</script> </script>
@@ -80,4 +128,15 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss'; @import '../../../styles/animations.scss';
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
}
</style> </style>
@@ -0,0 +1,195 @@
<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,183 +0,0 @@
<template>
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
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(';')" /> -->
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
<!-- <ul class="stock-list">
<li
v-for="(stockName, i) in (currentHistoryIndex == 0 ? timetable.stockString : stockHistory[currentHistoryIndex].stockString).split(';')"
:key="i"
>
<div>{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }} {{ stockName.split(':')[1] }}</div>
<TrainThumbnail :name="stockName" />
</li>
</ul> -->
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import 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';
.item-extra {
margin-top: 0.5em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen() {
justify-content: center;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
</style>
@@ -1,11 +1,6 @@
<template> <template>
<div class="item-general"> <div class="item-general">
<span <span class="general-train">
class="general-train"
tabindex="0"
@click.stop="showTimetable(timetable, $event.currentTarget)"
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
>
<span class="text--grayed">#{{ timetable.id }}</span> <span class="text--grayed">#{{ timetable.id }}</span>
<span class="badges" v-if="timetable.skr || timetable.twr"> <span class="badges" v-if="timetable.skr || timetable.twr">
@@ -29,7 +24,7 @@
</strong> </strong>
<strong <strong
v-if="isDonator(timetable.driverName)" v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator" class="text--donator"
:title="$t('donations.driver-message')" :title="$t('donations.driver-message')"
> >
@@ -66,6 +61,15 @@
: `${$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>
@@ -77,10 +81,16 @@ import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import donatorMixin from '../../../mixins/donatorMixin'; import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin, donatorMixin], mixins: [dateMixin, modalTrainMixin, styleMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: { props: {
timetable: { timetable: {
@@ -100,8 +110,8 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/responsive.scss'; @import '../../../styles/responsive';
@import '../../../styles/badge.scss'; @import '../../../styles/badge';
.item-general { .item-general {
display: flex; display: flex;
@@ -113,8 +123,22 @@ export default defineComponent({
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.info-date { .general-train {
margin-right: 0.5em; display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.general-time {
display: flex;
align-items: center;
gap: 0.5em;
} }
.badges { .badges {
@@ -139,13 +163,13 @@ export default defineComponent({
} }
} }
.general-train { .btn-timetable {
cursor: pointer;
display: flex; display: flex;
flex-wrap: wrap; padding: 0.2em 0.5em;
justify-content: center;
align-items: center; img {
gap: 0.25em; height: 1.25em;
}
} }
@include smallScreen { @include smallScreen {
@@ -21,7 +21,7 @@
<!-- Status --> <!-- Status -->
<TimetableStatus :timetable="timetable" /> <TimetableStatus :timetable="timetable" />
<button class="btn--option btn--show"> <button class="btn--action btn--show">
{{ $t('journal.stock-info') }} {{ $t('journal.stock-info') }}
<img <img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`" :src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
@@ -66,9 +66,9 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/variables.scss'; @import '../../../styles/variables';
@import '../../../styles/responsive.scss'; @import '../../../styles/responsive';
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection';
.btn--show { .btn--show {
display: flex; display: flex;
@@ -72,25 +72,22 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import http from '../../http'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
mixins: [dateMixin, styleMixin, listObserverMixin], mixins: [dateMixin, styleMixin],
components: { Loading }, components: { Loading },
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery> type: Object as PropType<ActiveScenery>
} }
}, },
@@ -98,7 +95,8 @@ export default defineComponent({
return { return {
historyList: [] as API.DispatcherHistory.Response, historyList: [] as API.DispatcherHistory.Response,
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
DataStatus: Status.Data DataStatus: Status.Data,
apiStore: useApiStore()
}; };
}, },
@@ -127,7 +125,7 @@ export default defineComponent({
}&countFrom=${countFrom}&countLimit=${countLimit}`; }&countFrom=${countFrom}&countLimit=${countLimit}`;
const historyAPIData: API.DispatcherHistory.Response = await ( const historyAPIData: API.DispatcherHistory.Response = await (
await http.get(requestString) await this.apiStore.client!.get(requestString)
).data; ).data;
this.dataStatus = Status.Data.Loaded; this.dataStatus = Status.Data.Loaded;
+2 -3
View File
@@ -14,8 +14,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station'; import { ActiveScenery, Station } from '../../typings/common';
import { OnlineScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -29,7 +28,7 @@ export default defineComponent({
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery> type: Object as PropType<ActiveScenery>
} }
} }
}); });
+2 -3
View File
@@ -89,8 +89,7 @@ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue'; import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue'; import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station'; import { ActiveScenery, Station } from '../../typings/common';
import { OnlineScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -106,7 +105,7 @@ export default defineComponent({
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery> type: Object as PropType<ActiveScenery>
} }
} }
}); });
@@ -1,38 +1,45 @@
<template> <template>
<section class="info-dispatcher"> <section class="info-dispatcher">
<div class="dispatcher" v-if="onlineScenery"> <div class="info-top" v-if="onlineScenery && onlineScenery.dispatcherExp != -1">
<span <span
class="dispatcher_level" class="dispatcher-level"
:style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)" :style="calculateExpStyle(onlineScenery.dispatcherExp, onlineScenery.dispatcherIsSupporter)"
> >
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }} {{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
</span> </span>
<router-link <router-link
class="dispatcher_name" class="dispatcher-name"
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`" :to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
> >
<span <span
class="text--donator" class="text--donator"
v-if="isDonator(onlineScenery.dispatcherName)" v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
> >
{{ onlineScenery.dispatcherName }} {{ onlineScenery.dispatcherName }}
</span> </span>
<span v-else>{{ onlineScenery.dispatcherName }}</span> <span v-else>{{ onlineScenery.dispatcherName }}</span>
</router-link> </router-link>
</div>
<span class="dispatcher_likes text--primary"> <div class="info-bottom">
<span
class="dispatcher-likes text--primary"
v-if="onlineScenery && onlineScenery.dispatcherExp != -1"
>
<img src="/images/icon-like.svg" alt="Likes count icon" /> <img src="/images/icon-like.svg" alt="Likes count icon" />
<span>{{ onlineScenery?.dispatcherRate || '0' }}</span> <span>{{ onlineScenery?.dispatcherRate || '0' }}</span>
</span> </span>
</div>
<span class="dispatcher-badge">
<StationStatusBadge <StationStatusBadge
:isOnline="onlineScenery ? true : false" :isOnline="onlineScenery ? true : false"
:dispatcherStatus="onlineScenery?.dispatcherStatus" :dispatcherStatus="onlineScenery?.dispatcherStatus"
:dispatcherTimestamp="onlineScenery?.dispatcherTimestamp" :dispatcherTimestamp="onlineScenery?.dispatcherTimestamp"
/> />
</span>
</div>
</section> </section>
</template> </template>
@@ -42,14 +49,21 @@ import dateMixin from '../../../mixins/dateMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import StationStatusBadge from '../../Global/StationStatusBadge.vue'; import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { OnlineScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
import donatorMixin from '../../../mixins/donatorMixin'; import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, donatorMixin], mixins: [styleMixin, dateMixin, routerMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: { props: {
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<ActiveScenery>,
required: false required: false
} }
}, },
@@ -59,19 +73,26 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.info-dispatcher { .info-dispatcher {
display: flex; font-size: 1.8em;
align-items: center; }
justify-content: center;
flex-wrap: wrap; .info-top {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
}
.info-bottom {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em; gap: 0.5em;
.dispatcher { margin-top: 0.5em;
font-size: 2em; }
&_level { .dispatcher-level {
display: inline-block;
margin-right: 0.3em;
background: firebrick; background: firebrick;
border-radius: 0.1em; border-radius: 0.1em;
@@ -82,22 +103,16 @@ export default defineComponent({
font-weight: bold; font-weight: bold;
} }
&_name { .dispatcher-likes {
cursor: pointer; display: flex;
margin-right: 0.25em; gap: 0.25em;
}
&_likes {
img { img {
height: 0.7em; width: 1em;
margin: 0 0.25em;
}
} }
} }
.status-badge { .dispatcher-badge {
font-size: 1.25em; font-size: 0.7em;
margin: 0.5em 0.25em;
}
} }
</style> </style>
@@ -5,7 +5,7 @@
class="icon-info" class="icon-info"
src="/images/icon-unknown.svg" src="/images/icon-unknown.svg"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('sceneries.info.unknown')"
/> />
</span> </span>
@@ -21,25 +21,19 @@
v-if="station?.generalInfo" v-if="station?.generalInfo"
class="scenery-icon icon-info" class="scenery-icon icon-info"
:class="station?.generalInfo.controlType.replace('+', '-')" :class="station?.generalInfo.controlType.replace('+', '-')"
:title="$t('desc.control-type') + $t(`controls.${station?.generalInfo.controlType}`)" :title="
v-html="getControlTypeAbbrev(station?.generalInfo.controlType)" $t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
"
> >
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span> </span>
<img
v-if="station?.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('desc.SUP')"
/>
<img <img
v-if="station?.generalInfo?.signalType" v-if="station?.generalInfo?.signalType"
class="icon-info" class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)" :title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
/> />
<img <img
@@ -47,7 +41,7 @@
class="icon-info" class="icon-info"
src="/images/icon-lock.svg" src="/images/icon-lock.svg"
alt="Non-public scenery" alt="Non-public scenery"
:title="$t('desc.non-public')" :title="$t('sceneries.info.non-public')"
/> />
<img <img
@@ -55,7 +49,7 @@
class="icon-info" class="icon-info"
src="/images/icon-unavailable.svg" src="/images/icon-unavailable.svg"
alt="Unavailable scenery" alt="Unavailable scenery"
:title="$t('desc.unavailable')" :title="$t('sceneries.info.unavailable')"
/> />
<img <img
@@ -63,7 +57,23 @@
class="icon-info" class="icon-info"
src="/images/icon-abandoned.svg" src="/images/icon-abandoned.svg"
alt="Abandoned scenery" alt="Abandoned scenery"
:title="$t('desc.abandoned')" :title="$t('sceneries.info.abandoned')"
/>
<img
v-if="station?.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<img
v-if="station?.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/> />
<img <img
@@ -71,19 +81,18 @@
class="icon-info" class="icon-info"
src="/images/icon-real.svg" src="/images/icon-real.svg"
alt="real scenery" alt="real scenery"
:title="`${$t('desc.real')} ${station.generalInfo.lines}`" :title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
/> />
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import stationInfoMixin from '../../../mixins/stationInfoMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station'; import { Station } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [stationInfoMixin, styleMixin], mixins: [styleMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -94,6 +103,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/icons.scss'; @import '../../../styles/icons.scss';
.info-icons { .info-icons {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -101,6 +111,7 @@ export default defineComponent({
margin: 1em; margin: 1em;
} }
.icon-info { .icon-info {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -1,11 +1,11 @@
<template> <template>
<section class="info-routes" v-if="station.generalInfo"> <section class="info-routes" v-if="station.generalInfo">
<div class="routes one-way" v-if="filteredOneWayRoutes.length > 0"> <div class="routes one-way" v-if="oneWayRoutes.length > 0">
<b>{{ $t('scenery.one-way-routes') }}</b> <b>{{ $t('scenery.one-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li
v-for="route in filteredOneWayRoutes" v-for="route in oneWayRoutes"
:key="route.routeName" :key="route.routeName"
@click="setActiveShowLength(route.routeName)" @click="setActiveShowLength(route.routeName)"
> >
@@ -24,12 +24,12 @@
</ul> </ul>
</div> </div>
<div class="routes two-way" v-if="filteredTwoWayRoutes.length > 0"> <div class="routes two-way" v-if="twoWayRoutes.length > 0">
<b>{{ $t('scenery.two-way-routes') }}</b> <b>{{ $t('scenery.two-way-routes') }}</b>
<ul class="routes-list"> <ul class="routes-list">
<li <li
v-for="route in filteredTwoWayRoutes" v-for="route in twoWayRoutes"
:key="route.routeName" :key="route.routeName"
@click="setActiveShowLength(route.routeName)" @click="setActiveShowLength(route.routeName)"
> >
@@ -52,10 +52,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station'; import { Station } from '../../../typings/common';
import { StationRoutesInfo } from '../../../store/typings';
const routeFilter = (route: StationRoutesInfo) => !route.hidden;
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -80,12 +77,12 @@ export default defineComponent({
}, },
computed: { computed: {
filteredOneWayRoutes() { oneWayRoutes() {
return this.station.generalInfo?.routes.oneWay.filter(routeFilter) || []; return this.station.generalInfo?.routes.single ?? [];
}, },
filteredTwoWayRoutes() { twoWayRoutes() {
return this.station.generalInfo?.routes.twoWay.filter(routeFilter) || []; return this.station.generalInfo?.routes.double ?? [];
} }
} }
}); });
@@ -8,7 +8,7 @@
<transition-group name="spawns-anim" tag="ul"> <transition-group name="spawns-anim" tag="ul">
<li <li
class="badge spawn badge-none" class="badge badge-none"
v-if="!onlineScenery || onlineScenery.spawns.length == 0" v-if="!onlineScenery || onlineScenery.spawns.length == 0"
key="no-spawns" key="no-spawns"
> >
@@ -16,13 +16,13 @@
</li> </li>
<li <li
class="badge spawn" class="badge spawn-badge"
v-for="(spawn, i) in sortedSpawns" v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + onlineScenery?.dispatcherName + i" :key="spawn.spawnName + onlineScenery?.dispatcherName + i"
:data-electrified="spawn.isElectrified" :data-electrified="spawn.isElectrified"
> >
<span class="spawn_name">{{ spawn.spawnName }}</span> <span class="name">{{ spawn.spawnName }}</span>
<span class="spawn_length">{{ spawn.spawnLength }}m</span> <span class="length">{{ spawn.spawnLength }}m</span>
</li> </li>
</transition-group> </transition-group>
</section> </section>
@@ -30,12 +30,12 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { OnlineScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
props: { props: {
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<ActiveScenery>,
required: false required: false
} }
}, },
@@ -59,19 +59,6 @@ ul {
position: relative; position: relative;
} }
.spawn {
color: white;
&_length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
}
}
.spawns-anim { .spawns-anim {
&-move, &-move,
&-enter-active, &-enter-active,
@@ -1,83 +0,0 @@
<template>
<section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''">
<span class="likes">
<img src="/images/icon-like" alt="Likes count icon" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span>
<span class="users">
<img src="/images/icon-user" alt="Users count icon" />
<span>{{ station.onlineInfo?.currentUsers || '0' }}</span>
/
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
</span>
<span class="spawns">
<img src="/images/icon-spawn" alt="Spawns count icon" />
<span>{{ station.onlineInfo?.spawns.length || '0' }}</span>
</span>
<span class="schedules">
<img src="/images/icon-timetable" alt="Timetables count icon" />
<span>
<span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
/
<span style="color: #bbb"
>{{
station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed)
.length || '0'
}}
</span>
</span>
</span>
</section>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({
props: {
station: {
type: Object as PropType<Station>,
required: true
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
.info-stats {
padding: 1rem 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: 1.65em;
&.no-stats {
opacity: 0.5;
}
& > span {
display: flex;
align-items: center;
margin: 0.3em;
}
.likes,
.spawns {
color: $accentCol;
}
span > img {
width: 1.2em;
margin-right: 0.5em;
}
}
</style>
@@ -32,14 +32,14 @@
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import { OnlineScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin, modalTrainMixin], mixins: [routerMixin, modalTrainMixin],
props: { props: {
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery>, type: Object as PropType<ActiveScenery>,
required: false required: false
} }
} }
+12 -29
View File
@@ -6,22 +6,14 @@
<span>{{ $t('scenery.timetables') }}</span> <span>{{ $t('scenery.timetables') }}</span>
<span> <span>
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all || 0 }}</span> <span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all ?? 0 }}</span>
<span> / </span> <span> / </span>
<span class="text--grayed"> <span class="text--grayed">
{{ onlineScenery?.scheduledTrainCount.confirmed || '0' }} {{ onlineScenery?.scheduledTrainCount.confirmed ?? 0 }}
</span> </span>
</span> </span>
<span class="header_links" v-if="station"> <span class="header_links" v-if="station">
<!-- <a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
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>
@@ -33,12 +25,12 @@
{{ (i > 0 && '&bull;') || '' }} {{ (i > 0 && '&bull;') || '' }}
<button <button
:key="cp.checkpointName" :key="cp"
class="checkpoint_item" class="checkpoint_item"
:class="{ current: chosenCheckpoint === cp.checkpointName }" :class="{ current: chosenCheckpoint === cp }"
@click="setCheckpoint(cp)" @click="setCheckpoint(cp)"
> >
{{ cp.checkpointName }} {{ cp }}
</button> </button>
</span> </span>
</div> </div>
@@ -74,7 +66,7 @@
class="timetable-item" class="timetable-item"
v-else v-else
v-for="scheduledTrain in computedScheduledTrains" v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId" :key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
tabindex="0" tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)" @keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@@ -186,12 +178,11 @@ 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 Station from '../../scripts/interfaces/Station';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { OnlineScenery } from '../../store/typings';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -205,7 +196,7 @@ export default defineComponent({
type: Object as PropType<Station> type: Object as PropType<Station>
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery> type: Object as PropType<ActiveScenery>
} }
}, },
@@ -231,7 +222,7 @@ export default defineComponent({
const chosenCheckpoint = ref( const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0 props.station?.generalInfo?.checkpoints?.length == 0
? '' ? ''
: props.station?.generalInfo?.checkpoints[0].checkpointName || null : props.station?.generalInfo?.checkpoints[0] ?? null
); );
return { return {
@@ -278,12 +269,11 @@ export default defineComponent({
loadSelectedOption() { loadSelectedOption() {
if (!this.station) return; if (!this.station) return;
this.chosenCheckpoint = this.chosenCheckpoint = this.station.generalInfo?.checkpoints[0] ?? this.station.name;
this.station.generalInfo?.checkpoints[0]?.checkpointName || this.station.name;
}, },
setCheckpoint(cp: { checkpointName: string }) { setCheckpoint(cp: string) {
this.chosenCheckpoint = cp.checkpointName; this.chosenCheckpoint = cp;
} }
} }
}); });
@@ -415,13 +405,6 @@ export default defineComponent({
width: 100%; width: 100%;
} }
.g-tooltip > .content {
z-index: 100;
color: white;
left: 110%;
}
img { img {
width: 1.1em; width: 1.1em;
} }
@@ -71,29 +71,27 @@
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
import { OnlineScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import http from '../../http'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin], mixins: [dateMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
}, },
onlineScenery: { onlineScenery: {
type: Object as PropType<OnlineScenery> type: Object as PropType<ActiveScenery>
} }
}, },
data() { data() {
return { return {
historyList: [] as API.TimetableHistory.Response, historyList: [] as API.TimetableHistory.Response,
apiStore: useApiStore(),
dataStatus: Status.Data.Loading, dataStatus: Status.Data.Loading,
DataStatus: Status.Data DataStatus: Status.Data
}; };
@@ -112,7 +110,7 @@ export default defineComponent({
try { try {
const response: API.TimetableHistory.Response = await ( const response: API.TimetableHistory.Response = await (
await http.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: { params: {
issuedFrom: this.station?.name || this.onlineScenery?.name issuedFrom: this.station?.name || this.onlineScenery?.name
} }
@@ -11,7 +11,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../store/typings'; import { ScheduledTrain, StopStatus } from '../../typings/common';
interface ScheduledTrainComp extends ScheduledTrain { interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string; stopStatusIndicator: string;
@@ -3,7 +3,7 @@
<div class="card_controls"> <div class="card_controls">
<button class="btn--filled btn--image" @click="toggleCard"> <button class="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" />
{{ $t('options.filters') }} [F] [F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span> <span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
</button> </button>
@@ -28,8 +28,8 @@
</div> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl"> <div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content"> <div class="card_content" @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>
@@ -108,6 +108,7 @@
:id="slider.id" :id="slider.id"
:min="slider.minRange" :min="slider.minRange"
:max="slider.maxRange" :max="slider.maxRange"
:step="slider.step"
v-model="slider.value" v-model="slider.value"
@change="handleInput" @change="handleInput"
/> />
@@ -136,7 +137,7 @@
:disabled="filterStore.areFiltersAtDefault" :disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault" :data-disabled="filterStore.areFiltersAtDefault"
> >
{{ $t('filters.reset') }} [R] {{ $t('filters.reset') }}
</button> </button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button> <button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div> </div>
@@ -170,7 +171,10 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '' chosenSearchScenery: '',
scrollTop: 0,
lastFocusedEl: null as HTMLElement | null
}), }),
setup() { setup() {
@@ -236,7 +240,10 @@ export default defineComponent({
isVisible(value: boolean) { isVisible(value: boolean) {
this.$nextTick(() => { this.$nextTick(() => {
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus(); if (value) {
(this.$refs['cardRef'] as HTMLDivElement).focus();
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
}
}); });
} }
}, },
@@ -247,6 +254,10 @@ export default defineComponent({
this.isVisible = !this.isVisible; this.isVisible = !this.isVisible;
}, },
onScroll(e: Event) {
this.scrollTop = (e.target as HTMLElement).scrollTop;
},
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
@@ -256,8 +267,6 @@ export default defineComponent({
}, },
handleAuthorsInput() { handleAuthorsInput() {
console.log(this.authorsInputValue);
this.filterStore.changeFilterValue('authors', this.authorsInputValue); this.filterStore.changeFilterValue('authors', this.authorsInputValue);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue); if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
@@ -433,23 +442,16 @@ h3.section-header {
} }
.card_actions { .card_actions {
width: 100%;
padding: 0.5em; padding: 0.5em;
.filter-option {
max-width: 50%;
margin: 0 auto;
}
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
width: 100%;
margin-top: 0.5em; margin-top: 0.5em;
button { button {
width: 50%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 0.5em; padding: 0.5em;
@@ -0,0 +1,151 @@
<template>
<div class="station-stats">
<div class="separator" />
<div>
{{ $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
>:
<b :style="calculateFactorStyle()">
{{ uFactor.toFixed(2) }}
</b>
| {{ $t('station-stats.avg-timetable-count') }}
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</div>
<div>
{{ $t('station-stats.single-track-count') }}
<b>{{ trackCount.oneWayElectric }}</b> {{ $t('station-stats.electrified') }} /
<b>{{ trackCount.oneWayOther }}</b> {{ $t('station-stats.not-electrified') }} |
{{ $t('station-stats.double-track-count') }} <b>{{ trackCount.twoWayElectric }}</b>
{{ $t('station-stats.electrified') }} / <b>{{ trackCount.twoWayOther }}</b>
{{ $t('station-stats.not-electrified') }} | {{ $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>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
mainStore: useMainStore()
};
},
methods: {
calculateFactorStyle() {
if (this.uFactor == 0) return '';
const norm = this.uFactor == 0 ? 1 : Math.max(Math.min(this.uFactor / 2, 1), 0);
const lerp = 120 * norm;
return `color: hsl(${lerp}, 100%, 60%)`;
}
},
computed: {
uFactor() {
const activeDispatchers = this.mainStore.activeSceneryList.filter(
(scenery) => scenery.region == this.mainStore.region.id && scenery.dispatcherId != -1
);
const activeTrains = this.mainStore.trainList.filter(
(train) => train.region == this.mainStore.region.id
);
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
},
avgTimetableCount() {
const scheduledTrainsTotal = this.mainStore.activeSceneryList.reduce<number>((acc, sc) => {
if (sc.region != this.mainStore.region.id) return acc;
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
return this.mainStore.activeSceneryList.length != 0
? scheduledTrainsTotal / this.mainStore.activeSceneryList.length
: 0;
},
trackCount() {
return this.mainStore.allStationInfo
.filter(
(st) =>
st.onlineInfo?.dispatcherId != -1 &&
st.onlineInfo?.region == this.mainStore.region.id &&
st.generalInfo?.routes
)
.reduce(
(acc, st) => {
[...st.generalInfo!.routes.single, ...st.generalInfo!.routes.double].forEach((r) => {
if (r.isInternal) return;
const keyName: keyof typeof acc = `${r.routeTracks == 2 ? 'twoWay' : 'oneWay'}${r.isElectric ? 'Electric' : 'Other'}`;
acc[keyName] += 1;
});
return acc;
},
{ oneWayElectric: 0, oneWayOther: 0, twoWayElectric: 0, twoWayOther: 0 }
);
},
spawnCount() {
return this.mainStore.activeSceneryList.reduce(
(acc, scenery) => {
scenery.spawns.forEach((spawn) => {
if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1;
if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1;
if (/LUZ|SM/i.test(spawn.spawnName)) acc['loco'] += 1;
if (/ALL/i.test(spawn.spawnName)) acc['all'] += 1;
});
return acc;
},
{ passenger: 0, freight: 0, loco: 0, all: 0 }
);
}
}
});
</script>
<style lang="scss" scoped>
.separator {
width: 100%;
height: 2px;
margin: 0.5em 0;
background-color: #aaa;
}
.station-stats {
text-align: center;
color: #ddd;
}
[data-factor-low='true'] {
color: #ddd;
}
[data-factor-mediocre='true'] {
color: lightgreen;
}
[data-factor-high='true'] {
color: greenyellow;
}
[data-factor-highest='true'] {
color: rgb(22, 245, 22);
}
</style>
+219 -204
View File
@@ -1,6 +1,10 @@
<template> <template>
<section class="station_table"> <section class="station_table">
<div class="table_wrapper"> <Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0"
/>
<div class="table_wrapper" v-else-if="displayedStations.length > 0">
<table> <table>
<thead> <thead>
<tr> <tr>
@@ -9,9 +13,10 @@
:key="headerName" :key="headerName"
@click="changeSorter(headerName)" @click="changeSorter(headerName)"
class="header-text" class="header-text"
:class="headerName"
> >
<span class="header_wrapper"> <span class="header_wrapper">
<div v-html="$t(`sceneries.${headerName}`)"></div> <div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<img <img
class="sort-icon" class="sort-icon"
@@ -27,12 +32,13 @@
:key="headerName" :key="headerName"
@click="changeSorter(headerName)" @click="changeSorter(headerName)"
class="header-image" class="header-image"
:class="headerName"
> >
<span class="header_wrapper"> <span class="header_wrapper">
<img <img
:src="`/images/icon-${headerName}.svg`" :src="`/images/icon-${headerName}.svg`"
:alt="headerName" :alt="headerName"
:title="$t(`sceneries.${headerName}`)" :title="$t(`sceneries.headers.${headerName}`)"
/> />
<img <img
@@ -48,24 +54,23 @@
<tbody> <tbody>
<tr <tr
class="station" v-for="station in displayedStations"
:class="{ 'last-selected': lastSelectedStationName == station.name }" :class="{ 'last-selected': lastSelectedStationName == station.name }"
v-for="(station, i) in stations" :key="station.name"
:key="i + station.name"
@click.left="setScenery(station.name)" @click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)" @click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)" @keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)" @keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0" tabindex="0"
> >
<td class="station_name" :class="station.generalInfo?.availability"> <td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{ <b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project station.generalInfo.project
}}</b> }}</b>
{{ station.name }} {{ station.name }}
</td> </td>
<td class="station_level"> <td class="station-level">
<span v-if="station.generalInfo"> <span v-if="station.generalInfo">
<span <span
v-if=" v-if="
@@ -82,7 +87,7 @@
<img <img
src="/images/icon-abandoned.svg" src="/images/icon-abandoned.svg"
alt="non-public" alt="non-public"
:title="$t('desc.abandoned')" :title="$t('sceneries.info.abandoned')"
/> />
</span> </span>
@@ -90,7 +95,7 @@
<img <img
src="/images/icon-lock.svg" src="/images/icon-lock.svg"
alt="non-public" alt="non-public"
:title="$t('desc.non-public')" :title="$t('sceneries.info.non-public')"
/> />
</span> </span>
@@ -98,7 +103,7 @@
<img <img
src="/images/icon-unavailable.svg" src="/images/icon-unavailable.svg"
alt="unavailable" alt="unavailable"
:title="$t('desc.unavailable')" :title="$t('sceneries.info.unavailable')"
/> />
</span> </span>
</span> </span>
@@ -106,19 +111,20 @@
<span v-else> ? </span> <span v-else> ? </span>
</td> </td>
<td class="station_status"> <td class="station-status">
<StationStatusBadge <StationStatusBadge
:isOnline="station.onlineInfo ? true : false" :isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus" :dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/> />
</td> </td>
<td class="station_dispatcher-name"> <td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName"> <span v-if="station.onlineInfo?.dispatcherName">
<b <b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
:title="$t('donations.dispatcher-message')"
@click.stop="openDonationModal" @click.stop="openDonationModal"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
> >
<img src="/images/icon-diamond.svg" alt="" /> <img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }} {{ station.onlineInfo.dispatcherName }}
@@ -130,9 +136,9 @@
</span> </span>
</td> </td>
<td class="station_dispatcher-exp"> <td class="station-dispatcher-exp">
<span <span
v-if="station.onlineInfo" v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style=" :style="
calculateExpStyle( calculateExpStyle(
station.onlineInfo.dispatcherExp, station.onlineInfo.dispatcherExp,
@@ -144,174 +150,181 @@
</span> </span>
</td> </td>
<td class="station_tracks twoway"> <td class="station-tracks">
<div v-if="station.generalInfo">
<span <span
v-if=" v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
station.generalInfo &&
station.generalInfo.routes.twoWayCatenaryRouteNames.length > 0
"
class="track catenary" class="track catenary"
:title="`Liczba zelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayCatenaryRouteNames.length}`" :title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
> >
{{ station.generalInfo.routes.twoWayCatenaryRouteNames.length }} {{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span> </span>
<span <span
v-if=" v-if="station.generalInfo.routes.singleOtherNames.length != 0"
station.generalInfo &&
station.generalInfo.routes.twoWayNoCatenaryRouteNames.length > 0
"
class="track no-catenary" class="track no-catenary"
:title="`Liczba niezelektryfikowanych szlaków dwutorowych: ${station.generalInfo.routes.twoWayNoCatenaryRouteNames.length}`" :title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
> >
{{ station.generalInfo.routes.twoWayNoCatenaryRouteNames.length }} {{ station.generalInfo.routes.singleOtherNames.length }}
</span>
<span class="separator"></span>
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.oneWayCatenaryRouteNames.length > 0
"
class="track catenary"
:title="`Liczba zelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.oneWayCatenaryRouteNames.length }}
</span>
<span
v-if="
station.generalInfo &&
station.generalInfo.routes.oneWayNoCatenaryRouteNames.length > 0
"
class="track no-catenary"
:title="`Liczba niezelektryfikowanych szlaków jednotorowych: ${station.generalInfo.routes.oneWayNoCatenaryRouteNames.length}`"
>
{{ station.generalInfo.routes.oneWayNoCatenaryRouteNames.length }}
</span> </span>
</div>
</td> </td>
<td class="station_info" v-if="station.generalInfo"> <td class="station-tracks">
<div v-if="station.generalInfo">
<span <span
class="scenery-icon icon-info" v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
:class="station.generalInfo.controlType.replace('+', '-')" class="track catenary"
:title="$t('desc.control-type') + $t(`controls.${station.generalInfo.controlType}`)" :title="`${$t('sceneries.info.double-track-routes-catenary')}${
v-html="getControlTypeAbbrev(station.generalInfo.controlType)" station.generalInfo.routes.doubleElectrifiedNames.length
}`"
> >
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span> </span>
<span> <span
<img v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
v-if="station.generalInfo.SUP" :title="`${$t('sceneries.info.double-track-routes-other')}${
src="/images/icon-SUP.svg" station.generalInfo.routes.doubleOtherNames.length
alt="SUP (RASP-UZK)" }`"
:title="$t('desc.SUP')" >
/> {{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span> </span>
<span>
<img <img
v-if="station.generalInfo?.signalType"
class="icon-info" class="icon-info"
v-if="station.generalInfo.signalType"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType" :alt="station.generalInfo.signalType"
:title="$t('desc.signals-type') + $t(`signals.${station.generalInfo.signalType}`)" :title="
$t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/> />
</span>
<span>
<img <img
v-if="station.generalInfo?.SUP"
class="icon-info" class="icon-info"
v-if="station.generalInfo && station.generalInfo.routes.sblRouteNames.length > 0" src="/images/icon-SUP.svg"
src="/images/icon-SBL.svg" alt="SUP (RASP-UZK)"
alt="SBL" :title="$t('sceneries.info.SUP')"
:title="$t('desc.SBL') + `${station.generalInfo.routes.sblRouteNames.join(',')}`"
/> />
</span>
</td>
<td class="station_info" v-else>
<img <img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<img
v-if="!station.generalInfo"
class="icon-info" class="icon-info"
src="/images/icon-unknown.svg" src="/images/icon-unknown.svg"
alt="icon-unknown" alt="icon-unknown"
:title="$t('desc.unknown')" :title="$t('sceneries.info.unknown')"
/> />
</td> </td>
<td class="station_users" :class="{ inactive: !station.onlineInfo }"> <td
<span>{{ station.onlineInfo?.currentUsers || 0 }}</span> class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/ /
<span>{{ station.onlineInfo?.maxUsers || 0 }}</span> <span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td> </td>
<td class="station_spawns" :class="{ inactive: !station.onlineInfo }"> <td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.spawns.length || 0 }}</span> <span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td> </td>
<td <td
class="station_schedules all" class="station-spawns"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }" :class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
> >
{{ station.onlineInfo?.scheduledTrainCount.all }} <span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td> </td>
<td <td
class="station_schedules unconfirmed" class="station-schedules all"
style="width: 30px" style="width: 30px"
:class="{ inactive: !station.onlineInfo }" :class="{ inactive: !station.onlineInfo }"
> >
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed }} {{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td> </td>
<td <td
class="station_schedules confirmed" class="station-schedules unconfirmed"
style="width: 30px" style="width: 30px"
:class="{ inactive: !station.onlineInfo }" :class="{ inactive: !station.onlineInfo }"
> >
{{ station.onlineInfo?.scheduledTrainCount.confirmed }} {{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<Loading v-if="apiStore.dataStatuses.connection == Status.Loading" /> <div class="no-stations" v-else>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
<div class="no-stations" v-else-if="stations.length == 0"> >)
{{ $t('sceneries.no-stations') }}
</div> </div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames'; import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue'; import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common'; import { Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({ export default defineComponent({
props: {
stations: {
type: Array as PropType<Station[]>,
required: true
}
},
emits: ['toggleDonationModal'], emits: ['toggleDonationModal'],
components: { Loading, StationStatusBadge }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin], mixins: [styleMixin, dateMixin],
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
@@ -322,28 +335,37 @@ export default defineComponent({
computed: { computed: {
sorterActive() { sorterActive() {
return this.stationFiltersStore.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 stationFiltersStore = useStationFiltersStore(); const stationFiltersStore = useStationFiltersStore();
return { return {
Status: Status.Data, Status: Status.Data,
stationFiltersStore, stationFiltersStore,
mainStore, mainStore,
apiStore apiStore,
tooltipStore
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.stations.find((station) => station.name === name); const station = this.displayedStations.find((station) => station.name === name);
if (!station) return; if (!station) return;
this.lastSelectedStationName = station.name; this.lastSelectedStationName = station.name;
this.tooltipStore.hide();
this.$router.push({ this.$router.push({
name: 'SceneryView', name: 'SceneryView',
@@ -357,6 +379,7 @@ export default defineComponent({
openDonationModal(e: Event) { openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationModal', true);
this.mainStore.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
this.tooltipStore.hide();
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
@@ -366,7 +389,7 @@ export default defineComponent({
}, },
changeSorter(headerName: HeadIdsTypes) { changeSorter(headerName: HeadIdsTypes) {
if (headerName == 'general' || headerName == 'routes') return; if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName); this.stationFiltersStore.changeSorter(headerName);
} }
@@ -381,34 +404,29 @@ export default defineComponent({
$rowCol: #424242; $rowCol: #424242;
.change-anim { .station_table {
&-enter-active, height: 80vh;
&-leave-active { min-height: 550px;
transition: opacity 100ms ease-in;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
.table_wrapper {
overflow: auto; overflow: auto;
overflow-y: hidden;
font-weight: 500; font-weight: 500;
} }
table { .no-stations {
white-space: nowrap; text-align: center;
border-collapse: collapse; font-size: 1.5em;
// min-width: 1350px;
width: 100%;
@include smallScreen() { padding: 1em;
min-width: auto;
background: #1a1a1a;
} }
table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
min-width: 1250px;
white-space: wrap;
thead tr { thead tr {
background-color: $bgCol; background-color: $bgCol;
} }
@@ -417,12 +435,41 @@ table {
position: sticky; position: sticky;
top: 0; top: 0;
&.header-text { &.station {
min-width: 140px; width: 12em;
}
&.min-lvl {
width: 4em;
}
&.status {
width: 10em;
}
&.dispatcher {
width: 12em;
}
&.dispatcher-lvl {
width: 6em;
}
&.routes-double,
&.routes-single {
width: 7em;
}
&.general {
width: 11em;
} }
&.header-image { &.header-image {
min-width: 60px; width: 3.5em;
&.user {
width: 5em;
}
} }
padding: 0.5em 0.25em; padding: 0.5em 0.25em;
@@ -447,7 +494,7 @@ table {
} }
} }
tr.station { tr {
background-color: $rowCol; background-color: $rowCol;
&:nth-child(even) { &:nth-child(even) {
@@ -461,10 +508,15 @@ tr.station {
} }
td { td {
padding: 0.25em 1em; padding: 0.15em 0;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
&.inactive {
opacity: 0.2;
}
@include smallScreen() { @include smallScreen() {
margin: 0; margin: 0;
@@ -474,9 +526,9 @@ tr.station {
} }
} }
td.station { .station-name {
&_name {
font-weight: bold; font-weight: bold;
max-width: 200px;
&.default { &.default {
color: $accentCol; color: $accentCol;
@@ -492,8 +544,8 @@ td.station {
} }
} }
&_level, .station-level,
&_dispatcher-exp { .station-dispatcher-exp {
span { span {
display: block; display: block;
@@ -509,51 +561,36 @@ td.station {
} }
} }
// &_dispatcher-name { .station-dispatcher-name {
// position: relative; img {
// }
&_dispatcher-name img {
max-width: 1.35em; max-width: 1.35em;
vertical-align: text-bottom; vertical-align: text-bottom;
} }
}
&_level { .station-level {
span { span {
background-color: #888; background-color: #888;
border-radius: 50%; border-radius: 50%;
} }
} }
&_info { .station-info {
display: flex;
align-items: center;
justify-content: center;
/* Images */
.icon-info { .icon-info {
display: flex; vertical-align: middle;
justify-content: center; line-height: 2.5em;
align-items: center;
width: 32px; width: 2.5em;
height: 32px; height: 2.5em;
font-size: 12px; font-size: 0.8em;
margin: 0 3px;
margin: 0 0.2em; outline: 2px solid #2b2b2b;
border-radius: 5px;
outline: 2px solid #444;
border-radius: 0.5em;
@include smallScreen() {
width: 24px;
height: 24px;
font-size: 10px;
}
} }
} }
&_tracks { .station-tracks {
.no-catenary { .no-catenary {
background-color: #939393; background-color: #939393;
} }
@@ -562,30 +599,22 @@ td.station {
background-color: #009dce; background-color: #009dce;
} }
.separator {
background-color: #b3b3b3;
padding: 2px;
}
.track { .track {
margin: 0 0.35em; display: inline-block;
padding: 0.35em; text-align: center;
font-size: 1.05em; width: 1.3em;
white-space: pre-wrap; padding: 0.35em 0;
font-size: 1.1em;
margin: 0 0.2em;
} }
} }
&_users, .station-schedules {
&_spawns,
&_schedules {
&.inactive {
opacity: 0.2;
}
}
}
.station_users {
span {
color: gold;
}
}
.station_schedules {
&.all { &.all {
color: gold; color: gold;
} }
@@ -598,18 +627,4 @@ td.station {
color: lime; color: lime;
} }
} }
.separator {
border-left: 3px solid #b3b3b3;
}
.no-stations {
text-align: center;
font-size: 1.5em;
padding: 1em;
margin: 1em 0;
background: #333;
}
</style> </style>
+6 -3
View File
@@ -16,6 +16,8 @@ export interface Filter {
SPE: boolean; SPE: boolean;
SUP: boolean; SUP: boolean;
noSUP: boolean; noSUP: boolean;
ASDEK: boolean;
noASDEK: boolean;
ręczne: boolean; ręczne: boolean;
'ręczne+SPK': boolean; 'ręczne+SPK': boolean;
'ręczne+SCS': boolean; 'ręczne+SCS': boolean;
@@ -34,6 +36,8 @@ export interface Filter {
minOneWay: number; minOneWay: number;
minTwoWayCatenary: number; minTwoWayCatenary: number;
minTwoWay: number; minTwoWay: number;
minVmax: number;
maxVmax: number;
'no-1track': boolean; 'no-1track': boolean;
'no-2track': boolean; 'no-2track': boolean;
'include-selected': boolean; 'include-selected': boolean;
@@ -42,14 +46,13 @@ export interface Filter {
nonPublic: boolean; nonPublic: boolean;
unavailable: boolean; unavailable: boolean;
abandoned: boolean; abandoned: boolean;
endingStatus: boolean; endingStatus: boolean;
afkStatus: boolean; afkStatus: boolean;
noSpaceStatus: boolean; noSpaceStatus: boolean;
unavailableStatus: boolean; unavailableStatus: boolean;
unsignedStatus: boolean; unsignedStatus: boolean;
authors: string; authors: string;
onlineFromHours: number; onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
} }
+38
View File
@@ -0,0 +1,38 @@
<template>
<div class="tooltip-content">
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
}
img {
height: 1em;
}
</style>
+39
View File
@@ -0,0 +1,39 @@
<template>
<div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="" />
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
gap: 0.5em;
padding: 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
}
img {
vertical-align: middle;
height: 1em;
margin-right: 0.5em;
}
</style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="spawns.length != 0">
<span v-for="(spawn, i) in spawns">
<template v-if="i > 0"> | </template>
<b>{{ spawn.spawnName }}</b> ({{ spawn.spawnLength }}m)
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { ScenerySpawn } from '../../typings/common';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
},
computed: {
spawns() {
if (this.tooltipStore.content == '') return [];
const parsedSpawns = JSON.parse(this.tooltipStore.content) as ScenerySpawn[];
return parsedSpawns ?? [];
}
}
});
</script>
<style scoped>
.tooltip-content {
width: 300px;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa;
}
</style>
+74
View File
@@ -0,0 +1,74 @@
<template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
export default defineComponent({
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
data() {
return {
tooltipStore: useTooltipStore()
};
},
watch: {
'tooltipStore.mousePos': {
deep: true,
// [x, y]
handler(val: [number, number]) {
this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%';
translateX = '-100%';
} else {
previewEl.style.left = `${val[0]}px`;
translateX = '-50%';
}
previewEl.style.top = `${val[1]}px`;
const isOutside =
val[1] + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)';
previewEl.style.transform = `translate(${translateX}, ${translateY})`;
});
}
}
}
});
</script>
<style lang="scss" scoped>
.tooltip {
position: absolute;
z-index: 250;
max-width: 400px;
text-align: center;
}
</style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="trains.length != 0">
<span v-for="(train, i) in trains">
<template v-if="i > 0"> | </template>
<b>{{ train.trainNo }}</b> {{ train.driverName }}
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { StationTrain } from '../../typings/common';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
},
computed: {
trains() {
if (this.tooltipStore.content == '') return [];
const parsedTrains = JSON.parse(this.tooltipStore.content) as StationTrain[];
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
}
}
});
</script>
<style scoped>
.tooltip-content {
width: 300px;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa;
}
</style>
@@ -0,0 +1,95 @@
<template>
<div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</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">
{{ tooltipStore.content.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore(),
imageState: 'loading'
};
},
mounted() {
this.imageState = 'loading';
},
watch: {
'tooltipStore.type'(prev, val) {
if (prev != val) this.imageState = 'loading';
}
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
},
onImageError(e: Event) {
this.imageState = 'error';
(e.target as HTMLElement).style.display = 'none';
}
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
width: 300px;
min-height: 200px;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em;
border-radius: 0.5em;
}
.loading-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
img {
width: 100%;
height: auto;
}
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
.error-placeholder {
height: 176px;
}
</style>
+3 -2
View File
@@ -29,8 +29,9 @@
<span <span
v-if=" v-if="
stop.duration || stop.duration ||
(stop.status == 'stopped' && stop.position != 'begin') || (stop.status == 'stopped' &&
stop.departureDelay != stop.arrivalDelay 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(', ', '-')"
+138 -75
View File
@@ -1,7 +1,8 @@
<template> <template>
<div class="train-info"> <div class="train-info" :data-extended="extended">
<section class="train-general"> <section class="train-general">
<div class="general-info"> <div class="general-top-bar">
<div>
<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 }}
@@ -11,10 +12,18 @@
class="timetable-warnings" class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR" v-if="train.timetableData?.TWR || train.timetableData?.SKR"
> >
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')"> <span
class="train-badge twr"
v-if="train.timetableData?.TWR"
:title="$t('general.TWR')"
>
TWR TWR
</span> </span>
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')"> <span
class="train-badge skr"
v-if="train.timetableData?.SKR"
:title="$t('general.SKR')"
>
SKR SKR
</span> </span>
</span> </span>
@@ -36,25 +45,42 @@
<div class="train-driver"> <div class="train-driver">
<b <b
v-if="apiStore.donatorsData.includes(train.driverName)" v-if="apiStore.donatorsData.includes(train.driverName)"
:title="$t('donations.driver-message')" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
> >
{{ train.driverName }} {{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" /> <img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</b> </b>
<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 class="general-timetable" v-if="train.timetableData"> <div class="general-timetable" v-if="train.timetableData">
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<img <span
v-if="getSceneriesWithComments(train.timetableData).length > 0" v-if="getSceneriesWithComments(train.timetableData).length > 0"
class="image-warning" data-tooltip-type="BaseTooltip"
src="/images/icon-warning.svg" :data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData train.timetableData
)})`" )})`"
/> >
<img class="image-warning" src="/images/icon-warning.svg" />
</span>
</div> </div>
<hr style="margin: 0.25em 0" /> <hr style="margin: 0.25em 0" />
@@ -67,7 +93,7 @@
</div> </div>
<div class="general-status"> <div class="general-status">
<div class="timetable-progress" v-if="train.timetableData"> <div class="status-timetable-progress" v-if="train.timetableData">
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" /> <ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="progress-distance"> <span class="progress-distance">
@@ -91,30 +117,44 @@
</div> </div>
</div> </div>
<div class="driver_position text--grayed" style="margin-top: 0.25em"> <div class="general-stats" v-if="extended">
<div>
<img src="/images/icon-length.svg" alt="length icon" />
{{ train.length }}m
</div>
<div>
<img src="/images/icon-mass.svg" alt="mass icon" />
{{ (train.mass / 1000).toFixed(1) }}t
</div>
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h
</div>
</div>
<div class="text--grayed" style="margin-top: 0.25em">
{{ displayTrainPosition(train) }} {{ displayTrainPosition(train) }}
</div> </div>
</section> </section>
<section class="train-stats"> <section class="train-stats" v-if="!extended">
<TrainThumbnail :name="train.locoType" :onlyFirstSegment="true" /> <StockList :trainStockList="train.stockList" :tractionOnly="true" />
<div class="text--grayed">
{{ train.locoType }}
<span v-if="train.stockList.length > 1">
&nbsp;&bull; {{ $t('trains.cars') }}:
<span class="count">{{ train.stockList.length - 1 }}</span>
</span>
</div>
<div> <div>
<span v-for="(stat, i) in STATS.main" :key="stat.name"> <span>{{ train.speed }}km/h</span>
<span v-if="i > 0"> &bull; </span>
<span <div>
>{{ `${~~((train as any)[stat.name] * (stat.multiplier || 1))}${stat.unit}` }} <span> {{ train.length }}m</span>
</span> &bull;
<span> {{ (train.mass / 1000).toFixed(1) }}t</span>
<span v-if="train.stockList.length > 1">
&bull;
{{ $t('trains.cars') }}: {{ train.stockList.length - 1 }}
</span> </span>
</div> </div>
</div>
</section> </section>
</div> </div>
</template> </template>
@@ -123,15 +163,16 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue'; import ProgressBar from '../Global/ProgressBar.vue';
import TrainThumbnail from '../Global/TrainThumbnail.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 modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin], mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
components: { ProgressBar, TrainThumbnail }, components: { ProgressBar, StockList },
props: { props: {
train: { train: {
@@ -139,8 +180,7 @@ export default defineComponent({
required: true required: true
}, },
extended: { extended: {
type: Boolean, type: Boolean
default: true
} }
}, },
@@ -149,25 +189,31 @@ export default defineComponent({
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore() apiStore: useApiStore()
}; };
},
methods: {
navigateToJournal() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-driver': this.train.driverName
}
});
this.closeModal();
}
} }
}); });
</script> </script>
<!-- Global style for TrainThumbnail -->
<style lang="scss">
.train-stats .train-thumbnail {
max-width: 100%;
}
</style>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
@import '../../styles/badge.scss'; @import '../../styles/badge.scss';
.image-warning { .image-warning {
height: 1em; height: 1em;
margin-left: 0.5em; margin-left: 0.5em;
vertical-align: middle;
} }
.train-stats { .train-stats {
@@ -178,7 +224,7 @@ export default defineComponent({
flex-direction: column; flex-direction: column;
text-align: center; text-align: center;
gap: 0.25em; line-height: 1.5em;
} }
.train-info { .train-info {
@@ -186,6 +232,10 @@ export default defineComponent({
grid-template-columns: 2fr 1fr; grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
&[data-extended='true'] {
grid-template-columns: 1fr;
}
padding: 1em; padding: 1em;
background-color: #1a1a1a; background-color: #1a1a1a;
@@ -220,14 +270,29 @@ export default defineComponent({
font-size: 0.8em; font-size: 0.8em;
} }
.general-info { .general-top-bar {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
& > div {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.25em; gap: 0.25em;
margin-right: 1.5em;
} }
}
.btn-timetable {
padding: 0.25em;
}
.btn-exit {
padding: 0.25em;
}
.general-status { .general-status {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -236,6 +301,27 @@ export default defineComponent({
gap: 0.25em; gap: 0.25em;
} }
.general-stats {
display: flex;
gap: 0.5em;
flex-wrap: wrap;
& > div {
display: flex;
align-items: center;
gap: 0.25em;
}
img {
width: 1.5em;
}
}
.general-timetable {
display: flex;
align-items: center;
}
.status-badges { .status-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -247,17 +333,7 @@ export default defineComponent({
} }
} }
.general-timetable { .status-timetable-progress {
display: flex;
align-items: center;
}
.timetable-warnings {
display: flex;
gap: 0.25em;
}
.timetable-progress {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
@@ -267,32 +343,19 @@ export default defineComponent({
margin-right: 0.25em; margin-right: 0.25em;
} }
.timetable-warnings {
display: flex;
gap: 0.25em;
}
@include smallScreen() { @include smallScreen() {
.train-info { .train-info {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1em 0; gap: 1em 0;
text-align: center;
font-size: 1.15em;
} }
.general-info, .btn-timetable > span {
.general-status, display: none;
.general-timetable {
justify-content: center;
}
.timetable-progress {
justify-content: center;
}
.comments {
flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
} }
} }
</style> </style>
+18 -29
View File
@@ -1,12 +1,8 @@
<template> <template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal"> <div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div> <div class="modal-background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0"> <div class="modal-content" ref="content" tabindex="0">
<button class="btn exit" @click="closeModal"> <TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<img src="/images/icon-exit.svg" alt="close card" />
</button>
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" /> <TrainSchedule :train="chosenTrain" tabindex="0" />
</div> </div>
</div> </div>
@@ -17,18 +13,28 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue'; import TrainSchedule from './TrainSchedule.vue';
import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
components: { TrainInfo, TrainSchedule }, components: { TrainInfo, TrainSchedule },
mixins: [modalTrainMixin], mixins: [modalTrainMixin],
activated() { computed: {
const contentEl = this.$refs['content'] as HTMLElement; chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
}
},
watch: {
chosenTrain(train: Train | undefined) {
this.$nextTick(() => { this.$nextTick(() => {
if (train) {
const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus(); contentEl.focus();
}
}); });
} }
}
}); });
</script> </script>
@@ -49,23 +55,6 @@ export default defineComponent({
} }
} }
.exit {
position: absolute;
top: 0;
right: 0;
margin: 0.5em 1em;
padding: 0.25em;
z-index: 201;
img {
width: 1.5rem;
vertical-align: middle;
}
}
.train-modal { .train-modal {
position: fixed; position: fixed;
top: 0; top: 0;
@@ -82,7 +71,7 @@ export default defineComponent({
text-align: left; text-align: left;
} }
.modal_background { .modal-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -94,14 +83,14 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.modal_content { .modal-content {
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
margin-top: 1em; margin-top: 1em;
width: 95vw; width: 95vw;
max-height: 96vh; max-height: 95vh;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; box-shadow: 0 0 15px 10px #0e0e0e;
+1 -1
View File
@@ -4,7 +4,7 @@
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button"> <button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-filter2.svg" alt="Open filters icon" /> <img src="/images/icon-filter2.svg" alt="Open filters icon" />
{{ $t('options.filters') }} [F] [F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span> <span class="active-indicator" v-if="currentOptionsActive"></span>
</button> </button>
+5 -71
View File
@@ -30,19 +30,12 @@
<StopLabel :stop="stop" /> <StopLabel :stop="stop" />
</span> </span>
<div class="stop_line" v-if="i < scheduleStops.length - 1"> <div class="stop_line">
<!-- Grid placeholder --> <!-- Grid placeholder -->
<div> <div></div>
<!-- <div class="speed-departure" v-if="stop.currentDepartureRoute">
{{ stop.currentDepartureRoute.routeSpeed }}
</div>
<div class="speed-next-arrival" v-if="stop.nextArrivalRoute">
{{ stop.nextArrivalRoute.routeSpeed }}
</div> -->
</div>
<div class="progress"> <div class="progress">
<div class="line line_connection"></div> <div class="line line_connection" v-if="i < scheduleStops.length - 1"></div>
</div> </div>
<div class="bottom-line-info"> <div class="bottom-line-info">
@@ -88,11 +81,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train';
import StopLabel from './StopLabel.vue'; 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';
export interface TrainScheduleStop { export interface TrainScheduleStop {
nameHtml: string; nameHtml: string;
@@ -121,13 +114,6 @@ export interface TrainScheduleStop {
sceneryHash: string; sceneryHash: string;
distance: number; distance: number;
// arrivalTrackCount: number;
// departureTrackCount: number;
// currentArrivalRoute?: StationRoutesInfo;
// currentDepartureRoute?: StationRoutesInfo;
// nextArrivalRoute?: StationRoutesInfo;
arrivalLine: string | null; arrivalLine: string | null;
departureLine: string | null; departureLine: string | null;
@@ -157,8 +143,6 @@ export default defineComponent({
computed: { computed: {
scheduleStops(): TrainScheduleStop[] { scheduleStops(): TrainScheduleStop[] {
let currentSceneryIndex = 0; let currentSceneryIndex = 0;
// let lastDepartureTrackCount = 2;
// let lastArrivalTrackCount = 2;
return ( return (
this.train.timetableData?.followingStops.map((stop, i, arr) => { this.train.timetableData?.followingStops.map((stop, i, arr) => {
@@ -170,33 +154,6 @@ export default defineComponent({
) )
currentSceneryIndex++; currentSceneryIndex++;
// const sceneryInfo = this.apiStore.sceneryData.find(
// (sd) =>
// sd.name.toLocaleLowerCase() ==
// this.timetableSceneryNames[currentSceneryIndex].toLocaleLowerCase()
// );
// const nextSceneryInfo = this.apiStore.sceneryData.find(
// (sd) =>
// sd.name.toLocaleLowerCase() ==
// this.timetableSceneryNames[currentSceneryIndex + 1]?.toLocaleLowerCase()
// );
// const currentDepartureRoute = sceneryInfo?.routesInfo.find(
// (r) => r.routeName == stop.departureLine
// );
// const currentArrivalRoute = sceneryInfo?.routesInfo.find(
// (r) => r.routeName == stop.arrivalLine
// );
// const nextArrivalRoute = nextSceneryInfo?.routesInfo.find(
// (r) => r.routeName == arr[i + 1]?.arrivalLine
// );
// lastDepartureTrackCount = currentDepartureRoute?.routeTracks ?? lastDepartureTrackCount;
// lastArrivalTrackCount = currentArrivalRoute?.routeTracks ?? lastArrivalTrackCount;
return { return {
nameHtml: stop.stopName, nameHtml: stop.stopName,
nameRaw: stop.stopNameRAW, nameRaw: stop.stopNameRAW,
@@ -217,16 +174,6 @@ export default defineComponent({
arrivalLine: stop.arrivalLine, arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine, departureLine: stop.departureLine,
// arrivalSpeed: nextArrivalRoute?.routeSpeed ?? null,
// departureSpeed: currentDepartureRoute?.routeSpeed ?? null,
// arrivalTrackCount: currentArrivalRoute?.routeTracks ?? lastArrivalTrackCount,
// departureTrackCount: currentDepartureRoute?.routeTracks ?? lastDepartureTrackCount,
// currentArrivalRoute,
// currentDepartureRoute,
// nextArrivalRoute,
type: stop.stopType, type: stop.stopType,
distance: stop.stopDistance, distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i), isActive: this.activeMinorStops.includes(i),
@@ -234,7 +181,7 @@ export default defineComponent({
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: '', sceneryHash: '',
sceneryName: this.timetableSceneryNames[currentSceneryIndex], sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed' status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
}; };
}) ?? [] }) ?? []
@@ -269,19 +216,6 @@ export default defineComponent({
} }
return activeMinorStopList; return activeMinorStopList;
},
timetableSceneryNames() {
if (!this.train.timetableData?.sceneries) return [];
return this.train.timetableData?.sceneries
.map(
(sceneryHash) =>
this.store.onlineSceneryList.find((st) => st.hash === sceneryHash)?.name ??
this.apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
sceneryHash
)
.reverse();
} }
}, },
+5 -6
View File
@@ -1,13 +1,13 @@
<template> <template>
<transition name="status-anim" mode="out-in" tag="div" class="train-table"> <transition name="status-anim" mode="out-in" tag="div" class="train-table">
<div :key="apiStore.dataStatuses.connection"> <div :key="apiStore.dataStatuses.connection">
<div class="table-info" key="offline" v-if="store.isOffline"> <div class="table-warning" key="offline" v-if="store.isOffline">
{{ $t('app.offline') }} {{ $t('app.offline') }}
</div> </div>
<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-info" 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') }}
</div> </div>
@@ -20,7 +20,7 @@
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)" @click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)" @keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
> >
<TrainInfo :train="train" /> <TrainInfo :train="train" :extended="false" />
</li> </li>
</transition-group> </transition-group>
</div> </div>
@@ -30,11 +30,10 @@
<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 modalTrainMixin from '../../mixins/modalTrainMixin';
import Train from '../../scripts/interfaces/Train';
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';
import { Status } from '../../typings/common'; import { Status, Train } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
@@ -105,7 +104,7 @@ export default defineComponent({
overflow-x: hidden; overflow-x: hidden;
} }
.table-info { .table-warning {
text-align: center; text-align: center;
padding: 1em 0; padding: 1em 0;
-14
View File
@@ -1,14 +0,0 @@
import { PropType, defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import { TrainScheduleStop } from './TrainSchedule.vue';
export default defineComponent({
mixins: [dateMixin],
props: {
stop: {
type: Object as PropType<TrainScheduleStop>,
required: true
}
}
});
+2 -2
View File
@@ -8732,7 +8732,7 @@
"departureDelay": 0, "departureDelay": 0,
"beginsHere": true, "beginsHere": true,
"terminatesHere": false, "terminatesHere": false,
"confirmed": 0, "confirmed": 1,
"stopped": 0, "stopped": 0,
"stopTime": null "stopTime": null
}, },
@@ -9380,7 +9380,7 @@
"stopType": "", "stopType": "",
"stopDistance": 123.62, "stopDistance": 123.62,
"pointId": "1663532077406", "pointId": "1663532077406",
"comments": null, "comments": "test121",
"mainStop": true, "mainStop": true,
"arrivalLine": "Sk", "arrivalLine": "Sk",
"arrivalTimestamp": 1701889320000, "arrivalTimestamp": 1701889320000,
File diff suppressed because it is too large Load Diff
+55 -2
View File
@@ -1,13 +1,14 @@
{ {
"optionSections": [ "optionSections": [
"status",
"timetables",
"reality", "reality",
"package-access", "package-access",
"access", "access",
"control", "control",
"addons",
"blockades", "blockades",
"signals", "signals",
"status" "addons"
], ],
"options": [ "options": [
@@ -138,6 +139,20 @@
"value": true, "value": true,
"defaultValue": 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", "id": "SBL",
"name": "SBL", "name": "SBL",
@@ -228,6 +243,20 @@
"section": "status", "section": "status",
"value": true, "value": true,
"defaultValue": true "defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
} }
], ],
"sliders": [ "sliders": [
@@ -236,6 +265,7 @@
"name": "minLevel", "name": "minLevel",
"minRange": 0, "minRange": 0,
"maxRange": 20, "maxRange": 20,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -244,14 +274,34 @@
"name": "maxLevel", "name": "maxLevel",
"minRange": 0, "minRange": 0,
"maxRange": 20, "maxRange": 20,
"step": 1,
"value": 20, "value": 20,
"defaultValue": 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", "id": "routes-1t-cat",
"name": "minOneWayCatenary", "name": "minOneWayCatenary",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -260,6 +310,7 @@
"name": "minOneWay", "name": "minOneWay",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -268,6 +319,7 @@
"name": "minTwoWayCatenary", "name": "minTwoWayCatenary",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -276,6 +328,7 @@
"name": "minTwoWay", "name": "minTwoWay",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
} }
-10
View File
@@ -1,10 +0,0 @@
import axios from 'axios';
const http = axios.create({
baseURL:
import.meta.env.VITE_API_MODE === 'development'
? 'http://localhost:3001'
: 'https://stacjownik.spythere.eu'
});
export default http;
+19
View File
@@ -0,0 +1,19 @@
import enLang from './locales/en.json';
import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n';
const i18n = createI18n({
locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang
},
enableLegacy: false
});
export default i18n;
+81 -28
View File
@@ -2,6 +2,7 @@
"donations": { "donations": {
"button-title": "TOSS A COIN", "button-title": "TOSS A COIN",
"header": "Toss a coin to Stacjownik!", "header": "Toss a coin to Stacjownik!",
"donator-title": "Project is supported by more than <b>{count}</b> people, including:",
"p1": "<b>Hello o7!</b> This is Spythere, the creator of Stacjownik, Pojazdownik and several other applications that enhance the gameplay of Train Driver 2!", "p1": "<b>Hello o7!</b> This is Spythere, the creator of Stacjownik, Pojazdownik and several other applications that enhance the gameplay of Train Driver 2!",
"p2": "{b1} is a completely free tool, created and continuously developed for the Train Driver 2 simulator community since 2020. However, a part of the project is sustained solely through my private financial contribution. Features such as {b2} or {b3} (operating on my {link} - to which you are warmly invited) must function on a dedicated server where they can collect and process data, and then display it on the website.", "p2": "{b1} is a completely free tool, created and continuously developed for the Train Driver 2 simulator community since 2020. However, a part of the project is sustained solely through my private financial contribution. Features such as {b2} or {b3} (operating on my {link} - to which you are warmly invited) must function on a dedicated server where they can collect and process data, and then display it on the website.",
"p2-b1": "Stacjownik", "p2-b1": "Stacjownik",
@@ -25,6 +26,13 @@
"TWR": "High risk freight train", "TWR": "High risk freight train",
"SKR": "Train with exceeded gauge" "SKR": "Train with exceeded gauge"
}, },
"update": {
"title": "Stacjownik update!",
"confirm": "ROGER THAT!",
"no-data": "No data about the latest app update has been found",
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
},
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
"trains": "TRAINS", "trains": "TRAINS",
@@ -40,12 +48,10 @@
"footer": { "footer": {
"discord": "Stacjownik Discord server" "discord": "Stacjownik Discord server"
}, },
"update": {
"title": "New version of the app is available!", "vehicle-preview": {
"paragraph1": "Enjoy the application and may the green signal be with you!", "loading": "Loading preview...",
"release-link": "Click here to browse version changelog (GitHub)", "error": "Oops! The vehicle preview seems to be missing! :/"
"confirm-button": "UPDATE NOW",
"later-button": "LATER"
}, },
"data-status": { "data-status": {
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!", "S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
@@ -57,20 +63,6 @@
"S5-dispatchers": "<b>S5 signal</b> <br> Cannot load dispatchers status data!", "S5-dispatchers": "<b>S5 signal</b> <br> Cannot load dispatchers status data!",
"S5-trains": "<b>S5 signal</b> <br> Cannot load online trains data!" "S5-trains": "<b>S5 signal</b> <br> Cannot load online trains data!"
}, },
"desc": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP application (level crossing remote control simulator)",
"TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unknown": "This scenery isn't recognizable right now",
"unavailable": "This scenery is unavailable",
"abandoned": "This scenery is no longer supported by its creators",
"real": "Scenery with real lines: "
},
"signals": { "signals": {
"title": "Signal type", "title": "Signal type",
"współczesna": "modern", "współczesna": "modern",
@@ -89,7 +81,20 @@
"ręczne+SCS": "manual + SCS", "ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)", "mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK", "mechaniczne+SPK": "levers + SPK",
"mechaniczne+SCS": "levers + SCS" "mechaniczne+SCS": "levers + SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
}, },
"status": { "status": {
"online": "UNTIL ", "online": "UNTIL ",
@@ -176,7 +181,9 @@
"signals": "SIGNALLING", "signals": "SIGNALLING",
"addons": "ADDITIONAL PROGRAMS", "addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING", "blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS" "status": "ONLINE STATUS",
"timetables": "ACTIVE TIMETABLES",
"spawns": "OPEN SPAWNS"
}, },
"all-available": "ALL AVAILABLE", "all-available": "ALL AVAILABLE",
@@ -210,6 +217,9 @@
"SUP": "SUP (RASP-UZK)", "SUP": "SUP (RASP-UZK)",
"noSUP": "WITHOUT SUP", "noSUP": "WITHOUT SUP",
"ASDEK": "ASDEK",
"noASDEK": "NO ASDEK",
"SBL": "AUTOMATIC (SBL)", "SBL": "AUTOMATIC (SBL)",
"PBL": "SEMIAUTOMATIC (PBL)", "PBL": "SEMIAUTOMATIC (PBL)",
@@ -219,14 +229,21 @@
"historical": "HISTORICAL", "historical": "HISTORICAL",
"free": "FREE", "free": "FREE",
"occupied": "OCCUPIED", "occupied": "OCCUPIED",
"withActiveTimetables": "ACTIVE",
"withoutActiveTimetables": "NO ACTIVE",
"sliders": { "sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL", "min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL", "max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"min-vmax": "MIN. SCENERY ROUTE SPEED",
"max-vmax": "MAX. SCENERY ROUTE SPEED",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES", "routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES", "routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES", "routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES" "routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
}, },
"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", "authors-button-title": "Search",
@@ -241,21 +258,54 @@
"close": "CLOSE FILTERS" "close": "CLOSE FILTERS"
}, },
"sceneries": { "sceneries": {
"station": "Station", "headers": {
"min-lvl": "Min. dispatcher\nlevel", "station": "Scenery",
"min-lvl": "Scenery\nlevel",
"status": "Status", "status": "Status",
"dispatcher": "Dispatcher", "dispatcher": "Dispatcher",
"dispatcher-lvl": "Dispatcher\nlevel", "dispatcher-lvl": "Dispatcher\nlevel",
"routes": "Routes\ndouble / single", "routes-single": "1-track\nroutes",
"routes-double": "2-track\nroutes",
"general": "General info", "general": "General info",
"user": "Drivers online", "user": "Drivers online",
"like": "Dispatcher rating",
"spawn": "Spawns online", "spawn": "Spawns online",
"timetableAll": "Active timetables", "timetableAll": "Active timetables",
"timetableConfirmed": "Confirmed timetables", "timetableConfirmed": "Confirmed timetables",
"timetableUnconfirmed": "Unconfirmed timetables", "timetableUnconfirmed": "Unconfirmed timetables"
},
"info": {
"control-type": "Control type: ",
"signals-type": "Signals type: ",
"SBL": "This scenery has automatic block signalling (ABS/SBL) system on following routes: ",
"SUP": "Requires the SUP program (level crossing remote control)",
"ASDEK": "Requires the ASDEK program (defect detection of moving rolling stock)",
"TWB-all": "This scenery has two-way route blockade on all routes",
"TWB-routes": "This scenery has two-way route blockade on following routes: ",
"default": "This scenery is available by default",
"non-public": "This scenery is not public",
"unavailable": "This scenery is unavailable",
"abandoned": "This scenery is no longer supported by its creators",
"unknown": "This scenery isn't recognizable right now",
"real": "Scenery with real lines: ",
"double-track-routes-catenary": "Electrified double-track routes count: ",
"single-track-routes-catenary": "Electrified single-track routes count: ",
"double-track-routes-other": "Not electrified double-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..."
}, },
"station-stats": {
"u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
"avg-timetable-count": "Average timetable count for one dispatcher:",
"single-track-count": "Available single track routes:",
"double-track-count": "Available double track routes:",
"electrified": "(electrified)",
"not-electrified": "(not electr.)",
"open-spawns": "Open spawns:"
},
"trains": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
"loading": "Loading train data...", "loading": "Loading train data...",
@@ -297,7 +347,9 @@
"last-seen-ago": "since {minutes} minutes", "last-seen-ago": "since {minutes} minutes",
"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"
}, },
"train-stats": { "train-stats": {
"stats-button": "STATISTICS", "stats-button": "STATISTICS",
@@ -333,6 +385,7 @@
"timetable-active": "ACTIVE", "timetable-active": "ACTIVE",
"timetable-fulfilled": "FULFILLED", "timetable-fulfilled": "FULFILLED",
"timetable-abandoned": "ABANDONED", "timetable-abandoned": "ABANDONED",
"timetable-online-button": "ONLINE TIMETABLE",
"online-since": "ONLINE SINCE", "online-since": "ONLINE SINCE",
"duty-lasted": "The duty lasted", "duty-lasted": "The duty lasted",
@@ -341,7 +394,7 @@
"minutes": "{value} min | {value} mins", "minutes": "{value} min | {value} mins",
"seconds": "{value} s", "seconds": "{value} s",
"stock-info": "EXTRA INFO", "stock-info": "DETAILS",
"stock-length": "Length", "stock-length": "Length",
"stock-mass": "Mass", "stock-mass": "Mass",
"stock-max-speed": "Max. speed", "stock-max-speed": "Max. speed",
+79 -24
View File
@@ -2,8 +2,9 @@
"donations": { "donations": {
"button-title": "GROSZA DAJ", "button-title": "GROSZA DAJ",
"header": "Grosza daj Stacjownikowi!", "header": "Grosza daj Stacjownikowi!",
"donator-title": "Projekt ma już ponad <b>{count}</b> wspierających, w tym:",
"p1": "<b>Hej o7!</b> Z tej strony Spythere, twórca Stacjownika, Pojazdownika oraz kilku innych aplikacji wspomagających rozgrywkę symulatora Train Driver 2!", "p1": "<b>Hej o7!</b> Z tej strony Spythere, twórca Stacjownika, Pojazdownika oraz kilku innych aplikacji wspomagających rozgrywkę symulatora Train Driver 2!",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdeczne zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.", "p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdecznie zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2-b1": "Stacjownik", "p2-b1": "Stacjownik",
"p2-b2": "Dziennik", "p2-b2": "Dziennik",
"p2-b3": "Stacjobot", "p2-b3": "Stacjobot",
@@ -25,6 +26,13 @@
"TWR": "Towar niebezpieczny wysokiego ryzyka", "TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia" "SKR": "Przekroczona skrajnia"
}, },
"update": {
"title": "Aktualizacja Stacjownika!",
"confirm": "PRZYJĄŁEM!",
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
},
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
"trains": "POCIĄGI", "trains": "POCIĄGI",
@@ -37,6 +45,10 @@
"footer": { "footer": {
"discord": "Serwer Discord Stacjownika" "discord": "Serwer Discord Stacjownika"
}, },
"vehicle-preview": {
"loading": "Ładowanie podglądu...",
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
},
"data-status": { "data-status": {
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!", "S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!", "S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
@@ -47,18 +59,6 @@
"S5-dispatchers": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o statusach dyżurnych ruchu!", "S5-dispatchers": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o statusach dyżurnych ruchu!",
"S5-trains": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o pociągach online!" "S5-trains": "<b>Sygnał S5</b> <br> Błąd podczas pobierania danych o pociągach online!"
}, },
"desc": {
"control-type": "Sterowanie:",
"signals-type": "Sygnalizacja:",
"SBL": "Sceneria posiada SBL na szlakach:",
"SUP": "Wymaga programu SUP do kontroli systemu RASP-UZK",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"unknown": "Nieznana sceneria",
"real": "Sceneria z realnymi liniami kolejowymi:",
"abandoned": "Sceneria wycofana z rozgrywki"
},
"signals": { "signals": {
"title": "Sygnalizacja", "title": "Sygnalizacja",
"współczesna": "współczesna", "współczesna": "współczesna",
@@ -77,7 +77,20 @@
"ręczne+SCS": "ręczne z SCS", "ręczne+SCS": "ręczne z SCS",
"mechaniczne": "mechaniczne", "mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne z SPK", "mechaniczne+SPK": "mechaniczne z SPK",
"mechaniczne+SCS": "mechaniczne z SCS" "mechaniczne+SCS": "mechaniczne z SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
}, },
"status": { "status": {
"online": "DO ", "online": "DO ",
@@ -163,9 +176,11 @@
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA", "control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI", "signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY", "addons": "SZCZEGÓŁY",
"blockades": "BLOKADY LINIOWE", "blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE" "status": "STATUS ONLINE",
"timetables": "AKTYWNE ROZKŁADY JAZDY",
"spawns": "OTWARTE SPAWNY"
}, },
"all-available": "WSZYSTKIE DOSTĘPNE", "all-available": "WSZYSTKIE DOSTĘPNE",
@@ -197,6 +212,9 @@
"SUP": "SUP (RASP-UZK)", "SUP": "SUP (RASP-UZK)",
"noSUP": "BEZ SUP", "noSUP": "BEZ SUP",
"ASDEK": "ASDEK",
"noASDEK": "BEZ ASDEK-a",
"SBL": "SAMOCZYNNA", "SBL": "SAMOCZYNNA",
"PBL": "PÓŁSAMOCZYNNA", "PBL": "PÓŁSAMOCZYNNA",
@@ -205,13 +223,17 @@
"semaphores": "KSZTAŁTOWA", "semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA", "mixed": "MIESZANA",
"historical": "HISTORYCZNA", "historical": "HISTORYCZNA",
"free": "WOLNA", "free": "WOLNA",
"occupied": "ZAJĘTA", "occupied": "ZAJĘTA",
"withActiveTimetables": "AKTYWNE",
"withoutActiveTimetables": "BEZ AKTYWNYCH",
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO", "max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)", "routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)", "routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
@@ -231,22 +253,52 @@
"close": "ZAMKNIJ FILTRY" "close": "ZAMKNIJ FILTRY"
}, },
"sceneries": { "sceneries": {
"station": "Stacja", "headers": {
"abbr": "Skrót\nposterunku", "station": "Sceneria",
"min-lvl": "Min. poziom\ndyżurnego", "min-lvl": "Poziom\nscenerii",
"status": "Status", "status": "Status",
"dispatcher": "Dyżurny", "dispatcher": "Dyżurny",
"dispatcher-lvl": "Poziom\ndyżurnego", "dispatcher-lvl": "Poziom\ndyżurnego",
"routes": "Szlaki\n2tor / 1tor", "routes-single": "Szlaki\n1-torowe",
"routes-double": "Szlaki\n2-torowe",
"general": "Informacje\nogólne", "general": "Informacje\nogólne",
"user": "Maszyniści online", "user": "Maszyniści online",
"like": "Ocena dyżurnego",
"spawn": "Otwarte spawny", "spawn": "Otwarte spawny",
"timetableAll": "Aktywne rozkłady jazdy", "timetableAll": "Aktywne rozkłady jazdy",
"timetableConfirmed": "Zatwierdzone rozkłady jazdy", "timetableConfirmed": "Zatwierdzone rozkłady jazdy",
"timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy", "timetableUnconfirmed": "Niezatwierdzone rozkłady jazdy"
},
"info": {
"control-type": "Sterowanie: ",
"signals-type": "Sygnalizacja: ",
"SBL": "Sceneria posiada SBL na szlakach: ",
"SUP": "Wymaga programu SUP do kontroli systemu RASP-UZK",
"ASDEK": "Wymaga programu ASDEK do detekcji stanów awaryjnych taboru w ruchu",
"default": "Sceneria dostępna domyślnie w paczce z grą",
"non-public": "Sceneria niepubliczna",
"unavailable": "Sceneria niedostępna",
"abandoned": "Sceneria wycofana z rozgrywki",
"unknown": "Nieznana sceneria",
"real": "Sceneria z realnymi liniami kolejowymi: ",
"double-track-routes-catenary": "Liczba zelektryfikowanych szlaków dwutorowych: ",
"single-track-routes-catenary": "Liczba zelektryfikowanych szlaków jednotorowych: ",
"double-track-routes-other": "Liczba niezelektryfikowanych szlaków dwutorowych: ",
"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ę..."
}, },
"station-stats": {
"u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
"avg-timetable-count": "Średnia liczba rozkładów jazdy na dyżurnego:",
"single-track-count": "Dostępne szlaki jednotorowe:",
"double-track-count": "Dostępne szlaki dwutorowe:",
"electrified": "(zelektr.)",
"not-electrified": "(niezelektr.)",
"open-spawns": "Otwarte spawny:"
},
"trains": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
"loading": "Pobieranie danych o pociągach...", "loading": "Pobieranie danych o pociągach...",
@@ -279,7 +331,9 @@
"scenery-offline": "Przejazd offline", "scenery-offline": "Przejazd offline",
"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"
}, },
"train-stats": { "train-stats": {
"stats-button": "STATYSTYKI", "stats-button": "STATYSTYKI",
@@ -321,8 +375,9 @@
"timetable-active": "AKTYWNY", "timetable-active": "AKTYWNY",
"timetable-fulfilled": "WYPEŁNIONY", "timetable-fulfilled": "WYPEŁNIONY",
"timetable-abandoned": "PORZUCONY", "timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE", "stock-info": "SZCZEGÓŁY",
"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.",
+2 -16
View File
@@ -2,26 +2,12 @@ import { createApp, Directive, ref } from 'vue';
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import enLang from './locales/en.json'; import i18n from './i18n';
import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import useCustomSW from './mixins/useCustomSW'; import useCustomSW from './mixins/useCustomSW';
const i18n = createI18n({ // Service worker
locale: 'pl',
legacy: false,
warnHtmlMessage: false,
fallbackLocale: 'pl',
messages: {
en: enLang,
pl: plLang
},
enableLegacy: false
});
// SW
useCustomSW(); useCustomSW();
const clickOutsideDirective: Directive = { const clickOutsideDirective: Directive = {
+1 -2
View File
@@ -1,6 +1,5 @@
import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings'; import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings';
import Train from '../scripts/interfaces/Train'; import { Train, TrainStop } from '../typings/common';
import { TrainStop } from '../store/typings';
function confirmedPercentage(stops: TrainStop[] | undefined) { function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1; if (!stops) return -1;
-16
View File
@@ -1,16 +0,0 @@
import { defineComponent } from 'vue';
import { useApiStore } from '../store/apiStore';
export default defineComponent({
data() {
return {
apiStore: useApiStore()
};
},
methods: {
isDonator(name: string) {
return this.apiStore.donatorsData.includes(name);
}
}
});
-27
View File
@@ -1,27 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].intersectionRatio > 0.5) actionFunction();
},
{ threshold: 0.2 }
);
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
}
}
});
+4 -7
View File
@@ -1,19 +1,15 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useMainStore() store: useMainStore(),
tooltipStore: useTooltipStore()
}; };
}, },
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
}
},
methods: { methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) { selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId; this.store.chosenModalTrainId = trainId;
@@ -23,6 +19,7 @@ export default defineComponent({
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
this.tooltipStore.hide();
setTimeout(() => { setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus(); (this.store.modalLastClickedTarget as any)?.focus();
-24
View File
@@ -1,24 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getControlTypeAbbrev(controlType: string) {
switch (controlType) {
case 'mechaniczne':
return 'M';
case 'SCS-SPK':
return 'S/S';
case 'ręczne':
return 'R';
case 'mechaniczne+SPK':
return 'M';
case 'ręczne+SPK':
return 'R';
case 'mechaniczne+SCS':
return 'M';
default:
return controlType;
}
}
}
});
+1 -7
View File
@@ -1,6 +1,5 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train'; import { Train, TrainStop } from '../typings/common';
import { TrainStop } from '../store/typings';
export default defineComponent({ export default defineComponent({
data: () => ({ data: () => ({
@@ -148,11 +147,6 @@ export default defineComponent({
if (distance < 1000) return `${distance}m`; if (distance < 1000) return `${distance}m`;
return `${(distance / 1000).toPrecision(2)}km`; return `${(distance / 1000).toPrecision(2)}km`;
},
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
} }
} }
}); });
+2 -4
View File
@@ -1,6 +1,4 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import JournalDispatchersVue from '../views/JournalDispatchers.vue';
import JournalTimetablesVue from '../views/JournalTimetables.vue';
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@@ -38,7 +36,7 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/journal/timetables', path: '/journal/timetables',
name: 'JournalTimetables', name: 'JournalTimetables',
component: JournalTimetablesVue, component: () => import('../views/JournalTimetables.vue'),
props: (route) => ({ props: (route) => ({
region: route.query.region region: route.query.region
}) })
@@ -46,7 +44,7 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/journal/dispatchers', path: '/journal/dispatchers',
name: 'JournalDispatchers', name: 'JournalDispatchers',
component: JournalDispatchersVue, component: () => import('../views/JournalDispatchers.vue'),
props: (route) => ({ props: (route) => ({
region: route.query.region region: route.query.region
}) })
+3 -1
View File
@@ -4,12 +4,14 @@ export const headIds = [
'status', 'status',
'dispatcher', 'dispatcher',
'dispatcher-lvl', 'dispatcher-lvl',
'routes', 'routes-single',
'routes-double',
'general' 'general'
] as const; ] as const;
export const headIconsIds = [ export const headIconsIds = [
'user', 'user',
'like',
'spawn', 'spawn',
'timetableAll', 'timetableAll',
'timetableUnconfirmed', 'timetableUnconfirmed',
-35
View File
@@ -1,35 +0,0 @@
import { Availability, OnlineScenery, ScheduledTrain } from '../../store/typings';
import { StationRoutes } from './StationRoutes';
export default interface Station {
name: string;
generalInfo?: {
name: string;
url: string;
abbr: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: {
checkpointName: string;
scheduledTrains: ScheduledTrain[];
}[];
};
onlineInfo?: OnlineScenery;
}
-13
View File
@@ -1,13 +0,0 @@
import { StationRoutesInfo } from '../../store/typings';
export interface StationRoutes {
oneWay: StationRoutesInfo[];
twoWay: StationRoutesInfo[];
/* [catenary, noCatenary] */
oneWayCatenaryRouteNames: string[];
oneWayNoCatenaryRouteNames: string[];
twoWayCatenaryRouteNames: string[];
twoWayNoCatenaryRouteNames: string[];
sblRouteNames: string[];
}
-37
View File
@@ -1,37 +0,0 @@
import { TrainStop } from '../../store/typings';
export default interface Train {
trainId: string;
mass: number;
length: number;
speed: number;
signal: string;
distance: number;
connectedTrack: string;
driverId: number;
trainNo: number;
driverName: string;
driverLevel: number;
currentStationName: string;
currentStationHash: string;
locoType: string;
online: boolean;
lastSeen: number;
region: string;
stockList: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: {
timetableId: number;
category: string;
route: string;
followingStops: TrainStop[];
TWR: boolean;
SKR: boolean;
routeDistance: number;
sceneries: string[];
};
}
@@ -1,7 +1,7 @@
import { Filter } from '../../components/StationsView/typings'; import { Filter } from '../../components/StationsView/typings';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { HeadIdsTypes } from '../data/stationHeaderNames'; import { HeadIdsTypes } from '../data/stationHeaderNames';
import Station from '../interfaces/Station'; import { Station } from '../../typings/common';
const dispatcherStatusPriority = [ const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN, Status.ActiveDispatcher.UNKNOWN,
@@ -54,10 +54,28 @@ export const sortStations = (
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0); diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break; 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': case 'user':
diff = diff =
(b.onlineInfo ? b.onlineInfo.currentUsers : -1) - (b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo ? a.onlineInfo.currentUsers : -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; break;
case 'spawn': case 'spawn':
@@ -93,26 +111,41 @@ export const sortStations = (
}; };
export const filterStations = (station: Station, filters: Filter) => { export const filterStations = (station: Station, filters: Filter) => {
if (!station.onlineInfo && filters['free']) return false; if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
if (station.onlineInfo) { if (station.onlineInfo) {
const { dispatcherStatus } = station.onlineInfo; const { dispatcherStatus } = station.onlineInfo;
const isEnding = dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus']; const excludeEnding =
dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const isNotSigned = const excludeNotSigned =
(dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN || (dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN ||
dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) && dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) &&
filters['unavailableStatus']; filters['unavailableStatus'];
const isAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus']; const excludeAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const isNoSpace = const excludeNoSpace =
dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus']; dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus'];
const isOccupied = station.onlineInfo && filters['occupied']; const excludeOccupied = filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE;
if (isEnding || isNotSigned || isAFK || isNoSpace || isOccupied) return false; const excludeActiveTTs =
(dispatcherStatus == Status.ActiveDispatcher.FREE ||
station.onlineInfo.scheduledTrainCount.all != 0) &&
filters['withActiveTimetables'];
if (
excludeEnding ||
excludeAFK ||
excludeNoSpace ||
excludeNotSigned ||
excludeOccupied ||
excludeActiveTTs
)
return false;
if ( if (
filters['onlineFromHours'] > 0 && filters['onlineFromHours'] > 0 &&
@@ -121,6 +154,12 @@ export const filterStations = (station: Station, filters: Filter) => {
return false; return false;
} }
const excludeNoActiveTTs =
filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0);
if (excludeNoActiveTTs) return false;
if ( if (
(station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) && (station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) &&
filters['nonPublic'] filters['nonPublic']
@@ -128,7 +167,7 @@ export const filterStations = (station: Station, filters: Filter) => {
return false; return false;
if (station.generalInfo) { if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, authors } = const { routes, availability, controlType, lines, reqLevel, signalType, SUP, ASDEK, authors } =
station.generalInfo; station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo) if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
@@ -150,26 +189,28 @@ export const filterStations = (station: Station, filters: Filter) => {
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned'; availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false; if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) 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 ( if (
filters['no-1track'] && filters['no-1track'] &&
(routes.oneWayCatenaryRouteNames.length != 0 || routes.oneWayNoCatenaryRouteNames.length != 0) (routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
) )
return false; return false;
if ( if (
filters['no-2track'] && filters['no-2track'] &&
(routes.twoWayCatenaryRouteNames.length != 0 || routes.twoWayNoCatenaryRouteNames.length != 0) (routes.doubleElectrifiedNames.length != 0 || routes.doubleOtherNames.length != 0)
) )
return false; return false;
if (routes.oneWayCatenaryRouteNames.length < filters['minOneWayCatenary']) return false; if (routes.singleElectrifiedNames.length < filters['minOneWayCatenary']) return false;
if (routes.oneWayNoCatenaryRouteNames.length < filters['minOneWay']) return false; if (routes.singleOtherNames.length < filters['minOneWay']) return false;
if (routes.twoWayCatenaryRouteNames.length < filters['minTwoWayCatenary']) return false; if (routes.doubleElectrifiedNames.length < filters['minTwoWayCatenary']) return false;
if (routes.twoWayNoCatenaryRouteNames.length < filters['minTwoWay']) return false; if (routes.doubleOtherNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false; if (filters[controlType]) return false;
if (filters[signalType]) return false; if (filters[signalType]) return false;
@@ -177,8 +218,11 @@ export const filterStations = (station: Station, filters: Filter) => {
if (filters['SUP'] && SUP) return false; if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false; if (filters['noSUP'] && !SUP) return false;
if (filters['SBL'] && routes.sblRouteNames.length > 0) return false; if (filters['ASDEK'] && ASDEK) return false;
if (filters['PBL'] && routes.sblRouteNames.length == 0) 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 ( if (
filters['authors'].length > 3 && filters['authors'].length > 3 &&
+54 -88
View File
@@ -1,65 +1,81 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import http from '../http';
import { API } from '../typings/api'; import { API } from '../typings/api';
import axios from 'axios';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import { StationJSONData } from './typings'; import { StationJSONData } from './typings';
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
timetables: Status.Data.Loading,
dispatchers: Status.Data.Loading,
trains: Status.Data.Loading
}, },
activeData: undefined as API.ActiveData.Response | undefined, activeData: undefined as API.ActiveData.Response | undefined,
rollingStockData: undefined as API.RollingStock.Response | undefined,
donatorsData: [] as API.Donators.Response, donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[], sceneryData: [] as StationJSONData[],
activeDataTimeout: undefined as number | undefined lastFetchData: new Date(),
client: undefined as AxiosInstance | undefined,
activeDataScheduler: undefined as number | undefined
}), }),
actions: { actions: {
async setupAPI() { async setupAPIData() {
// Static data let baseURL = 'https://stacjownik.spythere.eu';
this.fetchStockInfoData();
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
if (this.activeDataTimeout === undefined) this.startActiveDataScheduler(); switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
this.client = axios.create({
baseURL
});
this.connectToAPI();
}, },
// async setDataStatuses() { async connectToAPI() {
// if (!window.navigator.onLine) { // Static data
// this.dataStatuses.connection = Status.Data.Offline; this.fetchDonatorsData();
// this.dataStatuses.sceneries = Status.Data.Offline; this.fetchStationsGeneralInfo();
// this.dataStatuses.trains = Status.Data.Offline; },
// this.dataStatuses.dispatchers = Status.Data.Offline;
// this.dataStatuses.timetables = Status.Data.Offline;
// }
// if (!this.activeData?.activeSceneries) { async fetchActiveData() {
// this.dataStatuses.connection = Status.Data.Loaded; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
// this.dataStatuses.sceneries = Status.Data.Error;
// this.dataStatuses.trains = Status.Data.Error;
// this.dataStatuses.dispatchers = Status.Data.Error;
// return; try {
// } console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
// this.dataStatuses.connection = Status.Data.Loaded; const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
// this.dataStatuses.sceneries = Status.Data.Loaded;
// this.dataStatuses.trains = !this.activeData.trains ? Status.Data.Warning : Status.Data.Loaded; this.activeData = response.data;
// this.dataStatuses.dispatchers = Status.Data.Loaded; this.lastFetchData = new Date();
// }, this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania danych online:', error);
}
},
async fetchDonatorsData() { async fetchDonatorsData() {
try { try {
const response = await http.get<API.Donators.Response>('api/getDonators'); const response = await this.client!.get<API.Donators.Response>('api/getDonators');
this.donatorsData = response.data; this.donatorsData = response.data;
} catch (error) { } catch (error) {
@@ -67,60 +83,10 @@ export const useApiStore = defineStore('apiStore', {
} }
}, },
async fetchStockInfoData() {
try {
this.rollingStockData = (
await axios.get<API.RollingStock.Response>(
'https://raw.githubusercontent.com/Spythere/api/main/td2/data/stockInfo.json'
)
).data;
} catch (error) {
console.error('Ups! Wystąpił błąd podczas pobierania informacji o taborze z API:', error);
}
},
async startActiveDataScheduler() {
if (!window.navigator.onLine) {
this.dataStatuses.connection = Status.Data.Offline;
return;
}
if (import.meta.env.VITE_API_MODE === 'mock') {
const mockActiveData = await import('../data/mockActiveData.json');
this.dataStatuses.connection = Status.Data.Loaded;
this.activeData = mockActiveData;
console.warn('Stacjownik działa w trybie mockowania danych z WS');
return;
}
try {
const data = (await http.get<API.ActiveData.Response>('api/getActiveData')).data;
this.activeData = data;
this.dataStatuses.connection = Status.Data.Loaded;
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
console.error('Wystąpił błąd podczas pobierania danych online z API!');
} finally {
this.activeDataTimeout = window.setTimeout(
() => {
this.startActiveDataScheduler();
},
~~(1000 * (Math.random() * (25 - 20) + 25))
);
}
},
async stopActiveDataScheduler() {
window.clearTimeout(this.activeDataTimeout);
this.activeDataTimeout = undefined;
},
async fetchStationsGeneralInfo() { async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (await http.get<StationJSONData[]>('api/getSceneries')) const sceneryData: StationJSONData[] = (
.data; await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
if (!sceneryData) { if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Error; this.dataStatuses.sceneries = Status.Data.Error;
+171 -149
View File
@@ -1,21 +1,24 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import Train from '../scripts/interfaces/Train';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { OnlineScenery, ScheduledTrain, StoreState } from './typings'; import {
ActiveScenery,
import { Status } from '../typings/common'; ScheduledTrain,
import Station from '../scripts/interfaces/Station'; Station,
StationRoutes,
Status,
Train
} from '../typings/common';
import { useApiStore } from './apiStore'; import { useApiStore } from './apiStore';
import { API } from '../typings/api'; import { MainStoreState } from './typings';
import { StationRoutes } from '../scripts/interfaces/StationRoutes';
export const useMainStore = defineStore('store', { export const useMainStore = defineStore('mainStore', {
state: () => state: () =>
({ ({
region: { id: 'eu', value: 'PL1' }, region: { id: 'eu', value: 'PL1', name: 'PL1' },
isOffline: false, isOffline: false,
appUpdate: null,
dispatcherStatsName: '', dispatcherStatsName: '',
dispatcherStatsStatus: Status.Data.Initialized, dispatcherStatsStatus: Status.Data.Initialized,
@@ -26,9 +29,8 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined, chosenModalTrainId: undefined,
blockScroll: false,
modalLastClickedTarget: null modalLastClickedTarget: null
}) as StoreState, }) as MainStoreState,
getters: { getters: {
trainList(): Train[] { trainList(): Train[] {
@@ -42,6 +44,15 @@ export const useMainStore = defineStore('store', {
const timetable = train.timetable; const timetable = train.timetable;
const sceneryNames =
train.timetable?.sceneries?.map(
(sceneryHash) =>
apiStore.activeData?.activeSceneries?.find((st) => st.stationHash === sceneryHash)
?.stationName ??
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
sceneryHash
) ?? [];
return { return {
trainId: train.driverName + train.trainNo.toString(), trainId: train.driverName + train.trainNo.toString(),
@@ -77,43 +88,71 @@ export const useMainStore = defineStore('store', {
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()
} }
: undefined : undefined
} as Train; } as Train;
}); });
}, },
onlineSceneryList(state): OnlineScenery[] { // computed active sceneries
activeSceneryList(state): ActiveScenery[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
if (state.isOffline) return []; if (state.isOffline) return [];
if (!apiStore.activeData?.activeSceneries) return []; if (!apiStore.activeData?.activeSceneries) return [];
return apiStore.activeData?.activeSceneries.reduce((list, scenery) => { const offlineActiveSceneries = this.trainList.reduce((acc, train) => {
if (!train.timetableData) return acc;
train.timetableData.sceneryNames.forEach((name) => {
if (
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
apiStore.activeData?.activeSceneries?.findIndex(
(sc) =>
sc.stationName === name &&
sc.region == train.region &&
Date.now() - sc.lastSeen < 1000 * 60 * 2
) != -1
)
return acc;
acc.push({
name: name,
hash: '',
region: train.region,
maxUsers: 0,
currentUsers: 0,
spawns: [],
dispatcherName: '',
dispatcherRate: 0,
dispatcherId: -1,
dispatcherExp: -1,
dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1,
isOnline: false,
scheduledTrainCount: {
all: 0,
confirmed: 0,
unconfirmed: 0
}
});
});
return acc;
}, [] as ActiveScenery[]);
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 station = this.stationList.find((s) => s.name === scenery.stationName);
const scheduledTrains = getScheduledTrains(this.trainList, scenery, station?.generalInfo);
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery
);
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
const dispatcherTimestamp = const dispatcherTimestamp =
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
? Date.now() + 25500000 ? Date.now() + 25500000
@@ -133,90 +172,42 @@ export const useMainStore = defineStore('store', {
dispatcherId: scenery.dispatcherId, dispatcherId: scenery.dispatcherId,
dispatcherExp: scenery.dispatcherExp, dispatcherExp: scenery.dispatcherExp,
dispatcherIsSupporter: scenery.dispatcherIsSupporter, dispatcherIsSupporter: scenery.dispatcherIsSupporter,
scheduledTrains: scheduledTrains,
stationTrains: stationTrains,
dispatcherStatus: scenery.dispatcherStatus, dispatcherStatus: scenery.dispatcherStatus,
dispatcherTimestamp: dispatcherTimestamp, dispatcherTimestamp: dispatcherTimestamp,
isOnline: scenery.isOnline == 1, isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrainCount: { scheduledTrainCount: {
all: uniqueScheduledTrains.length, all: 0,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length, confirmed: 0,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length unconfirmed: 0
} }
}); });
return list; return list;
}, [] as OnlineScenery[]); }, [] as ActiveScenery[]);
},
stationList(): Station[] { const allActiveSceneries = [...onlineActiveSceneries, ...offlineActiveSceneries];
const apiStore = useApiStore();
return apiStore.sceneryData.map((scenery) => { for (let i = 0, n = allActiveSceneries.length; i < n; i++) {
const routes = scenery.routesInfo.reduce( const scenery = allActiveSceneries[i];
(acc, route) => {
const tracksKey = route.routeTracks == 2 ? 'twoWay' : 'oneWay';
const isElectric = route.isElectric;
const routesKey: keyof StationRoutes = `${tracksKey}${
!isElectric ? 'No' : ''
}CatenaryRouteNames`;
if (!route.isInternal) acc[routesKey].push(route.routeName); const station = this.stationList.find((s) => s.name === scenery.name);
if (route.isRouteSBL) acc['sblRouteNames'].push(route.routeName);
acc[tracksKey].push(route); const scheduledTrains = getScheduledTrains(
this.trainList,
return acc; station?.generalInfo,
}, scenery.name,
{ scenery.region
oneWay: [],
oneWayCatenaryRouteNames: [],
oneWayNoCatenaryRouteNames: [],
twoWay: [],
twoWayCatenaryRouteNames: [],
twoWayNoCatenaryRouteNames: [],
sblRouteNames: []
} as StationRoutes
); );
return {
name: scenery.name,
generalInfo: {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes,
checkpoints: scenery.checkpoints
? scenery.checkpoints
.split(';')
.map((sub) => ({ checkpointName: sub, scheduledTrains: [] }))
: []
}
};
});
}
},
actions: {
async processStationsOnlineInfo(activeData: API.ActiveData.Response) {
if (!activeData.activeSceneries) return;
const onlineSceneries = activeData.activeSceneries.reduce((acc, scenery) => {
const savedStation = this.stationList.find((st) => scenery.stationName === st.name);
if (scenery.isOnline !== 1 && Date.now() - scenery.lastSeen > 1000 * 60 * 2) return acc;
if (scenery.dispatcherStatus == Status.ActiveDispatcher.UNKNOWN) return acc;
const station = this.stationList.find((s) => s.name === scenery.stationName);
const scheduledTrains = getScheduledTrains(this.trainList, scenery, station?.generalInfo);
const stationTrains = getStationTrains( const stationTrains = getStationTrains(
this.trainList, this.trainList,
scheduledTrains, scheduledTrains,
this.region.id, this.region.id,
scenery scenery.name
); );
// Remove checkpoint duplicates // Remove checkpoint duplicates
@@ -228,65 +219,96 @@ export const useMainStore = defineStore('store', {
[] as ScheduledTrain[] [] as ScheduledTrain[]
); );
const dispatcherTimestamp = scenery.scheduledTrains = scheduledTrains;
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT scenery.stationTrains = stationTrains;
? Date.now() + 25500000
: scenery.dispatcherStatus > 5
? scenery.dispatcherStatus
: null;
const onlineInfo = { scenery.scheduledTrainCount = {
name: scenery.stationName,
hash: scenery.stationHash,
region: scenery.region,
maxUsers: scenery.maxUsers,
currentUsers: scenery.currentUsers,
spawns: parseSpawns(scenery.spawnString),
dispatcherName: scenery.dispatcherName,
dispatcherRate: scenery.dispatcherRate,
dispatcherId: scenery.dispatcherId,
dispatcherExp: scenery.dispatcherExp,
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
scheduledTrains: scheduledTrains,
stationTrains: stationTrains,
dispatcherStatus: scenery.dispatcherStatus,
dispatcherTimestamp: dispatcherTimestamp,
isOnline: scenery.isOnline == 1,
scheduledTrainCount: {
all: uniqueScheduledTrains.length, all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length, confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
}
}; };
}
if (savedStation) savedStation.onlineInfo = onlineInfo; return allActiveSceneries;
else
this.stationList.push({
name: onlineInfo.name,
onlineInfo: onlineInfo
});
acc.push(onlineInfo);
return acc;
}, [] as OnlineScenery[]);
// Reset online info of already offline sceneries
this.stationList
.filter(
(station) =>
station.onlineInfo &&
onlineSceneries.findIndex(
(os) => os.region == station.onlineInfo!.region && station.name == os.name
) != -1
)
.forEach((station) => (station.onlineInfo = undefined));
}, },
async changeRegion(region: StoreState['region']) { // computed station data
this.region = region; stationList(): Station[] {
const apiStore = useApiStore();
return apiStore.sceneryData.map((scenery) => {
const routes = scenery.routesInfo.reduce(
(acc, route) => {
if (route.hidden) return acc;
const tracksKey = route.routeTracks == 2 ? 'double' : 'single';
const isElectric = route.isElectric;
const routesKey: keyof StationRoutes = `${tracksKey}${
!isElectric ? 'Other' : 'Electrified'
}Names`;
if (!route.isInternal) acc[routesKey].push(route.routeName);
if (route.isRouteSBL) acc['sblNames'].push(route.routeName);
acc.minRouteSpeed =
acc.minRouteSpeed == 0
? route.routeSpeed
: Math.min(route.routeSpeed, acc.minRouteSpeed);
acc.maxRouteSpeed = Math.max(route.routeSpeed, acc.maxRouteSpeed);
acc[tracksKey].push(route);
return acc;
},
{
single: [],
singleElectrifiedNames: [],
singleOtherNames: [],
double: [],
doubleElectrifiedNames: [],
doubleOtherNames: [],
sblNames: [],
minRouteSpeed: 0,
maxRouteSpeed: 0
} as StationRoutes
);
return {
name: scenery.name,
generalInfo: {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? []
}
};
});
},
allStationInfo(): Station[] {
const onlineUnsavedStations = this.activeSceneryList
.filter(
(scenery) =>
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
scenery.region == this.region.id
)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...this.stationList.map((st) => ({
...st,
onlineInfo: this.activeSceneryList.find(
(os) => os.name == st.name && os.region == this.region.id
)
}))
];
} }
} }
}); });
+8 -21
View File
@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import inputData from '../data/options.json'; import inputData from '../data/options.json';
import { useMainStore } from './mainStore'; import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/filterUtils'; import { filterStations, sortStations } from '../scripts/utils/stationFilterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames'; import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager'; import StorageManager from '../managers/storageManager';
import { Filter } from '../components/StationsView/typings'; import { Filter } from '../components/StationsView/typings';
@@ -16,6 +16,8 @@ const filterInitStates: Filter = {
SPE: false, SPE: false,
SUP: false, SUP: false,
noSUP: false, noSUP: false,
ASDEK: false,
noASDEK: false,
ręczne: false, ręczne: false,
'ręczne+SPK': false, 'ręczne+SPK': false,
'ręczne+SCS': false, 'ręczne+SCS': false,
@@ -48,9 +50,11 @@ const filterInitStates: Filter = {
noSpaceStatus: false, noSpaceStatus: false,
unavailableStatus: false, unavailableStatus: false,
unsignedStatus: false, unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
maxVmax: 200,
minVmax: 0,
authors: '', authors: '',
onlineFromHours: 0 onlineFromHours: 0
}; };
@@ -71,25 +75,8 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
filteredStationList: (state) => { filteredStationList: (state) => {
const store = useMainStore(); const store = useMainStore();
const savedStationNames = store.stationList.map((s) => s.name);
const onlineUnsavedStations = store.onlineSceneryList return store.allStationInfo
.filter((os) => !savedStationNames.includes(os.name) && os.region == store.region.id)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...store.stationList.map((station) => ({
...station,
onlineInfo: store.onlineSceneryList.find(
(os) => os.name == station.name && os.region == store.region.id
)
}))
]
.filter((station) => filterStations(station, state.filters)) .filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive)); .sort((a, b) => sortStations(a, b, state.sorterActive));
} }
+56
View File
@@ -0,0 +1,56 @@
import { defineStore } from 'pinia';
const isTooltip = (v: any): v is TooltipType => tooltipKeys.includes(v);
export const tooltipKeys = [
'DonatorTooltip',
'BaseTooltip',
'VehiclePreviewTooltip',
'SpawnsTooltip',
'UsersTooltip'
] as const;
export type TooltipType = (typeof tooltipKeys)[number];
export const useTooltipStore = defineStore('tooltipStore', {
state: () => ({
mousePos: [0, 0],
type: null as TooltipType | null,
content: ''
}),
actions: {
show(_e: MouseEvent, type: string, value?: string) {
if (!isTooltip(type)) return;
this.type = type;
this.content = value ?? '';
},
hide() {
this.type = null;
this.content = '';
},
handle(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-tooltip-type'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.type != null) this.hide();
return;
}
const tooltipType = targetEl.getAttribute('data-tooltip-type');
const tooltipContent = targetEl.getAttribute('data-tooltip-content');
if (tooltipType && tooltipContent) this.show(e, tooltipType, tooltipContent);
else if (this.type != null) this.hide();
this.mousePos[0] = e.pageX;
this.mousePos[1] = e.pageY;
}
}
});
+5 -135
View File
@@ -1,43 +1,19 @@
import { API } from '../typings/api'; import { API } from '../typings/api';
import { Status } from '../typings/common'; import { Availability, StationRoutesInfo, Status } from '../typings/common';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface StoreState {
region: { id: string; value: string };
export interface MainStoreState {
region: { id: string; value: string; name: string };
isOffline: boolean; isOffline: boolean;
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response; dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string; driverStatsName: string;
driverStatsData?: API.DriverStats.Response; driverStatsData?: API.DriverStats.Response;
driverStatsStatus: Status.Data; driverStatsStatus: Status.Data;
chosenModalTrainId?: string; chosenModalTrainId?: string;
blockScroll: boolean;
modalLastClickedTarget: EventTarget | null; modalLastClickedTarget: EventTarget | null;
} }
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface StationJSONData { export interface StationJSONData {
name: string; name: string;
abbr: string; abbr: string;
@@ -53,6 +29,7 @@ export interface StationJSONData {
controlType: string; controlType: string;
SUP: boolean; SUP: boolean;
ASDEK: boolean;
// routes: string; // routes: string;
routesInfo: StationRoutesInfo[]; routesInfo: StationRoutesInfo[];
@@ -62,110 +39,3 @@ export interface StationJSONData {
availability: Availability; availability: Availability;
} }
export interface OnlineScenery {
name: string;
hash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawns: { spawnName: string; spawnLength: number; isElectrified: boolean }[];
dispatcherName: string;
dispatcherRate: number;
dispatcherId: number;
dispatcherExp: number;
dispatcherIsSupporter: boolean;
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
unconfirmed: number;
};
}
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number | null;
}
+58 -90
View File
@@ -1,7 +1,13 @@
import Station from '../scripts/interfaces/Station'; import {
import Train from '../scripts/interfaces/Train'; TrainStop,
import { API } from '../typings/api'; StopStatus,
import { ScheduledTrain, StationTrain, StopStatus, TrainStop } from './typings'; Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
export function getLocoURL(locoType: string): string { export function getLocoURL(locoType: string): string {
return `https://rj.td2.info.pl/dist/img/thumbnails/${ return `https://rj.td2.info.pl/dist/img/thumbnails/${
@@ -32,7 +38,7 @@ export function getStatusTimestamp(stationStatus: any): number {
return -1; return -1;
} }
export function parseSpawns(spawnString: string | null) { export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
if (!spawnString) return []; if (!spawnString) return [];
if (spawnString === 'NO_SPAWN') return []; if (spawnString === 'NO_SPAWN') return [];
@@ -42,7 +48,15 @@ export function parseSpawns(spawnString: string | null) {
const spawnLength = parseInt(spawnArray[2]); const spawnLength = parseInt(spawnArray[2]);
const isElectrified = spawnArray[3] == 'True'; const isElectrified = spawnArray[3] == 'True';
return { spawnName, spawnLength, isElectrified }; let spawnType: ScenerySpawnType = /EZT|POS|OSOB|PAS/i.test(spawnName)
? 'passenger'
: /TOW/i.test(spawnName)
? 'freight'
: /LUZ/i.test(spawnName)
? 'loco'
: 'all';
return { spawnName, spawnLength, isElectrified, spawnType };
}); });
} }
@@ -102,51 +116,33 @@ export function getCheckpointTrain(
let prevStationName = '', let prevStationName = '',
nextStationName = ''; nextStationName = '';
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex - 1; i >= 0; i--) {
if (/strong|podg/g.test(followingStops[i].stopName)) {
prevStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break;
}
}
for (let i = trainStopIndex + 1; i < followingStops.length; i++) {
if (/strong|podg/g.test(followingStops[i].stopName)) {
nextStationName = followingStops[i].stopNameRAW.replace(/,.*/g, '');
break;
}
}
let departureLine: string | null = null; let departureLine: string | null = null;
let arrivingLine: string | null = null; let arrivingLine: string | null = null;
for (let i = trainStopIndex; i < followingStops.length; i++) { let prevDepartureLine: string | null = null,
const currentStop = followingStops[i]; nextArrivalLine: string | null = null;
if (currentStop.departureLine == null) continue;
if (!/-|_|it|sbl/gi.test(currentStop.departureLine)) {
departureLine = currentStop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
break;
}
}
for (let i = trainStopIndex; i >= 0; i--) { for (let i = trainStopIndex; i >= 0; i--) {
const currentStop = followingStops[i]; const stop = followingStops[i];
if (currentStop.arrivalLine == null) continue; if (/strong|podg\.|pe\./g.test(stop.stopName) && !prevStationName && i <= trainStopIndex - 1)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (!/-|_|it|sbl/gi.test(currentStop.arrivalLine)) { if (stop.arrivalLine != null && !arrivingLine && !/-|_|it|sbl/gi.test(stop.arrivalLine)) {
arrivingLine = currentStop.arrivalLine; arrivingLine = stop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null; prevDepartureLine = followingStops[i - 1]?.departureLine || null;
}
}
break; 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;
} }
} }
@@ -177,8 +173,8 @@ export function getCheckpointTrain(
region: train.region, region: train.region,
arrivingLine, arrivingLine: arrivingLine,
departureLine, departureLine: departureLine,
nextArrivalLine, nextArrivalLine,
prevDepartureLine prevDepartureLine
@@ -187,59 +183,33 @@ export function getCheckpointTrain(
export function getScheduledTrains( export function getScheduledTrains(
trainList: Train[], trainList: Train[],
sceneryData: API.ActiveSceneries.Data, stationGeneralInfo: Station['generalInfo'],
stationGeneralInfo: Station['generalInfo'] stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] { ): ScheduledTrain[] {
const stationNameLower = sceneryData.stationName.toLocaleLowerCase(); // stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => { return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc; if (!train.timetableData) return acc;
if (train.region != sceneryData.region) return acc; if (train.region != region) return acc;
const timetable = train.timetableData; const timetable = train.timetableData;
if (!timetable.sceneries.includes(sceneryData.stationHash)) return acc; if (!timetable.sceneryNames.includes(stationName)) return acc;
const stopInfoIndex = timetable.followingStops.findIndex((stop) => { const checkpoints = [stationName];
const stopNameLower = stop.stopNameRAW.toLocaleLowerCase(); if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
return (
stationNameLower == stopNameLower ||
(!/(po\.|podg\.)/.test(stopNameLower) && stopNameLower.includes(stationNameLower)) ||
(!/(po\.|podg\.)/.test(stationNameLower) && stationNameLower.includes(stopNameLower)) ||
(stopNameLower.split(', podg.')[0] !== undefined &&
stationNameLower.startsWith(stopNameLower.split(', podg.')[0]))
);
});
const checkpointScheduledTrains: ScheduledTrain[] = []; const checkpointScheduledTrains: ScheduledTrain[] = [];
for (let i = 0; i < timetable.followingStops.length; i++) {
if (stopInfoIndex != -1) {
checkpointScheduledTrains.push(
getCheckpointTrain(train, stopInfoIndex, sceneryData.stationName)
);
}
stationGeneralInfo?.checkpoints?.forEach((checkpoint) => {
// if (checkpoint.checkpointName.toLocaleLowerCase() == stationNameLower) return;
if ( if (
checkpointScheduledTrains.findIndex( new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
(cpTrain) => timetable.followingStops[i].stopNameRAW
cpTrain.checkpointName.toLocaleLowerCase() ==
checkpoint.checkpointName.toLocaleLowerCase()
) != -1
) )
return; ) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
const index = timetable.followingStops.findIndex( }
(stop) => stop.stopNameRAW.toLowerCase() == checkpoint.checkpointName.toLowerCase() }
);
if (index > -1)
checkpointScheduledTrains.push(getCheckpointTrain(train, index, sceneryData.stationName));
});
acc.push(...checkpointScheduledTrains); acc.push(...checkpointScheduledTrains);
return acc; return acc;
@@ -250,14 +220,12 @@ export function getStationTrains(
trainList: Train[], trainList: Train[],
scheduledTrainList: ScheduledTrain[], scheduledTrainList: ScheduledTrain[],
region: string, region: string,
sceneryData: API.ActiveSceneries.Data stationName: string
): StationTrain[] { ): StationTrain[] {
return trainList return trainList
.filter( .filter(
(train) => (train) =>
train?.region === region && train?.region === region && train.online && train.currentStationName === stationName
train.online &&
train.currentStationName === sceneryData.stationName
) )
.map((train) => ({ .map((train) => ({
driverName: train.driverName, driverName: train.driverName,
+2 -1
View File
@@ -6,12 +6,13 @@
height: 90vh; height: 90vh;
min-height: 550px; min-height: 550px;
margin-top: 0.5em; margin-top: 0.5em;
position: relative;
padding-right: 0.2em; padding-right: 0.2em;
} }
.journal_wrapper { .journal_wrapper {
max-width: 1350px; max-width: 1500px;
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
+1 -2
View File
@@ -1,4 +1,4 @@
$animDuration: 150ms; $animDuration: 95ms;
$animType: ease-in-out; $animType: ease-in-out;
// List animation // List animation
@@ -72,7 +72,6 @@ $animType: ease-in-out;
&-enter-from, &-enter-from,
&-leave-to { &-leave-to {
transform: translateY(-25%);
opacity: 0; opacity: 0;
} }
} }
+13
View File
@@ -101,3 +101,16 @@
background-color: #be3728; background-color: #be3728;
} }
} }
.spawn-badge {
color: white;
.length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > .name {
background-color: #007599;
}
}
+36 -5
View File
@@ -1,4 +1,6 @@
@import 'fonts.scss'; @import 'fonts';
@import 'variables';
@import 'responsive';
:root { :root {
--clr-primary: #ffc014; --clr-primary: #ffc014;
@@ -18,12 +20,15 @@
--clr-donator: #f7a4ff; --clr-donator: #f7a4ff;
--no-scroll-padding: 17px;
--max-container-width: 1700px;
font-size: 16px; font-size: 16px;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 15px; width: var(--no-scroll-padding);
height: 15px; height: var(--no-scroll-padding);
background-color: transparent; background-color: transparent;
&-track { &-track {
@@ -46,11 +51,16 @@ body {
padding: 0; padding: 0;
font-family: 'Quicksand', sans-serif; font-family: 'Quicksand', sans-serif;
font-weight: 500; font-weight: 500;
text-rendering: optimizeLegibility !important;
-webkit-font-smoothing: antialiased !important;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden;
position: relative;
&.no-scroll { &.no-scroll {
overflow-y: hidden; overflow-y: hidden;
padding-right: 15px; padding-right: var(--no-scroll-padding);
@include smallScreen() { @include smallScreen() {
padding: 0; padding: 0;
@@ -203,6 +213,7 @@ a.a-button {
&.btn--action { &.btn--action {
background-color: #424242; background-color: #424242;
border-radius: 0.25em; border-radius: 0.25em;
font-weight: bold;
&:hover { &:hover {
background-color: #555; background-color: #555;
@@ -226,7 +237,7 @@ a.a-button {
padding: 0.35em 0.75em; padding: 0.35em 0.75em;
img { img {
width: 1.5em; width: 1.35em;
vertical-align: middle; vertical-align: middle;
} }
} }
@@ -288,3 +299,23 @@ a.a-button {
} }
} }
} }
// Basic tooltip
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(10px, -50%);
content: attr(data-tooltip);
color: white;
background-color: #171717;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.25em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: help;
}
+21 -17
View File
@@ -1,10 +1,22 @@
import { Status } from './common'; import { Status } from './common';
export enum APIDataStatus {
OK = 'OK',
WARNING = 'WARNING'
}
export namespace API { export namespace API {
export namespace ActiveData { export namespace ActiveData {
export interface APIStatuses {
stationsAPI: APIDataStatus;
trainsAPI: APIDataStatus;
dispatchersAPI: APIDataStatus;
sceneryRequirementsAPI: APIDataStatus;
}
export interface Response { export interface Response {
activeSceneries?: API.ActiveSceneries.Response; activeSceneries?: API.ActiveSceneries.Response;
trains?: API.ActiveTrains.Response; trains?: API.ActiveTrains.Response;
apiStatuses?: APIStatuses;
} }
} }
export namespace DispatcherHistory { export namespace DispatcherHistory {
@@ -116,8 +128,8 @@ export namespace API {
export type Response = Data[]; export type Response = Data[];
export interface Data { export interface Data {
id: string;
trainNo: number; trainNo: number;
mass: number; mass: number;
length: number; length: number;
speed: number; speed: number;
@@ -251,22 +263,6 @@ export namespace API {
export type Response = Data[]; export type Response = Data[];
} }
export namespace RollingStock {
export interface Response {
usage: Record<string, string>;
info: Info;
}
export interface Info {
'loco-e': [string, string, string, string, boolean][];
'loco-s': [string, string, string, string, boolean][];
'loco-szt': [string, string, string, string, boolean][];
'loco-ezt': [string, string, string, string, boolean][];
'car-passenger': [string, string, boolean, boolean, string][];
'car-cargo': [string, string, boolean, boolean, string][];
}
}
export namespace DailyStats { export namespace DailyStats {
export interface Response { export interface Response {
totalTimetables: number; totalTimetables: number;
@@ -368,3 +364,11 @@ export namespace GithubAPI {
} }
} }
} }
export namespace Websocket {
export interface Payload {
activeSceneries: API.ActiveSceneries.Response;
activeTrains: API.ActiveTrains.Response;
connectedSocketCount: number;
}
}
+214
View File
@@ -1,5 +1,18 @@
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export namespace Status { export namespace Status {
export enum ActiveDispatcher { export enum ActiveDispatcher {
FREE = -3,
INVALID = -2, INVALID = -2,
UNKNOWN = -1, UNKNOWN = -1,
NO_LIMIT = 0, NO_LIMIT = 0,
@@ -19,3 +32,204 @@ export namespace Status {
Warning = 3 Warning = 3
} }
} }
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface Train {
id: string;
trainId: string;
mass: number;
length: number;
speed: number;
signal: string;
distance: number;
connectedTrack: string;
driverId: number;
trainNo: number;
driverName: string;
driverLevel: number;
currentStationName: string;
currentStationHash: string;
locoType: string;
online: boolean;
lastSeen: number;
region: string;
stockList: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: {
timetableId: number;
category: string;
route: string;
followingStops: TrainStop[];
TWR: boolean;
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
};
}
export interface Station {
name: string;
generalInfo?: {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
onlineInfo?: ActiveScenery;
}
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
singleElectrifiedNames: string[];
singleOtherNames: string[];
doubleElectrifiedNames: string[];
doubleOtherNames: string[];
sblNames: string[];
minRouteSpeed: number;
maxRouteSpeed: number;
}
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface ActiveScenery {
name: string;
hash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawns: ScenerySpawn[];
dispatcherName: string;
dispatcherRate: number;
dispatcherId: number;
dispatcherExp: number;
dispatcherIsSupporter: boolean;
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
unconfirmed: number;
};
}
export interface ScenerySpawn {
spawnName: string;
spawnLength: number;
isElectrified: boolean;
spawnType: ScenerySpawnType;
}
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number | null;
}
+7 -4
View File
@@ -37,7 +37,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, provide, reactive, Ref, ref } from 'vue'; import { defineComponent, provide, reactive, Ref, ref } from 'vue';
import http from '../http';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { LocationQuery } from 'vue-router'; import { LocationQuery } from 'vue-router';
import { Journal } from '../components/JournalView/typings'; import { Journal } from '../components/JournalView/typings';
@@ -48,6 +47,7 @@ import JournalDispatchersList from '../components/JournalView/JournalDispatchers
import JournalOptions from '../components/JournalView/JournalOptions.vue'; import JournalOptions from '../components/JournalView/JournalOptions.vue';
import JournalHeader from '../components/JournalView/JournalHeader.vue'; import JournalHeader from '../components/JournalView/JournalHeader.vue';
import JournalStats from '../components/JournalView/JournalStats.vue'; import JournalStats from '../components/JournalView/JournalStats.vue';
import { useApiStore } from '../store/apiStore';
const statsButtons: Journal.StatsButton[] = [ const statsButtons: Journal.StatsButton[] = [
{ {
@@ -120,6 +120,7 @@ export default defineComponent({
return { return {
mainStore: useMainStore(), mainStore: useMainStore(),
apiStore: useApiStore(),
sorterActive, sorterActive,
searchersValues, searchersValues,
@@ -201,7 +202,7 @@ export default defineComponent({
try { try {
const statsData: API.DispatcherStats.Response = await ( const statsData: API.DispatcherStats.Response = await (
await http.get('api/getDispatcherStats', { await this.apiStore.client!.get('api/getDispatcherStats', {
params: { params: {
name: this.mainStore.dispatcherStatsName name: this.mainStore.dispatcherStatsName
} }
@@ -236,7 +237,9 @@ export default defineComponent({
this.countFromIndex = this.historyList.length; this.countFromIndex = this.historyList.length;
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await (
await http.get(`api/getDispatchers?${this.currentQuery}&countFrom=${this.countFromIndex}`) await this.apiStore.client!.get(
`api/getDispatchers?${this.currentQuery}&countFrom=${this.countFromIndex}`
)
).data; ).data;
if (!responseData) return; if (!responseData) return;
@@ -283,7 +286,7 @@ export default defineComponent({
if (reset) this.dataStatus = Status.Data.Loading; if (reset) this.dataStatus = Status.Data.Loading;
const responseData: API.DispatcherHistory.Response = await ( const responseData: API.DispatcherHistory.Response = await (
await http.get(`api/getDispatchers?${this.currentQuery}`) await this.apiStore.client!.get(`api/getDispatchers?${this.currentQuery}`)
).data; ).data;
if (!responseData) { if (!responseData) {
+7 -4
View File
@@ -53,7 +53,7 @@ import JournalTimetablesList from '../components/JournalView/JournalTimetables/J
import { Journal } from '../components/JournalView/typings'; import { Journal } from '../components/JournalView/typings';
import { Status } from '../typings/common'; import { Status } from '../typings/common';
import { API } from '../typings/api'; import { API } from '../typings/api';
import http from '../http'; import { useApiStore } from '../store/apiStore';
export const journalTimetableFilters: Journal.TimetableFilter[] = [ export const journalTimetableFilters: Journal.TimetableFilter[] = [
{ {
@@ -158,6 +158,7 @@ export default defineComponent({
data: () => ({ data: () => ({
journalTimetableFilters, journalTimetableFilters,
mainStore: useMainStore(), mainStore: useMainStore(),
apiStore: useApiStore(),
statsButtons: [ statsButtons: [
{ {
@@ -282,7 +283,9 @@ export default defineComponent({
this.mainStore.driverStatsStatus = Status.Data.Loading; this.mainStore.driverStatsStatus = Status.Data.Loading;
const statsData: API.DriverStats.Response = await ( const statsData: API.DriverStats.Response = await (
await http.get(`api/getDriverInfo?name=${this.mainStore.driverStatsName}`) await this.apiStore.client!.get(
`api/getDriverInfo?name=${this.mainStore.driverStatsName}`
)
).data; ).data;
this.mainStore.driverStatsData = statsData; this.mainStore.driverStatsData = statsData;
@@ -321,7 +324,7 @@ export default defineComponent({
this.currentQueryParams['countFrom'] = this.timetableHistory.length; this.currentQueryParams['countFrom'] = this.timetableHistory.length;
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.Response = await (
await http.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: { ...this.currentQueryParams } params: { ...this.currentQueryParams }
}) })
).data; ).data;
@@ -425,7 +428,7 @@ export default defineComponent({
try { try {
const responseData: API.TimetableHistory.Response = await ( const responseData: API.TimetableHistory.Response = await (
await http.get('api/getTimetables', { await this.apiStore.client!.get('api/getTimetables', {
params: this.currentQueryParams params: this.currentQueryParams
}) })
).data; ).data;
+6 -11
View File
@@ -147,7 +147,7 @@ export default defineComponent({
}, },
onlineSceneryInfo() { onlineSceneryInfo() {
return this.store.onlineSceneryList.find( return this.store.activeSceneryList.find(
(scenery) => (scenery) =>
scenery.name === this.station?.toString().replace(/_/g, ' ') && scenery.name === this.station?.toString().replace(/_/g, ' ') &&
scenery.region == this.store.region.id scenery.region == this.store.region.id
@@ -169,11 +169,7 @@ export default defineComponent({
loadSelectedCheckpoint() { loadSelectedCheckpoint() {
if (!this.stationInfo?.generalInfo?.checkpoints) return; if (!this.stationInfo?.generalInfo?.checkpoints) return;
if (this.stationInfo.generalInfo.checkpoints.length == 0) return; if (this.stationInfo.generalInfo.checkpoints.length == 0) return;
this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0].checkpointName; this.selectedCheckpoint = this.stationInfo.generalInfo.checkpoints[0];
},
selectCheckpoint(cp: { checkpointName: string }) {
this.selectedCheckpoint = cp.checkpointName;
} }
} }
}); });
@@ -214,13 +210,13 @@ button.back-btn {
.scenery-wrapper { .scenery-wrapper {
display: grid; display: grid;
grid-template-columns: 4fr 5fr; grid-template-columns: 4fr 6fr;
gap: 0 1em; gap: 0 1em;
position: relative; position: relative;
width: 100%; width: 100%;
max-width: 1700px; max-width: var(--max-container-width);
min-height: 100vh; min-height: 100vh;
margin: 1rem 0; margin: 1rem 0;
@@ -238,9 +234,8 @@ button.back-btn {
padding: 1em 0.5em; padding: 1em 0.5em;
height: 95vh; height: 95vh;
min-height: 550px; min-height: 750px;
max-height: 1000px; max-height: 1000px;
overflow: auto; overflow: auto;
display: flex; display: flex;
@@ -252,7 +247,7 @@ button.back-btn {
padding: 1em 0.5em; padding: 1em 0.5em;
height: 95vh; height: 95vh;
min-height: 550px; min-height: 750px;
max-height: 1000px; max-height: 1000px;
display: grid; display: grid;
+36 -40
View File
@@ -8,10 +8,20 @@
ref="filterCardRef" ref="filterCardRef"
/> />
<Donation :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" /> <button
class="btn-donation btn--image"
ref="btn"
@click="isDonationModalOpen = true"
@focus="isDonationModalOpen = false"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
</div> </div>
<StationTable :stations="computedStationList" @toggleDonationModal="toggleDonationModal" /> <DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
<StationTable @toggleDonationModal="toggleDonationModal" />
<StationStats />
</div> </div>
</section> </section>
</template> </template>
@@ -22,36 +32,29 @@ import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore'; import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import Donation from '../components/Global/Donation.vue'; import DonationModal from '../components/Global/DonationModal.vue';
import StationStats from '../components/StationsView/StationStats.vue';
export default defineComponent({ export default defineComponent({
components: { components: {
StationTable, StationTable,
StationFilterCard, StationFilterCard,
Donation StationStats,
DonationModal
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
modalHidden: true, isDonationModalOpen: false,
STORAGE_KEY: 'options_saved',
focusedStationName: '',
filterStore: useStationFiltersStore(),
store: useMainStore(),
isDonationModalOpen: false filterStore: useStationFiltersStore(),
store: useMainStore()
}), }),
mounted() { mounted() {
this.filterStore.setupFilters(); this.filterStore.setupFilters();
}, },
computed: {
computedStationList() {
return this.filterStore.filteredStationList;
}
},
methods: { methods: {
toggleDonationModal(value: boolean) { toggleDonationModal(value: boolean) {
this.isDonationModalOpen = value; this.isDonationModalOpen = value;
@@ -64,30 +67,6 @@ export default defineComponent({
@import '../styles/variables.scss'; @import '../styles/variables.scss';
@import '../styles/responsive.scss'; @import '../styles/responsive.scss';
@keyframes blinkAnim {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.indicator-anim {
&-enter-active,
&-leave-active {
transition: all 0.25s ease-in-out;
}
&-enter,
&-leave-to {
transform: translateY(100%);
opacity: 0;
}
}
.stations-view { .stations-view {
position: relative; position: relative;
display: flex; display: flex;
@@ -99,6 +78,7 @@ export default defineComponent({
.wrapper { .wrapper {
max-width: 100%; max-width: 100%;
width: var(--max-container-width);
} }
.stations-options { .stations-options {
@@ -108,4 +88,20 @@ export default defineComponent({
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
button.btn-donation {
$btnColor: #254069;
background-color: $btnColor;
&:hover {
background-color: lighten($btnColor, 5%);
}
@include smallScreen {
span {
display: none;
}
}
}
</style> </style>
+2 -2
View File
@@ -20,11 +20,11 @@ import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch }
import TrainOptions from '../components/TrainsView/TrainOptions.vue'; import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin'; import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings'; import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager'; import { filteredTrainList } from '../managers/trainFilterManager';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import { Train } from '../typings/common';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -126,7 +126,7 @@ export default defineComponent({
.trains_wrapper { .trains_wrapper {
margin: 1rem auto; margin: 1rem auto;
max-width: 1350px; max-width: 1500px;
} }
.trains_topbar { .trains_topbar {
+34 -28
View File
@@ -4,53 +4,50 @@ import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({ export default defineConfig({
server: { server: {
port: 5001 port: 5001,
open: true
}, },
publicDir: 'public', publicDir: 'public',
plugins: [ plugins: [
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['/images/*.png', '/fonts/*.woff', '/fonts/*.woff2'], includeAssets: ['/images/*.{png,svg,jpg}', '/fonts/*.{woff,woff2}'],
workbox: { workbox: {
disableDevLogs: true, disableDevLogs: true,
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'], globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
cleanupOutdatedCaches: true,
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'), urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'sceneries-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: new RegExp('^https://raw.githubusercontent.com/Spythere/api/*', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'github-api-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst', handler: 'CacheFirst',
options: { options: {
cacheName: 'images-cache', cacheName: 'spythere-sceneries-cache',
expiration: { cacheableResponse: {
maxEntries: 100, statuses: [0, 200]
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days }
}
}, },
{
urlPattern: new RegExp('^https://rj.td2.info.pl/dist/img/thumbnails/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'swdr-images-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200, 404] statuses: [0, 200, 404]
} }
} }
},
{
urlPattern: new RegExp('^https://static.spythere.eu/images/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'spythere-images-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
} }
] ]
}, },
@@ -59,5 +56,14 @@ export default defineConfig({
suppressWarnings: true suppressWarnings: true
} }
}) })
] ],
build: {
rollupOptions: {
output: {
entryFileNames: 'app-[name].js',
assetFileNames: 'app-[name].css',
chunkFileNames: 'chunk-[name].js'
}
}
}
}); });
+1946 -2462
View File
File diff suppressed because it is too large Load Diff