Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4fa7459e39 | |||
| 0602c12914 | |||
| 9d7e70c7e2 | |||
| 88b02b20a5 | |||
| 2b16213531 | |||
| 69c604f1e7 | |||
| 5386820b24 | |||
| c185a8a22e | |||
| 7af08f3cb8 | |||
| 5a684ddc66 | |||
| 5b5c0ea5c2 | |||
| 119d79b071 | |||
| 91ab3ad8ab | |||
| af12a299b6 | |||
| 221bba32d2 | |||
| 987819d42e | |||
| 125b43be4a | |||
| cdc188c5b0 | |||
| d10283c183 | |||
| 14dfa97cc5 | |||
| 6f99de8ec3 | |||
| b999e84b15 | |||
| e1f4a740ac | |||
| 0a88880e98 | |||
| ef105f680d | |||
| cfe8deff8b | |||
| 9337cb011c | |||
| 85aefd850b | |||
| a34eef098b | |||
| c8c1a15191 | |||
| 89b6361a1c | |||
| eae62a8064 | |||
| d643259102 | |||
| 966b36e39f | |||
| cbc812bdec | |||
| c7d2128bd9 | |||
| 836d9d03d9 | |||
| e23c334791 | |||
| 3d6267fa8e | |||
| f7499fe431 | |||
| dc0b0315e0 | |||
| 5e31948a5d | |||
| 4f42c0d878 | |||
| 7dda21e2a2 | |||
| 74df349a44 | |||
| c901b14715 | |||
| 41dda1e592 | |||
| 6f51f79c4c | |||
| a39acc1cc9 | |||
| f699be197b | |||
| 45947cd491 | |||
| 1bf7596b80 | |||
| ffee0d980e | |||
| 1f48e8d80b | |||
| 27b9e8253b | |||
| 5d088a0fac | |||
| ffe26a8fd2 | |||
| 1a39c9054b | |||
| 7073c0687c | |||
| c1fa2a13e1 | |||
| 1b8053faa3 | |||
| a5d7bfd037 | |||
| 9c87ff28b4 | |||
| 1faef31b12 | |||
| 97a829a21c | |||
| 83070ca391 | |||
| 67ce9c7365 | |||
| 83444f64d0 | |||
| a5f9f8901b | |||
| 0276e0754b | |||
| 0d495ede2d | |||
| 48c0a32017 | |||
| 26f2ced266 | |||
| 4f17b1a704 | |||
| 50068a239c | |||
| 662748f705 | |||
| 65c1ab809f | |||
| e7c8ba62d7 | |||
| 38a9f1987f | |||
| f90dfd3cc8 | |||
| 9b765c7fdd | |||
| 0f7e3e8820 | |||
| 1735444176 | |||
| 1d95b26e9c | |||
| 86fbaa2510 | |||
| b7db3edd9b | |||
| 72fa9523e8 | |||
| 7b07a43715 | |||
| 448c6e387e | |||
| 527c929b53 | |||
| b622df19f6 | |||
| 03e69b315c | |||
| f2c11bf2cf | |||
| 92c73b9ed9 | |||
| acc15619a9 | |||
| 3705325a9a | |||
| 1655aa2c94 | |||
| f38ad8fa81 | |||
| 1a7801259f | |||
| abd1c8b684 | |||
| 7f315b549e | |||
| 329c85b858 | |||
| dcef8cdac8 | |||
| 298f8a5f23 | |||
| 51d952ffee | |||
| 83b22e5978 | |||
| 87ad7b8ede | |||
| 440e11bdd9 | |||
| 84ecd3c175 | |||
| 72b3aef045 | |||
| 36ae24fdaf | |||
| 41e3d018e6 | |||
| d9faa486d2 | |||
| 89dc265e1b | |||
| 200e994ae6 | |||
| 150b7749ae | |||
| 0f8932b53c | |||
| 1365140802 | |||
| ce8bbe4c67 | |||
| 1d49de1c6b | |||
| b8574f9ea1 | |||
| ecced14cca | |||
| 212a87126d | |||
| 41e50b8207 | |||
| 565b0dfd8c | |||
| 40a0b47984 | |||
| ccca1c8752 | |||
| cf51045343 | |||
| 23a8b9e8d4 | |||
| c2f7eef146 | |||
| b34f8229cc | |||
| f1eee97d46 | |||
| d93be0b9be | |||
| 5190eed7ee | |||
| a6f284270e | |||
| 08422caa96 | |||
| 3a70d8f6a6 | |||
| e3e5eb3460 | |||
| 1819569234 | |||
| 3c78af4dc0 | |||
| 052ca08f01 | |||
| b01b2f8360 | |||
| bda369d13b | |||
| a8cac9ebe9 | |||
| 0d55a10ec2 | |||
| fa7b1c1629 | |||
| c99b5df4aa | |||
| 0b435c95a0 | |||
| 5d32145f13 | |||
| cb6ea1edb2 | |||
| 6a3974f899 | |||
| 2cbeef7611 | |||
| 43be04826d | |||
| d9986da354 | |||
| ac2269c5a5 | |||
| 6957120b3b | |||
| fc7a9be9dd | |||
| c0b892da97 | |||
| 907b75f12b | |||
| 3c3a114a38 | |||
| 47f824bef0 | |||
| 2d3e830cf9 | |||
| c888b3d386 | |||
| 645a70ef9c | |||
| 1cd93f09c4 | |||
| 6b4231496e | |||
| b72ee13bdb | |||
| ce053a5a82 | |||
| b08e39ae1a | |||
| dc27500237 | |||
| fe6972c1f8 | |||
| 47193181e5 | |||
| 08b9b72dcd | |||
| 7bbabdd7bf | |||
| c90be042e7 | |||
| 200318def7 | |||
| 430a05ab38 | |||
| f335ca8fc2 | |||
| 15e599fe3c | |||
| bd25914ed4 | |||
| 01ea259381 | |||
| aea26fa538 | |||
| 28d78cd2bc | |||
| a021deae96 | |||
| 8840576796 | |||
| 5018e21736 | |||
| a7fa1dfb6d | |||
| a3558c0b30 | |||
| ee159fd582 | |||
| 35c9fb7ef1 | |||
| e24097c240 | |||
| 01cbebd019 | |||
| 3a5ef7e025 | |||
| c78a5b4d67 | |||
| 023de9f7b8 | |||
| 1024e44cc0 | |||
| 580d404d4a | |||
| 6d1ef26ac1 | |||
| bf9799e0c3 | |||
| 1d13e31d79 | |||
| 16f272bd7d | |||
| 23ca33264c | |||
| 324ca3de4d | |||
| e0548e593c |
@@ -0,0 +1,23 @@
|
|||||||
|
name: Build & Deploy to VPS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
env:
|
||||||
|
PROJECT_NAME: stacjownik-td2
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Build the app
|
||||||
|
run: yarn && yarn build
|
||||||
|
- name: Setup SSH key for connection with the server
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.VPS_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa
|
||||||
|
- name: Send new files
|
||||||
|
run: rsync -avP -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa -p 2022" ./dist/ ${{ secrets.VPS_USER }}@${{ secrets.VPS_HOST }}:/var/www/$PROJECT_NAME --delete
|
||||||
@@ -15,13 +15,12 @@ pnpm-debug.log*
|
|||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
.vscode
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
node_modules
|
.vscode/settings.json
|
||||||
|
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# [STACJOWNIK TD2](https://stacjownik-td2.web.app)
|
# [STACJOWNIK TD2](https://stacjownik-td2.spythere.eu)
|
||||||
|
|
||||||
ODŚWIEŻANA LISTA SCENERII I SKŁADÓW ONLINE DLA [SYMULATORA TRAIN DRIVER 2](https://td2.info.pl)
|
ODŚWIEŻANA LISTA SCENERII I SKŁADÓW ONLINE DLA [SYMULATORA TRAIN DRIVER 2](https://td2.info.pl)
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ app.get('/api/getSceneries', (_, res) => {
|
|||||||
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/getVehicles', (_, res) => {
|
app.get('/api/getVehiclesData', (_, res) => {
|
||||||
res.sendFile(path.join(cwd(), 'endpoints', 'getVehicles.json'));
|
res.sendFile(path.join(cwd(), 'endpoints', 'getVehiclesData.json'));
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/getDonators', (_, res) => {
|
app.get('/api/getDonators', (_, res) => {
|
||||||
|
|||||||
@@ -20,26 +20,88 @@
|
|||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
|
||||||
<meta name="msapplication-TileColor" content="#da532c" />
|
<meta name="msapplication-TileColor" content="#da532c" />
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="fa/css/fontawesome.css" />
|
<link rel="stylesheet" href="/fa/css/fontawesome.css" />
|
||||||
<link rel="stylesheet" href="fa/css/brands.css" />
|
<link rel="stylesheet" href="/fa/css/brands.css" />
|
||||||
<link rel="stylesheet" href="fa/css/regular.css" />
|
<link rel="stylesheet" href="/fa/css/regular.css" />
|
||||||
<link rel="stylesheet" href="fa/css/solid.css" />
|
<link rel="stylesheet" href="/fa/css/solid.css" />
|
||||||
|
|
||||||
|
<!-- Preloads -->
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Bold.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Light.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Medium.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-Regular.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
href="/fonts/Quicksand-SemiBold.woff2"
|
||||||
|
as="font"
|
||||||
|
type="font/woff2"
|
||||||
|
crossorigin
|
||||||
|
/>
|
||||||
|
|
||||||
|
<link rel="preload" as="image" href="/images/stacjownik-header-logo.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-dispatcher.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-train.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-arrow-desc.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-pojazdownik.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-stats.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-filter2.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-user.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-like.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-gnr.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-spawn.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableAll.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableUnconfirmed.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-timetableConfirmed.svg" />
|
||||||
|
<link rel="preload" as="image" href="/images/icon-discord.png" />
|
||||||
|
|
||||||
|
<link rel="prefetch" as="image" href="/images/icon-arrow-asc.svg" />
|
||||||
|
<link rel="prefetch" as="image" href="/images/icon-diamond.svg" />
|
||||||
|
|
||||||
<!-- Static OpenGraph meta -->
|
<!-- Static OpenGraph meta -->
|
||||||
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
|
||||||
<meta property="og:url" content="https://stacjownik-td2.web.app/" />
|
<meta property="og:url" content="https://stacjownik-td2.spythere.eu/" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:title" content="Stacjownik" />
|
<meta property="og:title" content="Stacjownik" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
property="og:description"
|
||||||
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta
|
<meta
|
||||||
property="og:image"
|
property="og:image"
|
||||||
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/stacjownik.jpg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<meta property="og:image:width" content="1200" />
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630" />
|
<meta property="og:image:height" content="630" />
|
||||||
<meta property="og:site_name" content="Stacjownik" />
|
<meta property="og:site_name" content="Stacjownik" />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "stacjownik",
|
"name": "stacjownik",
|
||||||
"version": "1.30.5",
|
"version": "1.34.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
"vue-router": "^4.4.0"
|
"vue-router": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.3.1",
|
"@tsconfig/node24": "^24.0.4",
|
||||||
|
"@types/node": "^24.12.0",
|
||||||
"@types/showdown": "^2.0.6",
|
"@types/showdown": "^2.0.6",
|
||||||
"@vite-pwa/assets-generator": "^1.0.0",
|
"@vite-pwa/assets-generator": "^1.0.0",
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.8.1",
|
||||||
"axios": "^1.9.0",
|
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.1.4",
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-cz" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v240H0z"/>
|
||||||
|
<path fill="#d7141a" d="M0 240h640v240H0z"/>
|
||||||
|
<path fill="#11457e" d="M360 240 0 0v480z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-de" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fc0" d="M0 320h640v160H0z"/>
|
||||||
|
<path fill="#000001" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="red" d="M0 160h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 221 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-gb" viewBox="0 0 640 480">
|
||||||
|
<path fill="#012169" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#FFF" d="m75 0 244 181L562 0h78v62L400 241l240 178v61h-80L320 301 81 480H0v-60l239-178L0 64V0z"/>
|
||||||
|
<path fill="#C8102E" d="m424 281 216 159v40L369 281zm-184 20 6 35L54 480H0zM640 0v3L391 191l2-44L590 0zM0 0l239 176h-60L0 42z"/>
|
||||||
|
<path fill="#FFF" d="M241 0v480h160V0zM0 160v160h640V160z"/>
|
||||||
|
<path fill="#C8102E" d="M0 193v96h640v-96zM273 0v480h96V0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-it" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="#fff" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#009246" d="M0 0h213.3v480H0z"/>
|
||||||
|
<path fill="#ce2b37" d="M426.7 0H640v480H426.7z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 289 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-pl" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd">
|
||||||
|
<path fill="#fff" d="M640 480H0V0h640z"/>
|
||||||
|
<path fill="#dc143c" d="M640 480H0V240h640z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 219 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ru" viewBox="0 0 640 480">
|
||||||
|
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#0039a6" d="M0 160h640v160H0z"/>
|
||||||
|
<path fill="#d52b1e" d="M0 320h640v160H0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-se" viewBox="0 0 640 480">
|
||||||
|
<path fill="#005293" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#fecb00" d="M176 0v192H0v96h176v192h96V288h368v-96H272V0z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 209 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-sk" viewBox="0 0 640 480">
|
||||||
|
<path fill="#ee1c25" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#0b4ea2" d="M0 0h640v320H0z"/>
|
||||||
|
<path fill="#fff" d="M0 0h640v160H0z"/>
|
||||||
|
<path fill="#fff" d="M233 370.8c-43-20.7-104.6-61.9-104.6-143.2 0-81.4 4-118.4 4-118.4h201.3s3.9 37 3.9 118.4S276 350 233 370.8"/>
|
||||||
|
<path fill="#ee1c25" d="M233 360c-39.5-19-96-56.8-96-131.4s3.6-108.6 3.6-108.6h184.8s3.5 34 3.5 108.6C329 303.3 272.5 341 233 360"/>
|
||||||
|
<path fill="#fff" d="M241.4 209c10.7.2 31.6.6 50.1-5.6 0 0-.4 6.7-.4 14.4s.5 14.4.5 14.4c-17-5.7-38.1-5.8-50.2-5.7v41.2h-16.8v-41.2c-12-.1-33.1 0-50.1 5.7 0 0 .5-6.7.5-14.4s-.5-14.4-.5-14.4c18.5 6.2 39.4 5.8 50 5.6v-25.9c-9.7 0-23.7.4-39.6 5.7 0 0 .5-6.6.5-14.4 0-7.7-.5-14.4-.5-14.4 15.9 5.3 29.9 5.8 39.6 5.7-.5-16.4-5.3-37-5.3-37s9.9.7 13.8.7 13.8-.7 13.8-.7-4.8 20.6-5.3 37c9.7.1 23.7-.4 39.6-5.7 0 0-.5 6.7-.5 14.4s.5 14.4.5 14.4a119 119 0 0 0-39.7-5.7v26z"/>
|
||||||
|
<path fill="#0b4ea2" d="M233 263.3c-19.9 0-30.5 27.5-30.5 27.5s-6-13-22.2-13c-11 0-19 9.7-24.2 18.8 20 31.7 51.9 51.3 76.9 63.4 25-12 57-31.7 76.9-63.4-5.2-9-13.2-18.8-24.2-18.8-16.2 0-22.2 13-22.2 13S253 263.3 233 263.3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ua" viewBox="0 0 640 480">
|
||||||
|
<g fill-rule="evenodd" stroke-width="1pt">
|
||||||
|
<path fill="gold" d="M0 0h640v480H0z"/>
|
||||||
|
<path fill="#0057b8" d="M0 0h640v240H0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,15 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||||
|
<path d="M12 9v4" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 345 B |
|
Before Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,60 @@
|
|||||||
|
<svg width="79" height="127" viewBox="0 0 79 127" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="icon-loading">
|
||||||
|
<g id="Rectangle 37">
|
||||||
|
<rect id="Rectangle 38" x="36.9999" y="79" width="6" height="16" fill="#FF0000"/>
|
||||||
|
<rect id="Rectangle 40" x="36.9999" y="111" width="6" height="16" fill="#FF0000"/>
|
||||||
|
<rect id="Rectangle 39" x="36.9999" y="95" width="6" height="16" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<g id="Group 8">
|
||||||
|
<path id="Vector 15" d="M59.5018 41.0003H23.0018C-1.49817 41.0003 -0.498171 79.5003 23.0018 79.5003H59.5018C83.0018 79.5003 84.0018 41.0003 59.5018 41.0003Z" fill="#3F3E3E"/>
|
||||||
|
<g id="Group 51">
|
||||||
|
<circle id="light-left" cx="22.9999" cy="60" r="9" fill="#FF1313" fill-opacity="1"/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
attributeType="XML"
|
||||||
|
attributeName="opacity"
|
||||||
|
values="0.25;1;1;0.25;0.25"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="Group 51">
|
||||||
|
<circle id="light-right" cx="57.9999" cy="60" r="9" fill="#FF1313" fill-opacity="1"/>
|
||||||
|
|
||||||
|
<animate
|
||||||
|
attributeType="XML"
|
||||||
|
attributeName="opacity"
|
||||||
|
values="1;0.25;0.25;1;1"
|
||||||
|
dur="1s"
|
||||||
|
repeatCount="indefinite"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
</g>
|
||||||
|
<g id="Group 52">
|
||||||
|
<rect id="Rectangle 36" x="37.9999" y="10" width="4" height="31" fill="#525252"/>
|
||||||
|
<g id="Vector 23">
|
||||||
|
<path id="Rectangle 28" d="M4.09756 32.3241L10.9579 29.2933L14.1908 36.611L7.33047 39.6418L3.42724 36.9932L4.09756 32.3241Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 30" d="M10.9579 29.2933L20.105 25.2522L23.3379 32.5698L14.1908 36.611L12.5743 32.9521L10.9579 29.2933Z" fill="white"/>
|
||||||
|
<path id="Rectangle 34" d="M20.105 25.2522L29.2521 21.211L32.485 28.5287L23.3379 32.5698L21.7214 28.911L20.105 25.2522Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 35" d="M47.5463 13.1288L56.6934 9.08762L59.9263 16.4053L50.7792 20.4464L49.1627 16.7876L47.5463 13.1288Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 31" d="M29.2521 21.211L38.3992 17.1699L41.6321 24.4876L32.485 28.5287L30.8685 24.8699L29.2521 21.211Z" fill="white"/>
|
||||||
|
<path id="Rectangle 32" d="M38.3992 17.1699L47.5463 13.1288L50.7792 20.4464L41.6321 24.4876L40.0156 20.8287L38.3992 17.1699Z" fill="white"/>
|
||||||
|
<path id="Rectangle 33" d="M56.6934 9.08762L65.8404 5.04649L69.0734 12.3642L59.9263 16.4053L58.3098 12.7465L56.6934 9.08762Z" fill="white"/>
|
||||||
|
<path id="Rectangle 29" d="M73.1581 1.81359L65.8405 5.04649L69.0734 12.3642L76.391 9.13126L76.604 4.6642L73.1581 1.81359Z" fill="#FF0000"/>
|
||||||
|
</g>
|
||||||
|
<g id="Vector 24">
|
||||||
|
<path id="Rectangle 28_2" d="M6.36567 3.47438L13.3598 6.18222L10.4714 13.6426L3.47731 10.9348L2.59012 6.30195L6.36567 3.47438Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 30_2" d="M13.3597 6.18222L22.6852 9.79268L19.7969 17.2531L10.4714 13.6426L11.9156 9.91241L13.3597 6.18222Z" fill="white"/>
|
||||||
|
<path id="Rectangle 34_2" d="M22.6853 9.79268L32.0108 13.4031L29.1224 20.8635L19.7969 17.2531L21.2411 13.5229L22.6853 9.79268Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 35_2" d="M50.6617 20.6241L59.9872 24.2345L57.0989 31.6949L47.7734 28.0844L49.2176 24.3542L50.6617 20.6241Z" fill="#FF0000"/>
|
||||||
|
<path id="Rectangle 31_2" d="M32.0107 13.4031L41.3362 17.0136L38.4479 24.474L29.1224 20.8635L30.5666 17.1333L32.0107 13.4031Z" fill="white"/>
|
||||||
|
<path id="Rectangle 32_2" d="M41.3363 17.0136L50.6618 20.6241L47.7734 28.0844L38.4479 24.474L39.8921 20.7438L41.3363 17.0136Z" fill="white"/>
|
||||||
|
<path id="Rectangle 33_2" d="M59.9872 24.2345L69.3127 27.845L66.4243 35.3054L57.0988 31.6949L58.543 27.9647L59.9872 24.2345Z" fill="white"/>
|
||||||
|
<path id="Rectangle 29_2" d="M76.7731 30.7333L69.3127 27.845L66.4243 35.3054L73.8847 38.1937L77.194 35.1856L76.7731 30.7333Z" fill="#FF0000"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<svg width="39" height="23" viewBox="0 0 39 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="39" height="23" fill="#FF0F0F"/>
|
|
||||||
<rect width="39" height="11.5" fill="white"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 199 B |
@@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
|
|
||||||
<AppHeader :current-lang="store.currentLocale" @change-lang="changeLang" />
|
<AppHeader />
|
||||||
|
|
||||||
<main class="app_main">
|
<main class="app_main">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<keep-alive exclude="SceneryView">
|
<keep-alive>
|
||||||
<component :is="Component" :key="$route.name" />
|
<component :is="Component" :key="$route.name" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
@@ -30,7 +30,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
import { version } from '../package.json';
|
import { version } from '../package.json';
|
||||||
import { Status } from './typings/common';
|
import { Status } from './typings/common';
|
||||||
@@ -71,7 +70,7 @@ export default defineComponent({
|
|||||||
isUpdateCardOpen: false,
|
isUpdateCardOpen: false,
|
||||||
isWelcomeCardOpen: false,
|
isWelcomeCardOpen: false,
|
||||||
|
|
||||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
isOnProductionHost: /(stacjownik-td2)(\.web\.app|\.spythere\.eu)/.test(location.hostname)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
@@ -114,11 +113,15 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const releaseData = await (
|
const response = await fetch(
|
||||||
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
|
'https://api.github.com/repos/Spythere/stacjownik/releases/latest'
|
||||||
).data;
|
);
|
||||||
|
|
||||||
if (!releaseData) return;
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch release data from repository!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseData = await response.json();
|
||||||
|
|
||||||
this.store.appUpdate = {
|
this.store.appUpdate = {
|
||||||
version,
|
version,
|
||||||
@@ -130,7 +133,7 @@ export default defineComponent({
|
|||||||
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
|
(storageVersion != '' && storageVersion != version && this.isOnProductionHost) ||
|
||||||
import.meta.env.VITE_UPDATE_TEST === 'test';
|
import.meta.env.VITE_UPDATE_TEST === 'test';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
|
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
|
||||||
@@ -159,18 +162,11 @@ export default defineComponent({
|
|||||||
this.apiStore.connectToAPI();
|
this.apiStore.connectToAPI();
|
||||||
},
|
},
|
||||||
|
|
||||||
changeLang(lang: string) {
|
|
||||||
this.$i18n.locale = lang;
|
|
||||||
this.store.currentLocale = lang;
|
|
||||||
|
|
||||||
StorageManager.setStringValue('lang', lang);
|
|
||||||
},
|
|
||||||
|
|
||||||
loadLang() {
|
loadLang() {
|
||||||
const storageLang = StorageManager.getStringValue('lang');
|
const storageLang = StorageManager.getStringValue('lang');
|
||||||
|
|
||||||
if (storageLang) {
|
if (storageLang) {
|
||||||
this.changeLang(storageLang);
|
this.store.changeLocale(storageLang);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +175,7 @@ export default defineComponent({
|
|||||||
const naviLanguage = window.navigator.language.toString();
|
const naviLanguage = window.navigator.language.toString();
|
||||||
|
|
||||||
if (!naviLanguage.startsWith('pl')) {
|
if (!naviLanguage.startsWith('pl')) {
|
||||||
this.changeLang('en');
|
this.store.changeLocale('en');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -194,6 +190,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use './styles/animations';
|
@use './styles/animations';
|
||||||
|
@use './styles/global';
|
||||||
|
|
||||||
// APP
|
// APP
|
||||||
#app {
|
#app {
|
||||||
|
|||||||
@@ -7,13 +7,6 @@
|
|||||||
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
v{{ version }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<br />
|
|
||||||
<a href="https://discord.gg/x2mpNN3svk">
|
|
||||||
<img src="/images/icon-discord.png" alt="discord logo icon" /> <b class="text--discord">
|
|
||||||
{{ $t('footer.discord') }}
|
|
||||||
</b>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
<div style="display: none">∫ ukryta taktyczna całka do programowania w HTMLu</div>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="app_header">
|
<header class="app_header">
|
||||||
<div class="header_container">
|
<div class="header_container">
|
||||||
<div class="header_icons">
|
|
||||||
<span class="icons-top">
|
|
||||||
<img
|
|
||||||
src="/images/icon-pl.svg"
|
|
||||||
alt="icon-pl"
|
|
||||||
@click="changeLang('en')"
|
|
||||||
v-if="currentLang == 'pl'"
|
|
||||||
/>
|
|
||||||
<img src="/images/icon-en.jpg" alt="icon-en" @click="changeLang('pl')" v-else />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="header_body">
|
<div class="header_body">
|
||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
|
||||||
@@ -76,27 +64,12 @@ import RegionDropdown from '../Global/RegionDropdown.vue';
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { StatusIndicator, Clock, RegionDropdown },
|
components: { StatusIndicator, Clock, RegionDropdown },
|
||||||
|
|
||||||
emits: ['changeLang'],
|
|
||||||
|
|
||||||
props: {
|
|
||||||
currentLang: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
return {
|
return {
|
||||||
store: useMainStore()
|
store: useMainStore()
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
changeLang(lang: string) {
|
|
||||||
this.$emit('changeLang', lang);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
onlineTrainsCount() {
|
onlineTrainsCount() {
|
||||||
return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
|
return this.store.trainList.filter((train) => train.region == this.store.region.id).length;
|
||||||
@@ -111,7 +84,7 @@ export default defineComponent({
|
|||||||
isChristmas() {
|
isChristmas() {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
|
|
||||||
return date.getUTCMonth() == 11 && date.getUTCDate() >= 20 && date.getUTCDate() <= 31;
|
return date.getUTCMonth() == 11 && date.getUTCDate() >= 6 && date.getUTCDate() <= 31;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -141,7 +114,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
border-radius: 0 0 1em 1em;
|
border-radius: 0 0 1em 1em;
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
@@ -180,20 +153,12 @@ export default defineComponent({
|
|||||||
|
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
transform: translateX(85%);
|
transform: translateX(85%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ICONS
|
|
||||||
.icons-top {
|
|
||||||
img {
|
|
||||||
width: 2.5em;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// COUNTER
|
// COUNTER
|
||||||
.info_counter {
|
.info_counter {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
<h1>{{ $t('welcome.title') }}</h1>
|
<h1>{{ $t('welcome.title') }}</h1>
|
||||||
|
|
||||||
<div class="language-select">
|
<div class="language-select">
|
||||||
<button :data-active="$i18n.locale == 'pl'" @click="changeLang('pl')">
|
<button :data-active="$i18n.locale == 'pl'" @click="store.changeLocale('pl')">
|
||||||
<img src="/images/icon-pl.svg" alt="" width="45" />
|
<FlagIcon :language-id="0" width="2.5em" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button :data-active="$i18n.locale == 'en'" @click="changeLang('en')">
|
<button :data-active="$i18n.locale == 'en'" @click="store.changeLocale('en')">
|
||||||
<img src="/images/icon-en.jpg" alt="" width="45" />
|
<FlagIcon :language-id="1" width="2.5em" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,19 +63,19 @@
|
|||||||
</b>
|
</b>
|
||||||
|
|
||||||
<div class="apps-grid">
|
<div class="apps-grid">
|
||||||
<a class="app-item" href="https://pojazdownik-td2.web.app/" target="_blank">
|
<a class="app-item" href="https://pojazdownik-td2.spythere.eu/" target="_blank">
|
||||||
<img src="/images/icon-pojazdownik.svg" alt="pojazdownik app logo" />
|
<img src="/images/icon-pojazdownik.svg" alt="pojazdownik app logo" />
|
||||||
<h3 class="text--primary">Pojazdownik</h3>
|
<h3 class="text--primary">Pojazdownik</h3>
|
||||||
<p>{{ $t('welcome.pojazdownik-desc') }}</p>
|
<p>{{ $t('welcome.pojazdownik-desc') }}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="app-item" href="https://generator-td2.web.app/" target="_blank">
|
<a class="app-item" href="https://generator-td2.spythere.eu/" target="_blank">
|
||||||
<img src="/images/icon-gnr.svg" alt="generator app logo" />
|
<img src="/images/icon-gnr.svg" alt="generator app logo" />
|
||||||
<h3 class="text--primary">GeneraTOR</h3>
|
<h3 class="text--primary">GeneraTOR</h3>
|
||||||
<p>{{ $t('welcome.generator-desc') }}</p>
|
<p>{{ $t('welcome.generator-desc') }}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="app-item" href="https://srjp-td2.web.app/" target="_blank">
|
<a class="app-item" href="https://srjp-td2.spythere.eu/" target="_blank">
|
||||||
<img src="/images/icon-srjp.svg" alt="srjp app logo" />
|
<img src="/images/icon-srjp.svg" alt="srjp app logo" />
|
||||||
<h3 class="text--primary">Rozkładownik</h3>
|
<h3 class="text--primary">Rozkładownik</h3>
|
||||||
<p>{{ $t('welcome.srjp-desc') }}</p>
|
<p>{{ $t('welcome.srjp-desc') }}</p>
|
||||||
@@ -114,12 +114,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import Card from '../Global/Card.vue';
|
import Card from '../Global/Card.vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import StorageManager from '../../managers/storageManager';
|
import FlagIcon from '../Global/FlagIcon.vue';
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
const store = useMainStore();
|
const store = useMainStore();
|
||||||
|
|
||||||
const emit = defineEmits(['toggleCard']);
|
const emit = defineEmits(['toggleCard']);
|
||||||
@@ -130,13 +128,6 @@ const props = defineProps({
|
|||||||
function toggleCard(state: boolean) {
|
function toggleCard(state: boolean) {
|
||||||
emit('toggleCard', state);
|
emit('toggleCard', state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeLang(localeName: string) {
|
|
||||||
i18n.locale.value = localeName;
|
|
||||||
store.currentLocale = localeName;
|
|
||||||
|
|
||||||
StorageManager.setStringValue('lang', localeName);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -167,7 +158,7 @@ a.link {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
|
|
||||||
button[data-active='false'] img {
|
button[data-active='false'] ::v-deep(img) {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
|
||||||
<div class="content">
|
<div class="content" tabindex="0" ref="content">
|
||||||
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
<h1 class="content-title"><i class="fa-solid fa-wand-sparkles"></i> {{ $t('update.title') }}</h1>
|
||||||
|
|
||||||
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
|
||||||
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
|
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
|
||||||
@@ -13,7 +13,14 @@
|
|||||||
<p class="bottom-info">
|
<p class="bottom-info">
|
||||||
{{ $t('update.info-1') }}
|
{{ $t('update.info-1') }}
|
||||||
<br />
|
<br />
|
||||||
<span v-html="$t('update.info-2')"></span>
|
|
||||||
|
<i18n-t keypath="update.info-2">
|
||||||
|
<template v-slot:link>
|
||||||
|
<a href="https://github.com/Spythere/stacjownik/releases" target="_blank">{{
|
||||||
|
$t('update.info-2-link-text')
|
||||||
|
}}</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -51,7 +58,7 @@ export default defineComponent({
|
|||||||
watch: {
|
watch: {
|
||||||
isUpdateCardOpen(val: boolean) {
|
isUpdateCardOpen(val: boolean) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
if (val) (this.$refs['content'] as HTMLElement).focus();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,13 +86,13 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(h2) {
|
::v-deep(h2) {
|
||||||
padding: 0.25em 0;
|
padding: 0.5em 0;
|
||||||
border-bottom: 1px solid #aaa;
|
border-bottom: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep(ul) {
|
::v-deep(ul) {
|
||||||
list-style: initial;
|
list-style: disc;
|
||||||
padding: 1em;
|
padding: 0.5em 1.5em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +107,25 @@ export default defineComponent({
|
|||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-title {
|
||||||
|
color: var(--clr-primary);
|
||||||
|
color: transparent;
|
||||||
|
|
||||||
|
background: var(--clr-primary);
|
||||||
|
background: linear-gradient(90deg, var(--clr-primary) 30%, #ffffff 90%);
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
|
||||||
|
text-shadow: var(--clr-primary) 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.no-features {
|
.no-features {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 0 auto;
|
margin: 0.5em auto;
|
||||||
padding: 0.5em 0.75em;
|
padding: 0.5em 0.75em;
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
}
|
}
|
||||||
@@ -117,5 +137,6 @@ p.bottom-info {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="driver-top-actions">
|
<div class="driver-top-actions">
|
||||||
<div class="actions-container">
|
<div class="actions-container">
|
||||||
<div class="actions actions-left">
|
<div class="actions actions-left">
|
||||||
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
<button class="a-button btn--filled btn--image" @click="routerReturn">
|
||||||
<img src="/images/icon-back.svg" alt="train icon" />
|
<img src="/images/icon-back.svg" alt="train icon" />
|
||||||
<span>
|
<span>
|
||||||
{{ t('trains.driver-return-link') }}
|
{{ t('trains.driver-return-link') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions actions-right">
|
<div class="actions actions-right">
|
||||||
<a class="a-button btn--filled btn--image" :href="`https://srjp-td2.web.app/?id=${chosenTrain.id}`"
|
<a
|
||||||
target="_blank">
|
class="a-button btn--filled btn--image"
|
||||||
<span class="hidable">
|
:href="`https://srjp-td2.spythere.eu/?id=${chosenTrain.id}`"
|
||||||
{{ t('trains.driver-srjp-link') }}
|
target="_blank"
|
||||||
</span>
|
>
|
||||||
|
<span class="hidable">
|
||||||
|
{{ t('trains.driver-srjp-link') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
<img src="/images/icon-srjp.svg" alt="srjp icon" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<router-link :to="`/journal/timetables?search-driver=${chosenTrain.driverName}`"
|
<router-link
|
||||||
class="a-button btn--filled btn--image">
|
:to="`/profile?playerId=${chosenTrain.driverId}`"
|
||||||
<span class="hidable">
|
class="a-button btn--filled btn--image"
|
||||||
{{ t('trains.driver-journal-link') }}
|
>
|
||||||
</span>
|
<span class="hidable">
|
||||||
|
{{ t('trains.driver-profile-link') }}
|
||||||
<img src="/images/icon-train.svg" alt="train icon" />
|
</span>
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<img src="/images/icon-user.svg" alt="user icon" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -44,42 +48,40 @@ const router = useRouter();
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
chosenTrain: {
|
chosenTrain: {
|
||||||
type: Object as PropType<Train>,
|
type: Object as PropType<Train>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function routerReturn() {
|
function routerReturn() {
|
||||||
router.back();
|
router.back();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
|
||||||
.actions-container {
|
.actions-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-container>.actions>.a-button {
|
.actions-container > .actions > .a-button {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-radius: 0.5em 0.5em 0 0;
|
border-radius: 0.5em 0.5em 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen {
|
||||||
span.hidable {
|
span.hidable {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<StockList :trainStockList="chosenTrain.stockList" />
|
<StockList :trainStockList="chosenTrain.stockList" :key="chosenTrain.id" :showPreviews="true" />
|
||||||
<TrainSchedule :train="chosenTrain" />
|
<TrainSchedule :train="chosenTrain" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -205,7 +205,7 @@ const availableCategories = computed(() => {
|
|||||||
for (const stockName of stockList) {
|
for (const stockName of stockList) {
|
||||||
const [vehicleName, ...cargoList] = stockName.split(':');
|
const [vehicleName, ...cargoList] = stockName.split(':');
|
||||||
|
|
||||||
const vehicleData = apiStore.vehiclesData?.find((v) => v.name == vehicleName);
|
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
|
||||||
|
|
||||||
if (!vehicleData) continue;
|
if (!vehicleData) continue;
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button class="action-btn btn--filled">
|
|
||||||
<div class="button_content">
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
|
|
||||||
export default defineComponent({});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@use '../../styles/responsive';
|
|
||||||
|
|
||||||
.button_content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -81,13 +81,8 @@ export default defineComponent({
|
|||||||
|
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
|
||||||
.card {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flag-icon">
|
||||||
|
<img
|
||||||
|
:src="languageFlagSrc"
|
||||||
|
alt="language flag"
|
||||||
|
:style="{
|
||||||
|
width: width
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { getLanguageNameById } from '../../utils/languageUtils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
languageId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
width: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const languageFlagSrc = computed(
|
||||||
|
() => `/images/flags/${getLanguageNameById(props.languageId)}.svg`
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.flag-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flag-icon img {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -160,7 +160,7 @@ ul.options {
|
|||||||
|
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
z-index: 100;
|
z-index: 150;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
:vehicle-string="vehicleString"
|
:vehicle-string="vehicleString"
|
||||||
:images="images"
|
:images="images"
|
||||||
:image-fallbacks="imagesFallbacks"
|
:image-fallbacks="imagesFallbacks"
|
||||||
|
:show-previews="showPreviews"
|
||||||
|
:thumbnail-size="thumbnailSize"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -23,7 +25,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
trainStockList: { type: Array as PropType<string[]>, required: true },
|
trainStockList: { type: Array as PropType<string[]>, required: true },
|
||||||
tractionOnly: { type: Boolean, required: false }
|
tractionOnly: { type: Boolean, required: false },
|
||||||
|
showPreviews: { type: Boolean },
|
||||||
|
thumbnailSize: { type: Number }
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
<img
|
<img
|
||||||
v-for="(thumbnailImage, imageIndex) in images"
|
v-for="(thumbnailImage, imageIndex) in images"
|
||||||
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
|
:src="`https://stacjownik.spythere.eu/static/thumbnails/${thumbnailImage}.png`"
|
||||||
height="60"
|
:height="thumbnailSize || 70"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
data-tooltip-type="VehiclePreviewTooltip"
|
:data-crosshair-cursor="showPreviews"
|
||||||
|
:data-tooltip-type="showPreviews ? 'VehiclePreviewTooltip' : ''"
|
||||||
:data-tooltip-content="vehicleString"
|
:data-tooltip-content="vehicleString"
|
||||||
@error="onImageError($event, imageFallbacks[imageIndex])"
|
@error="onImageError($event, imageFallbacks[imageIndex])"
|
||||||
@load="onImageLoad"
|
@load="onImageLoad"
|
||||||
@@ -26,7 +27,9 @@ import { computed, PropType, Ref, ref } from 'vue';
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
vehicleString: { type: String, required: true },
|
vehicleString: { type: String, required: true },
|
||||||
images: { type: Object as PropType<string[]>, required: true },
|
images: { type: Object as PropType<string[]>, required: true },
|
||||||
imageFallbacks: { type: Object as PropType<string[]>, required: true }
|
imageFallbacks: { type: Object as PropType<string[]>, required: true },
|
||||||
|
showPreviews: { type: Boolean },
|
||||||
|
thumbnailSize: { type: Number }
|
||||||
});
|
});
|
||||||
|
|
||||||
const thumbRef = ref(null) as Ref<HTMLElement | null>;
|
const thumbRef = ref(null) as Ref<HTMLElement | null>;
|
||||||
@@ -56,16 +59,17 @@ function onImageLoad() {
|
|||||||
transition: opacity 100ms ease-in-out;
|
transition: opacity 100ms ease-in-out;
|
||||||
|
|
||||||
&[data-load-status='loading'] {
|
&[data-load-status='loading'] {
|
||||||
min-height: 60px;
|
min-height: 70px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stock-text {
|
.stock-text {
|
||||||
|
max-width: 90%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 0.9em;
|
font-size: 0.8em;
|
||||||
margin-bottom: 0.25em;
|
margin: 0 auto;
|
||||||
padding: 0.25em 0;
|
padding: 0.25em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +77,10 @@ function onImageLoad() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
cursor: crosshair;
|
|
||||||
|
|
||||||
padding: 0.5em 0;
|
padding: 0.5em 0;
|
||||||
|
|
||||||
|
&[data-crosshair-cursor='true'] {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="daily-stats">
|
<section class="daily-stats">
|
||||||
<span :data-active="statsStatus">
|
<span :data-active="apiStore.dataStatuses.dailyStatsData">
|
||||||
<h3>
|
<h3>
|
||||||
{{ $t('journal.daily-stats.title') }}
|
{{ $t('journal.daily-stats.title') }}
|
||||||
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
<b class="text--primary">{{ new Date().toLocaleDateString($i18n.locale) }}</b>
|
||||||
@@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
<hr class="header-separator" />
|
<hr class="header-separator" />
|
||||||
|
|
||||||
<b v-if="statsStatus == Status.Data.Loading">
|
<b v-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Loading">
|
||||||
{{ $t('app.loading') }}
|
{{ $t('app.loading') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b class="text--error" v-else-if="statsStatus == Status.Data.Error">
|
<b class="text--error" v-else-if="apiStore.dataStatuses.dailyStatsData == Status.Data.Error">
|
||||||
{{ $t('journal.stats-error') }}
|
{{ $t('journal.stats-error') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
@@ -20,42 +20,48 @@
|
|||||||
{{ $t('journal.daily-stats.info') }}
|
{{ $t('journal.daily-stats.info') }}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else-if="apiStore.dailyStatsData">
|
||||||
<ul class="stats-list">
|
<ul class="stats-list">
|
||||||
<li v-if="stats.totalTimetables">
|
<li v-if="apiStore.dailyStatsData.totalTimetables">
|
||||||
<i18n-t keypath="journal.daily-stats.total">
|
<i18n-t keypath="journal.daily-stats.total">
|
||||||
<template #count>
|
<template #count>
|
||||||
<b class="text--primary">
|
<b class="text--primary">
|
||||||
{{ stats.totalTimetables }}
|
{{ apiStore.dailyStatsData.totalTimetables }}
|
||||||
{{ $t('journal.daily-stats.count', stats.totalTimetables) }}
|
{{ $t('journal.daily-stats.count', apiStore.dailyStatsData.totalTimetables) }}
|
||||||
</b>
|
</b>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary"> {{ stats.distanceSum?.toFixed(2) }} km</b>
|
<b class="text--primary">
|
||||||
|
{{ apiStore.dailyStatsData.distanceSum?.toFixed(2) }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="stats.maxTimetable">
|
<li v-if="apiStore.dailyStatsData.maxTimetable">
|
||||||
<i18n-t keypath="journal.daily-stats.longest">
|
<i18n-t keypath="journal.daily-stats.longest">
|
||||||
<template #id>
|
<template #id>
|
||||||
<router-link :to="`/journal/timetables?search-train=%23${stats.maxTimetable.id}`">
|
<router-link
|
||||||
<b>{{ stats.maxTimetable.id }}</b>
|
:to="`/journal/timetables?search-train=%23${apiStore.dailyStatsData.maxTimetable.id}`"
|
||||||
|
>
|
||||||
|
<b>{{ apiStore.dailyStatsData.maxTimetable.id }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #author>
|
<template #author>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-dispatcher=${stats.maxTimetable.authorName}`"
|
:to="`/journal/timetables?search-dispatcher=${apiStore.dailyStatsData.maxTimetable.authorName}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.maxTimetable.authorName }}</b>
|
<b>{{ apiStore.dailyStatsData.maxTimetable.authorName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<b class="text--primary">{{ stats.maxTimetable.driverName }}</b>
|
<b class="text--primary">{{ apiStore.dailyStatsData.maxTimetable.driverName }}</b>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary">{{ stats.maxTimetable.routeDistance }} km</b>
|
<b class="text--primary"
|
||||||
|
>{{ apiStore.dailyStatsData.maxTimetable.routeDistance }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
@@ -101,35 +107,37 @@
|
|||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="stats.longestDuties.length > 0">
|
<li v-if="apiStore.dailyStatsData.longestDuties.length > 0">
|
||||||
<i18n-t keypath="journal.daily-stats.longest-duties">
|
<i18n-t keypath="journal.daily-stats.longest-duties">
|
||||||
<template #dispatcher>
|
<template #dispatcher>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/dispatchers?search-dispatcher=${stats.longestDuties[0].name}`"
|
:to="`/journal/dispatchers?search-dispatcher=${apiStore.dailyStatsData.longestDuties[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.longestDuties[0].name }}</b>
|
<b>{{ apiStore.dailyStatsData.longestDuties[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #station>{{ stats.longestDuties[0].station }}</template>
|
<template #station>{{ apiStore.dailyStatsData.longestDuties[0].station }}</template>
|
||||||
|
|
||||||
<template #duration>
|
<template #duration>
|
||||||
{{ calculateDuration(stats.longestDuties[0].duration) }}
|
{{ humanizeDuration(apiStore.dailyStatsData.longestDuties[0].duration) }}
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li v-if="stats.mostActiveDrivers.length > 0">
|
<li v-if="apiStore.dailyStatsData.mostActiveDrivers.length > 0">
|
||||||
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
<i18n-t keypath="journal.daily-stats.most-active-driver">
|
||||||
<template #driver>
|
<template #driver>
|
||||||
<router-link
|
<router-link
|
||||||
:to="`/journal/timetables?search-driver=${stats.mostActiveDrivers[0].name}`"
|
:to="`/journal/timetables?search-driver=${apiStore.dailyStatsData.mostActiveDrivers[0].name}`"
|
||||||
>
|
>
|
||||||
<b>{{ stats.mostActiveDrivers[0].name }}</b>
|
<b>{{ apiStore.dailyStatsData.mostActiveDrivers[0].name }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
<template #distance>
|
<template #distance>
|
||||||
<b class="text--primary">{{ stats.mostActiveDrivers[0].distance.toFixed(2) }} km</b>
|
<b class="text--primary"
|
||||||
|
>{{ apiStore.dailyStatsData.mostActiveDrivers[0].distance.toFixed(2) }} km</b
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</li>
|
</li>
|
||||||
@@ -151,7 +159,11 @@
|
|||||||
>
|
>
|
||||||
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
<span>{{ $t(`journal.daily-stats.${key}`) }}</span>
|
||||||
<span>
|
<span>
|
||||||
{{ Object.entries(stats.globalDiff).find(([k, v]) => k == key)?.[1] || '--' }}
|
{{
|
||||||
|
Object.entries(apiStore.dailyStatsData.globalDiff).find(
|
||||||
|
([k, v]) => k == key
|
||||||
|
)?.[1] || '--'
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -160,76 +172,25 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent } from 'vue';
|
import { computed, onMounted } from 'vue';
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
|
||||||
|
|
||||||
import { API } from '../../typings/api';
|
|
||||||
import { Status } from '../../typings/common';
|
|
||||||
import { useApiStore } from '../../store/apiStore';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import { humanizeDuration } from '../../composables/time';
|
||||||
|
|
||||||
export default defineComponent({
|
onMounted(() => {
|
||||||
name: 'journal-daily-stats',
|
apiStore.fetchDailyStats();
|
||||||
|
});
|
||||||
|
|
||||||
mixins: [dateMixin],
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
data() {
|
const topDispatchers = computed(() => {
|
||||||
return {
|
if (!apiStore.dailyStatsData || apiStore.dailyStatsData.mostActiveDispatchers.length == 0)
|
||||||
Status,
|
return [];
|
||||||
statsStatus: Status.Data.Loading,
|
|
||||||
intervalId: -1,
|
|
||||||
|
|
||||||
stats: {} as API.DailyStats.Response,
|
const maxCount = apiStore.dailyStatsData.mostActiveDispatchers[0].count;
|
||||||
apiStore: useApiStore()
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
activated() {
|
return apiStore.dailyStatsData.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
||||||
this.startFetchingDailyStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
deactivated() {
|
|
||||||
this.stopFetchingDailyStats();
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
topDispatchers() {
|
|
||||||
if (this.stats.mostActiveDispatchers.length == 0) return [];
|
|
||||||
const maxCount = this.stats.mostActiveDispatchers[0].count;
|
|
||||||
|
|
||||||
return this.stats.mostActiveDispatchers.filter((disp) => disp.count === maxCount);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
async fetchDailyTimetableStats() {
|
|
||||||
try {
|
|
||||||
const res: API.DailyStats.Response = await (
|
|
||||||
await this.apiStore.client!.get('api/getDailyStats')
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.stats = res;
|
|
||||||
|
|
||||||
this.statsStatus = Status.Data.Loaded;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
|
||||||
this.statsStatus = Status.Data.Error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
startFetchingDailyStats() {
|
|
||||||
this.fetchDailyTimetableStats();
|
|
||||||
|
|
||||||
if (this.intervalId != -1) return;
|
|
||||||
|
|
||||||
this.intervalId = window.setInterval(this.fetchDailyTimetableStats, 60000);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopFetchingDailyStats() {
|
|
||||||
clearInterval(this.intervalId);
|
|
||||||
this.intervalId = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -265,7 +226,7 @@ ul.stats-list {
|
|||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
h3 {
|
h3 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<li class="dispatcher-history-entry">
|
<li class="dispatcher-history-entry">
|
||||||
<div class="entry-info">
|
<div class="entry-info">
|
||||||
<span>
|
<span class="entry-info-left">
|
||||||
<span>
|
<div class="station-info">
|
||||||
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
<router-link :to="`/journal/dispatchers?search-station=${entry.stationName}`">
|
||||||
<b>{{ entry.stationName }}</b>
|
<b>{{ entry.stationName }}</b>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
<b class="text--grayed"> #{{ entry.stationHash }}</b>
|
||||||
</span>
|
•
|
||||||
•
|
<b
|
||||||
<b
|
v-if="entry.dispatcherLevel !== null"
|
||||||
v-if="entry.dispatcherLevel !== null"
|
class="level-badge dispatcher"
|
||||||
class="level-badge dispatcher"
|
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
||||||
:style="calculateExpStyle(entry.dispatcherLevel, entry.dispatcherIsSupporter)"
|
>
|
||||||
>
|
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
||||||
{{ entry.dispatcherLevel >= 2 ? entry.dispatcherLevel : 'L' }}
|
</b>
|
||||||
</b>
|
|
||||||
<b style="margin-left: 5px">
|
|
||||||
<span
|
<span
|
||||||
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(entry.dispatcherName)"
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
@@ -37,7 +36,11 @@
|
|||||||
>
|
>
|
||||||
{{ entry.dispatcherName }}
|
{{ entry.dispatcherName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b>
|
|
||||||
|
<span class="dispatcher-language" v-if="entry.dispatcherLanguageId != null">
|
||||||
|
<FlagIcon :language-id="entry.dispatcherLanguageId" width="1.75em" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span v-if="entry.timestampTo">
|
<span v-if="entry.timestampTo">
|
||||||
@@ -118,6 +121,7 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -125,7 +129,7 @@ export default defineComponent({
|
|||||||
showExtraInfo: { type: Boolean, required: true }
|
showExtraInfo: { type: Boolean, required: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
components: { StationStatusBadge },
|
components: { StationStatusBadge, FlagIcon },
|
||||||
mixins: [dateMixin, styleMixin],
|
mixins: [dateMixin, styleMixin],
|
||||||
emits: ['toggleShowExtraInfo'],
|
emits: ['toggleShowExtraInfo'],
|
||||||
|
|
||||||
@@ -164,6 +168,11 @@ export default defineComponent({
|
|||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dispatcher-language {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.entry-info {
|
.entry-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -185,6 +194,15 @@ export default defineComponent({
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.status-list {
|
.status-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -198,11 +216,15 @@ export default defineComponent({
|
|||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.entry-info {
|
.entry-info {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.station-info {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="journal-stats dispatcher" v-if="dispatcherName && stats">
|
|
||||||
<span class="loading" v-if="!stats.issuedTimetables && !stats.services">
|
|
||||||
{{ $t('journal.dispatcher-stats.empty') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-else>
|
|
||||||
<h3>
|
|
||||||
<i18n-t keypath="journal.dispatcher-stats.title">
|
|
||||||
<template #name>
|
|
||||||
<span class="text--primary">{{ dispatcherName.toUpperCase() }}</span>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<hr class="header-separator" />
|
|
||||||
|
|
||||||
<div class="info-stats">
|
|
||||||
<span class="badge stat-badge" v-if="stats.services">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.services-count') }}</span>
|
|
||||||
<span>{{ stats.services.count }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge" v-if="stats.services">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.service-max') }}</span>
|
|
||||||
<span>{{ calculateDuration(stats.services.durationMax) }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge" v-if="stats.services">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.service-avg') }}</span>
|
|
||||||
<span>{{ calculateDuration(stats.services.durationAvg) }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="section-separator" v-if="stats.issuedTimetables" />
|
|
||||||
|
|
||||||
<div class="info-stats" v-if="stats.issuedTimetables">
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.timetables-count') }}</span>
|
|
||||||
<span>{{ stats.issuedTimetables.count }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.timetables-sum') }}</span>
|
|
||||||
<span>{{ stats.issuedTimetables.distanceSum.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.timetables-max') }}</span>
|
|
||||||
<span>{{ stats.issuedTimetables.distanceMax.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.dispatcher-stats.timetables-avg') }}</span>
|
|
||||||
<span>{{ stats.issuedTimetables.distanceAvg.toFixed(2) }}km</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
|
||||||
import { useMainStore } from '../../../store/mainStore';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'journal-dispatcher-stats',
|
|
||||||
|
|
||||||
mixins: [dateMixin],
|
|
||||||
|
|
||||||
setup() {
|
|
||||||
const store = useMainStore();
|
|
||||||
|
|
||||||
return {
|
|
||||||
stats: store.dispatcherStatsData,
|
|
||||||
dispatcherName: store.dispatcherStatsName
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@use '../../../styles/journal-stats';
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="filters-options dropdown" @keydown.esc="showOptions = false">
|
<div class="dropdown filters-options" @keydown.esc="showOptions = false">
|
||||||
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
<div class="dropdown_background" v-if="showOptions" @click="showOptions = false"></div>
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
<label v-if="propName == 'search-date-from'" for="search-date">{{
|
||||||
$t(`options.search-${optionsType}-date`)
|
$t(`options.search-${optionsType}-date`)
|
||||||
}}</label>
|
}}</label>
|
||||||
|
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
<input
|
<input
|
||||||
class="search-input"
|
class="search-input"
|
||||||
@@ -120,15 +120,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="options_actions">
|
<div class="options_actions">
|
||||||
<button class="btn--action" @click="onResetButtonClick">
|
<button class="btn--action" @click="onResetButtonClick">
|
||||||
{{ $t('options.reset-button') }}
|
{{ $t('options.reset-button') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn--action" @click="onSearchButtonConfirm">
|
<button class="btn--action" @click="onSearchButtonConfirm">
|
||||||
{{ $t('options.search-button') }}
|
{{ $t('options.search-button') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -269,9 +269,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.searchTimeout = window.setTimeout(async () => {
|
this.searchTimeout = window.setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const suggestions: string[] = await (
|
const suggestions: string[] = await this.apiStore.client.get(
|
||||||
await this.apiStore.client!.get(`api/get${type}Suggestions?name=${value}`)
|
`api/get${type}Suggestions?name=${value}`
|
||||||
).data;
|
);
|
||||||
|
|
||||||
this[`${type}Suggestions`] = suggestions;
|
this[`${type}Suggestions`] = suggestions;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -330,4 +330,23 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@use '../../styles/dropdown-filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.dropdown_wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: calc(100% - 4.5em);
|
||||||
|
top: 3.5em;
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_content {
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options_actions {
|
||||||
|
padding: 0 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,89 +2,73 @@
|
|||||||
<div
|
<div
|
||||||
class="journal-stats dropdown"
|
class="journal-stats dropdown"
|
||||||
v-if="!mainStore.isOffline"
|
v-if="!mainStore.isOffline"
|
||||||
@keydown.esc="currentStatsTab = null"
|
@keydown.esc="isDropdownOpen = false"
|
||||||
>
|
>
|
||||||
<div
|
<div class="dropdown_background" v-if="isDropdownOpen" @click="isDropdownOpen = false"></div>
|
||||||
class="dropdown_background"
|
|
||||||
v-if="currentStatsTab !== null"
|
|
||||||
@click="currentStatsTab = null"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
|
<button class="btn--filled btn--image" @click="toggleDropdown">
|
||||||
|
<img :src="`/images/icon-stats.svg`" alt="stats icon" />
|
||||||
|
{{ $t('journal.daily-stats.button') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-for="button in statsButtons"
|
|
||||||
:key="button.tab"
|
|
||||||
class="btn--filled btn--image"
|
class="btn--filled btn--image"
|
||||||
:data-selected="button.tab == currentStatsTab"
|
:data-disabled="chosenPlayerId == -1"
|
||||||
:data-disabled="button.disabled"
|
@click="navigateToProfile"
|
||||||
:disabled="button.disabled"
|
|
||||||
@click="onTabButtonClick(button.tab)"
|
|
||||||
>
|
>
|
||||||
<img
|
<img :src="`/images/icon-user.svg`" alt="user icon" />
|
||||||
v-if="button.iconName"
|
{{ $t('profile.journal-button') }}
|
||||||
:src="`/images/icon-${button.iconName}.svg`"
|
|
||||||
:alt="button.iconName"
|
|
||||||
/>
|
|
||||||
{{ $t(button.localeKey) }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="dropdown-anim">
|
<transition name="dropdown-anim">
|
||||||
<div
|
<div class="dropdown_wrapper" v-if="isDropdownOpen">
|
||||||
class="dropdown_wrapper"
|
|
||||||
:class="{ 'dropdown-align-right': true }"
|
|
||||||
v-if="currentStatsTab !== null"
|
|
||||||
>
|
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component :is="currentStatsTab" :key="currentStatsTab"></component>
|
<JournalDailyStats />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import StorageManager from '../../managers/storageManager';
|
|
||||||
import { Journal } from './typings';
|
|
||||||
import JournalDailyStats from './JournalDailyStats.vue';
|
import JournalDailyStats from './JournalDailyStats.vue';
|
||||||
import JournalDispatcherStats from '../JournalView/JournalDispatchers/JournalDispatcherStats.vue';
|
import { useRouter } from 'vue-router';
|
||||||
import JournalDriverStats from '../JournalView/JournalTimetables/JournalDriverStats.vue';
|
|
||||||
|
|
||||||
export default defineComponent({
|
const router = useRouter();
|
||||||
components: { JournalDailyStats, JournalDriverStats, JournalDispatcherStats },
|
|
||||||
props: {
|
|
||||||
statsButtons: {
|
|
||||||
type: Array as PropType<Journal.StatsButton[]>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
Journal,
|
|
||||||
mainStore: useMainStore(),
|
|
||||||
currentStatsTab: null as Journal.StatsTab | null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
const props = defineProps({
|
||||||
onTabButtonClick(tab: Journal.StatsTab) {
|
chosenPlayerId: {
|
||||||
this.currentStatsTab = tab == this.currentStatsTab ? null : tab;
|
type: Number,
|
||||||
|
required: true
|
||||||
StorageManager.setStringValue('journalStatsTab', this.currentStatsTab ?? '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
const isDropdownOpen = ref(false);
|
||||||
|
|
||||||
|
function toggleDropdown() {
|
||||||
|
isDropdownOpen.value = !isDropdownOpen.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToProfile() {
|
||||||
|
if (props.chosenPlayerId == -1) return;
|
||||||
|
|
||||||
|
router.push(`/profile?playerId=${props.chosenPlayerId}`);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@use '../../styles/dropdown-filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
|
||||||
.dropdown_wrapper.dropdown-align-right {
|
.dropdown_wrapper {
|
||||||
left: auto;
|
left: auto;
|
||||||
right: 0;
|
right: 0;
|
||||||
max-width: 700px;
|
max-width: 700px;
|
||||||
|
top: 3.5em;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,209 +19,237 @@
|
|||||||
<div class="details-body" v-if="showExtraInfo">
|
<div class="details-body" v-if="showExtraInfo">
|
||||||
<div class="g-separator"></div>
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
<EntryStops :timetable="timetable" />
|
<div v-if="timetableDetails">
|
||||||
|
<EntryStops :timetable="timetableDetails" />
|
||||||
|
|
||||||
<div class="g-separator"></div>
|
|
||||||
|
|
||||||
<div class="timetable-specs">
|
|
||||||
<span class="badge specs-badge" v-if="timetable.authorName">
|
|
||||||
<span>{{ $t('journal.dispatcher-name') }}</span>
|
|
||||||
<span>{{ timetable.authorName }}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge specs-badge" v-if="timetable.trainMaxSpeed">
|
|
||||||
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
|
||||||
<span> {{ timetable.trainMaxSpeed }}km/h </span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge specs-badge" v-if="timetable.maxSpeed">
|
|
||||||
<span>{{ $t('journal.stock-max-speed') }}</span>
|
|
||||||
<span>{{ timetable.maxSpeed }}km/h</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stock-dangers" v-if="timetable.warningNotes">
|
|
||||||
<div class="g-separator"></div>
|
<div class="g-separator"></div>
|
||||||
|
|
||||||
<b>{{ $t('journal.stock-dangers') }}:</b>
|
<div class="timetable-specs">
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.authorName">
|
||||||
<ul>
|
<span>{{ $t('journal.dispatcher-name') }}</span>
|
||||||
<li v-if="timetable.twr">
|
<span>{{ timetableDetails.authorName }}</span>
|
||||||
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li v-if="timetable.skr">
|
|
||||||
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li v-if="timetable.hasDangerousCargo">
|
|
||||||
<b class="text--primary">{{ $t('warnings.TN') }}</b>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li v-if="timetable.hasExtraDeliveries">
|
|
||||||
<b class="text--primary">{{ $t('warnings.PN') }}</b>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="dangers-notes" v-if="timetable.warningNotes">
|
|
||||||
<h4>{{ $t('warnings.header-title') }}</h4>
|
|
||||||
<p>
|
|
||||||
<i>{{ timetable.warningNotes }}</i>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Historia zmian w składzie -->
|
|
||||||
<div v-if="timetable.stockString || stockHistory.length != 0">
|
|
||||||
<div class="g-separator"></div>
|
|
||||||
|
|
||||||
<b>{{ $t('journal.stock-preview') }}:</b>
|
|
||||||
|
|
||||||
<div class="stock-specs" style="margin-top: 0.5em">
|
|
||||||
<span class="badge specs-badge" v-if="timetable.stockLength">
|
|
||||||
<span>{{ $t('journal.stock-length') }}</span>
|
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
currentHistoryIndex == 0
|
|
||||||
? timetable.stockLength
|
|
||||||
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
|
|
||||||
}}m
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="badge specs-badge" v-if="timetable.stockMass">
|
<span class="badge specs-badge" v-if="timetableDetails.trainMaxSpeed">
|
||||||
<span>{{ $t('journal.stock-mass') }}</span>
|
<span>{{ $t('journal.stock-timetable-speed') }}</span>
|
||||||
<span>
|
<span> {{ timetableDetails.trainMaxSpeed }}km/h </span>
|
||||||
{{
|
</span>
|
||||||
Math.floor(
|
|
||||||
(currentHistoryIndex == 0
|
<span class="badge specs-badge" v-if="timetableDetails.maxSpeed">
|
||||||
? timetable.stockMass
|
<span>{{ $t('journal.stock-max-speed') }}</span>
|
||||||
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
|
<span>{{ timetableDetails.maxSpeed }}km/h</span>
|
||||||
)
|
|
||||||
}}t
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stock-history">
|
<div class="stock-dangers" v-if="timetableDetails.warningNotes">
|
||||||
<button class="btn btn--action" @click="copyStockToClipboard()">
|
<div class="g-separator"></div>
|
||||||
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<b>{{ $t('journal.stock-dangers') }}:</b>
|
||||||
v-for="(sh, i) in stockHistory"
|
|
||||||
:key="i"
|
<ul>
|
||||||
class="btn--action"
|
<li v-if="timetableDetails.twr">
|
||||||
:data-checked="i == currentHistoryIndex"
|
<b class="text--primary">{{ $t('warnings.TWR') }} (TWR)</b>
|
||||||
@click.stop="currentHistoryIndex = i"
|
</li>
|
||||||
>
|
|
||||||
{{ sh.updatedAt }}
|
<li v-if="timetableDetails.skr">
|
||||||
</button>
|
<b class="text--primary">{{ $t('warnings.SKR') }}</b>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="timetableDetails.hasDangerousCargo">
|
||||||
|
<b class="text--primary">{{ $t('warnings.TN') }}</b>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li v-if="timetableDetails.hasExtraDeliveries">
|
||||||
|
<b class="text--primary">{{ $t('warnings.PN') }}</b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="dangers-notes" v-if="timetableDetails.warningNotes">
|
||||||
|
<h4>{{ $t('warnings.header-title') }}</h4>
|
||||||
|
<p>
|
||||||
|
<i>{{ timetableDetails.warningNotes }}</i>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="timetable.stockString" style="margin-top: 1em">
|
<!-- Historia zmian w składzie -->
|
||||||
<StockList
|
<div v-if="timetableDetails.stockString || stockHistory.length != 0">
|
||||||
:trainStockList="
|
<div class="g-separator"></div>
|
||||||
(currentHistoryIndex == 0
|
|
||||||
? timetable.stockString
|
<b>{{ $t('journal.stock-preview') }}:</b>
|
||||||
: stockHistory[currentHistoryIndex].stockString
|
|
||||||
).split(';')
|
<div class="stock-specs" style="margin-top: 0.5em">
|
||||||
"
|
<span class="badge specs-badge" v-if="timetableDetails.stockLength">
|
||||||
/>
|
<span>{{ $t('journal.stock-length') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockLength
|
||||||
|
: stockHistory[currentHistoryIndex].stockLength || timetableDetails.stockLength
|
||||||
|
}}m
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="badge specs-badge" v-if="timetableDetails.stockMass">
|
||||||
|
<span>{{ $t('journal.stock-mass') }}</span>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
Math.floor(
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockMass
|
||||||
|
: stockHistory[currentHistoryIndex].stockMass || timetableDetails.stockMass) /
|
||||||
|
1000
|
||||||
|
)
|
||||||
|
}}t
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stock-history">
|
||||||
|
<button class="btn btn--action" @click="copyStockToClipboard()">
|
||||||
|
<i class="fa-regular fa-copy"></i> {{ $t('journal.stock-copy') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="(sh, i) in stockHistory"
|
||||||
|
:key="i"
|
||||||
|
class="btn--action"
|
||||||
|
:data-checked="i == currentHistoryIndex"
|
||||||
|
@click.stop="currentHistoryIndex = i"
|
||||||
|
>
|
||||||
|
{{ sh.updatedAt }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="timetableDetails.stockString" style="margin-top: 1em">
|
||||||
|
<StockList
|
||||||
|
:trainStockList="
|
||||||
|
(currentHistoryIndex == 0
|
||||||
|
? timetableDetails.stockString
|
||||||
|
: stockHistory[currentHistoryIndex].stockString
|
||||||
|
).split(';')
|
||||||
|
"
|
||||||
|
:showPreviews="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { computed, PropType, ref } from 'vue';
|
||||||
import StockList from '../../Global/StockList.vue';
|
|
||||||
import { API } from '../../../typings/api';
|
|
||||||
import { RouteLocationRaw } from 'vue-router';
|
import { RouteLocationRaw } from 'vue-router';
|
||||||
import EntryStops from './EntryStops.vue';
|
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
export default defineComponent({
|
import StockList from '../../Global/StockList.vue';
|
||||||
components: { StockList, EntryStops },
|
import EntryStops from './EntryStops.vue';
|
||||||
|
import { API } from '../../../typings/api';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
|
||||||
emits: ['toggleExtraInfo'],
|
const i18n = useI18n();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
props: {
|
const props = defineProps({
|
||||||
showExtraInfo: {
|
showExtraInfo: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true
|
||||||
},
|
|
||||||
timetable: {
|
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
currentHistoryIndex: 0,
|
|
||||||
i18n: useI18n()
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
stockHistory() {
|
|
||||||
return this.timetable.stockHistory
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((h) => {
|
|
||||||
const historyData = h.split('@');
|
|
||||||
return {
|
|
||||||
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
}),
|
|
||||||
stockString: historyData[1],
|
|
||||||
stockMass: Number(historyData[2]) || undefined,
|
|
||||||
stockLength: Number(historyData[3]) || undefined
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
driverRouteLocation(): RouteLocationRaw | null {
|
timetableEntry: {
|
||||||
if (this.timetable.terminated) return null;
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
return {
|
required: true
|
||||||
name: 'DriverView',
|
|
||||||
query: {
|
|
||||||
trainId: `${this.timetable.driverId}|${this.timetable.trainNo}|eu`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onImageError(e: Event) {
|
|
||||||
const imageEl = e.target as HTMLImageElement;
|
|
||||||
imageEl.src = '/images/icon-unknown.png';
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleExtraInfo() {
|
|
||||||
this.$emit('toggleExtraInfo', this.timetable.id);
|
|
||||||
},
|
|
||||||
|
|
||||||
copyStockToClipboard() {
|
|
||||||
const currentStockString =
|
|
||||||
this.stockHistory[this.currentHistoryIndex]?.stockString ?? this.timetable.stockString;
|
|
||||||
|
|
||||||
if (!currentStockString) {
|
|
||||||
alert(this.i18n.t('journal.stock-clipboard-failure'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(currentStockString)
|
|
||||||
.then(() => {
|
|
||||||
prompt(this.i18n.t('journal.stock-clipboard-success'), currentStockString);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
alert(this.i18n.t('journal.stock-clipboard-failure'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(['toggleExtraInfo']);
|
||||||
|
const currentHistoryIndex = ref(0);
|
||||||
|
|
||||||
|
const timetableDetails = ref<API.TimetableHistory.Data | null>(null);
|
||||||
|
|
||||||
|
const stockHistory = computed(() => {
|
||||||
|
return (
|
||||||
|
timetableDetails.value?.stockHistory
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((h) => {
|
||||||
|
const historyData = h.split('@');
|
||||||
|
return {
|
||||||
|
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(i18n.locale.value, {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}),
|
||||||
|
stockString: historyData[1],
|
||||||
|
stockMass: Number(historyData[2]) || undefined,
|
||||||
|
stockLength: Number(historyData[3]) || undefined
|
||||||
|
};
|
||||||
|
}) ?? []
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const driverRouteLocation = computed<RouteLocationRaw | null>(() => {
|
||||||
|
if (props.timetableEntry.terminated) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'DriverView',
|
||||||
|
query: {
|
||||||
|
trainId: `${props.timetableEntry.driverId}|${props.timetableEntry.trainNo}|eu`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchTimetableDetails() {
|
||||||
|
try {
|
||||||
|
const responseData = await apiStore.client.get<API.TimetableHistory.Response>(
|
||||||
|
'api/getTimetables',
|
||||||
|
{
|
||||||
|
timetableId: props.timetableEntry.id,
|
||||||
|
returnType: 'detailed'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!responseData || responseData.length != 1) {
|
||||||
|
timetableDetails.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timetableDetails.value = responseData[0];
|
||||||
|
} catch (error) {
|
||||||
|
// this.dataStatus = Status.Data.Error;
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleExtraInfo() {
|
||||||
|
if (props.showExtraInfo == false) {
|
||||||
|
await fetchTimetableDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
emits('toggleExtraInfo', timetableDetails.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyStockToClipboard() {
|
||||||
|
if (!timetableDetails.value) return;
|
||||||
|
|
||||||
|
const currentStockString =
|
||||||
|
stockHistory.value[currentHistoryIndex.value]?.stockString ??
|
||||||
|
timetableDetails.value.stockString;
|
||||||
|
|
||||||
|
if (!currentStockString) {
|
||||||
|
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(currentStockString)
|
||||||
|
.then(() => {
|
||||||
|
prompt(i18n.t('journal.stock-clipboard-success'), currentStockString);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
alert(i18n.t('journal.stock-clipboard-failure'));
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -299,7 +327,7 @@ hr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.timetable-specs {
|
.timetable-specs {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,10 @@
|
|||||||
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
<router-link v-else :to="`/journal/timetables?search-driver=${timetable.driverName}`">
|
||||||
<strong>{{ timetable.driverName }}</strong>
|
<strong>{{ timetable.driverName }}</strong>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<div v-if="timetable.driverLanguageId != null">
|
||||||
|
<FlagIcon :language-id="timetable.driverLanguageId" width="1.75em" />
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="general-time">
|
<span class="general-time">
|
||||||
@@ -83,7 +87,7 @@
|
|||||||
</b>
|
</b>
|
||||||
|
|
||||||
<b
|
<b
|
||||||
class="info-badge"
|
class="timetable-status-badge"
|
||||||
:class="{
|
:class="{
|
||||||
fulfilled: timetable.fulfilled,
|
fulfilled: timetable.fulfilled,
|
||||||
terminated: timetable.terminated && !timetable.fulfilled,
|
terminated: timetable.terminated && !timetable.fulfilled,
|
||||||
@@ -110,8 +114,10 @@ import dateMixin from '../../../mixins/dateMixin';
|
|||||||
import styleMixin from '../../../mixins/styleMixin';
|
import styleMixin from '../../../mixins/styleMixin';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: { FlagIcon },
|
||||||
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
mixins: [dateMixin, styleMixin, trainCategoryMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
@@ -122,7 +128,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,23 +171,6 @@ export default defineComponent({
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-badge {
|
|
||||||
padding: 0.05em 0.35em;
|
|
||||||
color: black;
|
|
||||||
|
|
||||||
&.terminated {
|
|
||||||
background-color: salmon;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fulfilled {
|
|
||||||
background-color: lightgreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color: lightblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-timetable {
|
.btn-timetable {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.2em 0.5em;
|
padding: 0.2em 0.5em;
|
||||||
@@ -191,7 +180,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.item-general {
|
.item-general {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default defineComponent({
|
|||||||
components: { ProgressBar },
|
components: { ProgressBar },
|
||||||
props: {
|
props: {
|
||||||
timetable: {
|
timetable: {
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="journal-stats driver" v-if="store.driverStatsData">
|
|
||||||
<span>
|
|
||||||
<h3>
|
|
||||||
<i18n-t keypath="journal.driver-stats.title">
|
|
||||||
<template #name>
|
|
||||||
<span class="text--primary">{{ store.driverStatsName.toUpperCase() }}</span>
|
|
||||||
</template>
|
|
||||||
</i18n-t>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<hr class="header-separator" />
|
|
||||||
|
|
||||||
<div class="info-stats">
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.longest-timetable') }}</span>
|
|
||||||
<span> {{ store.driverStatsData._max.routeDistance.toFixed(2) }}km </span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.avg-timetable') }}</span>
|
|
||||||
<span> {{ store.driverStatsData._avg.routeDistance.toFixed(2) }}km </span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="section-separator" />
|
|
||||||
|
|
||||||
<div class="info-stats">
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.timetables') }}</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._count.fulfilled }} /
|
|
||||||
{{ store.driverStatsData._count._all }}
|
|
||||||
|
|
||||||
<template v-if="store.driverStatsData._count._all > 0">
|
|
||||||
({{
|
|
||||||
(
|
|
||||||
(store.driverStatsData._count.fulfilled / store.driverStatsData._count._all) *
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}%)
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.distance') }}</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._sum.currentDistance.toFixed(2) }} /
|
|
||||||
{{ store.driverStatsData._sum.routeDistance.toFixed(2) }}km
|
|
||||||
|
|
||||||
<template v-if="store.driverStatsData._sum.routeDistance > 0">
|
|
||||||
({{
|
|
||||||
(
|
|
||||||
(store.driverStatsData._sum.currentDistance /
|
|
||||||
store.driverStatsData._sum.routeDistance) *
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}%)
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="badge stat-badge">
|
|
||||||
<span>{{ $t('journal.driver-stats.stations') }}</span>
|
|
||||||
<span>
|
|
||||||
{{ store.driverStatsData._sum.confirmedStopsCount }} /
|
|
||||||
{{ store.driverStatsData._sum.allStopsCount }}
|
|
||||||
|
|
||||||
<template v-if="store.driverStatsData._sum.allStopsCount > 0">
|
|
||||||
({{
|
|
||||||
(
|
|
||||||
(store.driverStatsData._sum.confirmedStopsCount /
|
|
||||||
store.driverStatsData._sum.allStopsCount) *
|
|
||||||
100
|
|
||||||
).toFixed(2)
|
|
||||||
}}%)
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent } from 'vue';
|
|
||||||
import { useMainStore } from '../../../store/mainStore';
|
|
||||||
import { Status } from '../../../typings/common';
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'journal-driver-stats',
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
store: useMainStore(),
|
|
||||||
Status: Status
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@use '../../../styles/journal-stats';
|
|
||||||
</style>
|
|
||||||
@@ -10,14 +10,14 @@
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div @click="toggleExtraInfo" style="cursor: pointer">
|
<div style="cursor: pointer">
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<EntryStatus :timetable="timetableEntry" />
|
<EntryStatus :timetable="timetableEntry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Extra -->
|
<!-- Extra -->
|
||||||
<EntryDetails
|
<EntryDetails
|
||||||
:timetable="timetableEntry"
|
:timetableEntry="timetableEntry"
|
||||||
:show-extra-info="showExtraInfo"
|
:show-extra-info="showExtraInfo"
|
||||||
@toggle-extra-info="toggleExtraInfo"
|
@toggle-extra-info="toggleExtraInfo"
|
||||||
/>
|
/>
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { API } from '../../../typings/api';
|
import { API } from '../../../typings/api';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
import { Journal } from '../typings';
|
|
||||||
|
|
||||||
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
|
||||||
import dateMixin from '../../../mixins/dateMixin';
|
import dateMixin from '../../../mixins/dateMixin';
|
||||||
@@ -41,7 +40,7 @@ import EntryDetails from './EntryDetails.vue';
|
|||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
timetableEntry: {
|
timetableEntry: {
|
||||||
type: Object as PropType<API.TimetableHistory.Data>,
|
type: Object as PropType<API.TimetableHistory.DataShort>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
showExtraInfo: {
|
showExtraInfo: {
|
||||||
@@ -60,74 +59,9 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
timetablePathDetails() {
|
|
||||||
if (!this.timetableEntry.path || this.timetableEntry.path == '') return null;
|
|
||||||
|
|
||||||
return this.timetableEntry.path.split(';').map((pathEl, i) => {
|
|
||||||
const [arrival, name, departure] = pathEl.split(',');
|
|
||||||
const sceneryName = name.split(' ').slice(0, -1).join(' ');
|
|
||||||
const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
arrival,
|
|
||||||
sceneryName,
|
|
||||||
sceneryHash,
|
|
||||||
departure,
|
|
||||||
isVisited: this.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
timetableStops(): Journal.TimetableStopDetails[] {
|
|
||||||
const timetableEntry = this.timetableEntry;
|
|
||||||
|
|
||||||
const stopNames = timetableEntry.sceneriesString.split('%');
|
|
||||||
|
|
||||||
return stopNames.reduce<Journal.TimetableStopDetails[]>((acc, stopName, i, arr) => {
|
|
||||||
const arrivalDate =
|
|
||||||
i == arr.length - 1
|
|
||||||
? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate)
|
|
||||||
: timetableEntry.checkpointArrivals.at(i);
|
|
||||||
|
|
||||||
const scheduledArrivalDate =
|
|
||||||
i == arr.length - 1
|
|
||||||
? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate)
|
|
||||||
: timetableEntry.checkpointArrivalsScheduled.at(i);
|
|
||||||
|
|
||||||
const departureDate =
|
|
||||||
i == 0
|
|
||||||
? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate)
|
|
||||||
: timetableEntry.checkpointDepartures.at(i);
|
|
||||||
|
|
||||||
const scheduledDepartureDate =
|
|
||||||
i == 0
|
|
||||||
? (timetableEntry.checkpointDeparturesScheduled.at(i) ??
|
|
||||||
timetableEntry.scheduledBeginDate)
|
|
||||||
: timetableEntry.checkpointDeparturesScheduled.at(i);
|
|
||||||
|
|
||||||
const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0;
|
|
||||||
const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || '';
|
|
||||||
|
|
||||||
acc.push({
|
|
||||||
stopName,
|
|
||||||
arrivalTimestamp: this.dateStringToTimestamp(arrivalDate),
|
|
||||||
scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate),
|
|
||||||
departureTimestamp: this.dateStringToTimestamp(departureDate),
|
|
||||||
scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate),
|
|
||||||
stopTime,
|
|
||||||
stopType,
|
|
||||||
isConfirmed: i < timetableEntry.confirmedStopsCount
|
|
||||||
});
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleExtraInfo() {
|
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||||
this.$emit('toggleShowExtraInfo');
|
this.$emit('toggleShowExtraInfo', data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -145,7 +79,7 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.entry-route {
|
.entry-route {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
v-for="(timetableEntry, i) in timetableHistory"
|
v-for="(timetableEntry, i) in timetableHistory"
|
||||||
:key="timetableEntry.id"
|
:key="timetableEntry.id"
|
||||||
:timetableEntry="timetableEntry"
|
:timetableEntry="timetableEntry"
|
||||||
:onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)"
|
:onToggleShowExtraInfo="toggleExtraInfo"
|
||||||
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
:showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
@@ -59,9 +59,11 @@ export default defineComponent({
|
|||||||
JournalTimetableEntry
|
JournalTimetableEntry
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emits: ['toggleExtraInfo'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
timetableHistory: {
|
timetableHistory: {
|
||||||
type: Array as PropType<API.TimetableHistory.Response>,
|
type: Array as PropType<API.TimetableHistory.ResponseShort>,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
scrollNoMoreData: {
|
scrollNoMoreData: {
|
||||||
@@ -75,32 +77,23 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
dataStatus: {
|
dataStatus: {
|
||||||
type: Number as PropType<Status.Data>
|
type: Number as PropType<Status.Data>
|
||||||
|
},
|
||||||
|
extraInfoIndexes: {
|
||||||
|
type: Object as PropType<number[]>,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
Status,
|
Status,
|
||||||
store: useMainStore(),
|
store: useMainStore()
|
||||||
extraInfoIndexes: [] as number[]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
'$route.query': {
|
|
||||||
deep: true,
|
|
||||||
handler() {
|
|
||||||
this.extraInfoIndexes.length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
toggleExtraInfo(id: number) {
|
toggleExtraInfo(data: API.TimetableHistory.Data | null) {
|
||||||
const existingIdx = this.extraInfoIndexes.indexOf(id);
|
this.$emit('toggleExtraInfo', data);
|
||||||
|
|
||||||
if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1);
|
|
||||||
else this.extraInfoIndexes.push(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -111,7 +104,7 @@ export default defineComponent({
|
|||||||
@use '../../../styles/journal-section';
|
@use '../../../styles/journal-section';
|
||||||
@use '../../../styles/responsive';
|
@use '../../../styles/responsive';
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.journal_item-info {
|
.journal_item-info {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export namespace Journal {
|
export namespace Journal {
|
||||||
export type DispatcherSearchKey =
|
export type DispatcherSearchKey =
|
||||||
|
| 'search-duty-id'
|
||||||
| 'search-dispatcher'
|
| 'search-dispatcher'
|
||||||
| 'search-station'
|
| 'search-station'
|
||||||
| 'search-date-from'
|
| 'search-date-from'
|
||||||
@@ -10,10 +11,12 @@ export namespace Journal {
|
|||||||
| 'search-train'
|
| 'search-train'
|
||||||
| 'search-date-from'
|
| 'search-date-from'
|
||||||
| 'search-dispatcher'
|
| 'search-dispatcher'
|
||||||
|
| 'search-includesScenery'
|
||||||
| 'search-issuedFrom'
|
| 'search-issuedFrom'
|
||||||
| 'search-terminatingAt'
|
| 'search-terminatingAt'
|
||||||
| 'search-via'
|
| 'search-via'
|
||||||
| 'select-categoryCode';
|
| 'select-categoryCode'
|
||||||
|
| 'search-headUnit';
|
||||||
|
|
||||||
export type TimetableSearchType = {
|
export type TimetableSearchType = {
|
||||||
[key in TimetableSearchKey]: string;
|
[key in TimetableSearchKey]: string;
|
||||||
@@ -61,19 +64,6 @@ export namespace Journal {
|
|||||||
default: boolean;
|
default: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StatsTab {
|
|
||||||
DRIVER_STATS = 'journal-driver-stats',
|
|
||||||
DISPATCHER_STATS = 'journal-dispatcher-stats',
|
|
||||||
DAILY_STATS = 'journal-daily-stats'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface StatsButton {
|
|
||||||
tab: StatsTab;
|
|
||||||
localeKey: string;
|
|
||||||
iconName: string;
|
|
||||||
disabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TimetableStopDetails {
|
export interface TimetableStopDetails {
|
||||||
stopName: string;
|
stopName: string;
|
||||||
arrivalTimestamp: number;
|
arrivalTimestamp: number;
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-history-list">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="history-menu">
|
||||||
|
<button
|
||||||
|
v-for="(filterState, filterKey) in activeFilterTypes"
|
||||||
|
class="menu-btn btn--option"
|
||||||
|
:data-active="filterState"
|
||||||
|
@click="toggleFilter(filterKey)"
|
||||||
|
>
|
||||||
|
{{ t(`profile.filters.${filterKey}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-list-box">
|
||||||
|
<Loading v-if="journalStatus == Status.Data.Loading" />
|
||||||
|
|
||||||
|
<div v-else-if="combinedJournal.length == 0" class="no-recent-history">
|
||||||
|
{{ t('profile.list.no-recent-history') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-else
|
||||||
|
v-for="entry in combinedJournal"
|
||||||
|
:to="
|
||||||
|
'trainNo' in entry.value
|
||||||
|
? `/journal/timetables?search-train=%23${entry.value.id}`
|
||||||
|
: `/journal/dispatchers?search-duty-id=${entry.value.id}`
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<!-- Date -->
|
||||||
|
<div class="entry-top-date">
|
||||||
|
<img
|
||||||
|
v-if="entry.type == 'Dispatcher'"
|
||||||
|
src="/images/icon-user.svg"
|
||||||
|
width="25"
|
||||||
|
alt="user icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-else-if="entry.type == 'Timetable'"
|
||||||
|
src="/images/icon-train.svg"
|
||||||
|
width="25"
|
||||||
|
alt="train icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img v-else src="/images/icon-timetable.svg" width="25" alt="timetable icon" />
|
||||||
|
|
||||||
|
<b
|
||||||
|
class="timestamp-indicator"
|
||||||
|
:data-online="
|
||||||
|
'isOnline' in entry.value
|
||||||
|
? entry.value.isOnline
|
||||||
|
: !entry.value.terminated && entry.type != 'IssuedTimetable'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ dateToLocaleString(entry.date, { dateStyle: 'long', timeStyle: 'short' }) }}
|
||||||
|
<span v-if="'timestampTo' in entry.value && entry.value.timestampTo">
|
||||||
|
-
|
||||||
|
<span v-if="new Date(entry.value.timestampTo).getDay() == entry.date.getDay()">{{
|
||||||
|
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||||
|
timeStyle: 'short'
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
<span v-else>{{
|
||||||
|
dateToLocaleString(new Date(entry.value.timestampTo), {
|
||||||
|
dateStyle: 'long',
|
||||||
|
timeStyle: 'short'
|
||||||
|
})
|
||||||
|
}}</span>
|
||||||
|
</span>
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timetables -->
|
||||||
|
<div v-if="'trainNo' in entry.value">
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ entry.value.trainCategoryCode }}
|
||||||
|
</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b>{{ entry.value.trainNo }}</b>
|
||||||
|
<b class="text--grayed" v-if="entry.type == 'IssuedTimetable'">
|
||||||
|
{{ ' ' }} {{ t('profile.list.for') }}: {{ entry.value.driverName }}
|
||||||
|
</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b>{{ entry.value.route.replace('|', ' > ') }}</b>
|
||||||
|
{{ ' ' }}
|
||||||
|
<b class="text--primary">{{ entry.value.currentDistance }} km</b>
|
||||||
|
<b> / {{ entry.value.routeDistance }} km</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dispatchers -->
|
||||||
|
<div v-else>
|
||||||
|
<b class="text--primary">{{ entry.value.stationName }}</b>
|
||||||
|
{{ ' - ' }}
|
||||||
|
<b class="timestamp-indicator" :data-online="entry.value.isOnline">
|
||||||
|
<span v-if="entry.value.isOnline">{{ t('profile.list.online-since') }}: </span>
|
||||||
|
<span>{{
|
||||||
|
humanizeDuration((entry.value.timestampTo || Date.now()) - entry.value.timestampFrom)
|
||||||
|
}}</span>
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
computed,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
PropType,
|
||||||
|
reactive,
|
||||||
|
ref
|
||||||
|
} from 'vue';
|
||||||
|
import { dateToLocaleString, humanizeDuration } from '../../composables/time';
|
||||||
|
import { API } from '../../typings/api';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
|
||||||
|
type JournalEntryType = 'Timetable' | 'Dispatcher' | 'IssuedTimetable';
|
||||||
|
|
||||||
|
interface JournalEntry {
|
||||||
|
type: JournalEntryType;
|
||||||
|
date: Date;
|
||||||
|
value: API.TimetableHistory.DataShort | API.DispatcherHistory.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
playerName: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
|
||||||
|
playerJournal: {
|
||||||
|
type: Object as PropType<API.PlayerJournal.Data>,
|
||||||
|
},
|
||||||
|
|
||||||
|
journalStatus: {
|
||||||
|
type: Number as PropType<Status.Data>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const activeFilterTypes = reactive<Record<JournalEntryType, boolean>>({
|
||||||
|
Timetable: true,
|
||||||
|
Dispatcher: true,
|
||||||
|
IssuedTimetable: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const combinedJournal = computed<JournalEntry[]>(() => {
|
||||||
|
if (!props.playerJournal || !props.playerName) return [];
|
||||||
|
|
||||||
|
const list = [
|
||||||
|
...props.playerJournal.timetables,
|
||||||
|
...props.playerJournal.duties,
|
||||||
|
...props.playerJournal.issuedTimetables
|
||||||
|
]
|
||||||
|
.reduce<JournalEntry[]>((acc, v) => {
|
||||||
|
// Timetable or dispatcher type
|
||||||
|
if ('trainNo' in v) {
|
||||||
|
const isIssued = v.authorName == props.playerName;
|
||||||
|
|
||||||
|
if (!isIssued && !activeFilterTypes['Timetable']) return acc;
|
||||||
|
if (isIssued && !activeFilterTypes['IssuedTimetable']) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
date: new Date(v.createdAt),
|
||||||
|
type: isIssued ? 'IssuedTimetable' : 'Timetable',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!activeFilterTypes['Dispatcher']) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
date: new Date(v.timestampFrom),
|
||||||
|
type: 'Dispatcher',
|
||||||
|
value: v
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [])
|
||||||
|
.sort((a, b) => {
|
||||||
|
return a.date.getTime() - b.date.getTime() > 0 ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return list;
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleFilter(filterType: JournalEntryType) {
|
||||||
|
const toggledState = !activeFilterTypes[filterType];
|
||||||
|
|
||||||
|
// Prevent switching off all filters at the same time (at least one must be active)
|
||||||
|
if (
|
||||||
|
toggledState === false &&
|
||||||
|
Object.values(activeFilterTypes).filter((v) => v === false).length ==
|
||||||
|
Object.values(activeFilterTypes).length - 1
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
activeFilterTypes[filterType] = toggledState;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-history-list {
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
& > h3 {
|
||||||
|
padding: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-menu {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1em;
|
||||||
|
background-color: var(--clr-tile);
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-btn {
|
||||||
|
padding: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #aaa;
|
||||||
|
|
||||||
|
&[data-active='true'] {
|
||||||
|
color: var(--clr-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list-box {
|
||||||
|
padding: 0 0.5em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list-box > a {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
background-color: var(--clr-bg-light);
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
text-align: initial;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-recent-history {
|
||||||
|
padding: 1em;
|
||||||
|
font-size: 1.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-top-date {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp-indicator {
|
||||||
|
color: #ccc;
|
||||||
|
|
||||||
|
&[data-online='true'] {
|
||||||
|
color: var(--clr-success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.midScreen {
|
||||||
|
.profile-history-list {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<div class="player-avatar">
|
||||||
|
<img
|
||||||
|
v-if="props.playerTD2Info && props.playerTD2Info.avatar"
|
||||||
|
:src="`https://td2.info.pl/index.php?action=dlattach;attach=${props.playerTD2Info.avatar};type=avatar`"
|
||||||
|
class="player-avatar-image"
|
||||||
|
ref="avatarImageRef"
|
||||||
|
alt="player image"
|
||||||
|
@load="onAvatarLoadSuccess"
|
||||||
|
@error="onAvatarLoadError"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="
|
||||||
|
avatarLoadingStatus == Status.Data.Error ||
|
||||||
|
(props.playerTD2Info && !props.playerTD2Info.avatar)
|
||||||
|
"
|
||||||
|
class="img-placeholder"
|
||||||
|
height="100"
|
||||||
|
src="/images/default-avatar.jpg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Loading v-else-if="avatarLoadingStatus == Status.Data.Loading" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType, ref, useTemplateRef } from 'vue';
|
||||||
|
import { Status } from '../../typings/common';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
import { Td2API } from '../../typings/api';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
playerTD2Info: {
|
||||||
|
type: Object as PropType<Td2API.UsersInfoByName.UserInfo>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatarImageRef = useTemplateRef('avatarImageRef');
|
||||||
|
const avatarLoadingStatus = ref<Status.Data>(Status.Data.Loading);
|
||||||
|
|
||||||
|
function onAvatarLoadSuccess() {
|
||||||
|
if (!avatarImageRef.value) return;
|
||||||
|
|
||||||
|
avatarLoadingStatus.value = Status.Data.Loaded;
|
||||||
|
avatarImageRef.value.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAvatarLoadError() {
|
||||||
|
if (!avatarImageRef.value) return;
|
||||||
|
|
||||||
|
avatarLoadingStatus.value = Status.Data.Error;
|
||||||
|
avatarImageRef.value.src = '/images/default-avatar.jpg';
|
||||||
|
avatarImageRef.value.style.opacity = '1';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.player-avatar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
min-height: 110px;
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
top: 50%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.player-avatar-image {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-recent-stats">
|
||||||
|
<h2 class="stats-header">
|
||||||
|
<img src="/images/icon-stats.svg" width="30" alt="stats icon" />
|
||||||
|
{{ t('profile.recent-stats.header') }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="month-stats-box">
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-train.svg" width="30" alt="train icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">{{ playerInfo.driverStatsLastMonth.countAll }}</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.timetables') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-spawn.svg" width="30" alt="spawn icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.driverStatsLastMonth.currentDistanceTotal?.toFixed(2) || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.distance') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-user.svg" width="30" alt="user icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStatsLastMonth.services?.count || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.duties') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="month-stat">
|
||||||
|
<div><img src="/images/icon-timetable.svg" width="30" alt="timetable icon" /></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStatsLastMonth.issuedTimetables?.count || 0 }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div>{{ t('profile.recent-stats.created-timetables') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { PropType } from 'vue';
|
||||||
|
import { API } from '../../typings/api';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
playerInfo: {
|
||||||
|
type: Object as PropType<API.PlayerInfo.Data>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-recent-stats {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-stats-box {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-stat {
|
||||||
|
background-color: var(--clr-bg-light);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 0.5em;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:nth-child(3) {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.month-stats-box {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
<template>
|
||||||
|
<section class="profile-summary">
|
||||||
|
<div class="player-info">
|
||||||
|
<div class="info-main">
|
||||||
|
<ProfilePlayerAvatar :playerTD2Info="playerTD2Info" />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }">
|
||||||
|
<a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank">
|
||||||
|
<img
|
||||||
|
v-if="isPlayerDonator"
|
||||||
|
src="/images/icon-diamond.svg"
|
||||||
|
width="25"
|
||||||
|
alt="diamond icon"
|
||||||
|
/>
|
||||||
|
{{ playerName }}
|
||||||
|
</a>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="player-badges">
|
||||||
|
<div class="badge-container" v-if="playerInfo.driverStats.driverLevel != null">
|
||||||
|
<span
|
||||||
|
class="level-badge driver"
|
||||||
|
:style="calculateExpStyles(playerInfo.driverStats.driverLevel)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
playerInfo.driverStats.driverLevel > 1 ? playerInfo.driverStats.driverLevel : 'L'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
{{ t('profile.stats.driver') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="badge-container" v-if="playerInfo.dispatcherStats.dispatcherLevel != null">
|
||||||
|
<span
|
||||||
|
class="level-badge dispatcher"
|
||||||
|
:style="calculateExpStyles(playerInfo.dispatcherStats.dispatcherLevel)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
playerInfo.dispatcherStats.dispatcherLevel > 1
|
||||||
|
? playerInfo.dispatcherStats.dispatcherLevel
|
||||||
|
: 'L'
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
{{ t('profile.stats.dispatcher') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-journal-links">
|
||||||
|
<router-link
|
||||||
|
class="a-button btn--action"
|
||||||
|
:to="`/journal/timetables?search-driver=${playerInfo.driverStats.driverName}`"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.timetables-journal') }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
class="a-button btn--action"
|
||||||
|
:to="`/journal/dispatchers?search-dispatcher=${playerInfo.dispatcherStats.dispatcherName}`"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.dispatchers-journal') }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="a-button btn--action"
|
||||||
|
:href="`https://td2.info.pl/profile/?u=${route.query.playerId}`"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ t('profile.stats.forum-profile') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current activities -->
|
||||||
|
<div
|
||||||
|
class="player-activities-box"
|
||||||
|
v-if="activeDispatches.length > 0 || activeTrains.length > 0"
|
||||||
|
>
|
||||||
|
<div class="info-activity" v-if="activeDispatches.length > 0">
|
||||||
|
<router-link
|
||||||
|
v-for="d in activeDispatches"
|
||||||
|
class="dispatcher-badge"
|
||||||
|
:to="`/scenery?station=${d.stationName}®ion=${d.region}`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-user.svg" width="25" alt="user icon" />
|
||||||
|
<b>{{ d.stationName }} ({{ getRegionNameById(d.region) }})</b>
|
||||||
|
<StationStatusBadge :isOnline="true" :dispatcherStatus="d.dispatcherStatus" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-activity" v-if="activeTrains.length > 0">
|
||||||
|
<router-link
|
||||||
|
v-for="t in activeTrains"
|
||||||
|
:to="`/driver?trainId=${t.id}`"
|
||||||
|
class="driver-badge"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-train.svg" width="25" alt="train icon" />
|
||||||
|
<span v-if="t.timetable" class="text--primary">{{ t.timetable.category }}</span>
|
||||||
|
<span>{{ t.trainNo }}</span>
|
||||||
|
•
|
||||||
|
<span>{{ t.currentStationName }} ({{ getRegionNameById(t.region) }})</span>
|
||||||
|
•
|
||||||
|
<span class="text--grayed">{{ t.stockString.split(';')[0] }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="player-stats">
|
||||||
|
<div class="stats-driver">
|
||||||
|
<h3 class="stats-header">
|
||||||
|
<img src="/images/icon-train.svg" width="30" alt="train icon" />
|
||||||
|
{{ t('profile.stats.header-driver') }}
|
||||||
|
</h3>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div v-if="playerInfo.driverStats.countAll > 0">
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.countFulfilled }} /
|
||||||
|
{{ playerInfo.driverStats.countAll }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.countFulfilled,
|
||||||
|
playerInfo.driverStats.countAll,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.fulfilled-timetables') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.currentDistanceTotal?.toFixed(2) }} /
|
||||||
|
{{ playerInfo.driverStats.routeDistanceTotal?.toFixed(2) }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.currentDistanceTotal || 0,
|
||||||
|
playerInfo.driverStats.routeDistanceTotal || 0,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.route-distance') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.confirmedStopsTotal }} /
|
||||||
|
{{ playerInfo.driverStats.allStopsTotal }} ({{
|
||||||
|
getCountPercentage(
|
||||||
|
playerInfo.driverStats.confirmedStopsTotal || 0,
|
||||||
|
playerInfo.driverStats.allStopsTotal || 0,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
}}%)
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.confirmed-stops') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.driverStats.routeDistanceMax || 0 }}km</b> -
|
||||||
|
{{ t('profile.stats.longest-timetable') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.driverStats.routeDistanceAvg?.toFixed(2) || 0 }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.avg-timetable-length') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text--grayed" v-else>
|
||||||
|
{{ t('profile.stats.no-timetable-stats') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="stats-dispatcher"
|
||||||
|
v-if="playerInfo.dispatcherStats && playerInfo.dispatcherStats.services?.count"
|
||||||
|
>
|
||||||
|
<h3 class="stats-header">
|
||||||
|
<img src="/images/icon-user.svg" width="30" alt="user icon" />
|
||||||
|
{{ t('profile.stats.header-dispatcher') }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.dispatcherStats.services.count }}</b> -
|
||||||
|
{{ t('profile.stats.duties-count') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{
|
||||||
|
humanizeDuration(playerInfo.dispatcherStats.services.durationMax)
|
||||||
|
}}</b>
|
||||||
|
- {{ t('profile.stats.longest-duty') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="playerInfo.dispatcherStats.issuedTimetables">
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">{{ playerInfo.dispatcherStats.issuedTimetables.count }}</b>
|
||||||
|
- {{ t('profile.stats.created-timetables-count') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStats.issuedTimetables.distanceMax }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.longest-created-timetable') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ playerInfo.dispatcherStats.issuedTimetables.distanceSum.toFixed(2) }}km
|
||||||
|
</b>
|
||||||
|
- {{ t('profile.stats.created-timetables-length-sum') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text--grayed" v-else>
|
||||||
|
{{ t('profile.stats.no-dispatcher-stats') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onActivated, onMounted, PropType, ref, watch } from 'vue';
|
||||||
|
import { API, Td2API } from '../../typings/api';
|
||||||
|
import { calculateExpStyles } from '../../composables/badge';
|
||||||
|
import { getCountPercentage } from '../../utils/calcUtils';
|
||||||
|
import { humanizeDuration } from '../../composables/time';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||||
|
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
|
||||||
|
import { getRegionNameById } from '../../utils/regionUtils';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
playerInfo: {
|
||||||
|
type: Object as PropType<API.PlayerInfo.Data>,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
|
||||||
|
playerTD2Info: {
|
||||||
|
type: Object as PropType<Td2API.UsersInfoByName.UserInfo>
|
||||||
|
},
|
||||||
|
|
||||||
|
playerName: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPlayerDonator = computed(() =>
|
||||||
|
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
|
||||||
|
);
|
||||||
|
|
||||||
|
const activeDispatches = computed(() => {
|
||||||
|
if (!props.playerName) return [];
|
||||||
|
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
|
||||||
|
|
||||||
|
return apiStore.activeData.activeSceneries.filter(
|
||||||
|
(sc) =>
|
||||||
|
sc.dispatcherName == props.playerName && (sc.lastSeen >= Date.now() - 60000 || sc.isOnline)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeTrains = computed(() => {
|
||||||
|
if (!props.playerName) return [];
|
||||||
|
if (!apiStore.activeData || !apiStore.activeData.trains) return [];
|
||||||
|
|
||||||
|
return apiStore.activeData.trains.filter(
|
||||||
|
(t) => t.driverName == props.playerName && (t.lastSeen >= Date.now() - 60000 || t.online)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/badge';
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.profile-summary {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-badges {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
& > .level-badge {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-journal-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-activity {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1em;
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
.dispatcher-badge {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.driver-badge {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
gap: 0.25em;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-info,
|
||||||
|
.player-stats > div {
|
||||||
|
background-color: var(--clr-tile);
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.midScreen {
|
||||||
|
.player-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.player-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -96,6 +96,7 @@ export default defineComponent({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as API.DispatcherHistory.Response,
|
historyList: [] as API.DispatcherHistory.Response,
|
||||||
|
lastStationName: '',
|
||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
DataStatus: Status.Data,
|
DataStatus: Status.Data,
|
||||||
apiStore: useApiStore()
|
apiStore: useApiStore()
|
||||||
@@ -103,10 +104,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async activated() {
|
async activated() {
|
||||||
// if (this.historyList.length == 0) {
|
this.historyList.length = 0;
|
||||||
|
|
||||||
const fetchedHistory = await this.fetchAPIData();
|
const fetchedHistory = await this.fetchAPIData();
|
||||||
if (fetchedHistory) this.historyList = fetchedHistory;
|
if (fetchedHistory) this.historyList = fetchedHistory;
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -126,9 +127,8 @@ export default defineComponent({
|
|||||||
this.station?.name || this.onlineScenery?.name
|
this.station?.name || this.onlineScenery?.name
|
||||||
}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
}&countFrom=${countFrom}&countLimit=${countLimit}`;
|
||||||
|
|
||||||
const historyAPIData: API.DispatcherHistory.Response = await (
|
const historyAPIData: API.DispatcherHistory.Response =
|
||||||
await this.apiStore.client!.get(requestString)
|
await this.apiStore.client.get(requestString);
|
||||||
).data;
|
|
||||||
|
|
||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
return historyAPIData;
|
return historyAPIData;
|
||||||
@@ -150,6 +150,7 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
@use '../../styles/scenery-history-table';
|
@use '../../styles/scenery-history-table';
|
||||||
|
@use '../../styles/badge';
|
||||||
|
|
||||||
.scenery-dispatchers-history {
|
.scenery-dispatchers-history {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -194,7 +195,7 @@ export default defineComponent({
|
|||||||
color: springgreen;
|
color: springgreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.journal-list > div {
|
.journal-list > div {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-header">
|
<section class="info-header">
|
||||||
<button
|
<button class="btn btn-return" :title="$t('scenery.return-btn')" @click="onReturnButtonClick">
|
||||||
class="btn btn-return"
|
|
||||||
:title="$t('scenery.return-btn')"
|
|
||||||
@click="onReturnButtonClick"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-back.svg" alt="return button" />
|
<img src="/images/icon-back.svg" alt="return button" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<a class="scenery-name" :href="station?.generalInfo?.url" target="_blank">
|
<div class="scenery-name">
|
||||||
{{ stationName.replace(/_/g, ' ') }}
|
<a v-if="station?.generalInfo" :href="station.generalInfo.url" target="_blank">
|
||||||
</a>
|
{{ stationName.replace(/_/g, ' ') }}
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="scenery-abbrev" v-if="station?.generalInfo?.abbr">
|
<span v-else> {{ stationName.replace(/_/g, ' ') }}</span>
|
||||||
{{ $t('scenery.abbrev') }} <b>{{ station.generalInfo.abbr }}</b>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
|
<div class="scenery-hash" v-if="onlineScenery?.hash">#{{ onlineScenery.hash }}</div>
|
||||||
@@ -61,15 +57,14 @@ function onReturnButtonClick() {
|
|||||||
.btn-return {
|
.btn-return {
|
||||||
$bgColor: #2b2b2b;
|
$bgColor: #2b2b2b;
|
||||||
background-color: $bgColor;
|
background-color: $bgColor;
|
||||||
margin-bottom: 0.5em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: color.adjust($color: $bgColor, $lightness: 15%);
|
background-color: color.adjust($color: $bgColor, $lightness: 15%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-name {
|
.scenery-name {
|
||||||
@@ -81,13 +76,7 @@ function onReturnButtonClick() {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scenery-abbrev {
|
|
||||||
font-size: 1.3em;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scenery-hash {
|
.scenery-hash {
|
||||||
margin-top: 0.5em;
|
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scenery-info">
|
<div class="scenery-info">
|
||||||
<section>
|
<section>
|
||||||
<SceneryInfoIcons :station="station" />
|
<div class="info-station-data" v-if="apiStore.dataStatuses.sceneries == Status.Data.Loaded">
|
||||||
<SceneryInfoGeneral :station="station" />
|
<SceneryInfoIcons :station="station" />
|
||||||
<SceneryInfoRoutes v-if="station" :station="station" />
|
<SceneryInfoGeneral :station="station" />
|
||||||
<SceneryInfoAuthors :station="station" />
|
<SceneryInfoRoutes v-if="station" :station="station" />
|
||||||
|
<SceneryInfoAuthors :station="station" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="margin: 1em 0; height: 2px; background-color: white"></div>
|
<div class="info-station-loading" v-else>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-divider"></div>
|
||||||
|
|
||||||
<!-- info dispatcher -->
|
|
||||||
<SceneryInfoDispatcher :onlineScenery="onlineScenery" />
|
<SceneryInfoDispatcher :onlineScenery="onlineScenery" />
|
||||||
|
|
||||||
<div class="info-lists">
|
<div class="info-online-lists">
|
||||||
<!-- user list -->
|
|
||||||
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
|
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
|
||||||
|
|
||||||
<!-- spawn list -->
|
|
||||||
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
|
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType } from 'vue';
|
||||||
|
import { ActiveScenery, Station, Status } from '../../typings/common';
|
||||||
|
|
||||||
import SceneryInfoDispatcher from './SceneryInfo/SceneryInfoDispatcher.vue';
|
import SceneryInfoDispatcher from './SceneryInfo/SceneryInfoDispatcher.vue';
|
||||||
import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
|
import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
|
||||||
@@ -32,47 +35,34 @@ import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
|
|||||||
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
|
||||||
import SceneryInfoGeneral from './SceneryInfo/SceneryInfoGeneral.vue';
|
import SceneryInfoGeneral from './SceneryInfo/SceneryInfoGeneral.vue';
|
||||||
import SceneryInfoAuthors from './SceneryInfo/SceneryInfoAuthors.vue';
|
import SceneryInfoAuthors from './SceneryInfo/SceneryInfoAuthors.vue';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
|
||||||
import { ActiveScenery, Station } from '../../typings/common';
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps({
|
||||||
components: {
|
station: {
|
||||||
SceneryInfoDispatcher,
|
type: Object as PropType<Station>
|
||||||
SceneryInfoGeneral,
|
|
||||||
SceneryInfoIcons,
|
|
||||||
SceneryInfoAuthors,
|
|
||||||
SceneryInfoUserList,
|
|
||||||
SceneryInfoSpawnList,
|
|
||||||
SceneryInfoRoutes
|
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
station: {
|
|
||||||
type: Object as PropType<Station>
|
|
||||||
},
|
|
||||||
|
|
||||||
onlineScenery: {
|
onlineScenery: {
|
||||||
type: Object as PropType<ActiveScenery>
|
type: Object as PropType<ActiveScenery>
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
@use '../../styles/badge';
|
|
||||||
|
|
||||||
h3.section-header {
|
|
||||||
margin: 0.5em 0;
|
|
||||||
padding: 0.3em;
|
|
||||||
|
|
||||||
|
.info-station-loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
font-size: 1.2em;
|
min-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-lists {
|
.info-online-lists {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
@@ -80,6 +70,12 @@ h3.section-header {
|
|||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-divider {
|
||||||
|
margin: 1em 0;
|
||||||
|
height: 3px;
|
||||||
|
background-color: #5b5b5b;
|
||||||
|
}
|
||||||
|
|
||||||
.scenery-topic a {
|
.scenery-topic a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@
|
|||||||
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
{{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<router-link
|
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
|
||||||
class="dispatcher-name"
|
|
||||||
:to="`/journal/dispatchers?search-dispatcher=${onlineScenery.dispatcherName}`"
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
class="text--donator"
|
class="text--donator"
|
||||||
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
|
||||||
@@ -21,6 +18,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
<span v-else>{{ onlineScenery.dispatcherName }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<FlagIcon :languageId="onlineScenery.dispatcherLanguageId" width="1.25em" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-bottom">
|
<div class="info-bottom">
|
||||||
@@ -51,9 +50,11 @@ import styleMixin from '../../../mixins/styleMixin';
|
|||||||
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
|
||||||
import { ActiveScenery } from '../../../typings/common';
|
import { ActiveScenery } from '../../../typings/common';
|
||||||
import { useApiStore } from '../../../store/apiStore';
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
import FlagIcon from '../../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [styleMixin, dateMixin, routerMixin],
|
mixins: [styleMixin, dateMixin, routerMixin],
|
||||||
|
components: { StationStatusBadge, FlagIcon },
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -66,8 +67,7 @@ export default defineComponent({
|
|||||||
type: Object as PropType<ActiveScenery>,
|
type: Object as PropType<ActiveScenery>,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
components: { StationStatusBadge }
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -5,51 +5,69 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<span>
|
<div>
|
||||||
<b>{{ $t('availability.title') }}:</b>
|
<span>
|
||||||
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
<a
|
||||||
|
v-if="station?.generalInfo"
|
||||||
<span v-if="station.generalInfo.reqLevel > -1">
|
:href="station.generalInfo.url"
|
||||||
-
|
class="forum-link"
|
||||||
{{
|
target="_blank"
|
||||||
$t(
|
>
|
||||||
'scenery.req-level',
|
{{ $t('scenery.forum-topic') }}
|
||||||
{ lvl: station.generalInfo.reqLevel },
|
</a>
|
||||||
station.generalInfo.reqLevel
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
• <b>{{ $t('controls.title') }}:</b>
|
•
|
||||||
{{ $t(`controls.${station.generalInfo.controlType}`) }}
|
<b>{{ $t('scenery.abbrev') }}</b> {{ station.generalInfo.abbr }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
• <b>{{ $t('signals.title') }}:</b>
|
• <b>{{ $t('availability.title') }}:</b>
|
||||||
{{ $t(`signals.${station.generalInfo.signalType}`) }}
|
{{ $t(`availability.${station.generalInfo.availability}`) }}
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="station.generalInfo.lines">
|
<span v-if="station.generalInfo.reqLevel > -1">
|
||||||
• <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }}
|
-
|
||||||
</span>
|
{{
|
||||||
|
$t(
|
||||||
|
'scenery.req-level',
|
||||||
|
{ lvl: station.generalInfo.reqLevel },
|
||||||
|
station.generalInfo.reqLevel
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span v-if="station.generalInfo.project">
|
<span>
|
||||||
• <b>{{ $t('scenery.project-title') }}: </b>
|
• <b>{{ $t('controls.title') }}:</b>
|
||||||
<a
|
{{ $t(`controls.${station.generalInfo.controlType}`) }}
|
||||||
style="color: salmon; text-decoration: underline; font-weight: bold"
|
</span>
|
||||||
:href="station.generalInfo.projectUrl"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{{ station.generalInfo.project }}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span v-if="additionalTools.length != 0">
|
<span>
|
||||||
• <b>{{ $t('scenery.additional-tools-title') }}: </b>
|
• <b>{{ $t('signals.title') }}:</b>
|
||||||
{{ additionalTools.join(', ') }}
|
{{ $t(`signals.${station.generalInfo.signalType}`) }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="station.generalInfo.lines">
|
||||||
|
• <b>{{ $t('scenery.lines-title') }}:</b> {{ station.generalInfo.lines }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="station.generalInfo.project">
|
||||||
|
• <b>{{ $t('scenery.project-title') }}: </b>
|
||||||
|
<a
|
||||||
|
style="color: salmon; text-decoration: underline; font-weight: bold"
|
||||||
|
:href="station.generalInfo.projectUrl"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ station.generalInfo.project }}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if="additionalTools.length != 0">
|
||||||
|
• <b>{{ $t('scenery.additional-tools-title') }}: </b>
|
||||||
|
{{ additionalTools.join(', ') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -84,9 +102,14 @@ export default defineComponent({
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
div {
|
.scenery-abbrev {
|
||||||
margin: 0 0.15em;
|
font-size: 1.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.forum-link {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,102 +1,101 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-icons">
|
<section class="info-icons-section">
|
||||||
<span v-if="!station || !station.generalInfo">
|
<div class="icons-box">
|
||||||
|
<span v-if="!station || !station.generalInfo">
|
||||||
|
<img
|
||||||
|
class="icon-info"
|
||||||
|
src="/images/icon-unknown.svg"
|
||||||
|
alt="icon-unknown"
|
||||||
|
:title="$t('sceneries.info.unknown')"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="station?.generalInfo && station?.generalInfo.reqLevel >= 0"
|
||||||
|
class="scenery-icon icon-info level"
|
||||||
|
:style="calculateExpStyles(station?.generalInfo.reqLevel)"
|
||||||
|
>
|
||||||
|
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
v-if="station?.generalInfo?.availability == 'nonPublic'"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-unknown.svg"
|
src="/images/icon-lock.svg"
|
||||||
alt="icon-unknown"
|
alt="Non-public scenery"
|
||||||
:title="$t('sceneries.info.unknown')"
|
:title="$t('sceneries.info.non-public')"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
<img
|
||||||
v-if="station?.generalInfo && station?.generalInfo.reqLevel >= 0"
|
v-if="station?.generalInfo?.availability == 'unavailable'"
|
||||||
class="scenery-icon icon-info level"
|
class="icon-info"
|
||||||
:style="calculateExpStyle(station?.generalInfo.reqLevel)"
|
src="/images/icon-unavailable.svg"
|
||||||
>
|
alt="Unavailable scenery"
|
||||||
{{ station?.generalInfo.reqLevel >= 2 ? station?.generalInfo.reqLevel : 'L' }}
|
:title="$t('sceneries.info.unavailable')"
|
||||||
</span>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.availability == 'nonPublic'"
|
v-if="station?.generalInfo?.availability == 'abandoned'"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-lock.svg"
|
src="/images/icon-abandoned.svg"
|
||||||
alt="Non-public scenery"
|
alt="Abandoned scenery"
|
||||||
:title="$t('sceneries.info.non-public')"
|
:title="$t('sceneries.info.abandoned')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<span
|
||||||
v-if="station?.generalInfo?.availability == 'unavailable'"
|
v-if="station?.generalInfo"
|
||||||
class="icon-info"
|
class="scenery-icon icon-info"
|
||||||
src="/images/icon-unavailable.svg"
|
:class="station?.generalInfo.controlType.replace('+', '-')"
|
||||||
alt="Unavailable scenery"
|
:title="
|
||||||
:title="$t('sceneries.info.unavailable')"
|
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
|
||||||
/>
|
"
|
||||||
|
>
|
||||||
|
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.availability == 'abandoned'"
|
v-if="station?.generalInfo?.signalType"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-abandoned.svg"
|
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
||||||
alt="Abandoned scenery"
|
:alt="station.generalInfo.signalType"
|
||||||
:title="$t('sceneries.info.abandoned')"
|
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<img
|
||||||
v-if="station?.generalInfo"
|
v-if="station?.generalInfo?.lines"
|
||||||
class="scenery-icon icon-info"
|
class="icon-info"
|
||||||
:class="station?.generalInfo.controlType.replace('+', '-')"
|
src="/images/icon-real.svg"
|
||||||
:title="
|
alt="real scenery"
|
||||||
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
|
:title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
|
||||||
"
|
/>
|
||||||
>
|
|
||||||
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.signalType"
|
v-if="station?.generalInfo?.SUP"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
|
src="/images/icon-SUP.svg"
|
||||||
:alt="station.generalInfo.signalType"
|
alt="SUP (RASP-UZK)"
|
||||||
:title="$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)"
|
:title="$t('sceneries.info.SUP')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
v-if="station?.generalInfo?.lines"
|
v-if="station?.generalInfo?.ASDEK"
|
||||||
class="icon-info"
|
class="icon-info"
|
||||||
src="/images/icon-real.svg"
|
src="/images/icon-ASDEK.svg"
|
||||||
alt="real scenery"
|
alt="dSAT ASDEK"
|
||||||
:title="`${$t('sceneries.info.real')} ${station.generalInfo.lines}`"
|
:title="$t('sceneries.info.ASDEK')"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<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')"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { PropType, defineComponent } from 'vue';
|
import { PropType } from 'vue';
|
||||||
import styleMixin from '../../../mixins/styleMixin';
|
|
||||||
import { Station } from '../../../typings/common';
|
import { Station } from '../../../typings/common';
|
||||||
|
import { calculateExpStyles } from '../../../composables/badge';
|
||||||
|
|
||||||
export default defineComponent({
|
defineProps({
|
||||||
mixins: [styleMixin],
|
station: {
|
||||||
props: {
|
type: Object as PropType<Station>
|
||||||
station: {
|
|
||||||
type: Object as PropType<Station>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -104,12 +103,12 @@ export default defineComponent({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../../styles/icons';
|
@use '../../../styles/icons';
|
||||||
|
|
||||||
.info-icons {
|
.icons-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
margin: 1em;
|
margin: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-info {
|
.icon-info {
|
||||||
@@ -118,6 +117,7 @@ export default defineComponent({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
width: 3em;
|
width: 3em;
|
||||||
|
height: 3em;
|
||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
|
|
||||||
border: 2px solid #4e4e4e;
|
border: 2px solid #4e4e4e;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<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="oneWayRoutes.length > 0">
|
<div class="routes one-way" v-if="singleRoutesAvailable.length > 0">
|
||||||
<button
|
<button
|
||||||
class="routes-btn"
|
class="routes-btn"
|
||||||
@click="toggleRoutesVisibility('single')"
|
@click="toggleRoutesVisibility('single')"
|
||||||
data-tooltip-type="BaseTooltip"
|
data-tooltip-type="BaseTooltip"
|
||||||
:data-tooltip-content="`${showInternalSingleRoutes ? $t('scenery.btn-hide-internal-routes') : $t('scenery.btn-show-internal-routes')}`"
|
:data-tooltip-content="`${showInternalSingleRoutes ? $t('scenery.btn-show-internal-routes') : $t('scenery.btn-hide-internal-routes')}`"
|
||||||
>
|
>
|
||||||
<b>{{ $t('scenery.one-way-routes') }}</b>
|
<b>{{ $t('scenery.one-way-routes') }}</b>
|
||||||
<i class="fa-solid" :class="`${showInternalSingleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
<i class="fa-solid" :class="`${showInternalSingleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li v-for="route in oneWayRoutes" :key="route.routeName">
|
<li v-for="route in singleRoutesFiltered" :key="route.routeName">
|
||||||
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
||||||
{{ route.routeName }}</span
|
{{ route.routeName }}</span
|
||||||
>
|
>
|
||||||
@@ -24,22 +24,29 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li v-if="singleRoutesFiltered.length == 0">
|
||||||
|
<span class="routes-hidden">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
|
{{ $t('scenery.routes-hidden') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="routes two-way" v-if="twoWayRoutes.length > 0">
|
<div class="routes two-way" v-if="doubleRoutesAvailable.length > 0">
|
||||||
<button
|
<button
|
||||||
class="routes-btn"
|
class="routes-btn"
|
||||||
@click="toggleRoutesVisibility('double')"
|
@click="toggleRoutesVisibility('double')"
|
||||||
data-tooltip-type="BaseTooltip"
|
data-tooltip-type="BaseTooltip"
|
||||||
:data-tooltip-content="`${showInternalDoubleRoutes ? $t('scenery.btn-hide-internal-routes') : $t('scenery.btn-show-internal-routes')}`"
|
:data-tooltip-content="`${showInternalDoubleRoutes ? $t('scenery.btn-show-internal-routes') : $t('scenery.btn-hide-internal-routes')}`"
|
||||||
>
|
>
|
||||||
<b>{{ $t('scenery.two-way-routes') }}</b>
|
<b>{{ $t('scenery.two-way-routes') }}</b>
|
||||||
<i class="fa-solid" :class="`${showInternalDoubleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
<i class="fa-solid" :class="`${showInternalDoubleRoutes ? 'fa-eye' : 'fa-eye-slash'}`"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="routes-list">
|
<ul class="routes-list">
|
||||||
<li v-for="route in twoWayRoutes" :key="route.routeName">
|
<li v-for="route in doubleRoutesFiltered" :key="route.routeName">
|
||||||
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
<span :class="{ 'no-catenary': !route.isElectric, internal: route.isInternal }">
|
||||||
{{ route.routeName }}
|
{{ route.routeName }}
|
||||||
</span>
|
</span>
|
||||||
@@ -54,6 +61,13 @@
|
|||||||
</span>
|
</span>
|
||||||
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
<span v-if="route.isRouteSBL" class="sbl">SBL</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li v-if="doubleRoutesFiltered.length == 0">
|
||||||
|
<span class="routes-hidden">
|
||||||
|
<i class="fa-solid fa-eye-slash"></i>
|
||||||
|
{{ $t('scenery.routes-hidden') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -102,20 +116,32 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
oneWayRoutes() {
|
singleRoutesAvailable() {
|
||||||
return (
|
return (
|
||||||
this.station.generalInfo?.routes.single
|
this.station.generalInfo?.routes.single
|
||||||
.filter((r) => !r.isInternal || r.isInternal == this.showInternalSingleRoutes)
|
.filter((r) => !r.hidden)
|
||||||
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
twoWayRoutes() {
|
doubleRoutesAvailable() {
|
||||||
return (
|
return (
|
||||||
this.station.generalInfo?.routes.double
|
this.station.generalInfo?.routes.double
|
||||||
.filter((r) => !r.isInternal || r.isInternal == this.showInternalDoubleRoutes)
|
.filter((r) => !r.hidden)
|
||||||
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
.sort((r1, r2) => r1.routeName.localeCompare(r2.routeName)) ?? []
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
singleRoutesFiltered() {
|
||||||
|
return this.singleRoutesAvailable.filter(
|
||||||
|
(r) => this.showInternalSingleRoutes || !r.isInternal
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
doubleRoutesFiltered() {
|
||||||
|
return this.doubleRoutesAvailable.filter(
|
||||||
|
(r) => this.showInternalDoubleRoutes || !r.isInternal
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -154,11 +180,6 @@ ul.routes-list {
|
|||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0.5em 0.25em;
|
margin: 0.5em 0.25em;
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
@@ -182,11 +203,16 @@ ul.routes-list {
|
|||||||
background-color: #303030;
|
background-color: #303030;
|
||||||
color: #cfcfcf;
|
color: #cfcfcf;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.sbl {
|
&.sbl {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
background-color: #404040;
|
background-color: #404040;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.routes-hidden {
|
||||||
|
background-color: #4b4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-radius: 0 0.5em 0.5em 0;
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-spawn-list">
|
<section class="info-spawn-list">
|
||||||
<h3 class="spawn-header section-header">
|
<h3 class="spawn-header">
|
||||||
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
<img src="/images/icon-spawn.svg" alt="Open spawns icon" />
|
||||||
{{ $t('scenery.spawns') }}
|
{{ $t('scenery.spawns') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
<span class="text--primary">{{ onlineScenery?.spawns.length || '0' }}</span>
|
||||||
@@ -53,10 +53,23 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3.spawn-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.spawns-anim {
|
.spawns-anim {
|
||||||
&-move,
|
&-move,
|
||||||
&-enter-active,
|
&-enter-active,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="info-user-list">
|
<section class="info-user-list">
|
||||||
<h3 class="user-header section-header">
|
<h3 class="user-header">
|
||||||
<img src="/images/icon-user.svg" alt="Users icon" />
|
<img src="/images/icon-user.svg" alt="Users icon" />
|
||||||
{{ $t('scenery.users') }}
|
{{ $t('scenery.users') }}
|
||||||
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
<span class="text--primary">{{ onlineScenery?.stationTrains?.length || 0 }}</span
|
||||||
@@ -29,7 +29,8 @@
|
|||||||
<i
|
<i
|
||||||
v-if="
|
v-if="
|
||||||
train.timetableData != undefined &&
|
train.timetableData != undefined &&
|
||||||
(train.lastSeen <= Date.now() - 60000 || !train.online)
|
train.lastSeen <= Date.now() - 60000 &&
|
||||||
|
!train.online
|
||||||
"
|
"
|
||||||
class="fa-solid fa-user-slash"
|
class="fa-solid fa-user-slash"
|
||||||
style="color: lightcoral"
|
style="color: lightcoral"
|
||||||
@@ -111,6 +112,8 @@ export default defineComponent({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
$no-timetable: #aaa;
|
$no-timetable: #aaa;
|
||||||
$departed: springgreen;
|
$departed: springgreen;
|
||||||
$stopped: #ffa600;
|
$stopped: #ffa600;
|
||||||
@@ -118,6 +121,17 @@ $online: gold;
|
|||||||
$terminated: salmon;
|
$terminated: salmon;
|
||||||
$disconnected: slategray;
|
$disconnected: slategray;
|
||||||
|
|
||||||
|
h3.user-header {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
padding: 0.3em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
.info-user-list {
|
.info-user-list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,247 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="scenery-timetable">
|
<section class="scenery-timetable">
|
||||||
<div class="timetable-header">
|
<SceneryTimetableHeader
|
||||||
<h3>
|
:station="station"
|
||||||
<img src="/images/icon-timetable.svg" alt="icon-timetable" />
|
:onlineScenery="onlineScenery"
|
||||||
<span>{{ $t('scenery.timetables') }}</span>
|
:chosenCheckpoint="chosenCheckpoint"
|
||||||
|
:showStockThumbnails="showStockThumbnails"
|
||||||
|
/>
|
||||||
|
|
||||||
<span>
|
<SceneryTimetableList
|
||||||
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all ?? 0 }}</span>
|
:station="station"
|
||||||
<span> / </span>
|
:onlineScenery="onlineScenery"
|
||||||
<span class="text--grayed">
|
:chosenCheckpoint="chosenCheckpoint"
|
||||||
{{ onlineScenery?.scheduledTrainCount.confirmed ?? 0 }}
|
:showStockThumbnails="showStockThumbnails"
|
||||||
</span>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="header_links" v-if="station && onlineScenery">
|
|
||||||
<a
|
|
||||||
:href="generatorHref"
|
|
||||||
target="_blank"
|
|
||||||
data-tooltip-type="HtmlTooltip"
|
|
||||||
:data-tooltip-content="`<b>${$t('scenery.gnr-link')}</b>`"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-gnr.svg" alt="GeneraTOR app icon" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:href="pragotronHref"
|
|
||||||
target="_blank"
|
|
||||||
data-tooltip-type="HtmlTooltip"
|
|
||||||
:data-tooltip-content="`<b>${$t('scenery.pragotron-link')}</b>`"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:href="tabliceZbiorczeHref"
|
|
||||||
target="_blank"
|
|
||||||
data-tooltip-type="HtmlTooltip"
|
|
||||||
:data-tooltip-content="`<b>${$t('scenery.tablice-link')}</b>`"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="timetable-checkpoints" v-if="station?.generalInfo?.checkpoints">
|
|
||||||
<template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
|
|
||||||
<template v-if="i > 0">•</template>
|
|
||||||
<router-link
|
|
||||||
class="checkpoint-item"
|
|
||||||
:class="{ current: chosenCheckpoint === ch }"
|
|
||||||
:to="`/scenery?station=${station.name}&checkpoint=${ch}`"
|
|
||||||
>{{ ch }}</router-link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timetable-checkpoints" v-else-if="onlineScenery">
|
|
||||||
<template v-for="(ch, i) in onlineScenery.missingCheckpoints" :key="i">
|
|
||||||
<template v-if="i > 0">•</template>
|
|
||||||
<router-link
|
|
||||||
class="checkpoint-item"
|
|
||||||
:class="{ current: chosenCheckpoint === ch }"
|
|
||||||
:to="`/scenery?station=${onlineScenery.name}&checkpoint=${ch}`"
|
|
||||||
>{{ ch }}</router-link
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timetable-list">
|
|
||||||
<transition-group name="list-anim">
|
|
||||||
<div
|
|
||||||
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
|
|
||||||
style="padding-bottom: 5em"
|
|
||||||
key="list-loading"
|
|
||||||
>
|
|
||||||
<Loading />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="timetable-item empty"
|
|
||||||
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
|
|
||||||
key="list-offline"
|
|
||||||
>
|
|
||||||
{{ $t('scenery.offline') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="timetable-item empty"
|
|
||||||
v-else-if="sceneryTimetables.length == 0"
|
|
||||||
key="list-no-timetables"
|
|
||||||
>
|
|
||||||
{{ $t('scenery.no-timetables') }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
class="timetable-item"
|
|
||||||
v-else
|
|
||||||
v-for="(row, i) in sceneryTimetables"
|
|
||||||
:key="row.train.id + i"
|
|
||||||
tabindex="0"
|
|
||||||
:to="row.train.driverRouteLocation"
|
|
||||||
>
|
|
||||||
<span class="timetable-general">
|
|
||||||
<span class="general-info">
|
|
||||||
<div class="info-train">
|
|
||||||
<!-- Cargo warnings & details badges -->
|
|
||||||
<span
|
|
||||||
class="train-badge twr"
|
|
||||||
v-if="row.train.timetableData!.twr"
|
|
||||||
data-tooltip-type="BaseTooltip"
|
|
||||||
:data-tooltip-content="$t('warnings.TWR')"
|
|
||||||
>
|
|
||||||
TWR
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="train-badge tn"
|
|
||||||
v-if="row.train.timetableData!.hasDangerousCargo"
|
|
||||||
data-tooltip-type="BaseTooltip"
|
|
||||||
:data-tooltip-content="$t('warnings.TN')"
|
|
||||||
>
|
|
||||||
TN
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="train-badge pn"
|
|
||||||
v-if="row.train.timetableData!.hasExtraDeliveries"
|
|
||||||
data-tooltip-type="BaseTooltip"
|
|
||||||
:data-tooltip-content="$t('warnings.PN')"
|
|
||||||
>
|
|
||||||
PN
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Train info -->
|
|
||||||
<span
|
|
||||||
data-tooltip-type="TrainInfoTooltip"
|
|
||||||
:data-tooltip-content="row.train.id"
|
|
||||||
class="tooltip-help"
|
|
||||||
>
|
|
||||||
<b class="text--primary">
|
|
||||||
{{ row.train.timetableData!.category }}
|
|
||||||
</b>
|
|
||||||
|
|
||||||
<b> {{ row.train.trainNo }}</b>
|
|
||||||
•
|
|
||||||
{{ row.train.driverName }}
|
|
||||||
|
|
||||||
<i
|
|
||||||
class="fa-solid fa-user-slash"
|
|
||||||
style="color: salmon"
|
|
||||||
v-if="!row.train.online && row.train.lastSeen <= Date.now() - 60000"
|
|
||||||
></i>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Train stop comments -->
|
|
||||||
<span
|
|
||||||
v-if="row.checkpointStop.comments"
|
|
||||||
class="stop-comments-icon"
|
|
||||||
data-tooltip-type="BaseTooltip"
|
|
||||||
:data-tooltip-content="row.checkpointStop.comments"
|
|
||||||
>
|
|
||||||
<img src="/images/icon-warning.svg" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-route">
|
|
||||||
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ScheduledTrainStatus :sceneryTimetableRow="row" />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="timetable-schedule">
|
|
||||||
<span class="schedule-arrival">
|
|
||||||
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
|
|
||||||
{{ $t('timetables.begins') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="arrival-time" v-else>
|
|
||||||
<div v-if="row.checkpointStop.arrivalDelay == 0">
|
|
||||||
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>
|
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
|
||||||
timestampToString(row.checkpointStop.arrivalTimestamp)
|
|
||||||
}}</s>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
|
|
||||||
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
|
|
||||||
}}{{ row.checkpointStop.arrivalDelay }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="schedule-stop">
|
|
||||||
<span class="stop-connection">
|
|
||||||
{{ row.currentElement.arrivalRouteExt }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stop-time">
|
|
||||||
{{ row.checkpointStop.stopTime || '' }}
|
|
||||||
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="stop-connection">
|
|
||||||
{{ row.currentElement.departureRouteExt }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="schedule-departure">
|
|
||||||
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
|
|
||||||
{{ $t('timetables.terminates') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span class="departure-time" v-else>
|
|
||||||
<div v-if="row.checkpointStop.departureDelay == 0">
|
|
||||||
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<div>
|
|
||||||
<s style="margin-right: 0.2em" class="text--grayed">{{
|
|
||||||
timestampToString(row.checkpointStop.departureTimestamp)
|
|
||||||
}}</s>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
|
|
||||||
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
|
|
||||||
}}{{ row.checkpointStop.departureDelay }})
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</router-link>
|
|
||||||
</transition-group>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -249,21 +20,21 @@
|
|||||||
import { computed, defineComponent, PropType, ref } from 'vue';
|
import { computed, defineComponent, PropType, ref } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import Loading from '../Global/Loading.vue';
|
import SceneryTimetableHeader from './SceneryTimetable/SceneryTimetableHeader.vue';
|
||||||
|
|
||||||
import dateMixin from '../../mixins/dateMixin';
|
import dateMixin from '../../mixins/dateMixin';
|
||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
import { useApiStore } from '../../store/apiStore';
|
import { useApiStore } from '../../store/apiStore';
|
||||||
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
import { ActiveScenery, Station } from '../../typings/common';
|
||||||
import { SceneryTimetableRow } from './typings';
|
import SceneryTimetableList from './SceneryTimetable/SceneryTimetableList.vue';
|
||||||
import { ActiveScenery, Station, TooltipTrainInfo, Train } from '../../typings/common';
|
import StorageManager from '../../managers/storageManager';
|
||||||
import { getTrainStopStatus, stopStatusPriority } from './utils';
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'SceneryTimetable',
|
name: 'SceneryTimetable',
|
||||||
|
|
||||||
components: { Loading, ScheduledTrainStatus },
|
components: { SceneryTimetableHeader, SceneryTimetableList },
|
||||||
|
|
||||||
mixins: [dateMixin, routerMixin, trainCategoryMixin],
|
mixins: [dateMixin, routerMixin, trainCategoryMixin],
|
||||||
|
|
||||||
@@ -277,7 +48,8 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
listOpen: false
|
listOpen: false,
|
||||||
|
showStockThumbnails: false
|
||||||
}),
|
}),
|
||||||
|
|
||||||
activated() {
|
activated() {
|
||||||
@@ -313,69 +85,6 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
|
||||||
tabliceZbiorczeHref() {
|
|
||||||
let url = `https://tablice-td2.web.app/?station=${this.station!.name}`;
|
|
||||||
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
|
|
||||||
pragotronHref() {
|
|
||||||
let url = `https://pragotron-td2.web.app/board?name=${this.station!.name}®ion=${this.mainStore.region.id}`;
|
|
||||||
if (this.chosenCheckpoint) url += `&checkpoint=${this.chosenCheckpoint}`;
|
|
||||||
|
|
||||||
return url;
|
|
||||||
},
|
|
||||||
|
|
||||||
generatorHref() {
|
|
||||||
return `https://generator-td2.web.app/?sceneryId=${this.onlineScenery!.name}|${this.onlineScenery!.region}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
sceneryTimetables(): SceneryTimetableRow[] {
|
|
||||||
if (!this.onlineScenery) return [];
|
|
||||||
|
|
||||||
const sceneryName = this.$route.query['station']?.toString().replace(/_/g, ' ') ?? '';
|
|
||||||
|
|
||||||
return this.onlineScenery.scheduledTrains
|
|
||||||
.filter(
|
|
||||||
(ct) =>
|
|
||||||
// ct.timetablePathElement.stationName == sceneryName &&
|
|
||||||
ct.train.region == this.mainStore.region.id &&
|
|
||||||
this.chosenCheckpoint &&
|
|
||||||
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
|
|
||||||
)
|
|
||||||
.map((ct) => {
|
|
||||||
const trainStopStatus = getTrainStopStatus(
|
|
||||||
ct.checkpointStop,
|
|
||||||
ct.train.currentStationName,
|
|
||||||
sceneryName
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
checkpointStop: ct.checkpointStop,
|
|
||||||
train: ct.train,
|
|
||||||
prevElement: ct.previousSceneryElement,
|
|
||||||
nextElement: ct.nextSceneryElement,
|
|
||||||
currentElement: ct.timetablePathElement,
|
|
||||||
status: trainStopStatus
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
|
|
||||||
return 1;
|
|
||||||
|
|
||||||
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
|
|
||||||
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
|
|
||||||
|
|
||||||
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
loadSelectedOption() {
|
loadSelectedOption() {
|
||||||
const queryCheckpoint = this.$route.query['checkpoint']?.toString();
|
const queryCheckpoint = this.$route.query['checkpoint']?.toString();
|
||||||
@@ -402,205 +111,16 @@ export default defineComponent({
|
|||||||
checkpointsListRef[0] ??
|
checkpointsListRef[0] ??
|
||||||
sceneryName;
|
sceneryName;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setCheckpoint(cp: string) {
|
|
||||||
this.chosenCheckpoint = cp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@use '../../styles/responsive';
|
|
||||||
@use '../../styles/animations';
|
|
||||||
@use '../../styles/badge';
|
|
||||||
|
|
||||||
.scenery-timetable {
|
.scenery-timetable {
|
||||||
|
display: grid;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow: hidden;
|
||||||
padding: 0 0.5em;
|
grid-template-rows: auto 1fr;
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-header {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 99;
|
|
||||||
|
|
||||||
background-color: #181818;
|
|
||||||
|
|
||||||
padding: 0.5em;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 25px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
gap: 0.5em;
|
|
||||||
font-size: 1.3em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.header_links {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.25em;
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable {
|
|
||||||
&-count {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-item {
|
|
||||||
margin: 0.5em auto;
|
|
||||||
padding: 0.5em;
|
|
||||||
max-width: 1100px;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1.2em 0.5em;
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
background: #353535;
|
|
||||||
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
&.empty {
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: 1.2em;
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-general {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-schedule {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 0.2em;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
max-width: 400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-checkpoints {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
flex-wrap: wrap;
|
|
||||||
font-size: 1.1em;
|
|
||||||
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkpoint-item {
|
|
||||||
color: #aaa;
|
|
||||||
display: inline;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--clr-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.timetable-list {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.general-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-train {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-train > .train-badge {
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-number {
|
|
||||||
color: var(--clr-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-route {
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-comments-icon > img {
|
|
||||||
width: 1.3em;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.schedule {
|
|
||||||
&-arrival,
|
|
||||||
&-departure {
|
|
||||||
font-size: 1.15em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-stop {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 0.5em;
|
|
||||||
align-items: end;
|
|
||||||
|
|
||||||
.stop-connection {
|
|
||||||
font-size: 0.95em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stop-time {
|
|
||||||
position: relative;
|
|
||||||
inline-size: max-content;
|
|
||||||
align-self: center;
|
|
||||||
font-size: 0.9em;
|
|
||||||
|
|
||||||
color: var(--clr-primary);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '\027F6';
|
|
||||||
display: block;
|
|
||||||
font-size: 2.2em;
|
|
||||||
line-height: 0.65em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrival-time.begins,
|
|
||||||
.departure-time.terminates {
|
|
||||||
font-size: 0.85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
|
||||||
.timetable-item {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scenery-timetable-header">
|
||||||
|
<h3>
|
||||||
|
<img src="/images/icon-timetable.svg" alt="icon-timetable" />
|
||||||
|
<span>{{ $t('scenery.timetables') }}</span>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
<span class="text--primary">{{ onlineScenery?.scheduledTrainCount.all ?? 0 }}</span>
|
||||||
|
<span> / </span>
|
||||||
|
<span class="text--grayed">
|
||||||
|
{{ onlineScenery?.scheduledTrainCount.confirmed ?? 0 }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, PropType } from 'vue';
|
||||||
|
import { Station, ActiveScenery } from '../../../typings/common';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
},
|
||||||
|
|
||||||
|
onlineScenery: {
|
||||||
|
type: Object as PropType<ActiveScenery>
|
||||||
|
},
|
||||||
|
|
||||||
|
chosenCheckpoint: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.scenery-timetable-header {
|
||||||
|
background-color: #181818;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 0.5em;
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scenery-timetable-list">
|
||||||
|
<!-- Checkpoints derived from station data -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="timetable-checkpoints"
|
||||||
|
v-if="station?.generalInfo && station.generalInfo.checkpoints.length > 0"
|
||||||
|
>
|
||||||
|
<template v-for="(ch, i) in station.generalInfo.checkpoints" :key="i">
|
||||||
|
<template v-if="i > 0">•</template>
|
||||||
|
<router-link
|
||||||
|
class="checkpoint-item"
|
||||||
|
:class="{ current: chosenCheckpoint === ch }"
|
||||||
|
:to="`/scenery?station=${station.name}&checkpoint=${ch}`"
|
||||||
|
>
|
||||||
|
{{ ch }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Missing checkpoints if scenery is not in database -->
|
||||||
|
<div
|
||||||
|
class="timetable-checkpoints"
|
||||||
|
v-else-if="onlineScenery && onlineScenery.missingCheckpoints.length > 0"
|
||||||
|
>
|
||||||
|
<template v-for="(ch, i) in onlineScenery.missingCheckpoints" :key="i">
|
||||||
|
<template v-if="i > 0">•</template>
|
||||||
|
<router-link
|
||||||
|
class="checkpoint-item"
|
||||||
|
:class="{ current: chosenCheckpoint === ch }"
|
||||||
|
:to="`/scenery?station=${onlineScenery.name}&checkpoint=${ch}`"
|
||||||
|
>
|
||||||
|
{{ ch }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else></div>
|
||||||
|
|
||||||
|
<div class="list-container">
|
||||||
|
<transition-group name="list-anim">
|
||||||
|
<div
|
||||||
|
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
|
||||||
|
style="padding-bottom: 5em"
|
||||||
|
key="list-loading"
|
||||||
|
>
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="timetable-item empty"
|
||||||
|
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
|
||||||
|
key="list-offline"
|
||||||
|
>
|
||||||
|
{{ $t('scenery.offline') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="timetable-item empty"
|
||||||
|
v-else-if="sceneryTimetables.length == 0"
|
||||||
|
key="list-no-timetables"
|
||||||
|
>
|
||||||
|
{{ $t('scenery.no-timetables') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<router-link
|
||||||
|
v-for="row in sceneryTimetables"
|
||||||
|
class="timetable-item"
|
||||||
|
:to="row.train.driverRouteLocation"
|
||||||
|
:key="row.train.id"
|
||||||
|
>
|
||||||
|
<div class="item-top">
|
||||||
|
<div class="top-general">
|
||||||
|
<span class="general-info">
|
||||||
|
<div class="info-train">
|
||||||
|
<!-- Cargo warnings & details badges -->
|
||||||
|
<span
|
||||||
|
class="train-badge twr"
|
||||||
|
v-if="row.train.timetableData!.twr"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.TWR')"
|
||||||
|
>
|
||||||
|
TWR
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge tn"
|
||||||
|
v-if="row.train.timetableData!.hasDangerousCargo"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.TN')"
|
||||||
|
>
|
||||||
|
TN
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="train-badge pn"
|
||||||
|
v-if="row.train.timetableData!.hasExtraDeliveries"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="$t('warnings.PN')"
|
||||||
|
>
|
||||||
|
PN
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Train info -->
|
||||||
|
<span
|
||||||
|
data-tooltip-type="TrainInfoTooltip"
|
||||||
|
:data-tooltip-content="row.train.id"
|
||||||
|
class="tooltip-help"
|
||||||
|
>
|
||||||
|
<b class="text--primary">
|
||||||
|
{{ row.train.timetableData!.category }}
|
||||||
|
</b>
|
||||||
|
|
||||||
|
<b> {{ row.train.trainNo }}</b>
|
||||||
|
•
|
||||||
|
{{ row.train.driverName }}
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="fa-solid fa-user-slash"
|
||||||
|
style="color: salmon"
|
||||||
|
v-if="!row.train.online && row.train.lastSeen <= Date.now() - 60000"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Train stop comments -->
|
||||||
|
<span
|
||||||
|
v-if="row.checkpointStop.comments"
|
||||||
|
class="stop-comments-icon"
|
||||||
|
data-tooltip-type="BaseTooltip"
|
||||||
|
:data-tooltip-content="row.checkpointStop.comments"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-warning.svg" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-route">
|
||||||
|
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScheduledTrainStatus :sceneryTimetableRow="row" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="top-schedule">
|
||||||
|
<span class="schedule-arrival">
|
||||||
|
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
|
||||||
|
{{ $t('timetables.begins') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="arrival-time" v-else>
|
||||||
|
<div v-if="row.checkpointStop.arrivalDelay == 0">
|
||||||
|
<span>{{ timestampToTimeString(row.checkpointStop.arrivalTimestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
|
timestampToTimeString(row.checkpointStop.arrivalTimestamp)
|
||||||
|
}}</s>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ timestampToTimeString(row.checkpointStop.arrivalRealTimestamp) }}
|
||||||
|
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
|
||||||
|
}}{{ row.checkpointStop.arrivalDelay }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="schedule-stop">
|
||||||
|
<span class="stop-connection">
|
||||||
|
{{ row.currentElement.arrivalRouteExt }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stop-time">
|
||||||
|
{{ row.checkpointStop.stopTime || '' }}
|
||||||
|
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="stop-connection">
|
||||||
|
{{ row.currentElement.departureRouteExt }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="schedule-departure">
|
||||||
|
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
|
||||||
|
{{ $t('timetables.terminates') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="departure-time" v-else>
|
||||||
|
<div v-if="row.checkpointStop.departureDelay == 0">
|
||||||
|
<span>{{ timestampToTimeString(row.checkpointStop.departureTimestamp) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
<s style="margin-right: 0.2em" class="text--grayed">{{
|
||||||
|
timestampToTimeString(row.checkpointStop.departureTimestamp)
|
||||||
|
}}</s>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{{ timestampToTimeString(row.checkpointStop.departureRealTimestamp) }}
|
||||||
|
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
|
||||||
|
}}{{ row.checkpointStop.departureDelay }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-stock-list" v-if="showStockThumbnails">
|
||||||
|
<StockList :trainStockList="row.train.stockList" :thumbnailSize="45" />
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list-actions" v-if="station && onlineScenery">
|
||||||
|
<a
|
||||||
|
:href="generatorHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.gnr-link')}</b>`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-gnr.svg" alt="GeneraTOR app icon" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="pragotronHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.pragotron-link')}</b>`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
:href="tabliceZbiorczeHref"
|
||||||
|
target="_blank"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t('scenery.tablice-link')}</b>`"
|
||||||
|
>
|
||||||
|
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="list-divider"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="thumbnails-btn"
|
||||||
|
data-tooltip-type="HtmlTooltip"
|
||||||
|
:data-tooltip-content="`<b>${$t(`scenery.btn-${showStockThumbnails ? 'show' : 'hide'}-timetable-thumbnails`)}</b>`"
|
||||||
|
@click="toggleThumbnails"
|
||||||
|
>
|
||||||
|
<i class="fa-solid" :class="`${showStockThumbnails ? 'fa-expand' : 'fa-compress'}`"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ComputedRef, onMounted, PropType, ref } from 'vue';
|
||||||
|
import { Station, ActiveScenery } from '../../../typings/common';
|
||||||
|
import { SceneryTimetableRow } from '../typings';
|
||||||
|
import { getTrainStopStatus, stopStatusPriorities } from '../utils';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useMainStore } from '../../../store/mainStore';
|
||||||
|
import { useApiStore } from '../../../store/apiStore';
|
||||||
|
import { timestampToTimeString } from '../../../composables/time';
|
||||||
|
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
|
||||||
|
import Loading from '../../Global/Loading.vue';
|
||||||
|
import StockList from '../../Global/StockList.vue';
|
||||||
|
import StorageManager from '../../../managers/storageManager';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
},
|
||||||
|
|
||||||
|
onlineScenery: {
|
||||||
|
type: Object as PropType<ActiveScenery>
|
||||||
|
},
|
||||||
|
|
||||||
|
chosenCheckpoint: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const mainStore = useMainStore();
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
const showStockThumbnails = ref(false);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleStockThumbnails();
|
||||||
|
});
|
||||||
|
|
||||||
|
const sceneryTimetables: ComputedRef<SceneryTimetableRow[]> = computed(() => {
|
||||||
|
if (!props.onlineScenery) return [];
|
||||||
|
|
||||||
|
const sceneryName = route.query['station']?.toString().replace(/_/g, ' ') ?? '';
|
||||||
|
|
||||||
|
return props.onlineScenery.scheduledTrains
|
||||||
|
.filter(
|
||||||
|
(ct) =>
|
||||||
|
// ct.timetablePathElement.stationName == sceneryName &&
|
||||||
|
ct.train.region == mainStore.region.id &&
|
||||||
|
props.chosenCheckpoint &&
|
||||||
|
ct.checkpointStop.stopNameRAW.toLowerCase() == props.chosenCheckpoint.toLowerCase()
|
||||||
|
)
|
||||||
|
.map((ct) => {
|
||||||
|
const trainStopStatus = getTrainStopStatus(
|
||||||
|
ct.checkpointStop,
|
||||||
|
ct.train.currentStationName,
|
||||||
|
sceneryName
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
checkpointStop: ct.checkpointStop,
|
||||||
|
train: ct.train,
|
||||||
|
prevElement: ct.previousSceneryElement,
|
||||||
|
nextElement: ct.nextSceneryElement,
|
||||||
|
currentElement: ct.timetablePathElement,
|
||||||
|
status: trainStopStatus
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (stopStatusPriorities.indexOf(a.status) - stopStatusPriorities.indexOf(b.status) < 0)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
if (stopStatusPriorities.indexOf(a.status) - stopStatusPriorities.indexOf(b.status) > 0)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
|
||||||
|
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
|
||||||
|
|
||||||
|
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabliceZbiorczeHref = computed(() => {
|
||||||
|
let url = `https://tablice-td2.web.app/?station=${props.station!.name}`;
|
||||||
|
if (props.chosenCheckpoint) url += `&checkpoint=${props.chosenCheckpoint}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pragotronHref = computed(() => {
|
||||||
|
let url = `https://pragotron-td2.spythere.eu/board?name=${props.station!.name}®ion=${mainStore.region.id}`;
|
||||||
|
if (props.chosenCheckpoint) url += `&checkpoint=${props.chosenCheckpoint}`;
|
||||||
|
|
||||||
|
return url;
|
||||||
|
});
|
||||||
|
|
||||||
|
const generatorHref = computed(() => {
|
||||||
|
return `https://generator-td2.spythere.eu/?sceneryId=${props.onlineScenery!.name}|${props.onlineScenery!.region}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleStockThumbnails() {
|
||||||
|
const storageVal = StorageManager.getBooleanValue('showStockThumbnails');
|
||||||
|
|
||||||
|
showStockThumbnails.value = storageVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleThumbnails() {
|
||||||
|
showStockThumbnails.value = !showStockThumbnails.value;
|
||||||
|
|
||||||
|
StorageManager.setBooleanValue('showStockThumbnails', showStockThumbnails.value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../../styles/responsive';
|
||||||
|
@use '../../../styles/animations';
|
||||||
|
@use '../../../styles/badge';
|
||||||
|
|
||||||
|
.scenery-timetable-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-general {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-schedule {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.2em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-checkpoints {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
font-size: 1.1em;
|
||||||
|
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkpoint-item {
|
||||||
|
color: #aaa;
|
||||||
|
display: inline;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.current {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
margin-top: 0.5em;
|
||||||
|
padding: 2px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-item {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
padding: 0.35em;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
background: #353535;
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-item > .item-top {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.2em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timetable-item > .item-stock-list {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-train {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-train > .train-badge {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-number {
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-route {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-comments-icon > img {
|
||||||
|
width: 1.3em;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-arrival,
|
||||||
|
.schedule-departure {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.schedule-stop {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
.stop-connection {
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stop-time {
|
||||||
|
position: relative;
|
||||||
|
inline-size: max-content;
|
||||||
|
align-self: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
|
||||||
|
color: var(--clr-primary);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '\027F6';
|
||||||
|
display: block;
|
||||||
|
font-size: 2.2em;
|
||||||
|
line-height: 0.65em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrival-time.begins,
|
||||||
|
.departure-time.terminates {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
|
||||||
|
.list-divider {
|
||||||
|
height: 80%;
|
||||||
|
width: 3px;
|
||||||
|
background-color: #6b6b6b;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnails-btn {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include responsive.smallScreen {
|
||||||
|
.timetable-item {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-actions {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
import { StopStatus } from '../../typings/common';
|
import { StopStatus } from '../../../typings/common';
|
||||||
import { SceneryTimetableRow } from './typings';
|
import { SceneryTimetableRow } from '../typings';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@@ -40,36 +40,28 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ $t('scenery.timetable-issued-date') }}
|
{{ $t('scenery.timetable-issued-date') }}
|
||||||
<b>
|
<b>
|
||||||
{{
|
{{ parseCreatedDate(timetableHistory, $i18n.locale) }}
|
||||||
localeDateTime(
|
|
||||||
timetableHistory.createdAt > timetableHistory.beginDate
|
|
||||||
? timetableHistory.beginDate
|
|
||||||
: timetableHistory.createdAt,
|
|
||||||
$i18n.locale
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</b></span
|
|
||||||
>
|
|
||||||
<span v-if="timetableHistory.authorName">
|
|
||||||
{{ $t('scenery.timetable-issued-by') }}
|
|
||||||
<b>
|
|
||||||
<router-link
|
|
||||||
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
|
||||||
>
|
|
||||||
{{ timetableHistory.authorName }}
|
|
||||||
</router-link>
|
|
||||||
</b>
|
</b>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{{ $t('scenery.timetable-issued-for') }}
|
{{ $t('scenery.timetable-issued-for') }}
|
||||||
<b>
|
<router-link
|
||||||
<router-link
|
class="journal-link"
|
||||||
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
|
||||||
>
|
>
|
||||||
{{ timetableHistory.driverName }}
|
{{ timetableHistory.driverName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</b>
|
</span>
|
||||||
|
|
||||||
|
<span v-if="timetableHistory.authorName">
|
||||||
|
{{ $t('scenery.timetable-issued-by') }}
|
||||||
|
<router-link
|
||||||
|
class="journal-link"
|
||||||
|
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
|
||||||
|
>
|
||||||
|
{{ timetableHistory.authorName }}
|
||||||
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -106,7 +98,7 @@ import { useApiStore } from '../../store/apiStore';
|
|||||||
import routerMixin from '../../mixins/routerMixin';
|
import routerMixin from '../../mixins/routerMixin';
|
||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
|
const historyModeList = ['includesScenery', 'issuedFrom', 'via', 'terminatingAt'] as const;
|
||||||
type HistoryMode = (typeof historyModeList)[number];
|
type HistoryMode = (typeof historyModeList)[number];
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@@ -123,7 +115,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
historyList: [] as API.TimetableHistory.Response,
|
historyList: [] as API.TimetableHistory.ResponseShort,
|
||||||
historyModeList,
|
historyModeList,
|
||||||
|
|
||||||
apiStore: useApiStore(),
|
apiStore: useApiStore(),
|
||||||
@@ -131,17 +123,19 @@ export default defineComponent({
|
|||||||
dataStatus: Status.Data.Loading,
|
dataStatus: Status.Data.Loading,
|
||||||
DataStatus: Status.Data,
|
DataStatus: Status.Data,
|
||||||
|
|
||||||
checkedHistoryMode: 'via' as HistoryMode
|
checkedHistoryMode: 'includesScenery' as HistoryMode
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async activated() {
|
async activated() {
|
||||||
|
this.checkedHistoryMode = 'includesScenery';
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
async fetchAPIData() {
|
async fetchAPIData() {
|
||||||
const stationName = this.$route.query['station'];
|
const stationName = this.$route.query['station'];
|
||||||
|
this.dataStatus = Status.Data.Loading;
|
||||||
|
|
||||||
if (!stationName) {
|
if (!stationName) {
|
||||||
this.historyList = [];
|
this.historyList = [];
|
||||||
@@ -152,25 +146,27 @@ export default defineComponent({
|
|||||||
const requestFilters: Record<string, any> = {};
|
const requestFilters: Record<string, any> = {};
|
||||||
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
requestFilters[this.checkedHistoryMode] = stationName.toString();
|
||||||
requestFilters.countLimit = 30;
|
requestFilters.countLimit = 30;
|
||||||
|
requestFilters['returnType'] = 'short';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response: API.TimetableHistory.Response = await (
|
const response: API.TimetableHistory.ResponseShort = await this.apiStore.client.get(
|
||||||
await this.apiStore.client!.get('api/getTimetables', {
|
'api/getTimetables',
|
||||||
params: requestFilters
|
requestFilters
|
||||||
})
|
);
|
||||||
).data;
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
this.historyList = response;
|
this.historyList = response;
|
||||||
|
|
||||||
this.dataStatus = Status.Data.Loaded;
|
this.dataStatus = Status.Data.Loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
this.dataStatus = Status.Data.Error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
checkHistoryMode(mode: HistoryMode) {
|
checkHistoryMode(mode: HistoryMode) {
|
||||||
this.checkedHistoryMode = mode;
|
this.checkedHistoryMode = mode;
|
||||||
this.dataStatus = Status.Data.Loading;
|
|
||||||
this.fetchAPIData();
|
this.fetchAPIData();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -181,6 +177,18 @@ export default defineComponent({
|
|||||||
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
parseCreatedDate(timetable: API.TimetableHistory.DataShort, locale: string) {
|
||||||
|
const createdDate =
|
||||||
|
timetable.createdAt > timetable.beginDate
|
||||||
|
? new Date(timetable.beginDate)
|
||||||
|
: new Date(timetable.createdAt);
|
||||||
|
|
||||||
|
return createdDate.toLocaleString(locale == 'pl' ? 'pl-PL' : 'en-GB', {
|
||||||
|
timeStyle: 'short',
|
||||||
|
dateStyle: 'medium'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Loading }
|
components: { Loading }
|
||||||
@@ -215,7 +223,15 @@ export default defineComponent({
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.35em;
|
padding: 0.35em;
|
||||||
min-width: 120px;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.journal-link {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #eee;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--clr-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div class="scenery-top-list">
|
||||||
|
<h2 class="header">{{ t('scenery.top-list.header') }}</h2>
|
||||||
|
|
||||||
|
<div class="top-actions">
|
||||||
|
<div class="actions-modes">
|
||||||
|
<button
|
||||||
|
v-for="mode in availableModes"
|
||||||
|
:class="`btn btn--option ${mode == currentListMode ? 'checked' : ''}`"
|
||||||
|
@click="selectListMode(mode)"
|
||||||
|
>
|
||||||
|
{{ t(`scenery.top-list.mode-${mode}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions-scopes">
|
||||||
|
<button
|
||||||
|
v-for="scope in availableScopes"
|
||||||
|
:class="`btn btn--option ${scope == currentListScope ? 'checked' : ''}`"
|
||||||
|
@click="selectListScope(scope)"
|
||||||
|
>
|
||||||
|
{{ t(`scenery.top-list.scope-${scope}`) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rating-list-wrapper">
|
||||||
|
<Loading v-if="listState == Status.Data.Loading" />
|
||||||
|
<div v-else-if="listState == Status.Data.Error">Ups, coś poszło nie tak...</div>
|
||||||
|
|
||||||
|
<ul v-else>
|
||||||
|
<li v-for="(value, i) in bestScoreList">
|
||||||
|
<div>
|
||||||
|
{{ t('scenery.top-list.place', i + 1) }} -
|
||||||
|
<router-link :to="`/profile?playerId=${value.dispatcherId}`">{{
|
||||||
|
value.dispatcherName
|
||||||
|
}}</router-link>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<b class="text--primary" v-if="currentListMode == 'dutyCount'">{{
|
||||||
|
t('scenery.top-list.duty-count', value.value)
|
||||||
|
}}</b>
|
||||||
|
|
||||||
|
<b class="text--primary" v-else-if="currentListMode == 'dispatcherRating'">{{
|
||||||
|
t('scenery.top-list.dispatcher-rating', value.value)
|
||||||
|
}}</b>
|
||||||
|
|
||||||
|
<b class="text--primary" v-else>
|
||||||
|
{{ t('scenery.top-list.duration') }}
|
||||||
|
{{ humanizeDuration(value.value) }}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, PropType, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useApiStore } from '../../store/apiStore';
|
||||||
|
import { Station, ActiveScenery, Status } from '../../typings/common';
|
||||||
|
import Loading from '../Global/Loading.vue';
|
||||||
|
import { humanizeDuration } from '../../composables/time';
|
||||||
|
|
||||||
|
interface SceneryBestScoreItem {
|
||||||
|
dispatcherName: string;
|
||||||
|
dispatcherId: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const apiStore = useApiStore();
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'SceneryTopList'
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
station: {
|
||||||
|
type: Object as PropType<Station>
|
||||||
|
},
|
||||||
|
|
||||||
|
onlineScenery: {
|
||||||
|
type: Object as PropType<ActiveScenery>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableModes = ['dutyCount', 'dispatcherRating', 'dutyDuration'] as const;
|
||||||
|
const availableScopes = ['name', 'hash'] as const;
|
||||||
|
|
||||||
|
type ListMode = (typeof availableModes)[number];
|
||||||
|
type ListScope = (typeof availableScopes)[number];
|
||||||
|
|
||||||
|
const currentListMode = ref<ListMode>('dutyCount');
|
||||||
|
const currentListScope = ref<ListScope>('name');
|
||||||
|
|
||||||
|
const listState = ref<Status.Data>(Status.Data.Loading);
|
||||||
|
|
||||||
|
const bestScoreList = ref<SceneryBestScoreItem[]>([]);
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchTopDispatchersList();
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectListMode(mode: ListMode) {
|
||||||
|
currentListMode.value = mode;
|
||||||
|
fetchTopDispatchersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectListScope(scope: ListScope) {
|
||||||
|
currentListScope.value = scope;
|
||||||
|
fetchTopDispatchersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTopDispatchersList() {
|
||||||
|
const searchedStationValue =
|
||||||
|
currentListScope.value == 'name'
|
||||||
|
? props.station?.name
|
||||||
|
: apiStore.sceneryData.find((sc) => sc.name == props.station!.name)?.hash;
|
||||||
|
|
||||||
|
bestScoreList.value = [];
|
||||||
|
|
||||||
|
if (!searchedStationValue) {
|
||||||
|
listState.value = Status.Data.Loaded;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
listState.value = Status.Data.Loading;
|
||||||
|
|
||||||
|
const response: SceneryBestScoreItem[] = await apiStore.client.get(`api/getSceneryBestScores`, {
|
||||||
|
[currentListScope.value]: searchedStationValue,
|
||||||
|
type: currentListMode.value,
|
||||||
|
countLimit: 40
|
||||||
|
});
|
||||||
|
|
||||||
|
bestScoreList.value = response;
|
||||||
|
listState.value = Status.Data.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
listState.value = Status.Data.Error;
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.scenery-top-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-modes,
|
||||||
|
.actions-scopes {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-list-wrapper {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-list-wrapper > ul {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.65em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-list-wrapper > ul > li {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
padding: 0.25em;
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
line-height: 1.5em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { StopStatus, TrainStop } from '../../typings/common';
|
import { StopStatus, TrainStop } from '../../typings/common';
|
||||||
|
|
||||||
export const stopStatusPriority = [
|
export const stopStatusPriorities = [
|
||||||
StopStatus.ONLINE,
|
StopStatus.ONLINE,
|
||||||
StopStatus.STOPPED,
|
StopStatus.STOPPED,
|
||||||
StopStatus.DEPARTED,
|
StopStatus.DEPARTED,
|
||||||
@@ -18,23 +18,31 @@ export function getTrainStopStatus(
|
|||||||
return StopStatus.TERMINATED;
|
return StopStatus.TERMINATED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
|
if (
|
||||||
|
!stopInfo.terminatesHere &&
|
||||||
|
stopInfo.confirmed &&
|
||||||
|
currentStationName.startsWith(sceneryName)
|
||||||
|
) {
|
||||||
return StopStatus.DEPARTED;
|
return StopStatus.DEPARTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
|
if (
|
||||||
|
!stopInfo.terminatesHere &&
|
||||||
|
stopInfo.confirmed &&
|
||||||
|
!currentStationName.startsWith(sceneryName)
|
||||||
|
) {
|
||||||
return StopStatus.DEPARTED_AWAY;
|
return StopStatus.DEPARTED_AWAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStationName == sceneryName && !stopInfo.stopped) {
|
if (currentStationName.startsWith(sceneryName) && !stopInfo.stopped) {
|
||||||
return StopStatus.ONLINE;
|
return StopStatus.ONLINE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStationName == sceneryName && stopInfo.stopped) {
|
if (currentStationName.startsWith(sceneryName) && stopInfo.stopped) {
|
||||||
return StopStatus.STOPPED;
|
return StopStatus.STOPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStationName != sceneryName) {
|
if (!currentStationName.startsWith(sceneryName)) {
|
||||||
return StopStatus.ARRIVING;
|
return StopStatus.ARRIVING;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="filter-slider-container">
|
||||||
|
<input
|
||||||
|
class="slider"
|
||||||
|
v-for="slider in sliderGroupsOptions[sliderGroup]"
|
||||||
|
type="range"
|
||||||
|
:name="slider.id"
|
||||||
|
:id="slider.id"
|
||||||
|
:min="slider.minRange"
|
||||||
|
:max="slider.maxRange"
|
||||||
|
:step="slider.step"
|
||||||
|
v-model="filters[slider.id]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="slider-track" @click="moveCloserSliderToMousePos"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { inject, PropType } from 'vue';
|
||||||
|
import { SliderGroup, sliderGroupsOptions } from '../../managers/stationFilterManager';
|
||||||
|
|
||||||
|
const filters = inject('StationsView_filters') as Record<string, any>;
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sliderGroup: {
|
||||||
|
type: String as PropType<SliderGroup>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change slider value that's the closest one to the mouse position on the slider track click
|
||||||
|
function moveCloserSliderToMousePos(e: MouseEvent) {
|
||||||
|
const { clientX, target } = e;
|
||||||
|
const { minRange, maxRange, step } = sliderGroupsOptions[props.sliderGroup][0];
|
||||||
|
|
||||||
|
const boundingRect = (target as HTMLElement).getBoundingClientRect();
|
||||||
|
const mouseX = clientX - boundingRect.left;
|
||||||
|
|
||||||
|
const leftSliderValue = filters[sliderGroupsOptions[props.sliderGroup][0].id];
|
||||||
|
const rightSliderValue = filters[sliderGroupsOptions[props.sliderGroup][1].id];
|
||||||
|
|
||||||
|
let mouseValue = Math.round((maxRange - minRange) * (mouseX / boundingRect.width));
|
||||||
|
|
||||||
|
// Adjust mouse value to the closest step point (divide by 10, get rounded number, then multiply by step)
|
||||||
|
mouseValue = Math.round(mouseValue / step) * step;
|
||||||
|
|
||||||
|
let sliderIndex =
|
||||||
|
Math.abs(leftSliderValue - mouseValue) < Math.abs(rightSliderValue - mouseValue) ? 0 : 1;
|
||||||
|
|
||||||
|
filters[sliderGroupsOptions[props.sliderGroup][sliderIndex].id] = mouseValue;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
|
.filter-slider-container {
|
||||||
|
position: relative;
|
||||||
|
padding: 0.5em;
|
||||||
|
height: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-track {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
border-radius: 1em;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #444;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 1.25em;
|
||||||
|
background: none;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 1em;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
&:hover ~ .slider-track {
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 1px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
border-radius: 1em;
|
||||||
|
background: var(--clr-primary);
|
||||||
|
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
border-radius: 1em;
|
||||||
|
background: var(--clr-primary);
|
||||||
|
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &:first-child::-webkit-slider-runnable-track {
|
||||||
|
// }
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
position: relative;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &:first-child::-moz-range-track {
|
||||||
|
// background: var(--clr-primary);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -21,9 +21,7 @@
|
|||||||
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="card_sceneries-search">
|
<section class="card_input-search">
|
||||||
<h3 class="section-header">{{ $t('filters.sceneries-search') }}</h3>
|
|
||||||
|
|
||||||
<datalist id="sceneries">
|
<datalist id="sceneries">
|
||||||
<option
|
<option
|
||||||
v-for="scenery in sortedStationList"
|
v-for="scenery in sortedStationList"
|
||||||
@@ -32,18 +30,59 @@
|
|||||||
></option>
|
></option>
|
||||||
</datalist>
|
</datalist>
|
||||||
|
|
||||||
<form action="javascript:void(0);" @submit="handleSceneriesInput">
|
<input
|
||||||
<input
|
v-model="chosenSearchScenery"
|
||||||
v-model="chosenSearchScenery"
|
id="scenery-search"
|
||||||
id="scenery-search"
|
list="sceneries"
|
||||||
list="sceneries"
|
:placeholder="$t('filters.sceneries-placeholder')"
|
||||||
:placeholder="$t('filters.sceneries-placeholder')"
|
@change="handleSceneriesInput"
|
||||||
@focus="preventKeyDown = true"
|
@focus="preventKeyDown = true"
|
||||||
@blur="preventKeyDown = false"
|
@blur="preventKeyDown = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
|
<button class="btn--action" @click="handleSceneriesInput">
|
||||||
</form>
|
{{ $t('filters.search-button-title') }}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<input
|
||||||
|
v-model="filters['lines']"
|
||||||
|
id="line-numbers-search"
|
||||||
|
:placeholder="$t('filters.line-numbers-placeholder')"
|
||||||
|
@focus="preventKeyDown = true"
|
||||||
|
@blur="preventKeyDown = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetLineNumbersInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset line numbers search" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<select id="author" name="authors" v-model="filters['authors']">
|
||||||
|
<option value="">{{ $t('filters.authors-placeholder') }}</option>
|
||||||
|
<option v-for="(author, i) in authorsOptions" :key="i" :value="author">
|
||||||
|
{{ author }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetAuthorsInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset authors search" />
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card_input-search">
|
||||||
|
<select id="projects" name="projects" v-model="filters['projects']">
|
||||||
|
<option value="">{{ $t('filters.projects-placeholder') }}</option>
|
||||||
|
<option v-for="(project, i) in projectsOptions" :key="i" :value="project">
|
||||||
|
{{ project }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button class="btn--action btn--image" @click="resetProjectsInput">
|
||||||
|
<img src="/images/icon-exit.svg" alt="reset projects search" />
|
||||||
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_options">
|
<section class="card_options">
|
||||||
@@ -52,7 +91,7 @@
|
|||||||
v-for="(sectionFilters, sectionKey) in filtersSections"
|
v-for="(sectionFilters, sectionKey) in filtersSections"
|
||||||
:key="sectionKey"
|
:key="sectionKey"
|
||||||
>
|
>
|
||||||
<h3 class="text--primary">
|
<h3 class="section-header">
|
||||||
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
|
||||||
{{ $t(`filters.sections.${sectionKey}`) }}
|
{{ $t(`filters.sections.${sectionKey}`) }}
|
||||||
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
<button @click="resetSectionFilters(sectionKey)">RESET</button>
|
||||||
@@ -82,7 +121,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_timestamp">
|
<section class="card_timestamp">
|
||||||
<h3 class="section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
<h3 class="hours-section-header">{{ $t('filters.minimum-hours-title') }}</h3>
|
||||||
|
|
||||||
<span class="clock">
|
<span class="clock">
|
||||||
<button class="btn--action" @click="subHour">-</button>
|
<button class="btn--action" @click="subHour">-</button>
|
||||||
@@ -97,44 +136,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card_authors-search">
|
|
||||||
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
|
|
||||||
|
|
||||||
<datalist id="authors" name="authors">
|
|
||||||
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<form action="javascript:void(0);" @submit="handleAuthorsInput">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="author"
|
|
||||||
list="authors"
|
|
||||||
name="authors"
|
|
||||||
v-model="authors"
|
|
||||||
:placeholder="$t('filters.authors-placeholder')"
|
|
||||||
@focus="preventKeyDown = true"
|
|
||||||
@blur="preventKeyDown = false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button class="btn--action">{{ $t('filters.search-button-title') }}</button>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card_sliders">
|
<section class="card_sliders">
|
||||||
<div class="slider" v-for="(slider, i) in sliderStates" :key="i">
|
<div class="option-slider" v-for="(sliderGroup, i) in sliderGroups" :key="i">
|
||||||
<input
|
<FilterSlider :sliderGroup="sliderGroup" />
|
||||||
class="slider-input"
|
|
||||||
type="range"
|
<span class="slider-value">
|
||||||
:name="slider.id"
|
{{ filters[sliderGroupsOptions[sliderGroup][0].id] }} -
|
||||||
:id="slider.id"
|
{{ filters[sliderGroupsOptions[sliderGroup][1].id] }}
|
||||||
:min="slider.minRange"
|
</span>
|
||||||
:max="slider.maxRange"
|
|
||||||
:step="slider.step"
|
|
||||||
v-model.number="filters[slider.id]"
|
|
||||||
/>
|
|
||||||
<span class="slider-value">{{ filters[slider.id] }}</span>
|
|
||||||
<div class="slider-content">
|
<div class="slider-content">
|
||||||
{{ $t(`filters.sliders.${slider.id}`) }}
|
{{ $t(`filters.sliders.${sliderGroups[i]}`) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -174,13 +186,15 @@ import routerMixin from '../../mixins/routerMixin';
|
|||||||
import { useMainStore } from '../../store/mainStore';
|
import { useMainStore } from '../../store/mainStore';
|
||||||
|
|
||||||
import FilterOption from './FilterOption.vue';
|
import FilterOption from './FilterOption.vue';
|
||||||
|
import FilterSlider from './FilterSlider.vue';
|
||||||
import StorageManager from '../../managers/storageManager';
|
import StorageManager from '../../managers/storageManager';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
filtersSections,
|
filtersSections,
|
||||||
sliderStates,
|
|
||||||
initFilters,
|
initFilters,
|
||||||
getChangedFilters
|
sliderGroups,
|
||||||
|
getChangedFilters,
|
||||||
|
sliderGroupsOptions
|
||||||
} from '../../managers/stationFilterManager';
|
} from '../../managers/stationFilterManager';
|
||||||
|
|
||||||
import { StationFilterSection } from '../../managers/stationFilterManager';
|
import { StationFilterSection } from '../../managers/stationFilterManager';
|
||||||
@@ -190,17 +204,17 @@ import { watch } from 'vue';
|
|||||||
const STORAGE_KEY = 'options_saved';
|
const STORAGE_KEY = 'options_saved';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { FilterOption },
|
components: { FilterOption, FilterSlider },
|
||||||
mixins: [keyMixin, routerMixin],
|
mixins: [keyMixin, routerMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
saveOptions: false,
|
saveOptions: false,
|
||||||
|
|
||||||
filtersSections,
|
filtersSections,
|
||||||
sliderStates,
|
sliderGroups,
|
||||||
|
sliderGroupsOptions,
|
||||||
|
|
||||||
minimumHours: 0,
|
minimumHours: 0,
|
||||||
authors: '',
|
|
||||||
|
|
||||||
currentRegion: { id: '', value: '' },
|
currentRegion: { id: '', value: '' },
|
||||||
|
|
||||||
@@ -255,13 +269,11 @@ export default defineComponent({
|
|||||||
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
.sort((s1, s2) => (s1.name > s2.name ? 1 : -1));
|
||||||
},
|
},
|
||||||
|
|
||||||
currentOptionsActive() {
|
authorsOptions() {
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
authorsHint() {
|
|
||||||
return this.store.stationList
|
return this.store.stationList
|
||||||
.reduce((acc, station) => {
|
.reduce((acc, station) => {
|
||||||
|
if (station.generalInfo?.hidden === true) return acc;
|
||||||
|
|
||||||
station.generalInfo?.authors?.forEach((author) => {
|
station.generalInfo?.authors?.forEach((author) => {
|
||||||
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
if (author.trim() != '' && !acc.includes(author.toLocaleLowerCase()))
|
||||||
acc.push(author.toLocaleLowerCase());
|
acc.push(author.toLocaleLowerCase());
|
||||||
@@ -270,6 +282,19 @@ export default defineComponent({
|
|||||||
return acc;
|
return acc;
|
||||||
}, [] as string[])
|
}, [] as string[])
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
},
|
||||||
|
|
||||||
|
projectsOptions() {
|
||||||
|
return this.store.stationList
|
||||||
|
.reduce((acc, station) => {
|
||||||
|
if (!station.generalInfo || !station.generalInfo.project || station.generalInfo.hidden)
|
||||||
|
return acc;
|
||||||
|
if (!acc.includes(station.generalInfo.project.trim()))
|
||||||
|
acc.push(station.generalInfo.project.trim());
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, [] as string[])
|
||||||
|
.sort((a, b) => a.localeCompare(b));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -294,8 +319,16 @@ export default defineComponent({
|
|||||||
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAuthorsInput() {
|
resetAuthorsInput() {
|
||||||
this.filters['authors'] = this.authors;
|
this.filters['authors'] = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
resetProjectsInput() {
|
||||||
|
this.filters['projects'] = '';
|
||||||
|
},
|
||||||
|
|
||||||
|
resetLineNumbersInput() {
|
||||||
|
this.filters['lines'] = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSceneriesInput() {
|
handleSceneriesInput() {
|
||||||
@@ -340,7 +373,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
// Reset local model values
|
// Reset local model values
|
||||||
this.minimumHours = 0;
|
this.minimumHours = 0;
|
||||||
this.authors = '';
|
|
||||||
|
|
||||||
// Reset global filters
|
// Reset global filters
|
||||||
Object.keys(this.filters).forEach((filterKey) => {
|
Object.keys(this.filters).forEach((filterKey) => {
|
||||||
@@ -384,6 +416,14 @@ export default defineComponent({
|
|||||||
@use '../../styles/animations';
|
@use '../../styles/animations';
|
||||||
|
|
||||||
h3.section-header {
|
h3.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
gap: 0.5em;
|
||||||
|
color: var(--clr-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3.hours-section-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
@@ -456,33 +496,26 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card_authors-search,
|
.card_input-search {
|
||||||
.card_sceneries-search {
|
|
||||||
margin: 1em 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
|
||||||
form {
|
button {
|
||||||
display: flex;
|
height: 100%;
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5em;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input,
|
||||||
width: 70%;
|
select {
|
||||||
max-width: 400px;
|
width: 100%;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
outline: 1px solid white;
|
border: 1px solid #aaa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-filters {
|
.section-filters {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
}
|
}
|
||||||
@@ -494,9 +527,11 @@ h3.section-header {
|
|||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -548,124 +583,40 @@ h3.section-header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.option-section h3 {
|
.option-section h3 {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 0.25em;
|
|
||||||
|
|
||||||
gap: 0.5em;
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0.15em;
|
padding: 0.15em;
|
||||||
color: coral;
|
color: coral;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.slider {
|
.card_sliders {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-slider {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 50px 1fr;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
grid-template-columns: 250px 100px 1fr;
|
||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
|
min-height: 35px;
|
||||||
|
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
&-value {
|
.slider-value {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
padding: 0.1em 0.2em;
|
padding: 0.1em 0.2em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
font-weight: bold;
|
||||||
|
|
||||||
&-input {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
min-width: 25%;
|
|
||||||
|
|
||||||
&:focus-visible ~ * {
|
|
||||||
color: gold;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
margin-top: -7px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
background: white;
|
|
||||||
border: 3px solid var(--clr-primary);
|
|
||||||
background-color: #333;
|
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
margin-top: -5px;
|
|
||||||
border: 3px solid var(--clr-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
|
||||||
height: 1em;
|
|
||||||
width: 1em;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
background: white;
|
|
||||||
border: 4px solid var(--clr-primary);
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
border: 3px solid var(--clr-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-slider-runnable-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-range-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-ms-track {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen {
|
||||||
.slider {
|
.option-slider {
|
||||||
display: flex;
|
grid-template-columns: 1fr;
|
||||||
flex-wrap: wrap;
|
}
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
&-input {
|
.slider-content {
|
||||||
width: 90%;
|
text-align: center;
|
||||||
}
|
|
||||||
|
|
||||||
&-content {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card_controls > button > p {
|
.card_controls > button > p {
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
<transition name="dropdown-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="dropdown_wrapper" v-if="showDropdown">
|
<div class="dropdown_wrapper" v-if="showDropdown">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="stats-title text--primary">
|
<h2 class="stats-title text--primary">
|
||||||
<img src="/images/icon-stats.svg" alt="Open filters icon" height="28" />
|
<img src="/images/icon-stats.svg" alt="Open filters icon" height="28" />
|
||||||
{{ $t('station-stats.title') }}
|
{{ $t('station-stats.title') }}
|
||||||
</h1>
|
</h2>
|
||||||
|
|
||||||
<hr style="margin: 0.5em 0" />
|
<hr style="margin: 0.5em 0" />
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export default defineComponent({
|
|||||||
@use '../../styles/badge';
|
@use '../../styles/badge';
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
h1.stats-title img {
|
.stats-title img {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,13 +278,21 @@ h1.stats-title img {
|
|||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown_wrapper {
|
||||||
|
top: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen {
|
||||||
h1.stats-title {
|
.stats-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-button > span {
|
.filter-button > span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
class="header-text"
|
class="header-text"
|
||||||
:class="headerName"
|
:class="headerName"
|
||||||
>
|
>
|
||||||
<span class="header_wrapper">
|
<div class="header_wrapper">
|
||||||
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||||
alt="sort icon"
|
alt="sort icon"
|
||||||
/>
|
/>
|
||||||
</span>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
|
|
||||||
<th
|
<th
|
||||||
@@ -52,14 +52,14 @@
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<router-link
|
<tr
|
||||||
v-for="station in filteredStationList"
|
v-for="station in filteredStationList"
|
||||||
class="a-row"
|
class="a-row"
|
||||||
role="row"
|
tabindex="0"
|
||||||
:key="station.name"
|
:key="station.name"
|
||||||
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
|
@click.right.prevent="openForumSite($event, station.generalInfo?.url)"
|
||||||
@keydown.space.prevent="openForumSite($event, station.generalInfo?.url)"
|
@click="getSceneryRoute(station)"
|
||||||
:to="getSceneryRoute(station)"
|
@keydown.enter="getSceneryRoute(station)"
|
||||||
>
|
>
|
||||||
<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">{{
|
||||||
@@ -132,7 +132,6 @@
|
|||||||
<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)"
|
||||||
@click.prevent="openDonationCard"
|
|
||||||
data-tooltip-type="DonatorTooltip"
|
data-tooltip-type="DonatorTooltip"
|
||||||
:data-tooltip-content="$t('donations.dispatcher-message')"
|
:data-tooltip-content="$t('donations.dispatcher-message')"
|
||||||
>
|
>
|
||||||
@@ -146,6 +145,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td class="station-dispatcher-lang">
|
||||||
|
<FlagIcon
|
||||||
|
v-if="station.onlineInfo && station.onlineInfo.dispatcherLanguageId != -1"
|
||||||
|
:language-id="station.onlineInfo.dispatcherLanguageId"
|
||||||
|
width="2.25em"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="station-dispatcher-exp">
|
<td class="station-dispatcher-exp">
|
||||||
<span
|
<span
|
||||||
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
|
||||||
@@ -314,7 +321,7 @@
|
|||||||
>
|
>
|
||||||
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
|
||||||
</td>
|
</td>
|
||||||
</router-link>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -344,11 +351,13 @@ import { useTooltipStore } from '../../store/tooltipStore';
|
|||||||
import { getChangedFilters } from '../../managers/stationFilterManager';
|
import { getChangedFilters } from '../../managers/stationFilterManager';
|
||||||
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
|
||||||
import { filterStations, sortStations } from './utils';
|
import { filterStations, sortStations } from './utils';
|
||||||
|
import { getLanguageNameById } from '../../utils/languageUtils';
|
||||||
|
import FlagIcon from '../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['toggleDonationCard'],
|
emits: ['toggleDonationCard'],
|
||||||
|
|
||||||
components: { Loading, StationStatusBadge },
|
components: { Loading, StationStatusBadge, FlagIcon },
|
||||||
mixins: [styleMixin, dateMixin],
|
mixins: [styleMixin, dateMixin],
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@@ -384,15 +393,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getSceneryRoute(station: Station) {
|
getSceneryRoute(station: Station) {
|
||||||
// TODO: Hide tooltips when navigating away
|
this.$router.push({
|
||||||
|
|
||||||
return {
|
|
||||||
name: 'SceneryView',
|
name: 'SceneryView',
|
||||||
query: {
|
query: {
|
||||||
station: station.name,
|
station: station.name,
|
||||||
region: this.$route.query.region || undefined
|
region: this.$route.query.region || undefined
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
openDonationCard(e: Event) {
|
openDonationCard(e: Event) {
|
||||||
@@ -438,7 +445,7 @@ export default defineComponent({
|
|||||||
$rowCol: #424242;
|
$rowCol: #424242;
|
||||||
|
|
||||||
.station_table {
|
.station_table {
|
||||||
height: calc(100vh - 11em);
|
height: calc(100vh - 17em);
|
||||||
max-height: 2000px;
|
max-height: 2000px;
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -459,78 +466,82 @@ table {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 1250px;
|
min-width: 1250px;
|
||||||
white-space: wrap;
|
white-space: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr {
|
||||||
|
background-color: var(--clr-bg3);
|
||||||
|
}
|
||||||
|
|
||||||
|
thead th {
|
||||||
|
background-color: var(--clr-bg3);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
padding: 0.5em 0.25em;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
|
||||||
|
&.station {
|
||||||
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead tr {
|
&.min-lvl {
|
||||||
background-color: var(--clr-bg3);
|
width: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
&.status {
|
||||||
&.station {
|
width: 10em;
|
||||||
width: 12em;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.min-lvl {
|
&.dispatcher {
|
||||||
width: 4em;
|
width: 12em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.status {
|
&.dispatcher-lang {
|
||||||
width: 10em;
|
width: 6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dispatcher {
|
&.dispatcher-lvl {
|
||||||
width: 12em;
|
width: 6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.dispatcher-lvl {
|
&.routes-double,
|
||||||
width: 6em;
|
&.routes-single {
|
||||||
}
|
width: 7em;
|
||||||
|
}
|
||||||
|
|
||||||
&.routes-double,
|
&.general {
|
||||||
&.routes-single {
|
width: 11em;
|
||||||
width: 7em;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&.general {
|
&.header-image {
|
||||||
width: 11em;
|
width: 3.5em;
|
||||||
}
|
|
||||||
|
|
||||||
&.header-image {
|
&.user {
|
||||||
width: 3.5em;
|
width: 5em;
|
||||||
|
|
||||||
&.user {
|
|
||||||
width: 5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
padding: 0.5em 0.25em;
|
|
||||||
background-color: var(--clr-bg3);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
|
|
||||||
span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 1.5em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tr,
|
thead th .header_wrapper {
|
||||||
.a-row {
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 1.5em;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr {
|
||||||
background-color: $rowCol;
|
background-color: $rowCol;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
@@ -550,6 +561,7 @@ tr,
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
height: 2.5em;
|
||||||
|
|
||||||
&.inactive {
|
&.inactive {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
@@ -566,6 +578,7 @@ tr,
|
|||||||
.station-name {
|
.station-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
padding: 0.25em;
|
||||||
|
|
||||||
&.default {
|
&.default {
|
||||||
color: var(--clr-primary);
|
color: var(--clr-primary);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const headIds = [
|
|||||||
'min-lvl',
|
'min-lvl',
|
||||||
'status',
|
'status',
|
||||||
'dispatcher',
|
'dispatcher',
|
||||||
|
'dispatcher-lang',
|
||||||
'dispatcher-lvl',
|
'dispatcher-lvl',
|
||||||
'routes-single',
|
'routes-single',
|
||||||
'routes-double',
|
'routes-double',
|
||||||
|
|||||||
@@ -120,35 +120,70 @@ function filterSliderValues(filters: Record<string, any>, generalInfo: StationGe
|
|||||||
const otherAvailability =
|
const otherAvailability =
|
||||||
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
|
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
|
||||||
|
|
||||||
const internalRoutes = routes.all.filter((r) => r.isInternal && !r.isRouteSBL && !r.hidden);
|
if (filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0)) return true;
|
||||||
|
if (filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0)) return true;
|
||||||
|
if (filters['minVmax'] > routes.maxRouteSpeed) return true;
|
||||||
|
if (filters['maxVmax'] < routes.minRouteSpeed) return true;
|
||||||
|
|
||||||
return (
|
if (filters['oneWay'] && routes.singleOtherNames.length > 0) return true;
|
||||||
filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0) ||
|
if (filters['oneWayCatenary'] && routes.singleElectrifiedNames.length > 0) return true;
|
||||||
filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0) ||
|
if (filters['twoWay'] && routes.doubleOtherNames.length > 0) return true;
|
||||||
filters['minVmax'] > routes.maxRouteSpeed ||
|
if (filters['twoWayCatenary'] && routes.doubleElectrifiedNames.length > 0) return true;
|
||||||
filters['maxVmax'] < routes.minRouteSpeed ||
|
|
||||||
(filters['no-1track'] && routes.single.length != 0) ||
|
if (filters['minOneWay'] > routes.singleOtherNames.length) return true;
|
||||||
(filters['no-2track'] && routes.double.length != 0) ||
|
if (filters['maxOneWay'] < routes.singleOtherNames.length) return true;
|
||||||
filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length ||
|
if (filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length) return true;
|
||||||
filters['minOneWay'] > routes.singleOtherNames.length ||
|
if (filters['maxOneWayCatenary'] < routes.singleElectrifiedNames.length) return true;
|
||||||
filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length ||
|
if (filters['minTwoWay'] > routes.doubleOtherNames.length) return true;
|
||||||
// filters['minTwoWay'] > routes.doubleOtherNames.length ||
|
if (filters['maxTwoWay'] < routes.doubleOtherNames.length) return true;
|
||||||
filters['minOneWayCatenaryInt'] >
|
if (filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length) return true;
|
||||||
internalRoutes.filter((r) => r.routeTracks == 1 && r.isElectric == true).length ||
|
if (filters['maxTwoWayCatenary'] < routes.doubleElectrifiedNames.length) return true;
|
||||||
filters['minOneWayInt'] >
|
|
||||||
internalRoutes.filter((r) => r.routeTracks == 1 && r.isElectric == false).length ||
|
if (filters['oneWayInt'] && routes.singleOtherInternalNames.length > 0) return true;
|
||||||
filters['minTwoWayCatenaryInt'] >
|
if (filters['oneWayCatenaryInt'] && routes.singleElectrifiedInternalNames.length > 0) return true;
|
||||||
internalRoutes.filter((r) => r.routeTracks == 2 && r.isElectric == true).length
|
if (filters['twoWayInt'] && routes.doubleOtherInternalNames.length > 0) return true;
|
||||||
);
|
if (filters['twoWayCatenaryInt'] && routes.doubleElectrifiedInternalNames.length > 0) return true;
|
||||||
|
|
||||||
|
// Internal routes
|
||||||
|
if (filters['minOneWayInt'] > routes.singleOtherInternalNames.length) return true;
|
||||||
|
if (filters['maxOneWayInt'] < routes.singleOtherInternalNames.length) return true;
|
||||||
|
if (filters['minOneWayCatenaryInt'] > routes.singleElectrifiedInternalNames.length) return true;
|
||||||
|
if (filters['maxOneWayCatenaryInt'] < routes.singleElectrifiedInternalNames.length) return true;
|
||||||
|
|
||||||
|
if (filters['minTwoWayInt'] > routes.doubleOtherInternalNames.length) return true;
|
||||||
|
if (filters['maxTwoWayInt'] < routes.doubleOtherInternalNames.length) return true;
|
||||||
|
if (filters['minTwoWayCatenaryInt'] > routes.doubleElectrifiedInternalNames.length) return true;
|
||||||
|
if (filters['maxTwoWayCatenaryInt'] < routes.doubleElectrifiedInternalNames.length) return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
|
||||||
return (
|
if (
|
||||||
filters['authors'].length > 3 &&
|
filters['authors'].length > 3 &&
|
||||||
!generalInfo.authors
|
generalInfo.authors &&
|
||||||
?.map((a) => a.toLocaleLowerCase())
|
!generalInfo.authors.some(
|
||||||
.includes(filters['authors'].toLocaleLowerCase())
|
(a) => a.toLocaleLowerCase() == filters['authors'].toLocaleLowerCase()
|
||||||
);
|
)
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (filters['projects'].length > 0 && generalInfo.project != filters['projects']) return true;
|
||||||
|
|
||||||
|
if (filters['lines'].length > 0) {
|
||||||
|
const linesNumbers = (filters['lines'] as string)
|
||||||
|
.split(',')
|
||||||
|
.map((l) => Number(l))
|
||||||
|
.filter((l) => !isNaN(l) && l != 0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!generalInfo.lines
|
||||||
|
?.split(',')
|
||||||
|
.map((l) => Number(l))
|
||||||
|
.some((l) => linesNumbers.includes(l))
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
||||||
@@ -187,6 +222,11 @@ export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
|||||||
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
|
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'dispatcher-lang':
|
||||||
|
diff =
|
||||||
|
(a.onlineInfo?.dispatcherLanguageId ?? -1) - (b.onlineInfo?.dispatcherLanguageId ?? -1);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'routes-single':
|
case 'routes-single':
|
||||||
diff =
|
diff =
|
||||||
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
|
||||||
@@ -243,7 +283,7 @@ export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
|
|||||||
return a.name.localeCompare(b.name);
|
return a.name.localeCompare(b.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const filterStations = (station: Station, filters: Record<string, any>) => {
|
export const filterStations = (station: Station, filters: Record<string, any>) => {
|
||||||
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
|
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
<div class="text--primary">
|
<div class="text--primary">
|
||||||
<b>{{ trainInfo.stockList[0] }}</b> • {{ trainInfo.length }}m •
|
<b>{{ trainInfo.stockList[0] }}</b> • {{ trainInfo.length }}m •
|
||||||
{{ (trainInfo.mass / 1000).toFixed(2) }}t
|
{{ (trainInfo.mass / 1000).toFixed(2) }}t
|
||||||
|
<span v-if="trainInfo.timetableData">
|
||||||
|
• vRJ:
|
||||||
|
{{
|
||||||
|
trainInfo.timetableData?.trainMaxSpeed ||
|
||||||
|
getStockSpeedLimit(trainInfo.stockList, trainInfo.mass)
|
||||||
|
}}km/h
|
||||||
|
</span>
|
||||||
|
<span v-else class="text--grayed font--italic">
|
||||||
|
• vMax:
|
||||||
|
{{ getStockSpeedLimit(trainInfo.stockList, trainInfo.mass) }}km/h
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text--grayed">
|
<div class="text--grayed">
|
||||||
@@ -46,9 +57,7 @@ export default defineComponent({
|
|||||||
return this.mainStore.trainList.find((t) => t.id === this.tooltipStore.content);
|
return this.mainStore.trainList.find((t) => t.id === this.tooltipStore.content);
|
||||||
},
|
},
|
||||||
|
|
||||||
lastSceneryStatus() {
|
lastSceneryStatus() {}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vehicle-props" v-if="vehicleData">
|
<div class="vehicle-props" v-if="vehicleGroup">
|
||||||
{{ vehicleData.group.speed }}km/h • {{ vehicleData.group.length }}m •
|
{{ vehicleGroup.speed }}km/h • {{ vehicleGroup.length }}m •
|
||||||
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
|
{{ (vehicleGroup.weight / 1000).toFixed(1) }}t
|
||||||
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,12 +73,18 @@ export default defineComponent({
|
|||||||
return this.tooltipStore.content.split(':')[0];
|
return this.tooltipStore.content.split(':')[0];
|
||||||
},
|
},
|
||||||
|
|
||||||
vehicleData() {
|
vehicleGroup() {
|
||||||
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
|
if (!this.apiStore.vehiclesData) return null;
|
||||||
|
|
||||||
|
const vehicle = this.apiStore.vehiclesData.vehicles.find((v) => v.name == this.vehicleName);
|
||||||
|
|
||||||
|
if (!vehicle) return null;
|
||||||
|
|
||||||
|
return this.apiStore.vehiclesData.vehicleGroups.find((g) => g.id == vehicle?.vehicleGroupsId);
|
||||||
},
|
},
|
||||||
|
|
||||||
vehicleCargo() {
|
vehicleCargo() {
|
||||||
const x = this.vehicleData?.group.cargoTypes?.find(
|
const x = this.vehicleGroup?.cargoTypes?.find(
|
||||||
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
(c) => c.id == this.tooltipStore.content.split(':')[1]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,10 @@
|
|||||||
|
|
||||||
<span v-else>{{ train.driverName }}</span>
|
<span v-else>{{ train.driverName }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="train-language-flag">
|
||||||
|
<FlagIcon :language-id="train.driverLanguageId" width="1.75em" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,7 +139,11 @@
|
|||||||
<img src="/images/icon-speed.svg" alt="speed icon" />
|
<img src="/images/icon-speed.svg" alt="speed icon" />
|
||||||
{{ train.speed }} km/h
|
{{ train.speed }} km/h
|
||||||
|
|
||||||
<span v-if="stockSpeedLimit != Infinity">
|
<span v-if="train.timetableData">
|
||||||
|
• vRJ: {{ train.timetableData.trainMaxSpeed }} km/h
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-else-if="stockSpeedLimit != Infinity">
|
||||||
•
|
•
|
||||||
<em
|
<em
|
||||||
class="text--grayed"
|
class="text--grayed"
|
||||||
@@ -195,10 +203,11 @@ import trainInfoMixin from '../../mixins/trainInfoMixin';
|
|||||||
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
import trainCategoryMixin from '../../mixins/trainCategoryMixin';
|
||||||
import ProgressBar from '../Global/ProgressBar.vue';
|
import ProgressBar from '../Global/ProgressBar.vue';
|
||||||
import StockList from '../Global/StockList.vue';
|
import StockList from '../Global/StockList.vue';
|
||||||
|
import FlagIcon from '../Global/FlagIcon.vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
|
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
|
||||||
components: { ProgressBar, StockList },
|
components: { ProgressBar, StockList, FlagIcon },
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
train: {
|
train: {
|
||||||
@@ -219,57 +228,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
stockSpeedLimit() {
|
stockSpeedLimit() {
|
||||||
let isPassenger = true;
|
return this.getStockSpeedLimit(this.train.stockList, this.train.mass);
|
||||||
|
|
||||||
// Check the whole consist speed limit
|
|
||||||
const vehicleMaxSpeed = this.train.stockList.reduce((acc, stockName, i) => {
|
|
||||||
const [vehicleName, vehicleCargo] = stockName.split(':');
|
|
||||||
|
|
||||||
const vehicleData = this.apiStore.vehiclesData?.find((v) => v.name == vehicleName);
|
|
||||||
|
|
||||||
if (!vehicleData) return acc;
|
|
||||||
|
|
||||||
let vehicleSpeed = vehicleData.group.speed;
|
|
||||||
|
|
||||||
if (vehicleData.type == 'wagon-freight') {
|
|
||||||
isPassenger = false;
|
|
||||||
|
|
||||||
if (vehicleCargo !== undefined && vehicleData.group.speedLoaded) {
|
|
||||||
vehicleSpeed = vehicleData.group.speedLoaded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.min(vehicleSpeed, acc);
|
|
||||||
}, Infinity);
|
|
||||||
|
|
||||||
// Check the head vehicle speed limit
|
|
||||||
const headLocoName = this.train.stockList[0];
|
|
||||||
const headLocoVehicleData = this.apiStore.vehiclesData?.find((v) => v.name == headLocoName);
|
|
||||||
|
|
||||||
// Omit speed check for head vehicle if there's no data for it
|
|
||||||
if (!headLocoName || !headLocoVehicleData || !headLocoVehicleData.group.massSpeeds)
|
|
||||||
return vehicleMaxSpeed;
|
|
||||||
|
|
||||||
const massSpeeds =
|
|
||||||
headLocoVehicleData.group.massSpeeds[
|
|
||||||
this.train.stockList.length == 1 ? 'none' : isPassenger ? 'passenger' : 'cargo'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Omit speed check if there's no data on mass speeds
|
|
||||||
if (!massSpeeds) return vehicleMaxSpeed;
|
|
||||||
|
|
||||||
// Number type for locomotives alone
|
|
||||||
if (typeof massSpeeds === 'number') return massSpeeds;
|
|
||||||
|
|
||||||
// Record type for passenger or cargo, find the closest range
|
|
||||||
const massKey = Object.keys(massSpeeds).findLast(
|
|
||||||
(massKey) => this.train.mass >= Number(massKey)
|
|
||||||
);
|
|
||||||
|
|
||||||
const massMaxSpeed = massKey ? massSpeeds[massKey] : Infinity;
|
|
||||||
|
|
||||||
return Math.min(massMaxSpeed, vehicleMaxSpeed);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
journalRouteLocation() {
|
journalRouteLocation() {
|
||||||
return {
|
return {
|
||||||
path: '/journal/timetables',
|
path: '/journal/timetables',
|
||||||
|
|||||||
@@ -210,6 +210,10 @@ export default defineComponent({
|
|||||||
@use '../../styles/dropdown';
|
@use '../../styles/dropdown';
|
||||||
@use '../../styles/dropdown-filters';
|
@use '../../styles/dropdown-filters';
|
||||||
|
|
||||||
|
.dropdown_wrapper {
|
||||||
|
top: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.search_content > div {
|
.search_content > div {
|
||||||
margin: 0.5em auto;
|
margin: 0.5em auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
|
|
||||||
<transition name="dropdown-anim">
|
<transition name="dropdown-anim">
|
||||||
<div class="dropdown_wrapper" v-if="showOptions">
|
<div class="dropdown_wrapper" v-if="showOptions">
|
||||||
<h1 class="text--primary">
|
<h2 class="stats-title text--primary">
|
||||||
<img src="/images/icon-stats.svg" alt="Open filters icon" height="28" />
|
<img src="/images/icon-stats.svg" alt="Open filters icon" height="28" />
|
||||||
{{ $t('train-stats.title') }}
|
{{ $t('train-stats.title') }}
|
||||||
</h1>
|
</h2>
|
||||||
|
|
||||||
<hr style="margin: 0.5em 0" />
|
<hr style="margin: 0.5em 0" />
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ export default defineComponent({
|
|||||||
@use '../../styles/badge';
|
@use '../../styles/badge';
|
||||||
@use '../../styles/responsive';
|
@use '../../styles/responsive';
|
||||||
|
|
||||||
h1 img {
|
.stats-title img {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,14 +250,15 @@ h3 {
|
|||||||
|
|
||||||
.dropdown_wrapper {
|
.dropdown_wrapper {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
|
top: 2.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.no-data {
|
.no-data {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.stats-title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export default defineComponent({
|
|||||||
@use '../../styles/animations';
|
@use '../../styles/animations';
|
||||||
|
|
||||||
.train-table {
|
.train-table {
|
||||||
height: calc(100vh - 11em);
|
height: calc(100vh - 17em);
|
||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<TrainInfo :train="train" />
|
<TrainInfo :train="train" />
|
||||||
|
|
||||||
<div class="train-stats">
|
<div class="train-stats">
|
||||||
<StockList :trainStockList="train.stockList" :tractionOnly="true" />
|
<StockList :trainStockList="train.stockList" :tractionOnly="true" :showPreviews="true" />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span>{{ train.speed }}km/h</span>
|
<span>{{ train.speed }}km/h</span>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export function calculateExpStyles(exp: number, isSupporter = false) {
|
||||||
|
const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666';
|
||||||
|
|
||||||
|
const fontColor = exp > 14 || exp == -1 ? 'white' : 'black';
|
||||||
|
const boxShadow = isSupporter ? `0 0 6px 2px ${bgColor};` : '';
|
||||||
|
|
||||||
|
return { 'background-color': bgColor, color: fontColor, 'box-shadow': boxShadow };
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
export function calculateDuration(timestampMs: number) {
|
||||||
|
const secondsTotal = Math.floor(timestampMs / 1000);
|
||||||
|
const minsTotal = Math.round(timestampMs / 60000);
|
||||||
|
const hoursTotal = Math.floor(minsTotal / 60);
|
||||||
|
const minsInHour = minsTotal % 60;
|
||||||
|
|
||||||
|
return {
|
||||||
|
secondsTotal,
|
||||||
|
minsTotal,
|
||||||
|
hoursTotal,
|
||||||
|
minsInHour
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function humanizeDuration(timestampMs: number, showSeconds = false) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const duration = calculateDuration(timestampMs);
|
||||||
|
|
||||||
|
return duration.minsTotal >= 60
|
||||||
|
? `${t('journal.hours', { value: duration.hoursTotal }, duration.hoursTotal)} ${t(
|
||||||
|
'journal.minutes',
|
||||||
|
{ value: duration.minsInHour },
|
||||||
|
duration.minsInHour
|
||||||
|
)}`
|
||||||
|
: showSeconds && duration.secondsTotal <= 60
|
||||||
|
? t('journal.seconds', { value: duration.secondsTotal }, duration.secondsTotal)
|
||||||
|
: t('journal.minutes', { value: duration.minsTotal }, duration.minsTotal);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateToLocaleString(date: Date, dateOptions: Intl.DateTimeFormatOptions) {
|
||||||
|
const { locale } = useI18n();
|
||||||
|
|
||||||
|
return date.toLocaleString(locale.value == 'pl' ? 'pl-PL' : 'en-GB', dateOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timestampToTimeString(timestamp: number) {
|
||||||
|
return new Date(timestamp).toLocaleTimeString('pl-PL', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export class HttpClient {
|
||||||
|
constructor(private readonly baseURL: string) {}
|
||||||
|
|
||||||
|
async get<T>(url: string, params?: Record<string, any>): Promise<T> {
|
||||||
|
const absoluteURL = new URL(this.baseURL + '/' + url);
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
Object.keys(params).forEach((key) => {
|
||||||
|
if (params[key] === undefined) return;
|
||||||
|
|
||||||
|
absoluteURL.searchParams.append(key, params[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetch(absoluteURL);
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
throw new Error(`Cannot fetch ${absoluteURL}: ${data.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.json();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,35 @@ import plLang from './locales/pl.json';
|
|||||||
|
|
||||||
import { createI18n } from 'vue-i18n';
|
import { createI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
function customRule(choice: number, choicesLength: number) {
|
||||||
|
if (choice === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const teen = choice > 10 && choice < 20;
|
||||||
|
const endsWithOne = choice % 10 === 1;
|
||||||
|
|
||||||
|
if (!teen && endsWithOne) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return choicesLength < 4 ? 2 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
legacy: false,
|
legacy: false,
|
||||||
warnHtmlMessage: false,
|
warnHtmlMessage: false,
|
||||||
fallbackLocale: 'pl',
|
fallbackLocale: 'pl',
|
||||||
|
|
||||||
|
pluralizationRules: {
|
||||||
|
pl: customRule
|
||||||
|
},
|
||||||
|
|
||||||
messages: {
|
messages: {
|
||||||
en: enLang,
|
en: enLang,
|
||||||
pl: plLang
|
pl: plLang
|
||||||
|
|||||||
@@ -56,11 +56,12 @@
|
|||||||
"refresh": "REFRESH"
|
"refresh": "REFRESH"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"title": "Stacjownik update!",
|
"title": "Stacjownik has been updated!",
|
||||||
"confirm": "ROGER THAT!",
|
"confirm": "ROGER THAT!",
|
||||||
"no-data": "No data about the latest app update has been found",
|
"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-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>"
|
"info-2": "The full app changelog available on {link}",
|
||||||
|
"info-2-link-text": "the project's GitHub page"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIES",
|
"sceneries": "SCENERIES",
|
||||||
@@ -76,10 +77,9 @@
|
|||||||
"tooltip-driver-offline": "Driver is offline",
|
"tooltip-driver-offline": "Driver is offline",
|
||||||
"tooltip-scenery-offline": "Scenery is offline",
|
"tooltip-scenery-offline": "Scenery is offline",
|
||||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||||
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR"
|
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||||
},
|
"gnr-link-content": "TRAIN ORDERS <br> GENERATOR",
|
||||||
"footer": {
|
"discord-link-content": "STACJOWNIK <br> DISCORD SERVER"
|
||||||
"discord": "Stacjownik Discord server"
|
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"EI": "domestic express",
|
"EI": "domestic express",
|
||||||
@@ -186,9 +186,11 @@
|
|||||||
"search-train": "Train no. / #",
|
"search-train": "Train no. / #",
|
||||||
"select-driver": "Choose a driver...",
|
"select-driver": "Choose a driver...",
|
||||||
"search-driver": "Driver name",
|
"search-driver": "Driver name",
|
||||||
|
"search-duty-id": "Duty ID",
|
||||||
"search-dispatcher": "Dispatcher name",
|
"search-dispatcher": "Dispatcher name",
|
||||||
"search-station": "Scenery name / #",
|
"search-station": "Scenery name / #",
|
||||||
"search-author": "Timetable author name",
|
"search-author": "Timetable author name",
|
||||||
|
"search-includesScenery": "Includes scenery name",
|
||||||
"search-issuedFrom": "Issuing scenery name",
|
"search-issuedFrom": "Issuing scenery name",
|
||||||
"search-via": "Via scenery name",
|
"search-via": "Via scenery name",
|
||||||
"search-terminatingAt": "Terminating scenery name",
|
"search-terminatingAt": "Terminating scenery name",
|
||||||
@@ -197,6 +199,7 @@
|
|||||||
"search-date-from": "Date (UTC+2 / CEST)",
|
"search-date-from": "Date (UTC+2 / CEST)",
|
||||||
"search-date-to": "Date (UTC+2 / CEST)",
|
"search-date-to": "Date (UTC+2 / CEST)",
|
||||||
"select-categoryCode": "Train category",
|
"select-categoryCode": "Train category",
|
||||||
|
"search-headUnit": "Traction unit (e.g. EP09, ET22-401)",
|
||||||
"sort-mass": "mass",
|
"sort-mass": "mass",
|
||||||
"sort-speed": "speed",
|
"sort-speed": "speed",
|
||||||
"sort-length": "length",
|
"sort-length": "length",
|
||||||
@@ -247,7 +250,9 @@
|
|||||||
"blockades": "BLOCK SIGNALLING",
|
"blockades": "BLOCK SIGNALLING",
|
||||||
"status": "ONLINE STATUS",
|
"status": "ONLINE STATUS",
|
||||||
"timetables": "ACTIVE TIMETABLES",
|
"timetables": "ACTIVE TIMETABLES",
|
||||||
"spawns": "OPEN SPAWNS"
|
"spawns": "OPEN SPAWNS",
|
||||||
|
"externalRoutes": "EXTERNAL ROUTES",
|
||||||
|
"internalRoutes": "INTERNAL ROUTES"
|
||||||
},
|
},
|
||||||
"changed-filters-count": "Changed filters:",
|
"changed-filters-count": "Changed filters:",
|
||||||
"no-changed-filters": "No changed filters",
|
"no-changed-filters": "No changed filters",
|
||||||
@@ -271,6 +276,7 @@
|
|||||||
"SCS": "SCS",
|
"SCS": "SCS",
|
||||||
"SCS-R": "SCS + MANUAL",
|
"SCS-R": "SCS + MANUAL",
|
||||||
"SCS-M": "SCS + MECH.",
|
"SCS-M": "SCS + MECH.",
|
||||||
|
"SCS-SPK": "SCS + SPK",
|
||||||
"SPE": "SPE",
|
"SPE": "SPE",
|
||||||
"manual": "MANUAL",
|
"manual": "MANUAL",
|
||||||
"mechanical": "MECHANICAL",
|
"mechanical": "MECHANICAL",
|
||||||
@@ -290,30 +296,37 @@
|
|||||||
"withoutActiveTimetables": "NO ACTIVE",
|
"withoutActiveTimetables": "NO ACTIVE",
|
||||||
"junction": "JUNCTIONS",
|
"junction": "JUNCTIONS",
|
||||||
"nonJunction": "OTHER",
|
"nonJunction": "OTHER",
|
||||||
|
|
||||||
|
"oneWay": "OTHER SINGLE TRACK",
|
||||||
|
"oneWayCatenary": "CATENARY SINGLE TRACK",
|
||||||
|
"twoWayCatenary": "CATENARY DOUBLE TRACK",
|
||||||
|
"twoWay": "OTHER DOUBLE TRACK",
|
||||||
|
"oneWayCatenaryInt": "CATENARY SINGLE TRACK",
|
||||||
|
"oneWayInt": "OTHER SINGLE TRACK",
|
||||||
|
"twoWayCatenaryInt": "CATENARY DOUBLE TRACK",
|
||||||
|
"twoWayInt": "OTHER DOUBLE TRACK",
|
||||||
|
|
||||||
"sliders": {
|
"sliders": {
|
||||||
"minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
|
"vMax": "ROUTE SPEED",
|
||||||
"maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
|
"level": "REQUIRED DISPATCHER LEVEL",
|
||||||
"minVmax": "MIN. SCENERY ROUTE SPEED",
|
"routeOneWay": "SINGLE TRACK ROUTES (OTHER)",
|
||||||
"maxVmax": "MAX. SCENERY ROUTE SPEED",
|
"routeOneWayCatenary": "SINGLE TRACK ROUTES (CATENARY)",
|
||||||
"minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
|
"routeTwoWayCatenary": "DOUBLE TRACK ROUTES (CATENARY)",
|
||||||
"minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
|
"routeTwoWay": "DOUBLE TRACK ROUTES (OTHER)",
|
||||||
"minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
|
"routeOneWayInternalCatenary": "INTERNAL SINGLE TRACK ROUTES (CATENARY)",
|
||||||
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES",
|
"routeOneWayInternal": "INTERNAL SINGLE TRACK ROUTES (OTHER)",
|
||||||
"minOneWayCatenaryInt": "MIN. INTERNAL CATENARY SINGLE TRACK ROUTES",
|
"routeTwoWayInternalCatenary": "INTERNAL DOUBLE TRACK ROUTES (CATENARY)",
|
||||||
"minOneWayInt": "MIN. INTERNAL OTHER SINGLE TRACK ROUTES",
|
"routeTwoWayInternal": "INTERNAL DOUBLE TRACK ROUTES (OTHER)"
|
||||||
"minTwoWayCatenaryInt": "MIN. INTERNAL CATENARY DOUBLE TRACK ROUTES",
|
|
||||||
"minTwoWayInt": "MIN. INTERNAL OTHER DOUBLE TRACK ROUTES"
|
|
||||||
},
|
},
|
||||||
"sceneries-search": "SCENERY SEARCH:",
|
"sceneries-placeholder": "Search for scenery",
|
||||||
"sceneries-placeholder": "Enter scenery name...",
|
"line-numbers-placeholder": "Line numbers (separated by commas)",
|
||||||
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
|
"authors-placeholder": "Scenery author",
|
||||||
"authors-placeholder": "Enter the author nickname...",
|
"projects-placeholder": "Scenery project",
|
||||||
"search-button-title": "SEARCH",
|
"search-button-title": "SEARCH",
|
||||||
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
|
"minimum-hours-title": "SHOW ONLY SCENERIES UNTIL:",
|
||||||
"now": "NOW",
|
"now": "NOW",
|
||||||
"hour": "h",
|
"hour": "h",
|
||||||
"no-limit": "NO LIMIT",
|
"no-limit": "NO LIMIT",
|
||||||
"include-selected": "INCLUDE SELECTED",
|
|
||||||
"save": "REMEMBER FILTERS",
|
"save": "REMEMBER FILTERS",
|
||||||
"reset": "RESET FILTERS",
|
"reset": "RESET FILTERS",
|
||||||
"close": "CLOSE FILTERS"
|
"close": "CLOSE FILTERS"
|
||||||
@@ -324,6 +337,7 @@
|
|||||||
"min-lvl": "Scenery\nlevel",
|
"min-lvl": "Scenery\nlevel",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"dispatcher": "Dispatcher",
|
"dispatcher": "Dispatcher",
|
||||||
|
"dispatcher-lang": "Language",
|
||||||
"dispatcher-lvl": "Dispatcher\nlevel",
|
"dispatcher-lvl": "Dispatcher\nlevel",
|
||||||
"routes-single": "1-track\nroutes",
|
"routes-single": "1-track\nroutes",
|
||||||
"routes-double": "2-track\nroutes",
|
"routes-double": "2-track\nroutes",
|
||||||
@@ -414,7 +428,7 @@
|
|||||||
"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!",
|
||||||
"driver-journal-link": "DRIVER JOURNAL",
|
"driver-profile-link": "PLAYER'S PROFILE",
|
||||||
"driver-srjp-link": "SRJP",
|
"driver-srjp-link": "SRJP",
|
||||||
"driver-return-link": "RETURN",
|
"driver-return-link": "RETURN",
|
||||||
"driver-not-found-header": "Train not found! :/",
|
"driver-not-found-header": "Train not found! :/",
|
||||||
@@ -545,7 +559,7 @@
|
|||||||
"no-users": "NO ACTIVE PLAYERS",
|
"no-users": "NO ACTIVE PLAYERS",
|
||||||
"no-spawns": "NO OPEN SPAWNS",
|
"no-spawns": "NO OPEN SPAWNS",
|
||||||
"no-scenery": "Oops! This scenery doesn't exist!",
|
"no-scenery": "Oops! This scenery doesn't exist!",
|
||||||
"return-btn": "BACK TO THE LAST SITE",
|
"return-btn": "BACK TO THE MAIN SITE",
|
||||||
"history-btn": "View the dispatcher history",
|
"history-btn": "View the dispatcher history",
|
||||||
"info-btn": "Return to the scenery view",
|
"info-btn": "Return to the scenery view",
|
||||||
"authors-title": "Scenery author | Scenery authors",
|
"authors-title": "Scenery author | Scenery authors",
|
||||||
@@ -555,27 +569,45 @@
|
|||||||
"additional-tools-title": "Additional tools",
|
"additional-tools-title": "Additional tools",
|
||||||
"one-way-routes": "Single track routes",
|
"one-way-routes": "Single track routes",
|
||||||
"two-way-routes": "Double track routes",
|
"two-way-routes": "Double track routes",
|
||||||
|
"routes-hidden": "Hidden internal routes",
|
||||||
"no-data": "No available data about this scenery",
|
"no-data": "No available data about this scenery",
|
||||||
"option-active-timetables": "Active timetables",
|
"option-active-timetables": "Active timetables",
|
||||||
"option-timetables-history": "Timetables history PL1",
|
"option-timetables-history": "Timetables history PL1",
|
||||||
"option-dispatchers-history": "Dispatchers history PL1",
|
"option-dispatchers-history": "Dispatchers history PL1",
|
||||||
"timetable-via": "ALL TIMETABLES",
|
"option-top-list": "Scenery records",
|
||||||
|
"btn-show-timetable-thumbnails": "Show rolling stock thumbnails",
|
||||||
|
"btn-hide-timetable-thumbnails": "Hide rolling stock thumbnails",
|
||||||
|
"timetable-includesScenery": "ALL TIMETABLES",
|
||||||
|
"timetable-via": "PASSES THROUGH",
|
||||||
"timetable-issuedFrom": "BEGINS HERE",
|
"timetable-issuedFrom": "BEGINS HERE",
|
||||||
"timetable-terminatingAt": "TERMINATES HERE",
|
"timetable-terminatingAt": "ENDS HERE",
|
||||||
"timetable-issued-date": "Issued",
|
"timetable-issued-date": "Issued",
|
||||||
"timetable-issued-by": " by:",
|
"timetable-issued-by": " by:",
|
||||||
"timetable-issued-for": " for driver:",
|
"timetable-issued-for": " for:",
|
||||||
"dispatcher-rate": "Rate:",
|
"dispatcher-rate": "Rate:",
|
||||||
"dispatcher-status-changes": "Status changes:",
|
"dispatcher-status-changes": "Status changes:",
|
||||||
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
|
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
|
||||||
"history-list-empty": "No recorded scenery history!",
|
"history-list-empty": "No recorded scenery history!",
|
||||||
"forum-topic": "Official {name} forum topic",
|
"forum-topic": "Scenery's forum topic",
|
||||||
"gnr-link": "Train orders generator",
|
"gnr-link": "Train orders generator",
|
||||||
"pragotron-link": "Timetable pallet board",
|
"pragotron-link": "Timetable pallet board",
|
||||||
"tablice-link": "Timetable summary board <br> (by Thundo)",
|
"tablice-link": "Timetable summary board <br> (by Thundo)",
|
||||||
"bottom-info": "Show full history in the Journal tab",
|
"bottom-info": "Show full history in the Journal tab",
|
||||||
"btn-show-internal-routes": "Show internal routes",
|
"btn-show-internal-routes": "Show internal routes",
|
||||||
"btn-hide-internal-routes": "Hide internal routes"
|
"btn-hide-internal-routes": "Hide internal routes",
|
||||||
|
"top-list": {
|
||||||
|
"header": "RECORDS ON THE SCENERY (PL1)",
|
||||||
|
"mode-dutyCount": "DUTIES",
|
||||||
|
"mode-dispatcherRating": "RATING",
|
||||||
|
"mode-dutyDuration": "DUTY DURATION",
|
||||||
|
"scope-name": "GENERAL",
|
||||||
|
"scope-hash": "CURRENT HASH",
|
||||||
|
|
||||||
|
"place": "{n}. place",
|
||||||
|
"dispatcher-rating": "Rating: {n}",
|
||||||
|
"duty-count": "No duties | 1 duty | Duties: {n}",
|
||||||
|
"duration": "Duration:"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"availability": {
|
"availability": {
|
||||||
"title": "Availability",
|
"title": "Availability",
|
||||||
@@ -604,9 +636,54 @@
|
|||||||
"desc-end": "The train terminates here",
|
"desc-end": "The train terminates here",
|
||||||
"desc-terminated": "The train has been terminated"
|
"desc-terminated": "The train has been terminated"
|
||||||
},
|
},
|
||||||
"history": {
|
"profile": {
|
||||||
"title": "TIMETABLE JOURNAL",
|
"journal-button": "PLAYER'S PROFILE",
|
||||||
"search-train": "Train no.",
|
"no-player-found": "Player not found! :/",
|
||||||
"search-driver": "Driver name"
|
"return-to-main": "Return to the main site",
|
||||||
|
|
||||||
|
"filters": {
|
||||||
|
"Timetable": "TIMETABLES",
|
||||||
|
"Dispatcher": "DISPATCHER DUTIES",
|
||||||
|
"IssuedTimetable": "ISSUED TIMETABLES"
|
||||||
|
},
|
||||||
|
|
||||||
|
"stats": {
|
||||||
|
"timetables-journal": "TIMETABLE JOURNAL",
|
||||||
|
"dispatchers-journal": "DISPATCHER JOURNAL",
|
||||||
|
"forum-profile": "FORUM PROFILE",
|
||||||
|
|
||||||
|
"driver": "DRIVER",
|
||||||
|
"dispatcher": "DISPATCHER",
|
||||||
|
|
||||||
|
"header-driver": "DRIVER'S STATS",
|
||||||
|
"fulfilled-timetables": "fulfilled timetables",
|
||||||
|
"route-distance": "confirmed timetables distance",
|
||||||
|
"confirmed-stops": "confirmed stations in timetables",
|
||||||
|
"longest-timetable": "longest timetable",
|
||||||
|
"avg-timetable-length": "average distance of all timetables",
|
||||||
|
"no-timetable-stats": "This player does not have any registered timetables in Stacjownik!",
|
||||||
|
|
||||||
|
"header-dispatcher": "DISPATCHER'S STATS",
|
||||||
|
"duties-count": "duties as dispatcher",
|
||||||
|
"longest-duty": "longest duty",
|
||||||
|
"created-timetables-count": "issued timetables as dispatcher",
|
||||||
|
"longest-created-timetable": "longest issued timetable",
|
||||||
|
"created-timetables-length-sum": "distance sum of issued timetables",
|
||||||
|
"no-dispatcher-stats": "No registered dispatcher duties in Stacjownik!"
|
||||||
|
},
|
||||||
|
|
||||||
|
"recent-stats": {
|
||||||
|
"header": "ACTIVITY STATISTICS (30 LAST DAYS)",
|
||||||
|
"timetables": "TIMETABLES",
|
||||||
|
"distance": "MADE KILOMETERS",
|
||||||
|
"duties": "DISPATCHER DUTIES",
|
||||||
|
"created-timetables": "ISSUED TIMETABLES"
|
||||||
|
},
|
||||||
|
|
||||||
|
"list": {
|
||||||
|
"for": "for",
|
||||||
|
"online-since": "online since",
|
||||||
|
"no-recent-history": "No recent activity in the simulator :("
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,11 +56,12 @@
|
|||||||
"refresh": "ODŚWIEŻ"
|
"refresh": "ODŚWIEŻ"
|
||||||
},
|
},
|
||||||
"update": {
|
"update": {
|
||||||
"title": "Aktualizacja Stacjownika!",
|
"title": "Stacjownik został zaktualizowany!",
|
||||||
"confirm": "PRZYJĄŁEM!",
|
"confirm": "PRZYJĄŁEM!",
|
||||||
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
|
"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-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>"
|
"info-2": "Pełny changelog dostępny na {link}",
|
||||||
|
"info-2-link-text": "GitHubie projektu"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"sceneries": "SCENERIE",
|
"sceneries": "SCENERIE",
|
||||||
@@ -73,10 +74,9 @@
|
|||||||
"tooltip-driver-offline": "Maszynista offline",
|
"tooltip-driver-offline": "Maszynista offline",
|
||||||
"tooltip-scenery-offline": "Sceneria offline",
|
"tooltip-scenery-offline": "Sceneria offline",
|
||||||
"pojazdownik-link-content": "POJAZDOWNIK",
|
"pojazdownik-link-content": "POJAZDOWNIK",
|
||||||
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH"
|
"language-tooltip-content": "JĘZYK / LANGUAGE",
|
||||||
},
|
"gnr-link-content": "GENERATOR <br> ROZKAZÓW PISEMNYCH",
|
||||||
"footer": {
|
"discord-link-content": "SERWER DISCORD <br> STACJOWNIKA"
|
||||||
"discord": "Serwer Discord Stacjownika"
|
|
||||||
},
|
},
|
||||||
"categories": {
|
"categories": {
|
||||||
"EI": "ekspres krajowy",
|
"EI": "ekspres krajowy",
|
||||||
@@ -183,9 +183,11 @@
|
|||||||
"search-train": "Nr pociągu / #",
|
"search-train": "Nr pociągu / #",
|
||||||
"search-driver": "Nick maszynisty",
|
"search-driver": "Nick maszynisty",
|
||||||
"select-driver": "Wybierz maszynistę...",
|
"select-driver": "Wybierz maszynistę...",
|
||||||
|
"search-duty-id": "ID służby",
|
||||||
"search-dispatcher": "Nick dyżurnego",
|
"search-dispatcher": "Nick dyżurnego",
|
||||||
"search-station": "Nazwa scenerii / #",
|
"search-station": "Nazwa scenerii / #",
|
||||||
"search-author": "Nick autora rozkładu jazdy",
|
"search-author": "Nick autora rozkładu jazdy",
|
||||||
|
"search-includesScenery": "Zawiera scenerię",
|
||||||
"search-issuedFrom": "Sceneria początkowa",
|
"search-issuedFrom": "Sceneria początkowa",
|
||||||
"search-via": "Przez scenerię",
|
"search-via": "Przez scenerię",
|
||||||
"search-terminatingAt": "Sceneria końcowa",
|
"search-terminatingAt": "Sceneria końcowa",
|
||||||
@@ -194,6 +196,7 @@
|
|||||||
"search-date-from": "Data (UTC+2 / CEST)",
|
"search-date-from": "Data (UTC+2 / CEST)",
|
||||||
"search-date-to": "Data (UTC+2 / CEST)",
|
"search-date-to": "Data (UTC+2 / CEST)",
|
||||||
"select-categoryCode": "Kategoria pociągu",
|
"select-categoryCode": "Kategoria pociągu",
|
||||||
|
"search-headUnit": "Pojazd trakcyjny (np. EP09, ET22-137)",
|
||||||
"sort-routeDistance": "kilometraż",
|
"sort-routeDistance": "kilometraż",
|
||||||
"sort-allStopsCount": "stacje",
|
"sort-allStopsCount": "stacje",
|
||||||
"sort-beginDate": "data",
|
"sort-beginDate": "data",
|
||||||
@@ -245,7 +248,9 @@
|
|||||||
"blockades": "BLOKADY LINIOWE",
|
"blockades": "BLOKADY LINIOWE",
|
||||||
"status": "STATUS ONLINE",
|
"status": "STATUS ONLINE",
|
||||||
"timetables": "AKTYWNE ROZKŁADY JAZDY",
|
"timetables": "AKTYWNE ROZKŁADY JAZDY",
|
||||||
"spawns": "OTWARTE SPAWNY"
|
"spawns": "OTWARTE SPAWNY",
|
||||||
|
"externalRoutes": "SZLAKI ZEWNĘTRZNE",
|
||||||
|
"internalRoutes": "SZLAKI WEWNĘTRZNE"
|
||||||
},
|
},
|
||||||
"changed-filters-count": "Zmienione filtry:",
|
"changed-filters-count": "Zmienione filtry:",
|
||||||
"no-changed-filters": "Brak zmienionych filtrów",
|
"no-changed-filters": "Brak zmienionych filtrów",
|
||||||
@@ -269,6 +274,7 @@
|
|||||||
"SCS": "SCS",
|
"SCS": "SCS",
|
||||||
"SCS-R": "SCS + RĘCZNE",
|
"SCS-R": "SCS + RĘCZNE",
|
||||||
"SCS-M": "SCS + MECH.",
|
"SCS-M": "SCS + MECH.",
|
||||||
|
"SCS-SPK": "SCS + SPK",
|
||||||
"SPE": "SPE",
|
"SPE": "SPE",
|
||||||
"manual": "RĘCZNE",
|
"manual": "RĘCZNE",
|
||||||
"SUP": "SUP (RASP-UZK)",
|
"SUP": "SUP (RASP-UZK)",
|
||||||
@@ -288,30 +294,37 @@
|
|||||||
"withoutActiveTimetables": "BEZ AKTYWNYCH",
|
"withoutActiveTimetables": "BEZ AKTYWNYCH",
|
||||||
"junction": "WĘZŁOWE",
|
"junction": "WĘZŁOWE",
|
||||||
"nonJunction": "INNE",
|
"nonJunction": "INNE",
|
||||||
|
|
||||||
|
"oneWay": "JEDNOTOROWE NIEZELEKTRYFIKOWANE",
|
||||||
|
"oneWayCatenary": "JEDNOTOROWE ZELEKTRYFIKOWANE",
|
||||||
|
"twoWayCatenary": "DWUTOROWE ZELEKTRYFIKOWANE",
|
||||||
|
"twoWay": "DWUTOROWE NIEZELEKTRYFIKOWANE",
|
||||||
|
"oneWayCatenaryInt": "JEDNOTOROWE ZELEKTRYFIKOWANE",
|
||||||
|
"oneWayInt": "JEDNOTOROWE NIEZELEKTRYFIKOWANE",
|
||||||
|
"twoWayCatenaryInt": "DWUTOROWE ZELEKTRYFIKOWANE",
|
||||||
|
"twoWayInt": "DWUTOROWE NIEZELEKTRYFIKOWANE",
|
||||||
|
|
||||||
"sliders": {
|
"sliders": {
|
||||||
"minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
|
"vMax": "PRĘDKOŚĆ SZLAKOWA",
|
||||||
"maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
|
"level": "WYMAGANY POZIOM DYŻURNEGO",
|
||||||
"minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
|
"routeOneWay": "SZLAKI 1-TOROWE NIEZELEKTR.",
|
||||||
"maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
|
"routeOneWayCatenary": "SZLAKI 1-TOROWE ZELEKTR.",
|
||||||
"minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
|
"routeTwoWayCatenary": "SZLAKI 2-TOROWE ZELEKTR.",
|
||||||
"minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
|
"routeTwoWay": "SZLAKI 2-TOROWE NIEZELEKTR.",
|
||||||
"minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
|
"routeOneWayInternalCatenary": "SZLAKI WEWN. 1-TOROWE ZELEKTR.",
|
||||||
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)",
|
"routeOneWayInternal": "SZLAKI WEWN. 1-TOROWE NIEZELEKTR.",
|
||||||
"minOneWayCatenaryInt": "SZLAKI JEDNOTOROWE ZELEKTR. WEWNĘTRZNE (MINIMUM)",
|
"routeTwoWayInternalCatenary": "SZLAKI WEWN. 2-TOROWE ZELEKTR.",
|
||||||
"minOneWayInt": "SZLAKI JEDNOTOROWE NIEZELEKTR. WEWNĘTRZNE (MINIMUM)",
|
"routeTwoWayInternal": "SZLAKI WEWN. 2-TOROWE NIEZELEKTR."
|
||||||
"minTwoWayCatenaryInt": "SZLAKI DWUTOROWE ZELEKTR. WEWNĘTRZNE (MINIMUM)",
|
|
||||||
"minTwoWayInt": "SZLAKI DWUTOROWE NIEZELEKTR. WEWNĘTRZNE (MINIMUM)"
|
|
||||||
},
|
},
|
||||||
"sceneries-search": "WYSZUKAJ SCENERIĘ:",
|
"sceneries-placeholder": "Wyszukaj scenerię",
|
||||||
"sceneries-placeholder": "Wpisz nazwę scenerii...",
|
"line-numbers-placeholder": "Numery linii (oddzielone przecinkami)",
|
||||||
"authors-search": "WYSZUKAJ AUTORA (uwzględnia inne filtry):",
|
"authors-placeholder": "Autor scenerii",
|
||||||
"authors-placeholder": "Wpisz nick autora...",
|
"projects-placeholder": "Projekt scenerii",
|
||||||
"search-button-title": "SZUKAJ",
|
"search-button-title": "SZUKAJ",
|
||||||
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
"minimum-hours-title": "POKAŻ TYLKO SCENERIE DOSTĘPNE MINIMUM DO:",
|
||||||
"now": "TERAZ",
|
"now": "TERAZ",
|
||||||
"hour": " godz.",
|
"hour": " godz.",
|
||||||
"no-limit": "BEZ LIMITU",
|
"no-limit": "BEZ LIMITU",
|
||||||
"include-selected": "POKAŻ ZAZNACZONE",
|
|
||||||
"save": "ZAPAMIĘTAJ FILTRY",
|
"save": "ZAPAMIĘTAJ FILTRY",
|
||||||
"reset": "RESETUJ FILTRY",
|
"reset": "RESETUJ FILTRY",
|
||||||
"close": "ZAMKNIJ FILTRY"
|
"close": "ZAMKNIJ FILTRY"
|
||||||
@@ -322,6 +335,7 @@
|
|||||||
"min-lvl": "Poziom\nscenerii",
|
"min-lvl": "Poziom\nscenerii",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"dispatcher": "Dyżurny",
|
"dispatcher": "Dyżurny",
|
||||||
|
"dispatcher-lang": "Język",
|
||||||
"dispatcher-lvl": "Poziom\ndyżurnego",
|
"dispatcher-lvl": "Poziom\ndyżurnego",
|
||||||
"routes-single": "Szlaki\n1-torowe",
|
"routes-single": "Szlaki\n1-torowe",
|
||||||
"routes-double": "Szlaki\n2-torowe",
|
"routes-double": "Szlaki\n2-torowe",
|
||||||
@@ -401,7 +415,7 @@
|
|||||||
"last-seen-ago": "od {minutes} minut",
|
"last-seen-ago": "od {minutes} minut",
|
||||||
"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",
|
||||||
"driver-journal-link": "DZIENNIK MASZYNISTY",
|
"driver-profile-link": "PROFIL GRACZA",
|
||||||
"driver-srjp-link": "SRJP",
|
"driver-srjp-link": "SRJP",
|
||||||
"driver-return-link": "POWRÓT",
|
"driver-return-link": "POWRÓT",
|
||||||
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
"driver-not-found-header": "Nie znaleziono pociągu! :/",
|
||||||
@@ -531,7 +545,7 @@
|
|||||||
"no-users": "BRAK AKTYWNYCH GRACZY",
|
"no-users": "BRAK AKTYWNYCH GRACZY",
|
||||||
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
|
"no-spawns": "BRAK OTWARTYCH SPAWNÓW",
|
||||||
"no-scenery": "Ups! Ta sceneria nie istnieje!",
|
"no-scenery": "Ups! Ta sceneria nie istnieje!",
|
||||||
"return-btn": "POWRÓT DO POPRZEDNIEJ STRONY",
|
"return-btn": "POWRÓT DO STRONY GŁÓWNEJ",
|
||||||
"history-btn": "Przejdź do widoku historii dyżurnych ruchu",
|
"history-btn": "Przejdź do widoku historii dyżurnych ruchu",
|
||||||
"info-btn": "Wróć do widoku scenerii",
|
"info-btn": "Wróć do widoku scenerii",
|
||||||
"authors-title": "Autor scenerii | Autorzy scenerii",
|
"authors-title": "Autor scenerii | Autorzy scenerii",
|
||||||
@@ -541,27 +555,45 @@
|
|||||||
"additional-tools-title": "Dodatkowe narzędzia",
|
"additional-tools-title": "Dodatkowe narzędzia",
|
||||||
"one-way-routes": "Szlaki jednotorowe",
|
"one-way-routes": "Szlaki jednotorowe",
|
||||||
"two-way-routes": "Szlaki dwutorowe",
|
"two-way-routes": "Szlaki dwutorowe",
|
||||||
|
"routes-hidden": "Ukryto szlaki wewnętrzne",
|
||||||
"no-data": "Brak informacji o tej scenerii",
|
"no-data": "Brak informacji o tej scenerii",
|
||||||
"option-active-timetables": "Aktywne rozkłady jazdy",
|
"option-active-timetables": "Aktywne rozkłady jazdy",
|
||||||
"option-timetables-history": "Historia rozkładów PL1",
|
"option-timetables-history": "Historia rozkładów PL1",
|
||||||
"option-dispatchers-history": "Historia dyżurów PL1",
|
"option-dispatchers-history": "Historia dyżurów PL1",
|
||||||
"timetable-via": "WSZYSTKIE RJ",
|
"option-top-list": "Rekordy scenerii",
|
||||||
|
"btn-show-timetable-thumbnails": "Pokazuj podglądy składów",
|
||||||
|
"btn-hide-timetable-thumbnails": "Ukrywaj podglądy składów",
|
||||||
|
"timetable-includesScenery": "WSZYSTKIE RJ",
|
||||||
|
"timetable-via": "PRZEJEŻDŻA",
|
||||||
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
|
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
|
||||||
"timetable-terminatingAt": "KOŃCZY BIEG",
|
"timetable-terminatingAt": "KOŃCZY BIEG",
|
||||||
"timetable-issued-date": "Wystawiony",
|
"timetable-issued-date": "Wystawiony: ",
|
||||||
"timetable-issued-by": " przez:",
|
"timetable-issued-by": " przez:",
|
||||||
"timetable-issued-for": " dla maszynisty:",
|
"timetable-issued-for": " dla:",
|
||||||
"dispatcher-rate": "Ocena:",
|
"dispatcher-rate": "Ocena:",
|
||||||
"dispatcher-status-changes": "Zmiany statusów:",
|
"dispatcher-status-changes": "Zmiany statusów:",
|
||||||
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
|
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
|
||||||
"history-list-empty": "Brak historii dla tej scenerii!",
|
"history-list-empty": "Brak historii dla tej scenerii!",
|
||||||
"forum-topic": "Oficjalny wątek scenerii {name}",
|
"forum-topic": "Wątek scenerii",
|
||||||
"gnr-link": "Generator rozkazów pisemnych",
|
"gnr-link": "Generator rozkazów pisemnych",
|
||||||
"pragotron-link": "Paletowa tablica informacyjna",
|
"pragotron-link": "Paletowa tablica informacyjna",
|
||||||
"tablice-link": "Tablica informacyjna zbiorcza <br> (autorstwa Thundo)",
|
"tablice-link": "Tablica informacyjna zbiorcza <br> (autorstwa Thundo)",
|
||||||
"bottom-info": "Pokaż pełną historię w zakładce Dziennika",
|
"bottom-info": "Pokaż pełną historię w zakładce Dziennika",
|
||||||
"btn-show-internal-routes": "Pokazuj szlaki wewnętrzne",
|
"btn-show-internal-routes": "Pokazuj szlaki wewnętrzne",
|
||||||
"btn-hide-internal-routes": "Ukrywaj szlaki wewnętrzne"
|
"btn-hide-internal-routes": "Ukrywaj szlaki wewnętrzne",
|
||||||
|
"top-list": {
|
||||||
|
"header": "REKORDY NA SCENERII (PL1)",
|
||||||
|
"mode-dutyCount": "DYŻURY",
|
||||||
|
"mode-dispatcherRating": "OCENA",
|
||||||
|
"mode-dutyDuration": "CZAS DYŻURU",
|
||||||
|
"scope-name": "OGÓLNIE",
|
||||||
|
"scope-hash": "OBECNY HASH",
|
||||||
|
|
||||||
|
"place": "{n}. miejsce",
|
||||||
|
"dispatcher-rating": "Ocena: {n}",
|
||||||
|
"duty-count": "Brak dyżurów | 1 dyżur | Dyżury: {n}",
|
||||||
|
"duration": "Czas:"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"availability": {
|
"availability": {
|
||||||
"title": "Dostępność",
|
"title": "Dostępność",
|
||||||
@@ -590,7 +622,54 @@
|
|||||||
"desc-end": "Pociąg kończy bieg",
|
"desc-end": "Pociąg kończy bieg",
|
||||||
"desc-terminated": "Pociąg zakończył bieg"
|
"desc-terminated": "Pociąg zakończył bieg"
|
||||||
},
|
},
|
||||||
"history": {
|
"profile": {
|
||||||
"title": "DZIENNIK ROZKŁADÓW JAZDY"
|
"journal-button": "PROFIL GRACZA",
|
||||||
|
"no-player-found": "Nie znaleziono gracza! :/",
|
||||||
|
"return-to-main": "Powrót do strony głównej",
|
||||||
|
|
||||||
|
"filters": {
|
||||||
|
"Timetable": "ROZKŁADY JAZDY",
|
||||||
|
"Dispatcher": "SŁUŻBY DYŻURNEGO",
|
||||||
|
"IssuedTimetable": "WYSTAWIONE RJ"
|
||||||
|
},
|
||||||
|
|
||||||
|
"stats": {
|
||||||
|
"timetables-journal": "DZIENNIK RJ",
|
||||||
|
"dispatchers-journal": "DZIENNIK DR",
|
||||||
|
"forum-profile": "PROFIL FORUM",
|
||||||
|
|
||||||
|
"driver": "MASZYNISTA",
|
||||||
|
"dispatcher": "DYŻURNY RUCHU",
|
||||||
|
|
||||||
|
"header-driver": "STATYSTYKI MASZYNISTY",
|
||||||
|
"fulfilled-timetables": "wypełnione rozkłady jazdy",
|
||||||
|
"route-distance": "zatwierdzony kilometraż w RJ",
|
||||||
|
"confirmed-stops": "potwierdzonych stacji w RJ",
|
||||||
|
"longest-timetable": "najdłuższy rozkład jazdy",
|
||||||
|
"avg-timetable-length": "średnia długość wszystkich rozkładów",
|
||||||
|
"no-timetable-stats": "Ten użytkownik nie posiada statystyk maszynisty zarejestrowanych przez Stacjownik!",
|
||||||
|
|
||||||
|
"header-dispatcher": "STATYSTYKI DYŻURNEGO RUCHU",
|
||||||
|
"duties-count": "służby jako dyżurny ruchu",
|
||||||
|
"longest-duty": "najdłuższa służba",
|
||||||
|
"created-timetables-count": "wystawione RJ jako dyżurny ruchu",
|
||||||
|
"longest-created-timetable": "najdłuższy wystawiony RJ",
|
||||||
|
"created-timetables-length-sum": "suma długości wystawionych RJ",
|
||||||
|
"no-dispatcher-stats": "Ten użytkownik nie posiada statystyk dyżurnego zarejestrowanych przez Stacjownik!"
|
||||||
|
},
|
||||||
|
|
||||||
|
"recent-stats": {
|
||||||
|
"header": "STATYSTYKI AKTYWNOŚCI (30 DNI)",
|
||||||
|
"timetables": "ROZKŁADÓW JAZDY",
|
||||||
|
"distance": "POKONANYCH KILOMETRÓW",
|
||||||
|
"duties": "SŁUŻB DYŻURNEGO",
|
||||||
|
"created-timetables": "WYSTAWIONYCH ROZKŁADÓW"
|
||||||
|
},
|
||||||
|
|
||||||
|
"list": {
|
||||||
|
"for": "dla",
|
||||||
|
"online-since": "online od",
|
||||||
|
"no-recent-history": "Brak ostatniej aktywności w symulatorze :("
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,24 @@
|
|||||||
import StorageManager from './storageManager';
|
import StorageManager from './storageManager';
|
||||||
|
|
||||||
|
export type SliderGroup =
|
||||||
|
| 'vMax'
|
||||||
|
| 'level'
|
||||||
|
| 'routeOneWay'
|
||||||
|
| 'routeOneWayCatenary'
|
||||||
|
| 'routeOneWayInternal'
|
||||||
|
| 'routeOneWayInternalCatenary'
|
||||||
|
| 'routeTwoWay'
|
||||||
|
| 'routeTwoWayCatenary'
|
||||||
|
| 'routeTwoWayInternal'
|
||||||
|
| 'routeTwoWayInternalCatenary';
|
||||||
|
|
||||||
|
export interface SliderOptions {
|
||||||
|
id: string;
|
||||||
|
minRange: number;
|
||||||
|
maxRange: number;
|
||||||
|
step: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const sections = [
|
export const sections = [
|
||||||
'status',
|
'status',
|
||||||
'timetables',
|
'timetables',
|
||||||
@@ -10,7 +29,9 @@ export const sections = [
|
|||||||
'control',
|
'control',
|
||||||
'blockades',
|
'blockades',
|
||||||
'signals',
|
'signals',
|
||||||
'addons'
|
'addons',
|
||||||
|
'externalRoutes',
|
||||||
|
'internalRoutes'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const initFilters = {
|
export const initFilters = {
|
||||||
@@ -31,15 +52,13 @@ export const initFilters = {
|
|||||||
mechanical: false,
|
mechanical: false,
|
||||||
'SPK-M': false,
|
'SPK-M': false,
|
||||||
'SCS-M': false,
|
'SCS-M': false,
|
||||||
|
'SCS-SPK': false,
|
||||||
modern: false,
|
modern: false,
|
||||||
semaphores: false,
|
semaphores: false,
|
||||||
historical: false,
|
historical: false,
|
||||||
mixed: false,
|
mixed: false,
|
||||||
SBL: false,
|
SBL: false,
|
||||||
PBL: false,
|
PBL: false,
|
||||||
'include-selected': false,
|
|
||||||
'no-1track': false,
|
|
||||||
'no-2track': false,
|
|
||||||
free: true,
|
free: true,
|
||||||
occupied: false,
|
occupied: false,
|
||||||
nonPublic: false,
|
nonPublic: false,
|
||||||
@@ -59,31 +78,111 @@ export const initFilters = {
|
|||||||
onlineFromHours: 0,
|
onlineFromHours: 0,
|
||||||
minLevel: 0,
|
minLevel: 0,
|
||||||
maxLevel: 20,
|
maxLevel: 20,
|
||||||
|
oneWay: false,
|
||||||
|
oneWayCatenary: false,
|
||||||
|
twoWay: false,
|
||||||
|
twoWayCatenary: false,
|
||||||
|
oneWayCatenaryInt: false,
|
||||||
|
oneWayInt: false,
|
||||||
|
twoWayInt: false,
|
||||||
|
twoWayCatenaryInt: false,
|
||||||
minOneWay: 0,
|
minOneWay: 0,
|
||||||
minOneWayCatenary: 0,
|
minOneWayCatenary: 0,
|
||||||
minTwoWayCatenary: 0,
|
|
||||||
minOneWayInt: 0,
|
|
||||||
minOneWayCatenaryInt: 0,
|
minOneWayCatenaryInt: 0,
|
||||||
|
minOneWayInt: 0,
|
||||||
|
minTwoWay: 0,
|
||||||
|
minTwoWayCatenary: 0,
|
||||||
|
minTwoWayInt: 0,
|
||||||
minTwoWayCatenaryInt: 0,
|
minTwoWayCatenaryInt: 0,
|
||||||
// minTwoWay: 0,
|
maxOneWay: 10,
|
||||||
authors: ''
|
maxOneWayCatenary: 10,
|
||||||
|
maxOneWayInt: 20,
|
||||||
|
maxOneWayCatenaryInt: 20,
|
||||||
|
maxTwoWay: 10,
|
||||||
|
maxTwoWayCatenary: 10,
|
||||||
|
maxTwoWayInt: 20,
|
||||||
|
maxTwoWayCatenaryInt: 20,
|
||||||
|
authors: '',
|
||||||
|
projects: '',
|
||||||
|
lines: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sliderStates = [
|
export const sliderGroups: SliderGroup[] = [
|
||||||
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 },
|
'vMax',
|
||||||
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
|
'level',
|
||||||
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
|
'routeOneWayCatenary',
|
||||||
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 },
|
'routeOneWay',
|
||||||
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
|
'routeTwoWayCatenary',
|
||||||
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
|
'routeTwoWay',
|
||||||
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
|
'routeOneWayInternalCatenary',
|
||||||
{ id: 'minOneWayInt', minRange: 0, maxRange: 5, step: 1 },
|
'routeOneWayInternal',
|
||||||
{ id: 'minOneWayCatenaryInt', minRange: 0, maxRange: 5, step: 1 },
|
'routeTwoWayInternalCatenary',
|
||||||
{ id: 'minTwoWayCatenaryInt', minRange: 0, maxRange: 5, step: 1 },
|
'routeTwoWayInternal'
|
||||||
// { id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 },
|
|
||||||
// { id: 'minTwoWayInt', minRange: 0, maxRange: 5, step: 1 }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const sliderGroupsOptions: Record<SliderGroup, SliderOptions[]> = {
|
||||||
|
vMax: [
|
||||||
|
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 20 },
|
||||||
|
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 20 }
|
||||||
|
],
|
||||||
|
level: [
|
||||||
|
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
|
||||||
|
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 }
|
||||||
|
],
|
||||||
|
routeOneWay: [
|
||||||
|
{ id: 'minOneWay', minRange: 0, maxRange: 10, step: 1 },
|
||||||
|
{ id: 'maxOneWay', minRange: 0, maxRange: 10, step: 1 }
|
||||||
|
],
|
||||||
|
routeOneWayCatenary: [
|
||||||
|
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 10, step: 1 },
|
||||||
|
{ id: 'maxOneWayCatenary', minRange: 0, maxRange: 10, step: 1 }
|
||||||
|
],
|
||||||
|
routeOneWayInternal: [
|
||||||
|
{ id: 'minOneWayInt', minRange: 0, maxRange: 20, step: 1 },
|
||||||
|
{ id: 'maxOneWayInt', minRange: 0, maxRange: 20, step: 1 }
|
||||||
|
],
|
||||||
|
routeOneWayInternalCatenary: [
|
||||||
|
{
|
||||||
|
id: 'minOneWayCatenaryInt',
|
||||||
|
minRange: 0,
|
||||||
|
maxRange: 20,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'maxOneWayCatenaryInt',
|
||||||
|
minRange: 0,
|
||||||
|
maxRange: 20,
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
routeTwoWay: [
|
||||||
|
{ id: 'minTwoWay', minRange: 0, maxRange: 10, step: 1 },
|
||||||
|
{ id: 'maxTwoWay', minRange: 0, maxRange: 10, step: 1 }
|
||||||
|
],
|
||||||
|
routeTwoWayCatenary: [
|
||||||
|
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 10, step: 1 },
|
||||||
|
{ id: 'maxTwoWayCatenary', minRange: 0, maxRange: 10, step: 1 }
|
||||||
|
],
|
||||||
|
routeTwoWayInternal: [
|
||||||
|
{ id: 'minTwoWayInt', minRange: 0, maxRange: 20, step: 1 },
|
||||||
|
{ id: 'maxTwoWayInt', minRange: 0, maxRange: 20, step: 1 }
|
||||||
|
],
|
||||||
|
routeTwoWayInternalCatenary: [
|
||||||
|
{
|
||||||
|
id: 'minTwoWayCatenaryInt',
|
||||||
|
minRange: 0,
|
||||||
|
maxRange: 20,
|
||||||
|
step: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'maxTwoWayCatenaryInt',
|
||||||
|
minRange: 0,
|
||||||
|
maxRange: 20,
|
||||||
|
step: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
export type StationFilter = keyof typeof initFilters;
|
export type StationFilter = keyof typeof initFilters;
|
||||||
export type StationFilterSection = (typeof sections)[number];
|
export type StationFilterSection = (typeof sections)[number];
|
||||||
|
|
||||||
@@ -95,9 +194,22 @@ export const filtersSections: Record<StationFilterSection, StationFilter[]> = {
|
|||||||
stationType: ['junction', 'nonJunction'],
|
stationType: ['junction', 'nonJunction'],
|
||||||
access: ['nonPublic', 'unavailable', 'abandoned'],
|
access: ['nonPublic', 'unavailable', 'abandoned'],
|
||||||
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
|
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
|
||||||
control: ['SPK', 'SCS', 'SPE', 'SPK-M', 'SCS-M', 'mechanical', 'SPK-R', 'SCS-R', 'manual'],
|
control: [
|
||||||
|
'SPK',
|
||||||
|
'SCS',
|
||||||
|
'SPE',
|
||||||
|
'SCS-SPK',
|
||||||
|
'SPK-M',
|
||||||
|
'SCS-M',
|
||||||
|
'mechanical',
|
||||||
|
'SPK-R',
|
||||||
|
'SCS-R',
|
||||||
|
'manual'
|
||||||
|
],
|
||||||
blockades: ['SBL', 'PBL'],
|
blockades: ['SBL', 'PBL'],
|
||||||
signals: ['modern', 'semaphores', 'mixed', 'historical']
|
signals: ['modern', 'semaphores', 'mixed', 'historical'],
|
||||||
|
externalRoutes: ['oneWayCatenary', 'oneWay', 'twoWayCatenary', 'twoWay'],
|
||||||
|
internalRoutes: ['oneWayCatenaryInt', 'oneWayInt', 'twoWayCatenaryInt', 'twoWayInt']
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupFilters(currentFilters: Record<string, any>) {
|
export function setupFilters(currentFilters: Record<string, any>) {
|
||||||
@@ -116,11 +228,12 @@ export function setupFilters(currentFilters: Record<string, any>) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
|
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
|
||||||
return (
|
return (
|
||||||
Object.keys(currentFilters).filter(
|
Object.keys(currentFilters).filter(
|
||||||
(filterKey) =>
|
(filterKey) =>
|
||||||
currentFilters[filterKey] !== initFilters[filterKey as keyof typeof initFilters]
|
currentFilters[filterKey].toString() !==
|
||||||
|
initFilters[filterKey as keyof typeof initFilters].toString()
|
||||||
) ?? []
|
) ?? []
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,10 @@
|
|||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { Train, TrainStop } from '../typings/common';
|
import { Train, TrainStop } from '../typings/common';
|
||||||
|
import { useApiStore } from '../store/apiStore';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
STATS: {
|
apiStore: useApiStore()
|
||||||
main: [
|
|
||||||
{
|
|
||||||
name: 'speed',
|
|
||||||
unit: 'km/h'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'length',
|
|
||||||
unit: 'm'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'mass',
|
|
||||||
unit: 't',
|
|
||||||
multiplier: 0.001
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
position: [
|
|
||||||
{
|
|
||||||
name: 'scenery',
|
|
||||||
prop: 'currentStationName'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'route',
|
|
||||||
prop: 'connectedTrack'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'signal',
|
|
||||||
prop: 'signal'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'distance',
|
|
||||||
prop: 'distance',
|
|
||||||
unit: 'm'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
@@ -150,6 +115,74 @@ 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`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getStockSpeedLimit(stockList: string[], trainMass: number) {
|
||||||
|
let isPassenger = true;
|
||||||
|
|
||||||
|
// Check the whole consist speed limit
|
||||||
|
const vehicleMaxSpeed = stockList.reduce((acc, stockName, i) => {
|
||||||
|
if (!this.apiStore.vehiclesData) return acc;
|
||||||
|
|
||||||
|
const [vehicleName, vehicleCargo] = stockName.split(':');
|
||||||
|
|
||||||
|
const vehicle = this.apiStore.vehiclesData.vehicles.find((v) => v.name == vehicleName);
|
||||||
|
|
||||||
|
if (!vehicle) return acc;
|
||||||
|
|
||||||
|
const vehicleGroup = this.apiStore.vehiclesData.vehicleGroups.find(
|
||||||
|
(g) => g.id == vehicle.vehicleGroupsId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!vehicleGroup) return acc;
|
||||||
|
|
||||||
|
let vehicleSpeed = vehicleGroup.speed;
|
||||||
|
|
||||||
|
if (vehicle.type == 'wagon-freight') {
|
||||||
|
isPassenger = false;
|
||||||
|
|
||||||
|
if (vehicleCargo !== undefined && vehicleGroup.speedLoaded) {
|
||||||
|
vehicleSpeed = vehicleGroup.speedLoaded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(vehicleSpeed, acc);
|
||||||
|
}, Infinity);
|
||||||
|
|
||||||
|
// Check the head vehicle speed limit
|
||||||
|
const headLocoName = stockList[0];
|
||||||
|
|
||||||
|
const headLocoVehicle = this.apiStore.vehiclesData!.vehicles.find(
|
||||||
|
(v) => v.name == headLocoName
|
||||||
|
);
|
||||||
|
|
||||||
|
const headLocoVehicleGroup = this.apiStore.vehiclesData!.vehicleGroups.find(
|
||||||
|
(g) => g.id == headLocoVehicle?.vehicleGroupsId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!headLocoVehicleGroup) return vehicleMaxSpeed;
|
||||||
|
|
||||||
|
// Omit speed check for head vehicle if there's no data for it
|
||||||
|
if (!headLocoName || !headLocoVehicle || !headLocoVehicleGroup.massSpeeds)
|
||||||
|
return vehicleMaxSpeed;
|
||||||
|
|
||||||
|
const massSpeeds =
|
||||||
|
headLocoVehicleGroup.massSpeeds[
|
||||||
|
stockList.length == 1 ? 'none' : isPassenger ? 'passenger' : 'cargo'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Omit speed check if there's no data on mass speeds
|
||||||
|
if (!massSpeeds) return vehicleMaxSpeed;
|
||||||
|
|
||||||
|
// Number type for locomotives alone
|
||||||
|
if (typeof massSpeeds === 'number') return massSpeeds;
|
||||||
|
|
||||||
|
// Record type for passenger or cargo, find the closest range
|
||||||
|
const massKey = Object.keys(massSpeeds).findLast((massKey) => trainMass >= Number(massKey));
|
||||||
|
|
||||||
|
const massMaxSpeed = massKey ? massSpeeds[massKey] : Infinity;
|
||||||
|
|
||||||
|
return Math.min(massMaxSpeed, vehicleMaxSpeed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
region: route.query.region
|
region: route.query.region
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'PlayerProfileView',
|
||||||
|
component: () => import('../views/PlayerProfileView.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/:catchAll(.*)',
|
path: '/:catchAll(.*)',
|
||||||
redirect: '/'
|
redirect: '/'
|
||||||
@@ -70,12 +75,12 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
scrollBehavior(to, from, savedPosition) {
|
scrollBehavior(to, from, savedPosition) {
|
||||||
if (
|
if (
|
||||||
(to.name == 'SceneryView' || to.name == 'DriverView') &&
|
(to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') &&
|
||||||
from.name !== to.name &&
|
from.name !== to.name &&
|
||||||
from.query['view'] === undefined &&
|
from.query['view'] === undefined &&
|
||||||
!savedPosition
|
!savedPosition
|
||||||
)
|
)
|
||||||
return { el: `.app_main`, behavior: 'instant', top: -13 };
|
return { el: `.app_main`, behavior: 'smooth', top: 0 };
|
||||||
|
|
||||||
if (savedPosition) return savedPosition;
|
if (savedPosition) return savedPosition;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,49 +2,49 @@ import { defineStore } from 'pinia';
|
|||||||
import { API } from '../typings/api';
|
import { API } from '../typings/api';
|
||||||
import { Status } from '../typings/common';
|
import { Status } from '../typings/common';
|
||||||
import { StationJSONData } from './typings';
|
import { StationJSONData } from './typings';
|
||||||
import axios, { AxiosInstance } from 'axios';
|
import { HttpClient } from '../http';
|
||||||
|
|
||||||
|
let baseURL = 'https://stacjownik.spythere.eu';
|
||||||
|
|
||||||
|
switch (import.meta.env.VITE_API_MODE) {
|
||||||
|
case 'development':
|
||||||
|
baseURL = 'http://localhost:3001';
|
||||||
|
break;
|
||||||
|
case 'mocking':
|
||||||
|
baseURL = 'http://localhost:3123';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
export const useApiStore = defineStore('apiStore', {
|
export const useApiStore = defineStore('apiStore', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
dataStatuses: {
|
dataStatuses: {
|
||||||
|
allData: Status.Data.Loading,
|
||||||
connection: Status.Data.Loading,
|
connection: Status.Data.Loading,
|
||||||
sceneries: Status.Data.Loading,
|
sceneries: Status.Data.Loading,
|
||||||
vehicles: Status.Data.Loading
|
vehicles: Status.Data.Loading,
|
||||||
|
dailyStatsData: Status.Data.Loading
|
||||||
},
|
},
|
||||||
|
|
||||||
activeData: undefined as API.ActiveData.Response | undefined,
|
activeData: undefined as API.ActiveData.Response | undefined,
|
||||||
vehiclesData: undefined as API.Vehicles.Response | undefined,
|
vehiclesData: undefined as API.VehiclesData.Response | undefined,
|
||||||
|
|
||||||
donatorsData: [] as API.Donators.Response,
|
donatorsData: [] as API.Donators.Response,
|
||||||
sceneryData: [] as StationJSONData[],
|
sceneryData: [] as StationJSONData[],
|
||||||
|
|
||||||
|
dailyStatsData: null as API.DailyStats.Response | null,
|
||||||
|
|
||||||
nextUpdateTime: 0,
|
nextUpdateTime: 0,
|
||||||
nextDataCheckTime: 0,
|
nextDataCheckTime: 0,
|
||||||
|
|
||||||
client: undefined as AxiosInstance | undefined,
|
client: new HttpClient(baseURL),
|
||||||
|
|
||||||
activeDataScheduler: undefined as number | undefined
|
activeDataScheduler: undefined as number | undefined
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
async setupAPIData() {
|
async setupAPIData() {
|
||||||
let baseURL = 'https://stacjownik.spythere.eu';
|
|
||||||
|
|
||||||
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();
|
this.connectToAPI();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -52,32 +52,35 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
window.requestAnimationFrame(this.updateTick);
|
window.requestAnimationFrame(this.updateTick);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTick(t: number) {
|
async updateTick(t: number) {
|
||||||
// Static data refresh
|
// Static data refresh
|
||||||
if (t >= this.nextDataCheckTime) {
|
if (t >= this.nextDataCheckTime) {
|
||||||
this.fetchDonatorsData();
|
await Promise.all([
|
||||||
this.fetchVehiclesInfo();
|
this.fetchStationsGeneralInfo(),
|
||||||
this.fetchStationsGeneralInfo();
|
this.fetchVehiclesInfo(),
|
||||||
|
this.fetchDonatorsData()
|
||||||
|
]);
|
||||||
|
|
||||||
this.nextDataCheckTime = t + 3600000;
|
this.nextDataCheckTime = t + 3600000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active data fefresh
|
// Active data fefresh
|
||||||
if (t >= this.nextUpdateTime) {
|
if (t >= this.nextUpdateTime) {
|
||||||
this.fetchActiveData();
|
await this.fetchActiveData();
|
||||||
this.nextUpdateTime = t + 20000;
|
this.nextUpdateTime = t + 31000;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.requestAnimationFrame(this.updateTick);
|
window.requestAnimationFrame(this.updateTick);
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchActiveData() {
|
async fetchActiveData() {
|
||||||
|
if (this.dataStatuses.connection == Status.Data.Offline) return;
|
||||||
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
|
const response = await this.client.get<API.ActiveData.Response>('api/getActiveData');
|
||||||
|
|
||||||
this.activeData = response.data;
|
this.activeData = response;
|
||||||
this.dataStatuses.connection = Status.Data.Loaded;
|
this.dataStatuses.connection = Status.Data.Loaded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatuses.connection = Status.Data.Error;
|
this.dataStatuses.connection = Status.Data.Error;
|
||||||
@@ -87,9 +90,9 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
|
|
||||||
async fetchDonatorsData() {
|
async fetchDonatorsData() {
|
||||||
try {
|
try {
|
||||||
const response = await this.client!.get<API.Donators.Response>('api/getDonators');
|
const response = await this.client.get<API.Donators.Response>('api/getDonators');
|
||||||
|
|
||||||
this.donatorsData = response.data;
|
this.donatorsData = response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
|
console.error('Ups! Wystąpił błąd podczas pobierania informacji o donatorach:', error);
|
||||||
}
|
}
|
||||||
@@ -97,9 +100,7 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
|
|
||||||
async fetchStationsGeneralInfo() {
|
async fetchStationsGeneralInfo() {
|
||||||
try {
|
try {
|
||||||
const sceneryData: StationJSONData[] = (
|
const sceneryData = await this.client.get<StationJSONData[]>(`api/getSceneries`);
|
||||||
await this.client!.get<StationJSONData[]>(`api/getSceneries`)
|
|
||||||
).data;
|
|
||||||
|
|
||||||
this.dataStatuses.sceneries = Status.Data.Loaded;
|
this.dataStatuses.sceneries = Status.Data.Loaded;
|
||||||
this.sceneryData = sceneryData;
|
this.sceneryData = sceneryData;
|
||||||
@@ -111,14 +112,27 @@ export const useApiStore = defineStore('apiStore', {
|
|||||||
|
|
||||||
async fetchVehiclesInfo() {
|
async fetchVehiclesInfo() {
|
||||||
try {
|
try {
|
||||||
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
|
const response = await this.client.get<API.VehiclesData.Response>('api/getVehiclesData');
|
||||||
|
|
||||||
this.vehiclesData = response.data;
|
this.vehiclesData = response;
|
||||||
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
|
this.dataStatuses.vehicles = response ? Status.Data.Loaded : Status.Data.Warning;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dataStatuses.vehicles = Status.Data.Error;
|
this.dataStatuses.vehicles = Status.Data.Error;
|
||||||
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
|
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchDailyStats() {
|
||||||
|
try {
|
||||||
|
const res = await this.client.get<API.DailyStats.Response>('api/getDailyStats');
|
||||||
|
|
||||||
|
this.dailyStatsData = res;
|
||||||
|
|
||||||
|
this.dataStatuses.dailyStatsData = Status.Data.Loaded;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...');
|
||||||
|
this.dataStatuses.dailyStatsData = Status.Data.Error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
} from '../typings/common';
|
} from '../typings/common';
|
||||||
import { useApiStore } from './apiStore';
|
import { useApiStore } from './apiStore';
|
||||||
import { MainStoreState } from './typings';
|
import { MainStoreState } from './typings';
|
||||||
|
import i18n from '../i18n';
|
||||||
|
import StorageManager from '../managers/storageManager';
|
||||||
|
|
||||||
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
|
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
|
||||||
const unknownSceneryCheckpoints: Map<string, Set<string>> = new Map();
|
const unknownSceneryCheckpoints: Map<string, Set<string>> = new Map();
|
||||||
@@ -24,19 +26,21 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
isOffline: false,
|
isOffline: false,
|
||||||
appUpdate: null,
|
appUpdate: null,
|
||||||
|
|
||||||
dispatcherStatsName: '',
|
|
||||||
dispatcherStatsStatus: Status.Data.Initialized,
|
|
||||||
|
|
||||||
driverStatsName: '',
|
|
||||||
driverStatsData: undefined,
|
|
||||||
driverStatsStatus: Status.Data.Initialized,
|
|
||||||
|
|
||||||
chosenModalTrainId: undefined,
|
chosenModalTrainId: undefined,
|
||||||
|
|
||||||
modalLastClickedTarget: null,
|
modalLastClickedTarget: null,
|
||||||
currentLocale: 'pl'
|
currentLocale: 'pl'
|
||||||
}) as MainStoreState,
|
}) as MainStoreState,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
changeLocale(localeName: string) {
|
||||||
|
(i18n.global.locale.value as any) = localeName;
|
||||||
|
this.currentLocale = localeName;
|
||||||
|
|
||||||
|
StorageManager.setStringValue('lang', localeName);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
trainList(): Train[] {
|
trainList(): Train[] {
|
||||||
const apiStore = useApiStore();
|
const apiStore = useApiStore();
|
||||||
@@ -73,6 +77,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
online: Boolean(train.online),
|
online: Boolean(train.online),
|
||||||
driverId: train.driverId,
|
driverId: train.driverId,
|
||||||
driverName: train.driverName,
|
driverName: train.driverName,
|
||||||
|
driverLanguageId: train.driverLanguageId,
|
||||||
currentStationName: train.currentStationName,
|
currentStationName: train.currentStationName,
|
||||||
currentStationHash: train.currentStationHash,
|
currentStationHash: train.currentStationHash,
|
||||||
connectedTrack: train.connectedTrack,
|
connectedTrack: train.connectedTrack,
|
||||||
@@ -244,6 +249,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
dispatcherIsSupporter: false,
|
dispatcherIsSupporter: false,
|
||||||
dispatcherStatus: Status.ActiveDispatcher.FREE,
|
dispatcherStatus: Status.ActiveDispatcher.FREE,
|
||||||
dispatcherTimestamp: -1,
|
dispatcherTimestamp: -1,
|
||||||
|
dispatcherLanguageId: -1,
|
||||||
|
|
||||||
isOnline: false,
|
isOnline: false,
|
||||||
|
|
||||||
@@ -290,6 +296,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
|
dispatcherIsSupporter: scenery.dispatcherIsSupporter,
|
||||||
dispatcherStatus: scenery.dispatcherStatus,
|
dispatcherStatus: scenery.dispatcherStatus,
|
||||||
dispatcherTimestamp: dispatcherTimestamp,
|
dispatcherTimestamp: dispatcherTimestamp,
|
||||||
|
dispatcherLanguageId: scenery.dispatcherLanguageId,
|
||||||
|
|
||||||
isOnline: scenery.isOnline == 1,
|
isOnline: scenery.isOnline == 1,
|
||||||
|
|
||||||
@@ -333,8 +340,12 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
const missingCheckpointsToAdd = unknownSceneryCheckpoints.get(scenery.name);
|
const missingCheckpointsToAdd = unknownSceneryCheckpoints.get(scenery.name);
|
||||||
|
|
||||||
if (missingCheckpointsToAdd) {
|
if (missingCheckpointsToAdd) {
|
||||||
checkpoints.push(...missingCheckpointsToAdd);
|
[...missingCheckpointsToAdd].forEach((cp) => {
|
||||||
scenery.missingCheckpoints.push(...missingCheckpointsToAdd);
|
if (cp.toLowerCase() == scenery.name.toLowerCase()) return;
|
||||||
|
|
||||||
|
checkpoints.push(cp);
|
||||||
|
scenery.missingCheckpoints.push(cp);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const uniqueTrainIds: string[] = [];
|
const uniqueTrainIds: string[] = [];
|
||||||
@@ -380,11 +391,13 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
|
|
||||||
const tracksKey = route.routeTracks == 2 ? 'double' : 'single';
|
const tracksKey = route.routeTracks == 2 ? 'double' : 'single';
|
||||||
const isElectric = route.isElectric;
|
const isElectric = route.isElectric;
|
||||||
|
|
||||||
const routesKey: keyof StationRoutes = `${tracksKey}${
|
const routesKey: keyof StationRoutes = `${tracksKey}${
|
||||||
!isElectric ? 'Other' : 'Electrified'
|
!isElectric ? 'Other' : 'Electrified'
|
||||||
}Names`;
|
}${route.isInternal ? 'Internal' : ''}Names`;
|
||||||
|
|
||||||
|
acc[routesKey].push(route.routeName);
|
||||||
|
|
||||||
if (!route.isInternal) acc[routesKey].push(route.routeName);
|
|
||||||
if (route.isRouteSBL) acc['sblNames'].push(route.routeName);
|
if (route.isRouteSBL) acc['sblNames'].push(route.routeName);
|
||||||
|
|
||||||
acc.minRouteSpeed =
|
acc.minRouteSpeed =
|
||||||
@@ -399,14 +412,21 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
all: [],
|
||||||
single: [],
|
single: [],
|
||||||
|
double: [],
|
||||||
|
|
||||||
singleElectrifiedNames: [],
|
singleElectrifiedNames: [],
|
||||||
singleOtherNames: [],
|
singleOtherNames: [],
|
||||||
double: [],
|
|
||||||
doubleElectrifiedNames: [],
|
doubleElectrifiedNames: [],
|
||||||
doubleOtherNames: [],
|
doubleOtherNames: [],
|
||||||
|
|
||||||
|
singleElectrifiedInternalNames: [],
|
||||||
|
singleOtherInternalNames: [],
|
||||||
|
doubleElectrifiedInternalNames: [],
|
||||||
|
doubleOtherInternalNames: [],
|
||||||
|
|
||||||
sblNames: [],
|
sblNames: [],
|
||||||
all: [],
|
|
||||||
minRouteSpeed: 0,
|
minRouteSpeed: 0,
|
||||||
maxRouteSpeed: 0
|
maxRouteSpeed: 0
|
||||||
} as StationRoutes
|
} as StationRoutes
|
||||||
@@ -414,7 +434,6 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: scenery.name,
|
name: scenery.name,
|
||||||
|
|
||||||
generalInfo: {
|
generalInfo: {
|
||||||
...scenery,
|
...scenery,
|
||||||
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
authors: scenery.authors?.split(',').map((a) => a.trim()),
|
||||||
@@ -429,7 +448,7 @@ export const useMainStore = defineStore('mainStore', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
allStationInfo(): Station[] {
|
allStationInfo(): Station[] {
|
||||||
const onlineUnsavedStations = this.activeSceneryList
|
const onlineUnsavedStations: Station[] = this.activeSceneryList
|
||||||
.filter(
|
.filter(
|
||||||
(scenery) =>
|
(scenery) =>
|
||||||
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
|
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
|
||||||
|
|||||||
@@ -5,11 +5,6 @@ export interface MainStoreState {
|
|||||||
region: { id: string; value: string; name: string };
|
region: { id: string; value: string; name: string };
|
||||||
isOffline: boolean;
|
isOffline: boolean;
|
||||||
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
|
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
|
||||||
dispatcherStatsName: string;
|
|
||||||
dispatcherStatsData?: API.DispatcherStats.Response;
|
|
||||||
driverStatsName: string;
|
|
||||||
driverStatsData?: API.DriverStats.Response;
|
|
||||||
driverStatsStatus: Status.Data;
|
|
||||||
chosenModalTrainId?: string;
|
chosenModalTrainId?: string;
|
||||||
modalLastClickedTarget: EventTarget | null;
|
modalLastClickedTarget: EventTarget | null;
|
||||||
currentLocale: string;
|
currentLocale: string;
|
||||||
@@ -23,6 +18,7 @@ export interface StationJSONData {
|
|||||||
project: string;
|
project: string;
|
||||||
projectUrl: string;
|
projectUrl: string;
|
||||||
hash: string;
|
hash: string;
|
||||||
|
hidden: boolean;
|
||||||
|
|
||||||
reqLevel: number;
|
reqLevel: number;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$animDuration: 95ms;
|
$animDuration: 120ms;
|
||||||
$animType: ease-in-out;
|
$animType: ease-in-out;
|
||||||
|
|
||||||
// List animation
|
// List animation
|
||||||
|
|||||||
@@ -135,3 +135,20 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.timetable-status-badge {
|
||||||
|
padding: 0.05em 0.35em;
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&.terminated {
|
||||||
|
background-color: salmon;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.fulfilled {
|
||||||
|
background-color: lightgreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: lightblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,7 +85,7 @@ h1.option-title {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
h1 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
.dropdown_wrapper {
|
.dropdown_wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: calc(100% + 0.5em);
|
top: 0;
|
||||||
|
|
||||||
background-color: var(--clr-bg3);
|
background-color: var(--clr-bg3);
|
||||||
box-shadow: 0 0 5px 1px var(--clr-primary);
|
box-shadow: 0 0 5px 1px var(--clr-primary);
|
||||||
@@ -34,16 +34,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 550px;
|
max-width: 550px;
|
||||||
|
|
||||||
max-height: 750px;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
|
||||||
.dropdown_wrapper {
|
|
||||||
font-size: 1.1em;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
--clr-bg2: #1b1b1b;
|
--clr-bg2: #1b1b1b;
|
||||||
--clr-bg3: #1d1d1d;
|
--clr-bg3: #1d1d1d;
|
||||||
--clr-view-bg: #1a1a1a;
|
--clr-view-bg: #1a1a1a;
|
||||||
|
--clr-bg-light: #2b2b2b;
|
||||||
|
|
||||||
|
--clr-tile: #181818;
|
||||||
|
|
||||||
--clr-accent: #1085b3;
|
--clr-accent: #1085b3;
|
||||||
--clr-accent2: #ff3d5d;
|
--clr-accent2: #ff3d5d;
|
||||||
@@ -23,6 +26,8 @@
|
|||||||
|
|
||||||
--clr-donator: #f7a4ff;
|
--clr-donator: #f7a4ff;
|
||||||
|
|
||||||
|
--clr-success: springgreen;
|
||||||
|
|
||||||
--no-scroll-padding: 17px;
|
--no-scroll-padding: 17px;
|
||||||
--max-container-width: 1700px;
|
--max-container-width: 1700px;
|
||||||
|
|
||||||
@@ -30,9 +35,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: var(--no-scroll-padding);
|
// width: var(--no-scroll-padding);
|
||||||
height: var(--no-scroll-padding);
|
// height: var(--no-scroll-padding);
|
||||||
background-color: transparent;
|
|
||||||
|
|
||||||
&-track {
|
&-track {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
@@ -49,6 +53,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--clr-bg);
|
background: var(--clr-bg);
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -89,7 +94,8 @@ select {
|
|||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input,
|
||||||
|
select {
|
||||||
background: none;
|
background: none;
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@@ -225,6 +231,12 @@ ul {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font {
|
||||||
|
&--italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
a.a-button {
|
a.a-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -324,19 +336,6 @@ a.a-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen {
|
@include responsive.smallScreen {
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 0.5em;
|
|
||||||
height: 0.5em;
|
|
||||||
|
|
||||||
&-track {
|
|
||||||
background-color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-thumb {
|
|
||||||
background-color: #777;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-tooltip]:hover::after,
|
[data-tooltip]:hover::after,
|
||||||
[data-tooltip]:focus::after {
|
[data-tooltip]:focus::after {
|
||||||
transform: translate(-50%, 2em);
|
transform: translate(-50%, 2em);
|
||||||
@@ -352,3 +351,22 @@ a.a-button {
|
|||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.g-checkbox {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
.list_wrapper {
|
.list_wrapper {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
height: calc(100vh - 12.5em);
|
height: calc(100vh - 21em);
|
||||||
min-height: 700px;
|
min-height: 500px;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
padding: 1em 0;
|
padding: 1em 0;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.journal_refreshed-date {
|
.journal_refreshed-date {
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn--load-data {
|
.btn--load-data {
|
||||||
@@ -68,7 +67,7 @@
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include responsive.smallScreen{
|
@include responsive.smallScreen {
|
||||||
.journal_top-bar {
|
.journal_top-bar {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
gap: 0.25em;
|
gap: 0.25em;
|
||||||
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&-input {
|
&-input {
|
||||||
@@ -62,7 +61,3 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
select.search-input {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||