147 Commits

Author SHA1 Message Date
Spythere ba4a068df7 Merge pull request #10 from Spythere/development
v1.1.3 hotfix
2026-04-07 01:50:47 +02:00
Spythere 9afbd3b1eb hotfix(timetable): incorrect display for departure lines; removed doubling 0.000km and excessive top border 2026-04-06 20:24:41 +02:00
Spythere d27b66cfd9 Merge pull request #9 from Spythere/development
v1.1.3
2026-04-06 15:47:02 +02:00
Spythere 8fea27f5a6 chore(release): fixed footer title 2026-04-06 15:44:27 +02:00
Spythere 1d6838ef53 chore(navbar): added link to version's github release changelog 2026-04-06 15:23:24 +02:00
Spythere ce3546745b chore(timetable): added 0.000 km at the top of timetable 2026-04-06 15:02:33 +02:00
Spythere 06e70afc29 fix(timetable): VP & VL columns width 2026-04-06 14:53:37 +02:00
Spythere cd8c29f327 chore(api): improved fetching data from api; changed to new vehicles data endpoint 2026-04-05 01:28:23 +02:00
Spythere cd22a23aef fix(timetable): light mode directional arrow style 2026-04-05 01:02:04 +02:00
Spythere 5c56c0d63f refactor(http): replaced axios to native fetch api 2026-04-05 00:59:53 +02:00
Spythere eaa7771ca7 chore(tailwind): changed purge to content 2026-04-05 00:58:28 +02:00
Spythere 99fca9f65d chore(timetable): improved directional arrow design 2026-04-05 00:54:57 +02:00
Spythere eb3c42de8f chore(http): added error throw on bad response 2026-04-04 23:41:47 +02:00
Spythere 345c5764f9 bump(version): v1.1.3 2026-04-04 23:41:14 +02:00
Spythere 4c8afe5018 chore(timetable): added vertical directional arrow 2026-04-04 23:40:53 +02:00
Spythere c15939d5d0 chore(workflows): detached old domain from updating on main branch changes 2026-03-10 00:08:27 +01:00
Spythere fa71587ca7 Merge pull request #8 from Spythere/development
Added information bar about migration to a new domain
2026-03-10 00:06:08 +01:00
Spythere f76a5f8603 chore(packages): updated package.json dependencies 2026-03-10 00:03:53 +01:00
Spythere 1a377ddfd8 chore(app): added info bar about migration to a new domain 2026-03-09 23:53:57 +01:00
Spythere a164918d3a chore(workflows): added deploying files to VPS 2025-12-15 13:33:03 +01:00
Spythere 96a14778c8 Merge pull request #7 from Spythere/development
v1.1.2
2025-07-06 16:18:35 +02:00
Spythere 712813aba7 bump: v1.1.2 2025-07-06 16:02:02 +02:00
Spythere 874568d3e4 fix: loco load for multiple units 2025-07-06 16:01:37 +02:00
Spythere 0fae903884 chore: departure real line number (wip) 2025-07-06 15:50:30 +02:00
Spythere 022e937a17 fix: table border alignment 2025-07-06 15:21:45 +02:00
Spythere 812c34ae31 fix: table borders for left & right track speed limits separation 2025-07-06 02:43:59 +02:00
Spythere 0c3e9e7ae2 chore: added support of seconds in schedule dates 2025-07-06 02:33:52 +02:00
Spythere 3cd7201883 fix: 0 load for loco only 2025-07-06 02:32:20 +02:00
Spythere 3c8fad0759 chore: added support for left & right track vmax 2025-07-01 21:56:51 +02:00
Spythere 7397e3fd87 chore: packages upgrade 2025-06-28 16:19:54 +02:00
Spythere c767ee6d2b Merge pull request #6 from Spythere/development
v1.1.1
2025-06-22 14:13:27 +02:00
Spythere 63262ccf4a chore: added timetable bottom warnings info 2025-06-22 14:10:17 +02:00
Spythere 62736d4b04 bump: v1.1.1 2025-06-22 13:42:42 +02:00
Spythere 59d694b233 chore: subtracting loco weight for "obc. lok." cell 2025-06-22 13:39:29 +02:00
Spythere 1df3510df1 Merge pull request #5 from Spythere/development
hotfix: PWA installation settings
2025-05-03 14:19:16 +02:00
Spythere 87f173a645 hotfix: PWA installation settings 2025-05-03 14:18:34 +02:00
Spythere ceb0a49932 Merge pull request #4 from Spythere/development
v1.1.0
2025-05-03 14:13:51 +02:00
Spythere 4c9a560a4a fix: accessibility; minor improvements 2025-05-03 14:04:55 +02:00
Spythere b5edfb8d3e fix: views render logic 2025-05-03 13:49:01 +02:00
Spythere 7aaf620d6a fix: head units regex 2025-05-03 13:47:51 +02:00
Spythere c296ef6dcd fix: build files 2025-05-03 13:38:19 +02:00
Spythere 696d196b05 fix: translations 2025-05-03 13:36:27 +02:00
Spythere 05c04e4aa1 feat: fullscreen mode 2025-05-03 13:35:39 +02:00
Spythere 93acfdb780 fix: names corrections for multiple units 2025-05-02 19:30:19 +02:00
Spythere 4a96ed3852 chore: added icons to storage items 2025-05-02 17:56:02 +02:00
Spythere 35ded92a64 chore: build fixes, missing translations, PWA caching 2025-05-02 17:18:00 +02:00
Spythere 2f946a37b4 chore: print styles; Czech timetable (concept) 2025-05-02 17:08:25 +02:00
Spythere 86185a8f98 chore: added Czech variation of working timetable 2025-04-30 20:31:45 +02:00
Spythere c4473673a7 restruct: timetable view 2025-04-30 18:51:39 +02:00
Spythere 2c660b556e chore: updated gitignore 2025-04-29 22:11:07 +02:00
Spythere 5969b4202c chore: updated translations 2025-04-29 20:33:15 +02:00
Spythere 2d1b573101 chore: improved inputs & warnings styles 2025-04-29 20:30:39 +02:00
Spythere 24875d674f chore: changed icons pack 2025-04-29 20:00:44 +02:00
Spythere b3ee8bd119 chore: fetching data 2025-04-29 01:55:09 +02:00
Spythere 26e348b0be chore: update prompt hover 2025-04-28 13:56:01 +02:00
Spythere 4e8aabe05e feat: offline mode; PWA 2025-04-28 00:10:44 +02:00
Spythere f4aa0b28a1 fix: unable to use print mode when loading timetable using 'id' query parameter 2025-04-27 18:00:55 +02:00
Spythere dc7d0a7ccc fix: doubling warnings 2025-04-25 02:23:22 +02:00
Spythere 4d3d2c68de hotfix: build 2025-04-25 02:16:54 +02:00
Spythere 9515c77620 bump: v1.1.0 2025-04-25 02:15:01 +02:00
Spythere 17d5574d0c feat: journal timetable view mode 2025-04-25 02:14:45 +02:00
Spythere 3b68056acc chore: added storage switching between local and api modes 2025-04-18 02:32:02 +02:00
Spythere ca393057e4 Merge pull request #3 from Spythere/development
v1.0.5
2025-04-15 20:45:48 +02:00
Spythere be64e4b61a bump: 1.0.5 2025-04-15 20:44:52 +02:00
Spythere fab96589c1 chore: limiting speed in VP and VL columns to the Vmax column 2025-04-15 20:44:40 +02:00
Spythere e028905048 Merge pull request #2 from Spythere/development
v1.0.4
2025-04-14 15:34:08 +02:00
Spythere 0c21a8cb65 chore: code comments, print display 2025-04-14 15:33:43 +02:00
Spythere 0a41aa2828 bump: v1.0.4 2025-04-14 14:28:28 +02:00
Spythere bf7d00e29c chore: removed obsolete console logs 2025-04-14 14:27:25 +02:00
Spythere 6bd928d18e chore: removed external site scrollbar 2025-04-14 14:26:45 +02:00
Spythere 5c46209fd3 Merge pull request #1 from Spythere/development
v1.0.3
2025-02-11 19:16:48 +01:00
Spythere 4de4991ff6 fix: generating arrival km 2025-02-10 21:23:36 +01:00
Spythere 0431153326 hotfix: border colors & rendering 2025-02-10 17:32:24 +01:00
Spythere 6a23821f9c bump: v1.0.3 2025-02-10 17:10:09 +01:00
Spythere a34c8807de chore: improved data within Km column 2025-02-10 17:09:49 +01:00
Spythere 56dcca3d5b chore: update README.md 2025-02-07 20:40:20 +01:00
Spythere 91cf7b955a bump: v1.0.2 2025-02-07 20:36:34 +01:00
Spythere dacc0bc09d fix: incorrect arrival speed at first element 2025-02-07 20:36:24 +01:00
Spythere 336530cff9 chore: added APO support 2025-02-06 22:34:30 +01:00
Spythere 371e8085a9 hotfix: build 2025-02-02 15:05:57 +01:00
Spythere b8548b865b bump: v1.0.1 2025-02-02 14:46:13 +01:00
Spythere c0bdee939d feat: loading timetable from url params 2025-02-02 14:45:59 +01:00
Spythere ff14e362bb hotfix: parsing stops 2025-01-31 22:50:31 +01:00
Spythere 9094a0b784 chore: updated meta banner url 2025-01-31 21:46:02 +01:00
Spythere dc9389a7c7 bump: v1.0.0 2025-01-31 19:01:39 +01:00
Spythere 2d7159c844 hotfix: code improvements 2025-01-31 19:01:28 +01:00
Spythere 93311a130c chore: scrollbar corner style 2025-01-31 18:47:06 +01:00
Spythere 5616fbd7cf chore: list animations 2025-01-31 18:45:03 +01:00
Spythere d4e365d311 bump: v0.5.0 2025-01-31 18:01:07 +01:00
Spythere e43663c541 chore: added storage timetables filtering, deleting and information 2025-01-31 18:00:53 +01:00
Spythere ea99c68911 chore: settings card; language button 2025-01-31 17:04:55 +01:00
Spythere fb9cff3a00 hotfix: displaying stock length 2025-01-31 16:05:08 +01:00
Spythere 25fcd20e94 chore: added pl & en locales 2025-01-31 16:04:41 +01:00
Spythere 40213944e6 bump: v0.4.1 2025-01-31 02:23:24 +01:00
Spythere 984bbccaf5 chore: timetable warnings; locale test 2025-01-31 02:23:12 +01:00
Spythere 518b2da700 chore: i18n setup 2025-01-31 02:11:09 +01:00
Spythere a252140b4f bump: v0.4.0 2025-01-30 21:56:49 +01:00
Spythere 2472814a03 hotfix: styles 2025-01-30 21:56:28 +01:00
Spythere 5f264e8b63 feat/restruct: saving timetables to local storage 2025-01-30 21:52:17 +01:00
Spythere c334b5bfd3 chore: files cleanup 2025-01-29 22:42:44 +01:00
Spythere 0638bfff31 chore: config 2025-01-29 22:42:01 +01:00
Spythere eac4cad809 hotfix: build errors 2025-01-29 19:47:58 +01:00
Spythere 0d625a3192 hotfix: dark mode 2025-01-29 19:46:49 +01:00
Spythere bf51ac34f1 fix: vmax align 2025-01-29 19:43:40 +01:00
Spythere c8381d1222 bump: v0.3.0 2025-01-29 19:39:09 +01:00
Spythere 795b10959f chore: added css tooltips 2025-01-29 19:38:47 +01:00
Spythere 2a3f4ca1ef chore: added light, dark & print modes 2025-01-29 19:20:48 +01:00
Spythere a650a2f719 chore: added custom scrollbar 2025-01-29 16:13:38 +01:00
Spythere 23e7d04dfa chore: added train vmax field 2025-01-29 16:02:06 +01:00
Spythere b901176e8c chore: data refresh 2025-01-28 18:52:27 +01:00
Spythere ddd8bcc462 chore: changed print dialog file name 2025-01-28 18:51:28 +01:00
Spythere 519d5ec5fa chore: added route control abbrevs 2025-01-28 00:21:42 +01:00
Spythere c862164f69 chore: added displaying real route lines 2025-01-27 22:42:47 +01:00
Spythere 818144c894 bump: v0.2.0 2025-01-27 19:13:45 +01:00
Spythere 0174ddb8ab chore: added outdated data indicator 2025-01-27 19:13:31 +01:00
Spythere 8c7ffc7913 restruct: divide logic and layout into components 2025-01-27 18:25:05 +01:00
Spythere 5c6910df63 chore: add api data status info 2025-01-27 15:09:01 +01:00
Spythere 745b769070 chore: add vmax of internal routes 2025-01-27 15:08:30 +01:00
Spythere 3add3db2f2 chore: added api mocking indicator 2025-01-27 02:36:09 +01:00
Spythere f19a256153 chore: added api mocking 2025-01-27 02:31:08 +01:00
Spythere 48e8129902 chore: updated opengraph meta banner images 2025-01-26 00:11:51 +01:00
Spythere 4b420f6eec chore: added version indication 2025-01-25 23:42:21 +01:00
Spythere 2556851f3f chore: added print and refresh buttons 2025-01-25 23:42:03 +01:00
Spythere ec1f0416c7 chore: added navbar and generated time info 2025-01-25 23:19:46 +01:00
Spythere c5e1f304d2 chore: reactivity, layout improvements 2025-01-25 22:45:43 +01:00
Spythere 684a400e46 chore: add up to three visible locomotives, if there any 2025-01-25 20:35:11 +01:00
Spythere bb26082358 chore: gray out loco specs in the subsequent rows; stopTime correction 2025-01-25 20:15:37 +01:00
Spythere dcaf0d0ea3 fix: remove "pt" appearing at the first stop 2025-01-25 19:26:28 +01:00
Spythere a2aea77768 chore: style improvements 2025-01-25 19:25:45 +01:00
Spythere aea657d04d fix: timetable print mode 2025-01-25 19:20:00 +01:00
Spythere 00608bc667 chore: change select order to alphabetical by driver name 2025-01-25 18:52:23 +01:00
Spythere b67635886d restruct: move internal corrections to the separate json file 2025-01-25 18:49:44 +01:00
Spythere a5275b7f25 chore: meta tags 2025-01-25 16:00:27 +01:00
Spythere 958c8d3b65 chore: optimized table layout 2025-01-25 15:28:54 +01:00
Spythere 684bbdac31 chore: routeCorrections update 2025-01-25 02:03:43 +01:00
Spythere d1adcd8287 fix: table page-break 2025-01-25 02:01:37 +01:00
Spythere 051d6b22b8 chore: style adjustments for printing 2025-01-24 23:42:24 +01:00
Spythere 2d47534333 chore: cleanup 2025-01-24 23:31:02 +01:00
Spythere 2e5513b968 chore: added logo icons & manifest 2025-01-24 23:30:14 +01:00
Spythere af88628d15 chore: route speed & track corrections, hot fixes 2025-01-24 23:01:07 +01:00
Spythere 255a294e40 hotfix: github workflows build command 2025-01-24 19:55:51 +01:00
Spythere c97a525f24 chore: github workflows setup 2025-01-24 19:54:27 +01:00
Spythere e3268a689c chore: update README.md 2025-01-24 19:43:54 +01:00
Spythere 706f3ea9f8 hotfix: readme 2025-01-24 19:41:10 +01:00
Spythere 34df7eede5 chore: updated readme 2025-01-24 19:40:31 +01:00
Spythere 31c4e43762 Merge branch 'master' 2025-01-24 19:37:29 +01:00
Spythere fb019a1e40 Initial commit 2025-01-24 19:34:17 +01:00
67 changed files with 6658 additions and 919 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"projects": {
"default": "srjp-td2"
}
}
@@ -0,0 +1,20 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
on:
push:
branches:
- main-old
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SRJP_TD2 }}
channelId: live
projectId: srjp-td2
@@ -0,0 +1,21 @@
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR
on: pull_request
permissions:
checks: write
contents: read
pull-requests: write
jobs:
build_and_preview:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: yarn && yarn build
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: ${{ secrets.GITHUB_TOKEN }}
firebaseServiceAccount: ${{ secrets.FIREBASE_SERVICE_ACCOUNT_SRJP_TD2 }}
projectId: srjp-td2
@@ -0,0 +1,17 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: '${{ secrets.WEBHOOK_URL }}'
color: '10038562'
footer_title: 'Changelog - Rozkładownik SRJP'
footer_timestamp: true
+23
View File
@@ -0,0 +1,23 @@
name: Build & Deploy to VPS
on:
push:
branches:
- main
env:
PROJECT_NAME: srjp-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
+7
View File
@@ -11,6 +11,9 @@ node_modules
dist
dist-ssr
*.local
.env
.env.*
/dev-dist
# Editor directories and files
.vscode/*
@@ -22,3 +25,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# api-mock
/api-mock/endpoints/
/api-mock/*.lock
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Spythere
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5 -3
View File
@@ -1,5 +1,7 @@
# Vue 3 + TypeScript + Vite
# Rozkładownik TD2
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
### Aplikacja pozwalająca na wygenerowanie służbowego rozkładu jazdy dla pociągu w symulatorze [Train Driver 2](https://web.td2.info.pl/pl/)
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
~ by Spythere
![srjp-banner2](https://github.com/user-attachments/assets/fcc32652-6430-4ef3-8f91-538179dcf520)
+33
View File
@@ -0,0 +1,33 @@
import { existsSync } from 'fs';
import { mkdir, writeFile } from 'fs/promises';
async function fetchJSONEndpointData(url, fileName) {
try {
const res = await fetch(url);
const data = await res.json();
await writeFile(`./endpoints/${fileName}`, JSON.stringify(data));
return true;
} catch (error) {
console.error(error);
}
return false;
}
async function main() {
if (!existsSync('endpoints')) await mkdir('endpoints');
Promise.all(
['getActiveData', 'getDonators', 'getSceneries', 'getVehiclesData'].map((endpointName) =>
fetchJSONEndpointData(
`https://stacjownik.spythere.eu/api/${endpointName}`,
`${endpointName}.json`
)
)
).then(() => {
console.log('Endpoints downloaded!');
});
}
main();
+28
View File
@@ -0,0 +1,28 @@
import express from 'express';
import path from 'path';
import { cwd } from 'process';
import cors from 'cors';
const app = express();
app.use(cors());
app.get('/api/getActiveData', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getActiveData.json'));
});
app.get('/api/getSceneries', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
});
app.get('/api/getVehiclesData', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getVehiclesData.json'));
});
app.get('/api/getDonators', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getDonators.json'));
});
app.listen(3123, () => {
console.log('Mocking API server...');
});
+18
View File
@@ -0,0 +1,18 @@
{
"name": "api-mock",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"fetch": "node fetchEndpoints.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.3"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
+30 -3
View File
@@ -1,12 +1,39 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>Rozkładownik TD2 - SRJP</title>
<meta name="keywords" content="Rozkładownik, TD2, Train Driver 2, srjp-td2, SRJP, rozkładownik, stacjownik, pojazdownik, td2.info.pl" />
<meta name="description" content="Pomocnik maszynisty i dyżurnego symulatora Train Driver 2" />
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="apple-mobile-web-app-title" content="SRJP" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Static OpenGraph meta -->
<meta name="description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
<meta property="og:url" content="https://srjp-td2.web.app" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Rozkładownik TD2 - SRJP" />
<meta property="og:description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/srjp-banner2.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="Rozkładownik TD2 - SRJP" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Rozkładownik TD2 - SRJP" />
<meta name="twitter:description" content="Generator rozkładów jazdy dla symulatora Train Driver 2" />
<meta property="og:image" content="https://raw.githubusercontent.com/Spythere/api/main/thumbnails/srjp-banner2.png" />
</head>
<body>
<body >
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
# Logger Function
log() {
local message="$1"
local type="$2"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color
local endcolor="\033[0m"
case "$type" in
"info") color="\033[38;5;79m" ;;
"success") color="\033[1;32m" ;;
"error") color="\033[1;31m" ;;
*) color="\033[1;34m" ;;
esac
echo -e "${color}${timestamp} - ${message}${endcolor}"
}
# Error handler function
handle_error() {
local exit_code=$1
local error_message="$2"
log "Error: $error_message (Exit Code: $exit_code)" "error"
exit $exit_code
}
# Function to check for command availability
command_exists() {
command -v "$1" &> /dev/null
}
check_os() {
if ! [ -f "/etc/debian_version" ]; then
echo "Error: This script is only supported on Debian-based systems."
exit 1
fi
}
# Function to Install the script pre-requisites
install_pre_reqs() {
log "Installing pre-requisites" "info"
# Run 'apt-get update'
if ! apt-get update -y; then
handle_error "$?" "Failed to run 'apt-get update'"
fi
# Run 'apt-get install'
if ! apt-get install -y apt-transport-https ca-certificates curl gnupg; then
handle_error "$?" "Failed to install packages"
fi
if ! mkdir -p /usr/share/keyrings; then
handle_error "$?" "Makes sure the path /usr/share/keyrings exist or run ' mkdir -p /usr/share/keyrings' with sudo"
fi
rm -f /usr/share/keyrings/nodesource.gpg || true
rm -f /etc/apt/sources.list.d/nodesource.list || true
# Run 'curl' and 'gpg' to download and import the NodeSource signing key
if ! curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /usr/share/keyrings/nodesource.gpg; then
handle_error "$?" "Failed to download and import the NodeSource signing key"
fi
# Explicitly set the permissions to ensure the file is readable by all
if ! chmod 644 /usr/share/keyrings/nodesource.gpg; then
handle_error "$?" "Failed to set correct permissions on /usr/share/keyrings/nodesource.gpg"
fi
}
# Function to configure the Repo
configure_repo() {
local node_version=$1
arch=$(dpkg --print-architecture)
if [ "$arch" != "amd64" ] && [ "$arch" != "arm64" ] && [ "$arch" != "armhf" ]; then
handle_error "1" "Unsupported architecture: $arch. Only amd64, arm64, and armhf are supported."
fi
echo "deb [arch=$arch signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$node_version nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null
# N|solid Config
echo "Package: nsolid" | tee /etc/apt/preferences.d/nsolid > /dev/null
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nsolid > /dev/null
# Nodejs Config
echo "Package: nodejs" | tee /etc/apt/preferences.d/nodejs > /dev/null
echo "Pin: origin deb.nodesource.com" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
echo "Pin-Priority: 600" | tee -a /etc/apt/preferences.d/nodejs > /dev/null
# Run 'apt-get update'
if ! apt-get update -y; then
handle_error "$?" "Failed to run 'apt-get update'"
else
log "Repository configured successfully."
log "To install Node.js, run: apt-get install nodejs -y" "info"
log "You can use N|solid Runtime as a node.js alternative" "info"
log "To install N|solid Runtime, run: apt-get install nsolid -y \n" "success"
fi
}
# Define Node.js version
NODE_VERSION="23.x"
# Check OS
check_os
# Main execution
install_pre_reqs || handle_error $? "Failed installing pre-requisites"
configure_repo "$NODE_VERSION" || handle_error $? "Failed configuring repository"
+14 -10
View File
@@ -1,26 +1,30 @@
{
"name": "srjp-td2",
"private": true,
"version": "0.0.0",
"version": "1.1.3",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --mode staging",
"dev:mock": "vite --mode development & yarn --cwd ./api-mock start",
"mock:setup": "cd ./api-mock && yarn && yarn fetch",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.9",
"pinia": "^2.3.1",
"vue": "^3.5.13"
"lucide-vue-next": "^0.577.0",
"pinia": "^3.0.4",
"vue": "^3.5.30",
"vue-i18n": "11.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"@vue/tsconfig": "^0.7.0",
"@vitejs/plugin-vue": "^6.0.4",
"@vue/tsconfig": "^0.9.0",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vue-tsc": "^2.2.0"
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^3.2.5"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="512" height="512"><svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" rx="256" fill="#151414"></rect>
<path d="M72.4253 291.986V279.965H120.201C123.283 279.965 124.824 278.424 124.824 275.342V264.246C124.824 261.266 123.54 259.571 120.971 259.16L90.9189 252.995C78.5898 250.529 72.4253 242.259 72.4253 228.183V219.553C72.4253 202.292 81.0557 193.662 98.3164 193.662H133.608L143.934 201.675V213.696H99.2411C96.1588 213.696 94.6177 215.237 94.6177 218.32V228.337C94.6177 231.214 95.9019 232.909 98.4705 233.423L128.523 239.433C140.852 241.899 147.016 250.17 147.016 264.246V274.109C147.016 291.37 138.386 300 121.125 300H82.7509L72.4253 291.986ZM167.651 300V193.662H219.433C236.694 193.662 245.324 202.292 245.324 219.553V237.122C245.324 249.964 240.546 257.978 230.991 261.163L248.406 295.377L245.786 300H226.676L207.874 263.013H189.843V300H167.651ZM189.843 242.978H218.508C221.591 242.978 223.132 241.437 223.132 238.355V218.32C223.132 215.237 221.591 213.696 218.508 213.696H189.843V242.978ZM262.96 274.109V253.766H285.153V275.342C285.153 278.424 286.694 279.965 289.776 279.965H310.736C313.818 279.965 315.359 278.424 315.359 275.342V213.696H286.386V193.662H337.551V274.109C337.551 291.37 328.921 300 311.66 300H288.852C271.591 300 262.96 291.37 262.96 274.109ZM361.948 300V193.662H413.731C430.991 193.662 439.622 202.292 439.622 219.553V240.204C439.622 257.465 430.991 266.095 413.731 266.095H384.141V300H361.948ZM384.141 246.06H412.806C415.888 246.06 417.429 244.519 417.429 241.437V218.32C417.429 215.237 415.888 213.696 412.806 213.696H384.141V246.06Z" fill="white"></path>
<path d="M304.958 332.848V322.831H348.418V332.848H332.236V376H321.14V332.848H304.958ZM356.61 376V322.831H376.799C391.285 322.831 398.529 330.074 398.529 344.561V354.27C398.529 368.757 391.285 376 376.799 376H356.61ZM367.706 365.983H377.415C384.093 365.983 387.432 362.643 387.432 355.965V342.866C387.432 336.187 384.093 332.848 377.415 332.848H367.706V365.983ZM407.35 376V358.662C407.35 351.624 410.432 347.489 416.597 346.256L430.852 343.405C432.136 343.148 432.779 342.3 432.779 340.862V335.16C432.779 333.619 432.008 332.848 430.467 332.848H408.891V326.838L414.054 322.831H430.929C439.56 322.831 443.875 327.146 443.875 335.776V340.785C443.875 347.823 440.792 351.958 434.628 353.191L420.372 356.042C419.088 356.299 418.446 357.147 418.446 358.585V365.983H443.875V376H407.35Z" fill="#E63E3E"></path>
</svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="50" height="30" viewBox="0 0 50 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25 30L0 0H50L25 30Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 149 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5 fill-white">
<path fill-rule="evenodd" d="M5 2.75C5 1.784 5.784 1 6.75 1h6.5c.966 0 1.75.784 1.75 1.75v3.552c.377.046.752.097 1.126.153A2.212 2.212 0 0 1 18 8.653v4.097A2.25 2.25 0 0 1 15.75 15h-.241l.305 1.984A1.75 1.75 0 0 1 14.084 19H5.915a1.75 1.75 0 0 1-1.73-2.016L4.492 15H4.25A2.25 2.25 0 0 1 2 12.75V8.653c0-1.082.775-2.034 1.874-2.198.374-.056.75-.107 1.127-.153L5 6.25v-3.5Zm8.5 3.397a41.533 41.533 0 0 0-7 0V2.75a.25.25 0 0 1 .25-.25h6.5a.25.25 0 0 1 .25.25v3.397ZM6.608 12.5a.25.25 0 0 0-.247.212l-.693 4.5a.25.25 0 0 0 .247.288h8.17a.25.25 0 0 0 .246-.288l-.692-4.5a.25.25 0 0 0-.247-.212H6.608Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 736 B

+20
View File
@@ -0,0 +1,20 @@
{
"name": "Rozkładownik TD2",
"short_name": "SRJP",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#151414",
"display": "standalone",
"start_url": "."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+119 -376
View File
@@ -1,394 +1,137 @@
<template>
<div class="app">
<select name="trains" id="trains-select" class="mb-2 bg-zinc-800 p-1 rounded-md" v-model="selectedTrainId">
<option :value="train.id" v-for="train in timetableTrains">{{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }}</option>
</select>
<div class="text-white min-h-screen bg-zinc-950 print:bg-white">
<!-- PWA update prompt -->
<transition name="slide-anim">
<UpdatePrompt v-if="needRefresh" @onUpdateClick="updateApp()" />
</transition>
<table class="srjp-table">
<thead>
<tr>
<th width="50" class="border border-white">Nr linii</th>
<th width="100" class="border border-white">Km</th>
<th width="40" class="border border-white" colspan="2">V<sub>D</sub></th>
<th width="250" class="border border-white">Stacja</th>
<th width="100" class="border border-white">Godzina</th>
<th width="50" class="border border-white text-xs p-0">
<table class="header-table">
<tbody>
<tr>
<td>Lok I</td>
</tr>
<tr>
<td>Lok II</td>
</tr>
<tr>
<td>Lok III</td>
</tr>
</tbody>
</table>
</th>
<th width="60" class="border border-white text-xs p-0">
<table class="header-table">
<tbody>
<tr>
<td>Obc. lok.</td>
</tr>
<tr>
<td>. poc.</td>
</tr>
</tbody>
</table>
</th>
<th width="50" class="border border-white">Vmax</th>
</tr>
</thead>
<tbody v-if="computedTimetable">
<tr v-for="(row, i) in computedTimetable">
<td class="text-center align-top border border-white">{{ row.realLine }}</td>
<td class="border border-white">
<table>
<tbody>
<tr>
<td class="align-top">{{ row.arrivalKm }}</td>
</tr>
<tr>
<td class="align-bottom">{{ row.departureKm }}</td>
</tr>
</tbody>
</table>
</td>
<!-- :class="{ 'border-t-4': i > 0 && computedTimetable[i - 1].sceneryName != row.sceneryName }" -->
<!-- :class="{ 'border-ts': i > 0 && computedTimetable[i - 1].sceneryName != row.sceneryName }" -->
<td class="text-center align-top font-bold p-0 border-l-4" colspan="2">
<table>
<tbody>
<tr
:class="{
'align-top': i == 0 || computedTimetable[i - 1].departureTracks == row.arrivalTracks,
'border-t': i != 0 && computedTimetable[i - 1].departureSpeed != row.arrivalSpeed,
}"
>
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'">
{{
i == 0 ||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
? row.arrivalSpeed
: '&nbsp;'
}}
<!-- {{ row.arrivalTracks }} -->
</td>
<td v-if="row.arrivalTracks == 2" class="border-l">
{{
i == 0 ||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
? row.arrivalSpeed
: '&nbsp;'
}}
<!-- {{ row.arrivalTracks }} -->
</td>
</tr>
<tr
class=""
:class="{
'border-b': row.departureSpeed != row.arrivalSpeed || i == computedTimetable.length - 1,
'border-t': row.arrivalTracks != row.departureTracks,
'align-bottom': row.arrivalTracks == row.departureTracks,
}"
>
<td :colspan="row.departureTracks == 2 ? '1' : '2'">
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : '&nbsp;' }}
<!-- {{ row.departureTracks }} -->
</td>
<td v-if="row.departureTracks == 2" class="border-l">
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : '&nbsp;' }}
<!-- {{ row.departureTracks }} -->
</td>
</tr>
<!-- <tr
class="align-top"
:class="{
'border-b':
row.arrivalSpeed && row.departureSpeed && (row.arrivalSpeed != row.departureSpeed || row.arrivalTracks != row.departureTracks),
}"
>
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'">{{ row.arrivalSpeed || ' ' }}</td>
<td v-if="row.arrivalTracks == 2" class="border-l">{{ row.arrivalSpeed || ' ' }}</td>
</tr>
<tr
class="align-bottom"
:class="{
'border-b':
i < computedTimetable.length - 1 &&
(computedTimetable[i + 1].arrivalSpeed != row.departureSpeed || computedTimetable[i + 1].arrivalTracks != row.departureTracks),
}"
>
<td :colspan="row.departureTracks == 2 ? '1' : '2'">{{ row.departureSpeed || ' ' }}</td>
<td v-if="row.departureTracks == 2" class="border-l">{{ row.departureSpeed || ' ' }}</td>
</tr> -->
</tbody>
</table>
</td>
<td class="p-1 border border-white">
<div class="flex flex-col h-full justify-between">
<div :class="{ 'font-bold': row.isMain }">
{{ row.pointName }}
<span v-if="row.stopType"> ; {{ row.stopType }}</span>
</div>
<div class="flex justify-between">
<span>{{ row.pointKm }}</span>
<span>R1, PP</span>
</div>
</div>
</td>
<td class="border border-white">
<table>
<tbody>
<tr class="text-center align-top">
<td class="border-r-[1px] border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
{{
(row.scheduledArrivalDate?.getTime() || 0) != (row.scheduledDepartureDate?.getTime() || 0)
? row.scheduledArrivalDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })
: '|'
}}
</td>
<td width="30">{{ row.driveTime ? Math.floor(row.driveTime / 60000) : '' }}</td>
</tr>
<tr class="text-center align-bottom">
<td class="border-r-[1px] border-r-white" :class="{ 'font-bold': row.stopTime > 0 }">
{{ row.scheduledDepartureDate?.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }) }}
</td>
<td width="30" class="font-bold">{{ row.stopTime || '' }}</td>
</tr>
</tbody>
</table>
</td>
<td class="p-0 text-center border border-white">
<table>
<tbody>
<tr class="border-b-[1px] border-b-white">
<td>{{ selectedTrain!.stockString.split(';')[0].split('-')[0] }}</td>
</tr>
<tr class="border-b-[1px] border-b-white">
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
</td>
<td class="p-0 text-center border border-white">
<table>
<tbody>
<tr class="border-b-[1px] border-b-white">
<td>{{ Math.floor(selectedTrain!.mass / 1000) }}</td>
</tr>
<tr>
<td>{{ selectedTrain!.length }}</td>
</tr>
</tbody>
</table>
</td>
<td class="text-center border border-white">70</td>
</tr>
</tbody>
</table>
<!-- Content -->
<Navbar v-if="!globalStore.fullscreenMode" />
<MainContainer />
<!-- Migrate Info -->
<transition name="slide-anim">
<MigrateInfo v-if="globalStore.isMigrationInfoOpen" />
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
<script lang="ts" setup>
import Navbar from './components/App/Navbar.vue';
import MainContainer from './components/App/MainContainer.vue';
import UpdatePrompt from './components/App/UpdatePrompt.vue';
import { onMounted } from 'vue';
import { useApiStore } from './stores/api.store';
import { useGlobalStore } from './stores/global.store';
import { useI18n } from 'vue-i18n';
import { useRegisterSW } from 'virtual:pwa-register/vue';
import { DataStatus } from './types/api.types';
import MigrateInfo from './components/App/MigrateInfo.vue';
const additionalData = {
// Mijanki
passings: ['Stolnica Wielka'],
// SHP
shpSystems: [],
// 4-stawne SBL
sbl4: [],
};
const originalDocumentTitle = document.title;
interface StopRow {
pointName: string;
pointKm: string;
isMain: boolean;
stopTime: number;
stopType: string;
scheduledArrivalDate: Date | null;
scheduledDepartureDate: Date | null;
realLine: string;
driveTime: number;
controlAbbrevs: string[];
additionalAbbrevs: string[];
const apiStore = useApiStore();
const globalStore = useGlobalStore();
const i18n = useI18n();
sceneryName: string;
const { needRefresh, updateServiceWorker } = useRegisterSW({ immediate: true });
arrivalKm: string;
onMounted(async () => {
setupLocale();
setupDarkMode();
handleMigrationInfo();
setupOfflineMode();
loadStorageTimetables();
setupAfterPrintClose();
arrivalSpeed: number;
arrivalTracks: number;
departureKm: string;
departureSpeed: number;
departureTracks: number;
}
export default defineComponent({
data: () => ({
selectedTrainId: '',
globalStore: useGlobalStore(),
}),
mounted() {
this.globalStore.setupData();
},
computed: {
timetableTrains() {
return this.globalStore.activeData?.trains.filter((train) => train.timetable != undefined) ?? [];
},
selectedTrain() {
return this.timetableTrains.find((train) => train.id == this.selectedTrainId);
},
computedTimetable() {
if (!this.selectedTrain) return null;
const timetable = this.selectedTrain.timetable;
if (!timetable) return null;
const timetablePath = timetable.path.split(';').map((pathEl) => {
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
const sceneryData = this.globalStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
const arrivalLineData = arrivalLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null : null;
const departureLineData = departureLine ? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null : null;
return {
sceneryName,
arrivalLine: arrivalLine ?? '',
arrivalLineData,
departureLine: departureLine ?? '',
departureLineData,
};
});
const stopRows: StopRow[] = [];
let currentPathIndex = 0;
let currentPath = timetablePath[0];
let lastDepartureTimestamp = 0;
let arrivalKm = 0,
arrivalSpeed = currentPath.departureLineData?.routeSpeed ?? 0,
arrivalTracks = currentPath.departureLineData?.routeTracks ?? 0;
let departureSpeed = currentPath.departureLineData?.routeSpeed ?? 0,
departureTracks = currentPath.departureLineData?.routeTracks ?? 2;
let checkEntryAsFirst = true;
for (const stop of timetable.stopList) {
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
arrivalKm = stop.stopDistance;
arrivalSpeed = currentPath.arrivalLineData?.routeSpeed ?? 0;
arrivalTracks = currentPath.arrivalLineData?.routeTracks ?? 2;
departureSpeed = arrivalSpeed;
departureTracks = arrivalTracks;
}
if (/^<strong>|, (podg|po)$|^(!_, pe)$/.test(stop.stopName)) {
let rowData: StopRow = {
isMain: /^<strong>/.test(stop.stopName),
pointKm: stop.stopDistance.toFixed(3),
pointName: stop.stopNameRAW,
scheduledArrivalDate: stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null,
scheduledDepartureDate: stop.departureTimestamp ? new Date(stop.departureTimestamp) : null,
stopTime: stop.stopTime ?? 0,
stopType: stop.stopType,
sceneryName: currentPath.sceneryName,
realLine: '-',
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
additionalAbbrevs: [],
controlAbbrevs: [],
arrivalKm: arrivalKm.toFixed(3),
departureKm: stop.stopDistance.toFixed(3),
arrivalSpeed: arrivalSpeed,
arrivalTracks: arrivalTracks,
departureSpeed: departureSpeed,
departureTracks: departureTracks,
};
arrivalKm = stop.stopDistance;
checkEntryAsFirst = false;
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
stopRows.push(rowData);
}
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
// Reverse search for last scenery checkpoint
for (let i = stopRows.length - 1; i > 0; i--) {
stopRows[i].departureTracks = currentPath.departureLineData?.routeTracks ?? 0;
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
stopRows[i].departureSpeed = currentPath.departureLineData?.routeSpeed ?? 0;
stopRows[i].departureTracks = currentPath.departureLineData?.routeTracks ?? 0;
break;
}
stopRows[i].arrivalTracks = currentPath.departureLineData?.routeTracks ?? 0;
}
currentPath = timetablePath[++currentPathIndex];
checkEntryAsFirst = true;
}
}
return stopRows;
},
},
await apiStore.setupAPIData();
handleQueries();
});
</script>
<style scoped>
table {
width: 100%;
height: 100%;
border-collapse: collapse;
function updateApp() {
updateServiceWorker(true);
needRefresh.value = false;
}
.srjp-table {
min-width: 750px;
}
function loadStorageTimetables() {
if (!window.localStorage.getItem('savedTimetables')) return;
.no-bottom-border {
border-bottom-color: transparent;
}
@media print {
table {
page-break-inside: auto;
try {
globalStore.storageTimetables = JSON.parse(window.localStorage.getItem('savedTimetables')!);
} catch (error) {
alert('Ups! Coś poszło nie tak podczas pobierania zapisanych RJ!');
}
}
</style>
function setupDarkMode() {
globalStore.darkMode =
localStorage.currentTheme === 'dark' ||
(!('currentTheme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
}
function setupAfterPrintClose() {
window.addEventListener('afterprint', () => {
document.title = originalDocumentTitle;
});
}
function setupLocale() {
if (window.localStorage.getItem('locale') == null) {
const browserLang = window.navigator.language;
if (browserLang == 'pl-PL') i18n.locale.value = browserLang == 'pl-PL' ? 'pl' : 'en';
window.localStorage.setItem('locale', i18n.locale.value);
} else {
i18n.locale.value = window.localStorage.getItem('locale')!;
}
}
function setupOfflineMode() {
apiStore.connectionMode = !navigator.onLine ? 'offline' : 'online';
window.addEventListener('offline', () => {
apiStore.connectionMode = 'offline';
apiStore.journalTimetablesData = null;
apiStore.activeData = null;
});
window.addEventListener('online', () => {
apiStore.connectionMode = 'online';
apiStore.journalDataStatus = DataStatus.SUCCESS;
apiStore.setupAPIData();
});
}
function handleQueries() {
const query = new URLSearchParams(window.location.search);
if (query.has('id')) {
const id = query.get('id')!;
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
if (queryTrain) {
globalStore.selectedTrainId = id;
globalStore.selectedActiveTrain = queryTrain;
}
}
}
function handleMigrationInfo() {
// Show only on old domain
if (location.hostname !== 'srjp-td2.web.app' && location.hostname != 'localhost') return;
const showInfo = localStorage.getItem('showMigrationInfo');
// Do not show if already acknowledged
if (showInfo === 'false') return;
setTimeout(() => {
globalStore.isMigrationInfoOpen = true;
}, 2000);
}
</script>
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

+17
View File
@@ -0,0 +1,17 @@
<template>
<div v-if="globalStore.generatedDate" class="text-center text-sm text-zinc-400 mt-1 print:hidden">
Rozkład wygenerowany w
{{
globalStore.generatedMs
}}ms - aktualny dla godziny:
{{
globalStore.generatedDate.toLocaleTimeString()
}}
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
const globalStore = useGlobalStore();
</script>
+22
View File
@@ -0,0 +1,22 @@
<template>
<main
class="grid print:block print:bg-white p-3 mx-auto max-w-[800px] min-h-[300px] gap-1 relative"
:class="{
'grid-rows-[auto_auto_1fr] h-[calc(100vh-40px)]': !globalStore.fullscreenMode,
'grid-rows-[1fr] h-screen': globalStore.fullscreenMode
}"
>
<SearchContainer v-if="!globalStore.fullscreenMode" />
<TimetableWarnings v-if="!globalStore.fullscreenMode" />
<TimetableContainer />
</main>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import TimetableContainer from '../Timetable/TimetableContainer.vue';
import TimetableWarnings from '../Timetable/TimetableWarnings.vue';
import SearchContainer from '../TimetableSearch/SearchContainer.vue';
const globalStore = useGlobalStore();
</script>
+30
View File
@@ -0,0 +1,30 @@
<template>
<div class="fixed z-10 bottom-0 left-0 p-1 text-center w-full bg-yellow-400 text-black font-bold">
<div class="flex justify-center items-center flex-wrap gap-2">
<i18n-t keypath="migrate-info.line-1" for="migrate-info" tag="div">
<a href="https://srjp-td2.spythere.eu/" target="_blank" class="underline">
https://srjp-td2.spythere.eu/
</a>
</i18n-t>
<button
class="p-1 bg-zinc-700 text-white rounded-md hover:bg-zinc-500 focus-visible:bg-zinc-500"
@click="onAcceptButtonClick"
>
{{ t('migrate-info.accept-btn') }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useGlobalStore } from '../../stores/global.store';
const store = useGlobalStore();
const { t } = useI18n();
function onAcceptButtonClick() {
store.isMigrationInfoOpen = false;
localStorage.setItem('showMigrationInfo', 'false');
}
</script>
+40
View File
@@ -0,0 +1,40 @@
<template>
<nav class="bg-zinc-900 w-full p-1 print:hidden flex justify-between items-center relative">
<div class="flex items-center">
<img src="/favicon.svg" class="size-8 inline" alt="SRJP logo" />
<b class="ml-2 text-lg">
Rozkładownik TD2
<sup class="font-semibold text-zinc-300">
<a :href="releaseHref" target="_blank">v{{ version }}</a>
</sup>
</b>
</div>
<div>
<button
class="bg-slate-600 p-1 px-2 rounded-md hover:bg-slate-500 flex items-center"
@click="changeLang()"
>
<GlobeIcon :size="18" class="mr-2" /> {{ i18n.locale.value == 'pl' ? 'POL' : 'ENG' }}
</button>
</div>
<!-- <div v-if="apiMode == 'mocking'"><ExclamationTriangleIcon class="size-6 inline mr-1 text-yellow-400" /> API mocking</div> -->
</nav>
</template>
<script setup lang="ts">
import { GlobeIcon } from 'lucide-vue-next';
import { version } from '../../../package.json';
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
const i18n = useI18n();
function changeLang(locale?: string) {
i18n.locale.value = (locale ?? i18n.locale.value == 'pl') ? 'en' : 'pl';
window.localStorage.setItem('locale', i18n.locale.value);
}
const releaseHref = computed(() => `https://github.com/Spythere/srjp-td2/releases/tag/v${version}`);
// const apiMode = import.meta.env.VITE_API_MODE;
</script>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="fixed top-0 left-0 w-screen h-screen z-10 bg-black bg-opacity-70" v-if="globalStore.showSettings" @click="globalStore.showSettings = false">
<!-- <div class="fixed top-0 left-0 w-screen h-screen z-10" v-show="showSettings" @click="showSettings = false"></div> -->
<!-- <div class="absolute mt-2 top-full right-0 p-1 rounded-md bg-slate-600 w-full z-20 max-w-[250px]" v-show="showSettings"></div> -->
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] bg-zinc-800 p-2 h-[500px] z-20">
<div class="grid grid-cols-2 gap-2">
<button
class="bg-slate-900 p-1 rounded-md hover:bg-slate-800"
:class="{ 'font-bold text-yellow-400': i18n.locale.value == 'pl' }"
@click="chooseLang('pl')"
>
POLSKI
</button>
<button
class="bg-slate-900 p-1 rounded-md hover:bg-slate-800"
:class="{ 'font-bold text-yellow-400': i18n.locale.value == 'en' }"
@click="chooseLang('en')"
>
ENGLISH
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useGlobalStore } from '../../stores/global.store';
const i18n = useI18n();
const globalStore = useGlobalStore();
function chooseLang(locale: string) {
i18n.locale.value = locale;
window.localStorage.setItem('locale', i18n.locale.value);
globalStore.showSettings = false;
}
</script>
<style scoped></style>
+28
View File
@@ -0,0 +1,28 @@
<template>
<div class="fixed z-50 bottom-0 right-0">
<button
@click="onUpdateClick"
class="p-3 m-3 bg-cyan-600 rounded-md text-xl hover:scale-105 transition-transform"
ref="updateBtnEl"
>
<div>{{ $t('update-prompt.line1') }}</div>
<u>{{ $t('update-prompt.line2') }}</u>
</button>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue';
import { ref } from 'vue';
const emit = defineEmits(['onUpdateClick']);
const updateBtnEl = ref<HTMLElement | null>(null);
function onUpdateClick() {
emit('onUpdateClick');
}
onMounted(() => {
updateBtnEl.value?.focus();
});
</script>
@@ -0,0 +1,55 @@
<template>
<!-- Button closing fullscreen mode, relative to MainContainer -->
<button
v-if="globalStore.fullscreenMode"
class="absolute right-6 top-3 p-1 rounded-md bg-green-600 hover:bg-green-500 print:hidden z-50"
@click="() => (globalStore.fullscreenMode = false)"
>
<Minimize2Icon :size="22" />
</button>
<!-- If there is no timetable chosen -->
<div
class="overflow-auto text-center font-bold text-zinc-400 p-2 min-h-full"
v-if="globalStore.currentTimetableData == null"
>
<component :is="viewModes[globalStore.viewMode]" />
</div>
<div
class="overflow-auto text-center font-bold text-white p-2 min-h-full"
v-else-if="apiStore.apiDataStatus == DataStatus.LOADING"
>
Pobieranie danych...
</div>
<div
class="overflow-auto text-center font-bold text-red-500 p-2 min-h-full"
v-else-if="apiStore.apiDataStatus == DataStatus.ERROR"
>
Ups! Coś poszło nie tak przy pobieraniu danych! :/
</div>
<!-- Timetable render based on current view mode -->
<CurrentTimetableView v-else />
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import LocalStorageView from '../TimetableViews/LocalStorageView.vue';
import JournalStorageView from '../TimetableViews/JournalStorageView.vue';
import ActiveDataView from '../TimetableViews/ActiveDataView.vue';
import CurrentTimetableView from '../TimetableViews/CurrentTimetableView.vue';
import { Minimize2Icon } from 'lucide-vue-next';
import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
const viewModes: Record<typeof globalStore.viewMode, any> = {
active: ActiveDataView,
storage: LocalStorageView,
journal: JournalStorageView
};
</script>
@@ -0,0 +1,771 @@
<template>
<div>
<h2 class="p-1 font-bold w-max">
{{ globalStore.currentTimetableData!.category }}
{{ globalStore.currentTimetableData!.trainNo }} {{ $t('headers.relation') }}
{{ globalStore.currentTimetableData!.route.replace('|', ' - ') }}
</h2>
<table
class="table-fixed mt-2 w-full border-collapse overflow-hidden"
v-if="computedTimetableRows.length > 0"
>
<thead>
<tr>
<th width="40" class="border border-black dark:border-white">
{{ $t('headers.line_no') }}
</th>
<th width="100" class="border border-black dark:border-white">
{{ $t('headers.line_km') }}
</th>
<th width="42" class="border border-black dark:border-white">V<sub>P</sub></th>
<th width="40" class="border border-black dark:border-white">V<sub>L</sub></th>
<th width="200" class="border border-black dark:border-white">
{{ $t('headers.station') }}
</th>
<th width="100" class="border border-black dark:border-white">
{{ $t('headers.time') }}
</th>
<th width="50" class="border border-black dark:border-white text-xs p-0">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="border-b border-b-black dark:border-b-white">
<td class="">{{ $t('headers.loco_1') }}</td>
</tr>
<tr class="border-b border-b-black dark:border-b-white">
<td>{{ $t('headers.loco_2') }}</td>
</tr>
<tr>
<td>{{ $t('headers.loco_3') }}</td>
</tr>
</tbody>
</table>
</th>
<th width="55" class="border border-black dark:border-white text-xs relative">
<div class="absolute top-0 left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="border-b border-b-black dark:border-b-white">
<td>{{ $t('headers.mass') }}</td>
</tr>
<tr>
<td>{{ $t('headers.length') }}</td>
</tr>
</tbody>
</table>
</div>
</th>
<th width="50" class="border border-black dark:border-white">{{ $t('headers.vmax') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, i) in computedTimetableRows">
<!-- Line no. -->
<td
class="text-center align-top border-l border-l-black dark:border-l-white relative"
:class="{
'border-t border-t-black dark:border-t-white':
row.lastRowRef != null && row.lastRowRef.departureLineNumber != row.arrivalLineNumber,
'border-b border-b-black dark:border-b-white': i == computedTimetableRows.length - 1
}"
>
<div class="absolute -top-[0.5px] left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<!-- Arrival Line -->
<tr class="align-top">
<td>
{{
row.lastRowRef == null ||
row.lastRowRef.departureLineNumber != row.arrivalLineNumber
? row.arrivalLineNumber
: '&nbsp;'
}}
</td>
</tr>
<!-- Departure Line -->
<tr class="align-top">
<td v-if="row.departureLineNumber != row.arrivalLineNumber" class="border-t">
{{ row.departureLineNumber }}
</td>
<td v-else>&nbsp;</td>
</tr>
</tbody>
</table>
</div>
</td>
<!-- Km -->
<td
class="border border-black dark:border-white border-t-0 border-b-1 relative p-0"
:class="{
'border-b-0': i != computedTimetableRows.length - 1
}"
>
<div class="absolute -top-[0.5px] left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<!-- Arrival Km -->
<tr>
<td
class="align-top border-t"
v-if="
row.lastRowRef &&
(row.lastRowRef.departureSpeedL != row.arrivalSpeedL ||
row.lastRowRef.departureSpeedP != row.arrivalSpeedP ||
row.lastRowRef.departureTracks != row.arrivalTracks ||
row.lastRowRef.departureLineNumber != row.arrivalLineNumber)
"
>
&nbsp;{{ row.arrivalKm }}
</td>
<td class="align-top" v-else-if="row.lastRowRef == null">
&nbsp;{{ row.arrivalKm }}
</td>
<td v-else>&nbsp;</td>
</tr>
<!-- Departure Km -->
<tr
:class="{
'border-black dark:border-white border-t align-top':
row.arrivalTracks != row.departureTracks ||
row.departureSpeedL != row.arrivalSpeedL ||
row.departureSpeedP != row.arrivalSpeedP ||
row.departureLineNumber != row.arrivalLineNumber,
hidden:
row.arrivalTracks == row.departureTracks &&
row.departureSpeedL == row.arrivalSpeedL &&
row.departureSpeedP == row.arrivalSpeedP &&
row.departureLineNumber == row.arrivalLineNumber
}"
>
<td>&nbsp;{{ row.departureKm }}</td>
</tr>
</tbody>
</table>
</div>
</td>
<!-- Vp, Vl -->
<td
class="text-center align-top p-0 border-l-black dark:border-l-white relative"
:class="{
'border-b border-b-black dark:border-b-white': i == computedTimetableRows.length - 1
}"
colspan="2"
>
<!-- Direction line arrow -->
<div
class="absolute h-0 w-0 border-x-transparent border-x-[6px] -left-[4px] border-t-[25px] -bottom-[12px] z-30 border-t-black dark:border-t-white print:border-t-black"
v-if="i == computedTimetableRows.length - 1"
></div>
<div class="absolute -top-[0.5px] left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="align-top">
<td
class="font-bold border-l-4 border-l-black dark:border-l-white"
:class="{
'border-t border-t-black dark:border-t-white':
row.lastRowRef != null &&
row.lastRowRef.departureSpeedP != row.arrivalSpeedP
}"
:colspan="row.arrivalTracks == 2 ? '1' : '2'"
width="35"
>
{{
row.lastRowRef == null ||
row.lastRowRef.departureSpeedP != row.arrivalSpeedP ||
row.lastRowRef.departureTracks != row.arrivalTracks
? row.arrivalSpeedP
: '&nbsp; '
}}
</td>
<td
v-if="row.arrivalTracks == 2"
class="border-l border-l-black dark:border-l-white"
:class="{
'border-t border-t-black dark:border-t-white':
row.lastRowRef != null &&
row.lastRowRef.departureSpeedL != row.arrivalSpeedL
}"
width="35"
>
{{
row.lastRowRef == null ||
row.lastRowRef.departureSpeedL != row.arrivalSpeedL ||
row.lastRowRef.departureTracks != row.arrivalTracks
? row.arrivalSpeedL
: '&nbsp; '
}}
</td>
</tr>
<tr
class="border-l-4 border-l-black dark:border-l-white"
:class="{
'border-t border-t-black dark:border-t-white align-top':
row.arrivalTracks != row.departureTracks ||
row.departureSpeedL != row.arrivalSpeedL ||
row.departureSpeedP != row.arrivalSpeedP
}"
>
<td
:colspan="row.departureTracks == 2 ? '1' : '2'"
class="font-bold"
width="35"
>
{{
row.departureSpeedP != row.arrivalSpeedP ||
row.departureTracks != row.arrivalTracks
? row.departureSpeedP
: '&nbsp; '
}}
</td>
<td
v-if="row.departureTracks == 2"
class="border-l border-l-black dark:border-l-white"
width="35"
>
{{
row.departureSpeedL != row.arrivalSpeedL ||
row.departureTracks != row.arrivalTracks
? row.departureSpeedL
: '&nbsp; '
}}
</td>
</tr>
</tbody>
</table>
</div>
</td>
<!-- Station -->
<td class="border border-black dark:border-white relative">
<div class="absolute top-0 left-0 w-full h-full">
<div class="flex flex-col h-full justify-between p-1">
<div :class="{ 'font-bold': row.isMain }">
{{ row.pointName }}
<span v-if="row.stopTime"> ; {{ row.stopType || 'pt' }}</span>
</div>
<div class="flex justify-between">
<span>{{ row.pointKm }}</span>
<span>{{ row.abbrevs.join(', ') }}</span>
</div>
</div>
</div>
</td>
<!-- Time -->
<td class="p-0 border border-black dark:border-white relative">
<div class="absolute top-0 left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="text-center align-top h-full">
<td
class="border-r-[1px] border-r-black dark:border-r-white"
:class="{ 'font-bold': row.stopTime > 0 }"
>
<span
v-if="
(row.scheduledArrivalDate?.getTime() || 0) !=
(row.scheduledDepartureDate?.getTime() || 0)
"
>
{{
row.scheduledArrivalDate?.toLocaleTimeString('pl-PL', {
hour: '2-digit',
minute: '2-digit'
})
}}<sup
v-if="
row.scheduledArrivalDate && row.scheduledArrivalDate.getSeconds() != 0
"
>{{ Math.floor((row.scheduledArrivalDate.getSeconds() / 60) * 10) }}</sup
>
</span>
<span v-else> | </span>
</td>
<td width="30">{{ row.driveTime > 0 ? row.driveTime / 60000 : '' }}</td>
</tr>
<tr class="text-center align-bottom h-full">
<td
class="border-r-[1px] border-r-black dark:border-r-white"
:class="{ 'font-bold': row.stopTime > 0 }"
>
<span>{{
row.scheduledDepartureDate?.toLocaleTimeString('pl-PL', {
hour: '2-digit',
minute: '2-digit'
})
}}</span>
<sup
v-if="
row.scheduledDepartureDate && row.scheduledDepartureDate.getSeconds() != 0
"
>{{ Math.floor((row.scheduledDepartureDate.getSeconds() / 60) * 10) }}</sup
>
</td>
<td width="30" class="font-bold">{{ row.stopTime || '' }}</td>
</tr>
</tbody>
</table>
</div>
</td>
<!-- Locos -->
<td
class="p-0 text-center border border-black dark:border-white relative h-24 text-sm"
:class="{ 'text-stone-400 ': i > 0 }"
>
<table class="h-full w-full border-collapse">
<tbody>
<tr class="border-b-[1px] border-b-black dark:border-b-white">
<td>{{ row.headUnits[0] }}</td>
</tr>
<tr class="border-b-[1px] border-b-black dark:border-b-white">
<td>{{ row.headUnits[1] ?? '&nbsp;' }}</td>
</tr>
<tr>
<td>{{ row.headUnits[2] ?? '&nbsp;' }}</td>
</tr>
</tbody>
</table>
</td>
<!-- Load / Length -->
<td
class="p-0 text-center border border-black dark:border-white relative"
:class="{ 'text-stone-400 ': i > 0 }"
>
<div class="absolute top-0 left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="border-b-[1px] border-b-black dark:border-b-white">
<td>{{ Math.floor(row.stockMass / 1000) }}</td>
</tr>
<tr>
<td>{{ row.stockLength }}</td>
</tr>
</tbody>
</table>
</div>
</td>
<!-- Vmax-->
<td
class="text-center border border-black dark:border-white"
:class="{ 'text-stone-400 ': i > 0 }"
>
{{ row.stockVmax }}
</td>
</tr>
</tbody>
</table>
<div class="mt-2">
<b><u>Kursuje:</u></b>
<div>
- {{ parseTimetableRunDate(computedTimetableRows[0].scheduledDepartureDate!) }}
<span
v-if="
computedTimetableRows[
computedTimetableRows.length - 1
].scheduledArrivalDate!.getDate() !=
computedTimetableRows[0].scheduledDepartureDate!.getDate()
"
>
-
{{
parseTimetableRunDate(
computedTimetableRows[computedTimetableRows.length - 1].scheduledArrivalDate!
)
}}
</span>
</div>
<div v-if="timetableWarnings.length != 0">
<b><u>Uwagi do rozkładu:</u></b>
<div>- {{ timetableWarnings }}</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
import { parseTimetableRunDate } from '../../utils/dateUtils';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
// Tymczasowa tabelka z posterunkami APO
const apoNames = ['Stary Kisielin, pe', 'Czerwony Dwór, pe', 'Szczejkowice, pe'];
const timetableWarnings = computed(() => {
const timetableData = globalStore.currentTimetableData;
if (!timetableData) return '';
return timetableData.warningNotes;
});
const computedTimetableRows = computed(() => {
const timetableData = globalStore.currentTimetableData;
if (!timetableData) return [];
let timeFrom = Date.now();
const stockVmax = timetableData.trainMaxSpeed,
stockMass = timetableData.mass,
stockLength = timetableData.length;
const timetablePath = parseTimetablePath(timetableData.path);
const stopRows: StopRow[] = [];
let lastRowRef: StopRow | null = null;
let currentPathIndex = 0;
let currentPath = timetablePath[0];
let lastDepartureTimestamp = 0;
let arrivalSpeedL = 0,
arrivalSpeedP = 0;
let departureSpeedL = 0,
departureSpeedP = 0;
let arrivalKm = 0,
arrivalTracks = 0,
departureTracks = 2,
arrivalLineNumber = 0,
departureLineNumber = 0,
abbrevs = [] as string[];
if (currentPath.departureLineData) {
departureSpeedL = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
departureSpeedP = currentPath.departureLineData.routeSpeedExit
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
: departureSpeedL;
departureTracks = currentPath.departureLineData.routeTracks;
arrivalSpeedL = departureSpeedL;
arrivalSpeedP = departureSpeedP;
arrivalTracks = departureTracks;
departureLineNumber = currentPath.departureLineData?.realLineNo ?? 0;
arrivalLineNumber = departureLineNumber;
abbrevs = getAbbrevs(currentPath.departureLineData);
}
// console.debug('=========== ' + timetableData.trainNo + ' ===========');
const stopList = parseStopListString(timetableData.stopListString);
for (const stop of stopList) {
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
if (arrivalKm >= stop.stopDistance)
arrivalKm =
(Number(stopRows[stopRows.length - 1].departureKm ?? '0') + stop.stopDistance) / 2;
if (currentPath.arrivalLineData) {
arrivalSpeedP = Math.min(currentPath.arrivalLineData.routeSpeed, stockVmax);
arrivalSpeedL = currentPath.arrivalLineData.routeSpeedExit
? Math.min(currentPath.arrivalLineData.routeSpeedExit, stockVmax)
: arrivalSpeedP;
arrivalTracks = currentPath.arrivalLineData.routeTracks;
arrivalLineNumber = currentPath.arrivalLineData.realLineNo ?? 0;
abbrevs = getAbbrevs(currentPath.arrivalLineData);
}
departureSpeedL = arrivalSpeedL;
departureSpeedP = arrivalSpeedP;
departureLineNumber = arrivalLineNumber;
departureTracks = arrivalTracks;
}
if (
stop.mainStop ||
(/^podg|po|pe$/.test(stop.stopNameRAW) && !/^sbl/i.test(stop.stopNameRAW))
) {
let correctedDepartureSpeedL = 0,
correctedDepartureSpeedP = 0,
correctedDepartureTracks = 0;
const internalRouteInfo = stop.departureLine
? currentPath.sceneryData?.routesInfo.find(
(route) => route.isInternal && route.routeName == stop.departureLine
)
: undefined;
if (internalRouteInfo) {
correctedDepartureSpeedL = Math.min(internalRouteInfo.routeSpeed, stockVmax);
correctedDepartureSpeedP = internalRouteInfo.routeSpeedExit
? Math.min(internalRouteInfo.routeSpeedExit, stockVmax)
: correctedDepartureSpeedL;
departureSpeedL = correctedDepartureSpeedL;
departureSpeedP = correctedDepartureSpeedP;
arrivalLineNumber = internalRouteInfo.realLineNo ?? arrivalLineNumber;
departureLineNumber = internalRouteInfo.realLineNo ?? departureLineNumber;
abbrevs = getAbbrevs(internalRouteInfo);
correctedDepartureTracks = internalRouteInfo.routeTracks;
departureTracks = internalRouteInfo.routeTracks;
if (stopRows.length == 0) {
arrivalSpeedL = departureSpeedL;
arrivalSpeedP = departureSpeedP;
arrivalTracks = departureTracks;
arrivalLineNumber = departureLineNumber;
}
}
let pointAbbrevs = [];
if (apoNames.includes(stop.stopNameRAW))
pointAbbrevs.unshift(`APO ${currentPath.sceneryData?.abbr}`);
let rowData: StopRow = {
isMain: stop.mainStop,
pointKm: stop.stopDistance.toFixed(3),
pointName: stop.stopNameRAW,
scheduledArrivalDate: stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null,
scheduledDepartureDate: stop.departureTimestamp ? new Date(stop.departureTimestamp) : null,
stopTime: stop.stopTime ? (stop.departureTimestamp - stop.arrivalTimestamp) / 60000 : 0,
stopType: stop.stopType,
sceneryName: currentPath.sceneryName,
arrivalLineNumber: arrivalLineNumber == 0 ? '' : arrivalLineNumber.toString(),
departureLineNumber: departureLineNumber == 0 ? '' : departureLineNumber.toString(),
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
abbrevs: [...pointAbbrevs, ...abbrevs],
arrivalKm: arrivalKm.toFixed(3),
departureKm: stop.stopDistance.toFixed(3),
arrivalSpeedL,
arrivalSpeedP,
arrivalTracks: arrivalTracks,
departureSpeedL,
departureSpeedP,
departureTracks: departureTracks,
headUnits: timetableData.headUnits,
stockVmax,
stockLength,
stockMass,
lastRowRef
};
// console.debug(stop.stopNameRAW, stop.departureLine);
arrivalKm = stop.stopDistance;
arrivalSpeedL = correctedDepartureSpeedL || arrivalSpeedL;
arrivalSpeedP = correctedDepartureSpeedP || arrivalSpeedP;
arrivalTracks = correctedDepartureTracks || arrivalTracks;
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
lastRowRef = rowData;
stopRows.push(rowData);
}
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
arrivalKm = stop.stopDistance;
// Reverse search for last scenery checkpoint
if (currentPath.departureLineData) {
if (
currentPath.departureLineData.routeLength != 0 &&
!currentPath.departureLineData.isRouteSBL
)
arrivalKm = stop.stopDistance + currentPath.departureLineData.routeLength / 1000;
if (
stopRows[stopRows.length - 1].isMain &&
currentPath.departureLineData.isRouteSBL &&
stop.departureLine == currentPath.departureLine
)
arrivalKm = stop.stopDistance + currentPath.departureLineData.routeLength / 1000;
for (let i = stopRows.length - 1; i >= 0; i--) {
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
stopRows[i].departureSpeedL = Math.min(
currentPath.departureLineData.routeSpeed,
stockVmax
);
stopRows[i].departureSpeedP = currentPath.departureLineData.routeSpeedExit
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
: stopRows[i].departureSpeedL;
// stopRows[i].arrivalLineNumber =
// currentPath.departureLineData.realLineNo?.toString() ?? '';
stopRows[i].departureLineNumber =
currentPath.departureLineData.realLineNo?.toString() ?? '';
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
stopRows[i].departureSpeedL = Math.min(
currentPath.departureLineData.routeSpeed,
stockVmax
);
stopRows[i].departureSpeedP = currentPath.departureLineData.routeSpeedExit
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
: stopRows[i].departureSpeedL;
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
// console.log(
// stop.departureLine,
// currentPath.sceneryName,
// stop.stopDistance,
// currentPath.departureLineData.routeLength,
// currentPath.departureLineData.isRouteSBL,
// currentPath.departureLineData.realLineNo
// );
abbrevs = getAbbrevs(currentPath.departureLineData);
stopRows[i].abbrevs = abbrevs;
break;
}
stopRows[i].arrivalSpeedP = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
stopRows[i].arrivalSpeedL = currentPath.departureLineData.routeSpeedExit
? Math.min(currentPath.departureLineData.routeSpeedExit, stockVmax)
: stopRows[i].arrivalSpeedP;
stopRows[i].arrivalTracks = currentPath.departureLineData.routeTracks;
stopRows[i].arrivalLineNumber =
currentPath.departureLineData.realLineNo?.toString() ?? '';
}
}
currentPath = timetablePath[++currentPathIndex];
}
}
let timeTo = Date.now();
globalStore.generatedMs = timeTo - timeFrom;
return stopRows;
});
function parseTimetablePath(path: string): TimetablePathData[] {
return path.split(';').map((pathEl) => {
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
const sceneryData = apiStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
const arrivalLineData = arrivalLine
? (sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null)
: null;
const departureLineData = departureLine
? (sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null)
: null;
return {
sceneryName,
sceneryData: sceneryData ?? null,
arrivalLine: arrivalLine ?? '',
departureLine: departureLine ?? '',
arrivalLineData,
departureLineData
};
});
}
function parseStopListString(stopsString: string) {
//${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}
return stopsString.split('~~').map((stop) => {
const [
arrivalLine,
arrivalTimestamp,
stopNameRAW,
stopDetails,
isMainStop,
stopDistance,
departureTimestamp,
departureLine
] = stop.split(';');
const [stopTime, stopType] = stopDetails.split('_');
return {
arrivalLine,
arrivalTimestamp: parseInt(arrivalTimestamp),
stopNameRAW,
stopTime: stopTime ?? 0,
stopType: stopType ?? null,
mainStop: isMainStop == 'true',
stopDistance: parseFloat(stopDistance),
departureTimestamp: parseInt(departureTimestamp),
departureLine
};
});
}
function getAbbrevs(routeData: SceneryRoute) {
const abbrevs = [];
if (routeData.isRouteSBL == true)
abbrevs.push(
`${routeData.routeSpeed > 130 ? '4' : ''}S${routeData.routeTracks == 2 ? 'S' : ''}`
);
else if (routeData.routeTracks == 2) abbrevs.push('PP');
return abbrevs;
}
</script>
<style scoped>
@media print {
th,
tr,
td {
border-color: theme('colors.black');
}
table {
page-break-after: auto;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
thead {
display: table-header-group;
}
}
</style>
@@ -0,0 +1,394 @@
<template>
<div>
<h2 class="font-semibold text-center text-3xl">
{{ globalStore.currentTimetableData!.category }}
{{ globalStore.currentTimetableData!.trainNo }}
</h2>
<h3 class="font-medium text-center">
{{ globalStore.currentTimetableData!.route.replace('|', ' - ') }}
</h3>
<p class="mt-2 text-center">
Kursuje: {{ timetableDate.toLocaleDateString('pl-PL', { day: '2-digit' }) }}.{{
romanMonthDigits[timetableDate.getMonth()]
}}.{{ timetableDate.toLocaleDateString('pl-PL', { year: 'numeric' }) }}
</p>
<p class="mt-2">
Lokomotywa elektryczna {{ globalStore.currentTimetableData!.headUnits[0] }}, waga:
{{ (globalStore.currentTimetableData!.mass / 1000).toFixed(1) }} t, długość:
{{ globalStore.currentTimetableData!.length }} m
</p>
<p></p>
<table class="table-fixed w-full border-collapse h-full">
<thead>
<tr>
<!-- Name -->
<th
width="250"
class="font-normal border border-black dark:border-white border-l-transparent"
>
<MapPinIcon :size="20" class="mx-auto" />
</th>
<!-- Info -->
<th width="50" class="font-normal border border-black dark:border-white">
<CircleAlertIcon :size="20" class="mx-auto" />
</th>
<!-- Drive time -->
<th width="30" class="font-normal border border-black dark:border-white">
<TimerIcon :size="20" class="mx-auto" />
</th>
<!-- Arrival -->
<th width="70" class="font-normal border border-black dark:border-white">
<CalendarArrowUpIcon :size="20" class="mx-auto" />
</th>
<!-- Stop time -->
<th width="40" class="font-normal border border-black dark:border-white">
<HandIcon :size="20" class="mx-auto" />
</th>
<!-- Departure -->
<th width="70" class="font-normal border border-black dark:border-white">
<CalendarArrowDownIcon :size="20" class="mx-auto" />
</th>
<!-- vMax -->
<th
width="80"
class="font-normal border border-black dark:border-white border-r-transparent"
>
<CircleGaugeIcon :size="20" class="mx-auto" />
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(row, i) in computedTimetableRows"
:class="{ 'bg-slate-100 dark:bg-zinc-900 print:bg-gray-300': i % 2 == 0 }"
class="leading-none"
>
<td class="px-2 font-thin text-nowrap overflow-hidden overflow-ellipsis">
<span :class="{ 'font-semibold': row.isMain, 'font-normal': !row.isMain }">
{{ row.pointName }}</span
><span
>.............................................................................................
</span>
</td>
<td
class="border border-black dark:border-white border-t-transparent border-b-transparent"
></td>
<td
class="border border-black dark:border-white border-t-transparent border-b-transparent text-center font-bold"
>
{{ row.driveTime ? Math.floor(row.driveTime / 60000) : '' }}
</td>
<td
class="border border-black dark:border-white border-t-transparent border-b-transparent text-right font-bold px-2"
>
<span v-if="row.stopType == 'pt'">+ </span>
<span> {{ row.arrivalDateStr }} </span>
</td>
<td
class="border border-black dark:border-white border-t-transparent border-b-transparent text-center font-semibold"
>
{{ row.stopTime || '' }}
</td>
<td
class="border border-black dark:border-white border-t-transparent border-b-transparent text-right font-bold px-2 relative"
>
<span
class="absolute right-[-3px] border-r-[5px] border-black"
:class="{
'top-0 h-[calc(100%+1px)]':
row.arrivalTracks == row.departureTracks && row.arrivalTracks == 2,
'top-0 h-[calc(50%+1px)]': row.arrivalTracks > row.departureTracks,
'top-1/2 h-[calc(50%+1px)]': row.arrivalTracks < row.departureTracks
}"
></span>
{{ row.departureDateStr }}
</td>
<!-- v-if="
i == 0 || (i > 0 && computedTimetableRows[i - 1].departureSpeed != row.arrivalSpeed)
" -->
<td class="text-center font-bold">
<span v-if="i == 0 || computedTimetableRows[i - 1].departureSpeed != row.arrivalSpeed">
{{ i == 0 ? row.departureSpeed : row.arrivalSpeed }}
<span v-if="row.arrivalSpeed != row.departureSpeed">
/
{{ row.departureSpeed }}
</span>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useGlobalStore } from '../../stores/global.store';
import type { StopRowCZ, TimetablePathData } from '../../types/common.types';
import { useApiStore } from '../../stores/api.store';
import {
CalendarArrowDownIcon,
CalendarArrowUpIcon,
CircleAlertIcon,
CircleGaugeIcon,
HandIcon,
MapPinIcon,
TimerIcon
} from 'lucide-vue-next';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
const timetableDate = ref(new Date());
const romanMonthDigits = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
const computedTimetableRows = computed(() => {
const timetableData = globalStore.currentTimetableData;
if (!timetableData) return [];
let timeFrom = Date.now();
const stockVmax = timetableData.trainMaxSpeed,
stockMass = Math.floor(timetableData.mass / 1000),
stockLength = timetableData.length;
const timetablePath = parseTimetablePath(timetableData.path);
const stopRows: StopRowCZ[] = [];
let currentPathIndex = 0;
let currentPath = timetablePath[0];
let lastDepartureTimestamp = 0;
let arrivalSpeed = 0,
departureSpeed = 0,
arrivalTracks = 0,
departureTracks = 2;
if (currentPath.departureLineData) {
departureSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
arrivalSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
departureTracks = currentPath.departureLineData.routeTracks;
arrivalTracks = currentPath.departureLineData.routeTracks;
}
const stopList = parseStopListString(timetableData.stopListString);
timetableDate.value = new Date(stopList[0].departureTimestamp);
stopList.forEach((stop) => {
if (stop.arrivalLine && stop.arrivalLine == currentPath.arrivalLine) {
if (currentPath.arrivalLineData) {
arrivalSpeed = Math.min(currentPath.arrivalLineData.routeSpeed, stockVmax);
arrivalTracks = currentPath.arrivalLineData.routeTracks;
}
departureSpeed = arrivalSpeed;
departureTracks = arrivalTracks;
}
if (
stop.mainStop ||
(/^podg|po|pe$/.test(stop.stopNameRAW) && !/^sbl/i.test(stop.stopNameRAW))
) {
let correctedDepartureSpeed = 0,
correctedDepartureTracks = 0;
const internalRouteInfo = stop.departureLine
? currentPath.sceneryData?.routesInfo.find(
(route) => route.isInternal && route.routeName == stop.departureLine
)
: undefined;
if (internalRouteInfo) {
correctedDepartureSpeed = Math.min(internalRouteInfo.routeSpeed, stockVmax);
departureSpeed = Math.min(internalRouteInfo.routeSpeed, stockVmax);
correctedDepartureTracks = internalRouteInfo.routeTracks;
departureTracks = internalRouteInfo.routeTracks;
if (stopRows.length == 0) {
arrivalSpeed = departureSpeed;
arrivalTracks = departureTracks;
}
}
const scheduledArrivalDate = stop.arrivalTimestamp ? new Date(stop.arrivalTimestamp) : null;
const scheduledDepartureDate = stop.departureTimestamp
? new Date(stop.departureTimestamp)
: null;
let arrivalDateStr =
scheduledArrivalDate?.toLocaleTimeString('pl-PL', {
hour: 'numeric',
minute: '2-digit'
}) ?? '';
let departureDateStr =
scheduledDepartureDate?.toLocaleTimeString('pl-PL', {
hour: 'numeric',
minute: '2-digit'
}) ?? '';
if (
stopRows.length > 0 &&
stopRows[stopRows.length - 1]?.scheduledArrivalDate?.getHours() ==
scheduledArrivalDate?.getHours()
) {
arrivalDateStr = arrivalDateStr.split(':').slice(1).join(' ');
}
if (
stopRows[stopRows.length - 1]?.scheduledDepartureDate?.getHours() ==
scheduledDepartureDate?.getHours()
) {
departureDateStr = departureDateStr.split(':').slice(1).join(' ');
}
let rowData: StopRowCZ = {
isMain: stop.mainStop,
pointKm: stop.stopDistance.toFixed(3),
pointName: stop.stopNameRAW,
scheduledArrivalDate,
scheduledDepartureDate,
stopTime: stop.stopTime ? (stop.departureTimestamp - stop.arrivalTimestamp) / 60000 : 0,
stopType: stop.stopType,
sceneryName: currentPath.sceneryName,
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
arrivalSpeed: arrivalSpeed,
departureSpeed: departureSpeed,
arrivalTracks,
departureTracks,
headUnits: timetableData.headUnits,
stockVmax,
stockLength,
stockMass,
arrivalDateStr,
departureDateStr
};
arrivalSpeed = correctedDepartureSpeed || arrivalSpeed;
arrivalTracks = correctedDepartureTracks || arrivalTracks;
if (stop.departureTimestamp) lastDepartureTimestamp = stop.departureTimestamp;
stopRows.push(rowData);
}
if (stop.departureLine && stop.departureLine == currentPath.departureLine) {
// Reverse search for last scenery checkpoint
if (currentPath.departureLineData) {
for (let i = stopRows.length - 1; i >= 0; i--) {
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
stopRows[i].departureSpeed = Math.min(
currentPath.departureLineData.routeSpeed,
stockVmax
);
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
stopRows[i].departureSpeed = Math.min(
currentPath.departureLineData.routeSpeed,
stockVmax
);
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
break;
}
stopRows[i].arrivalSpeed = Math.min(currentPath.departureLineData.routeSpeed, stockVmax);
stopRows[i].arrivalTracks = currentPath.departureLineData.routeTracks;
}
}
currentPath = timetablePath[++currentPathIndex];
}
});
let timeTo = Date.now();
globalStore.generatedMs = timeTo - timeFrom;
return stopRows;
});
function parseTimetablePath(path: string): TimetablePathData[] {
return path.split(';').map((pathEl) => {
const [arrivalLine, scenery, departureLine] = pathEl.split(',');
const sceneryName = scenery.split(' ').slice(0, -1).join(' ');
const sceneryData = apiStore.sceneryData?.find((sc) => sc.name == sceneryName) ?? null;
const arrivalLineData = arrivalLine
? sceneryData?.routesInfo.find((rt) => rt.routeName == arrivalLine) ?? null
: null;
const departureLineData = departureLine
? sceneryData?.routesInfo.find((rt) => rt.routeName == departureLine) ?? null
: null;
return {
sceneryName,
sceneryData: sceneryData ?? null,
arrivalLine: arrivalLine ?? '',
departureLine: departureLine ?? '',
arrivalLineData,
departureLineData
};
});
}
function parseStopListString(stopsString: string) {
//${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''};${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${stop.departureLine ?? ''}
return stopsString.split('~~').map((stop) => {
const [
arrivalLine,
arrivalTimestamp,
stopNameRAW,
stopDetails,
isMainStop,
stopDistance,
departureTimestamp,
departureLine
] = stop.split(';');
const [stopTime, stopType] = stopDetails.split('_');
return {
arrivalLine,
arrivalTimestamp: parseInt(arrivalTimestamp),
stopNameRAW,
stopTime: Number(stopTime ?? 0),
stopType: stopType ?? null,
mainStop: isMainStop == 'true',
stopDistance: parseFloat(stopDistance),
departureTimestamp: parseInt(departureTimestamp),
departureLine
};
});
}
</script>
@@ -0,0 +1,103 @@
<template>
<!-- Local -->
<div class="print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
<div class="flex gap-2 mt-1">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
<div>
<InfoIcon :size="20" />
</div>
<i18n-t keypath="storage-preview-info" tag="span">
<template v-slot:id>
<b>#{{ globalStore.currentTimetableData.timetableId }}</b>
</template>
<template v-slot:driverName>
<b>{{ globalStore.currentTimetableData.driverName }}</b>
</template>
<template v-slot:date>
<b>{{
new Date(
globalStore.currentTimetableData?.journalCreatedAt ??
globalStore.currentTimetableData.savedTimestamp
).toLocaleString()
}}</b>
</template>
</i18n-t>
</div>
<button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
@click="removeTimetable(globalStore.currentTimetableData.timetableId)"
>
<Trash2Icon />
</button>
<button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
@click="globalStore.selectedStorageTimetable = null"
>
<Undo2Icon />
</button>
</div>
</div>
<!-- Journal -->
<div class="print:hidden" v-else-if="globalStore.currentTimetableData?.journalCreatedAt">
<div class="flex gap-2 mt-1">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
<div>
<InfoIcon :size="20" />
</div>
<i18n-t keypath="journal-preview-info" tag="span">
<template v-slot:id>
<b>#{{ globalStore.currentTimetableData.timetableId }}</b>
</template>
<template v-slot:driverName>
<b>{{ globalStore.currentTimetableData.driverName }}</b>
</template>
<template v-slot:date>
<b>{{
new Date(globalStore.currentTimetableData.journalCreatedAt).toLocaleString()
}}</b>
</template>
</i18n-t>
</div>
<button
class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800 rounded-md"
@click="globalStore.selectedJournalTimetable = null"
>
<Undo2Icon />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import { useI18n } from 'vue-i18n';
import type { TimetableData } from '../../types/common.types';
import { InfoIcon, Trash2Icon, Undo2Icon } from 'lucide-vue-next';
const globalStore = useGlobalStore();
const i18n = useI18n();
function removeTimetable(timetableId: number) {
const isConfirmed = confirm(i18n.t('delete-timetable-confirm'));
if (!isConfirmed) return;
try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
delete savedTimetablesJSON[timetableId];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
globalStore.selectedStorageTimetable = null;
} catch (error) {}
}
</script>
@@ -0,0 +1,42 @@
<template>
<select
v-model="globalStore.selectedTrainId"
@change="selectTrain"
name="trains"
id="trains-select"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:disabled="apiStore.apiDataStatus != DataStatus.SUCCESS"
aria-label="Active train select"
>
<option :value="null" disabled>
{{
apiStore.apiDataStatus == DataStatus.LOADING
? $t('data-loading-text')
: $t('train-select-placeholder')
}}
</option>
<option :value="train.id" v-for="train in globalStore.activeTimetableTrains">
{{ train.driverName }} | {{ train.timetable?.category }} {{ train.trainNo }} [{{
getRegionNameById(train.region)
}}]
</option>
</select>
</template>
<script setup lang="ts">
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import { DataStatus } from '../../types/api.types';
import { getRegionNameById } from '../../utils/trainUtils';
const apiStore = useApiStore();
const globalStore = useGlobalStore();
function selectTrain() {
if (!apiStore.activeData) return;
globalStore.selectedActiveTrain =
globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ??
null;
}
</script>
@@ -0,0 +1,129 @@
<template>
<div class="flex gap-2 items-center" v-if="globalStore.currentTimetableData == null">
<div class="flex gap-2 w-full">
<input
v-model="globalStore.journalTimetableSearch.driverName"
type="text"
@keydown.enter="fetchJournalTimetables"
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
}`"
:disabled="
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
"
:placeholder="$t('journal-driver-search-placeholder')"
/>
<input
v-model="globalStore.journalTimetableSearch.route"
type="text"
@keydown.enter="fetchJournalTimetables"
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
}`"
:disabled="
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
"
:placeholder="$t('journal-route-search-placeholder')"
/>
<input
v-model="globalStore.journalTimetableSearch.date"
type="date"
@keydown.enter="fetchJournalTimetables"
:class="`bg-zinc-800 p-1 rounded-md print:hidden w-full ${
apiStore.connectionMode == 'offline' ? 'opacity-35' : ''
}`"
:disabled="
apiStore.journalDataStatus == DataStatus.LOADING || apiStore.connectionMode == 'offline'
"
:placeholder="$t('journal-date-search-placeholder')"
/>
</div>
<button
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
v-if="globalStore.viewMode == 'journal'"
@click="clearSearch"
>
<Trash2Icon />
</button>
<button
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
v-if="globalStore.viewMode == 'journal'"
@click="fetchJournalTimetables"
>
<SearchIcon />
</button>
</div>
</template>
<script setup lang="ts">
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import { DataStatus, type JournalTimetablesShortResponse } from '../../types/api.types';
import { SearchIcon, Trash2Icon } from 'lucide-vue-next';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
async function fetchJournalTimetables() {
const searchValues = globalStore.journalTimetableSearch;
let fetchParams: Record<string, any> = {};
if (searchValues['driverName']) {
fetchParams['driverName'] = searchValues['driverName'].trim();
}
if (searchValues['date']) {
let dateFromStr = new Date(searchValues['date']).toISOString();
let dateTo = new Date(dateFromStr);
dateTo.setDate(dateTo.getDate() + 1);
fetchParams['dateFrom'] = dateFromStr;
fetchParams['dateTo'] = dateTo.toISOString();
}
if (searchValues['route']) {
const [routeFrom, routeTo] = searchValues['route'].split('-');
if (routeFrom) {
fetchParams['issuedFrom'] = routeFrom.trim();
}
if (routeTo) {
fetchParams['terminatingAt'] = routeTo.trim();
}
}
fetchParams['hasStopsDetails'] = 1;
fetchParams['returnType'] = 'short';
try {
apiStore.journalDataStatus = DataStatus.LOADING;
const response = await apiStore.client.get<JournalTimetablesShortResponse>(
'api/getTimetables',
fetchParams
);
apiStore.journalDataStatus = DataStatus.SUCCESS;
apiStore.journalTimetablesData = response;
} catch (error) {
apiStore.journalDataStatus = DataStatus.ERROR;
apiStore.journalTimetablesData = null;
console.error(error);
}
}
function clearSearch() {
Object.keys(globalStore.journalTimetableSearch).forEach(
(k) => ((globalStore.journalTimetableSearch as any)[k] = '')
);
apiStore.journalTimetablesData = null;
}
</script>
@@ -0,0 +1,33 @@
<template>
<div class="flex gap-2" v-if="globalStore.currentTimetableData == null">
<div class="w-full">
<input
v-model="globalStore.localTimetableSearch"
type="text"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:placeholder="$t('train-search-placeholder')"
/>
</div>
<div>
<button
class="bg-zinc-800 hover:bg-zinc-700 p-1 rounded-md"
v-if="globalStore.viewMode == 'storage'"
@click="clearSearch"
>
<Trash2Icon />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { Trash2Icon } from 'lucide-vue-next';
import { useGlobalStore } from '../../stores/global.store';
const globalStore = useGlobalStore();
function clearSearch() {
globalStore.localTimetableSearch = '';
}
</script>
@@ -0,0 +1,26 @@
<template>
<div class="flex gap-2 flex-col print:hidden">
<!-- Top Actions & Modes -->
<SearchModeActions />
<!-- Active Data Search -->
<ActiveSearchInput v-if="globalStore.viewMode == 'active'" />
<!-- Local Storage Search -->
<LocalSearchInput v-else-if="globalStore.viewMode == 'storage'" />
<!-- Journal Serach -->
<JournalSearchInput v-else />
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import ActiveSearchInput from './ActiveSearchInput.vue';
import JournalSearchInput from './JournalSearchInput.vue';
import LocalSearchInput from './LocalSearchInput.vue';
import SearchModeActions from './SearchModeActions.vue';
const globalStore = useGlobalStore();
</script>
@@ -0,0 +1,179 @@
<template>
<div class="flex justify-between gap-2">
<div class="flex gap-2">
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'active'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('active')"
aria-label="Active data view mode"
>
<WifiIcon />
</button>
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'storage'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('storage')"
aria-label="Storage view mode"
>
<ArchiveIcon />
</button>
<button
:class="`p-1 rounded-md ${
globalStore.viewMode == 'journal'
? 'bg-green-600 hover:bg-green-500'
: 'bg-zinc-800 hover:bg-zinc-700'
}`"
@click="toggleViewMode('journal')"
aria-label="Journal view mode"
>
<HistoryIcon />
</button>
</div>
<div class="flex gap-2">
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700"
@click="toggleDarkMode"
aria-label="Dark mode toggle"
>
<MoonIcon v-if="globalStore.darkMode" />
<SunIcon v-else />
</button>
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
@click="toggleFullscreenMode()"
:disabled="globalStore.currentTimetableData == null"
aria-label="Full screen toggle"
>
<FullscreenIcon :size="24" />
</button>
<button
class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700 disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
@click="openPrintingWindow"
aria-label="Print mode"
>
<PrinterIcon />
</button>
<button
class="p-1 rounded-md disabled:opacity-60 disabled:hover:bg-zinc-800"
:disabled="globalStore.currentTimetableData == null"
:class="{
'bg-green-600 hover:bg-green-700': isTimetableSaved,
'bg-zinc-800 hover:bg-zinc-700': !isTimetableSaved
}"
@click="saveToStorage"
aria-label="Save timetable to storage"
>
<FolderDownIcon />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, watch } from 'vue';
import { useGlobalStore } from '../../stores/global.store';
import type { ViewMode, TimetableData } from '../../types/common.types';
import {
ArchiveIcon,
FolderDownIcon,
FullscreenIcon,
HistoryIcon,
MoonIcon,
PrinterIcon,
SunIcon,
WifiIcon
} from 'lucide-vue-next';
const globalStore = useGlobalStore();
// Computed
const isTimetableSaved = computed(() => {
if (!globalStore.currentTimetableData) return false;
return Object.keys(globalStore.storageTimetables).includes(
`${globalStore.currentTimetableData.timetableId}`
);
});
// Watchers
watch(
() => globalStore.selectedActiveTrain,
(curr) => {
if (curr != null) {
globalStore.generatedDate = new Date();
}
}
);
// Methods
function toggleViewMode(viewMode: ViewMode) {
globalStore.viewMode = viewMode;
}
function toggleDarkMode() {
globalStore.darkMode = !globalStore.darkMode;
window.localStorage.setItem('currentTheme', globalStore.darkMode ? 'dark' : 'light');
}
function saveToStorage() {
if (globalStore.currentTimetableData == null) return;
try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
if (savedTimetablesJSON[globalStore.currentTimetableData.timetableId] !== undefined) {
globalStore.selectedStorageTimetable =
savedTimetablesJSON[globalStore.currentTimetableData.timetableId];
globalStore.viewMode = 'storage';
return;
}
savedTimetablesJSON[globalStore.currentTimetableData.timetableId] = {
...globalStore.currentTimetableData,
savedTimestamp: Date.now()
};
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
} catch (error) {}
}
function openPrintingWindow() {
if (globalStore.selectedActiveTrain != null) {
const date = `${globalStore
.generatedDate!.toLocaleDateString('pl-PL')
.replace(/\./g, '-')}--${globalStore
.generatedDate!.toLocaleTimeString('pl-PL')
.replace(/:/g, '-')}`;
document.title = `${globalStore.selectedActiveTrain.driverName} ; ${
globalStore.selectedActiveTrain.timetable!.category
} ${globalStore.selectedActiveTrain.trainNo}
${globalStore.selectedActiveTrain.timetable?.route.replace('|', ' - ')} ; ${date}`;
}
window.print();
}
function toggleFullscreenMode() {
globalStore.fullscreenMode = !globalStore.fullscreenMode;
}
</script>
@@ -0,0 +1,12 @@
<template>
<div>
<div v-if="apiStore.connectionMode == 'online'">{{ $t('train-select-info') }}</div>
<div v-else class="bg-red-500 text-white p-2">{{ $t('data-offline-mode') }}</div>
</div>
</template>
<script setup lang="ts">
import { useApiStore } from '../../stores/api.store';
const apiStore = useApiStore();
</script>
@@ -0,0 +1,23 @@
<template>
<div
class="overflow-auto p-1 bg-white print:bg-white dark:bg-zinc-950 print:text-black text-black dark:text-white min-h-full relative"
:class="{ dark: globalStore.darkMode }"
>
<!-- <button
v-if="globalStore.fullscreenMode"
class="fixed right-0 top-0 bg-green-600 p-1 m-1 rounded-md hover:bg-green-500 print:hidden"
@click="() => (globalStore.fullscreenMode = false)"
>
<Minimize2Icon :size="22" />
</button> -->
<TimetableContent />
</div>
</template>
<script setup lang="ts">
import { useGlobalStore } from '../../stores/global.store';
import TimetableContent from '../Timetable/TimetableContent.vue';
const globalStore = useGlobalStore();
</script>
@@ -0,0 +1,93 @@
<template>
<div class="text-white">
<h2
class="font-bold p-2 bg-zinc-700 mb-3 text-2xl flex items-center gap-2 justify-center flex-wrap"
>
<HistoryIcon :size="25" />
{{ $t('journal-preview-title') }}
</h2>
<div v-if="apiStore.connectionMode == 'offline'" class="bg-red-500 p-2">
{{ $t('data-offline-mode') }}
</div>
<div v-else-if="apiStore.journalDataStatus == DataStatus.LOADING" class="bg-zinc-900 p-2">
{{ $t('data-loading-text') }}
</div>
<div v-else-if="apiStore.journalDataStatus == DataStatus.ERROR" class="bg-red-500 p-2">
{{ $t('data-loading-error-text') }}
</div>
<div
v-else-if="!apiStore.journalTimetablesData"
class="text-zinc-400 mt-2"
v-html="$t('journal-empty-info')"
></div>
<div v-else-if="apiStore.journalTimetablesData.length == 0">
<p class="text-zinc-300 mb-2">
{{ $t('journal-no-data') }}
</p>
<b class="text-red-300">
{{ $t('journal-reminder-text') }}
</b>
</div>
<div v-else>
<ul>
<li
v-for="timetable in apiStore.journalTimetablesData"
class="flex gap-1 w-full my-2"
@click="fetchTimetableDetails(timetable.id)"
>
<button class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left">
<div class="text-zinc-300">
#{{ timetable.id }} &bull;
{{ new Date(timetable.createdAt!).toLocaleString() }}
</div>
<b>
{{ timetable.driverName }} | {{ timetable.trainCategoryCode }}
{{ timetable.trainNo }}
</b>
{{ timetable.route.replace('|', ' > ') }}
</button>
</li>
</ul>
<div v-if="apiStore.journalTimetablesData.length > 0">
<hr class="border-t-2 border-t-gray-500" />
<p class="text-zinc-400 text-sm">
{{ $t('journal-footer-text') }}
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { HistoryIcon } from 'lucide-vue-next';
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import { DataStatus } from '../../types/api.types';
import type { JournalTimetableDetailed } from '../../types/common.types';
const apiStore = useApiStore();
const globalStore = useGlobalStore();
async function fetchTimetableDetails(id: number) {
try {
const response = await apiStore.client.get<JournalTimetableDetailed[]>('api/getTimetables', {
timetableId: id,
returnType: 'detailed'
});
if (response.length > 0) globalStore.selectedJournalTimetable = response[0];
} catch (error) {
globalStore.selectedJournalTimetable = null;
console.error(error);
}
}
</script>
@@ -0,0 +1,113 @@
<template>
<div class="text-white">
<div class="font-bold p-2 bg-zinc-700 mb-3">
<div class="text-2xl flex items-center gap-2 justify-center flex-wrap">
<ArchiveIcon :size="25" />
<span>{{ $t('storage-preview-title') }}</span>
</div>
</div>
<div
v-if="
globalStore.selectedStorageTimetable == null &&
Object.keys(globalStore.storageTimetables).length == 0
"
class="text-zinc-400"
>
{{ $t('storage-empty-info') }}
</div>
<div class="font-bold p-2 bg-zinc-800 mb-3" v-else-if="filteredTimetables.length == 0">
{{ $t('storage-preview-empty') }}
</div>
<ul v-else>
<li v-for="timetable in filteredTimetables" class="flex gap-1 w-full my-2">
<button
class="bg-zinc-900 p-2 w-full cursor-pointer hover:bg-zinc-800 text-left"
@click="selectTimetable(timetable)"
>
<div class="text-zinc-300 flex gap-x-2 items-center flex-wrap">
<span>#{{ timetable.timetableId }}</span>
<i class="flex items-center gap-1"><ArchiveIcon :size="18" :stroke-width="3" /> {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</i>
<i
v-if="timetable.journalCreatedAt"
class="flex items-center gap-0.5"
:title="
$t('storage-journal-timetable-placeholder', {
date: new Date(timetable.journalCreatedAt).toLocaleDateString('pl-PL')
})
"
>
<HistoryIcon :size="18" :stroke-width="3" />
{{ new Date(timetable.journalCreatedAt).toLocaleDateString('pl-PL') }}
</i>
</div>
<b>{{ timetable.driverName }} | {{ timetable.category }} {{ timetable.trainNo }}</b>
{{ timetable.route.replace('|', ' > ') }}
</button>
<button
class="bg-zinc-900 p-2 hover:bg-zinc-800"
@click="removeTimetable(timetable.timetableId)"
>
<Trash2Icon />
</button>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useGlobalStore } from '../../stores/global.store';
import type { TimetableData } from '../../types/common.types';
import { ArchiveIcon, HistoryIcon, Trash2Icon } from 'lucide-vue-next';
const globalStore = useGlobalStore();
const i18n = useI18n();
const filteredTimetables = computed(() => {
let timetables = Object.values(globalStore.storageTimetables);
if (globalStore.localTimetableSearch.length != 0)
timetables = timetables.filter((st) =>
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
.toLocaleLowerCase()
.includes(globalStore.localTimetableSearch.toLocaleLowerCase())
);
timetables.sort((a, b) => {
return (b.savedTimestamp ?? 0) - (a.savedTimestamp ?? 0);
});
return timetables;
});
function selectTimetable(timetable: TimetableData) {
globalStore.selectedStorageTimetable = timetable;
}
function removeTimetable(timetableId: number) {
const isConfirmed = confirm(i18n.t('delete-timetable-confirm'));
if (!isConfirmed) return;
try {
const savedTimetablesStorage = localStorage.getItem('savedTimetables');
let savedTimetablesJSON: Record<number, TimetableData> = savedTimetablesStorage
? JSON.parse(savedTimetablesStorage)
: {};
delete savedTimetablesJSON[timetableId];
localStorage.setItem('savedTimetables', JSON.stringify(savedTimetablesJSON));
globalStore.storageTimetables = savedTimetablesJSON;
} catch (error) {
console.error(error);
}
}
</script>
+23
View File
@@ -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.status}`);
}
return data.json();
}
}
+17
View File
@@ -0,0 +1,17 @@
import { createI18n } from 'vue-i18n';
import localePL from './locales/pl.json';
import localeEN from './locales/en.json';
const i18n = createI18n({
locale: 'en',
fallbackLocale: 'pl',
legacy: false,
messages: {
pl: localePL,
en: localeEN,
},
});
export default i18n;
+53
View File
@@ -0,0 +1,53 @@
{
"data-loading-text": "Loading data...",
"data-loading-error-text": "Oops! An error occurent while loading data from the server!",
"train-select-placeholder": "Choose active train from the list",
"train-select-info": "Choose active train to generate SRJP timetable",
"train-search-placeholder": "Enter TT details (number, route, user)",
"update-prompt": {
"line1": "New version of SRJP is available!",
"line2": "Click here to update the app!"
},
"data-offline-mode": "You're currently using the offline mode of the SRJP app - server data is unavailable!",
"headers": {
"line_no": "Line\nno.",
"line_km": "Km",
"station": "Station",
"time": "Time",
"loco_1": "Loco I",
"loco_2": "Loco II",
"loco_3": "Loco III",
"mass": "Loco load",
"length": "Train len.",
"vmax": "Vmax",
"relation": "Route"
},
"migrate-info": {
"line-1": "Rozkładownik is being moved to a new domain - {0}! You can still use the current website, but it will no longer be updated and will be shut down in the nearest future!",
"accept-btn": "ROGER THAT!"
},
"storage-empty-header": "ARCHIVED TIMETABLES SEARCH MODE",
"storage-empty-info": "Timetables will be shown here after their archiving.",
"storage-preview-title": "ARCHIVED TIMETABLES",
"storage-preview-empty": "No entries found for given parameters",
"storage-preview-info": "Archived timetable {id} for user {driverName} from: {date}",
"storage-preview-button-text": "Return",
"storage-journal-timetable-placeholder": "Saved historical timetable from day {date}",
"delete-timetable-confirm": "Are you sure that you want to delete this timetable?",
"journal-preview-title": "TIMETABLES JOURNAL",
"journal-empty-info": "Enter timetable details in the text fields above (use at least one field).<br>Up to 15 newest timetables will be shown.",
"journal-driver-search-placeholder": "Driver",
"journal-date-search-placeholder": "Date",
"journal-route-search-placeholder": "Route",
"journal-preview-info": "Historical timetable {id} for user {driverName} from: {date}",
"journal-no-data": "No data for the current search! Check if the data you entered is correct.",
"journal-reminder-text": "Warning: detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!",
"journal-footer-text": "Detailed timetables data for SRJP purpose are collected since 1st February 2025 and only for users who support Stacjownik project!"
}
+53
View File
@@ -0,0 +1,53 @@
{
"data-loading-text": "Ładowanie danych...",
"data-loading-error-text": "Ups! Wystąpił błąd podczas pobierania danych z serwera!",
"train-select-placeholder": "Wybierz pociąg z listy",
"train-select-info": "Wybierz aktywny pociąg, aby wygenerować SRJP",
"train-search-placeholder": "Wpisz szczegóły RJ (nr, relacja, gracz)",
"update-prompt": {
"line1": "Nowa wersja SRJP jest dostępna!",
"line2": "Kliknij, aby zaktualizować aplikację!"
},
"data-offline-mode": "Korzystasz z trybu offline aplikacji SRJP - dane serwerowe są niedostępne!",
"headers": {
"line_no": "Nr\nlinii",
"line_km": "Km",
"station": "Stacja",
"time": "Godzina",
"loco_1": "Lok I",
"loco_2": "Lok II",
"loco_3": "Lok III",
"mass": "Obc. lok.",
"length": "Dł. poc.",
"vmax": "Vmax",
"relation": "Relacja"
},
"migrate-info": {
"line-1": "Rozkładownik zostaje przeniesiony na nową domenę - {0}! Możesz korzystać z obecnej strony, jednak nie będzie ona otrzymywać już aktualizacji i w przyszłości zostanie wyłączona!",
"accept-btn": "PRZYJĄŁEM!"
},
"storage-empty-header": "TRYB WYSZUKIWANA ZAPISANYCH ROZKŁADÓW JAZDY",
"storage-empty-info": "Użyj funkcji zapisu rozkładu jazdy, aby go tutaj wyświetlić.",
"storage-preview-title": "ZAPISANE ROZKŁADY JAZDY",
"storage-preview-empty": "Nie znaleziono żadnych wpisów dla podanych parametrów",
"storage-preview-info": "Rozkład archiwalny {id} maszynisty {driverName} z dnia {date}",
"storage-preview-button-text": "Powróć",
"storage-journal-timetable-placeholder": "Zapisany historyczny rozkład jazdy z dnia {date}",
"delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?",
"journal-preview-title": "DZIENNIK ROZKŁADÓW JAZDY",
"journal-empty-info": "Wpisz dane rozkładu korzystając z pól tekstowych powyżej (co najmniej jednego).<br>W przypadku wielu rozkładów jazdy wyświetli się maks. 15 najnowszych.",
"journal-driver-search-placeholder": "Maszynista",
"journal-date-search-placeholder": "Data",
"journal-route-search-placeholder": "Relacja",
"journal-preview-info": "Rozkład historyczny {id} maszynisty {driverName} z dnia {date}",
"journal-no-data": "Brak wyników dla obecnego wyszukiwania! Sprawdź czy wpisałeś poprawnie dane.",
"journal-reminder-text": "Uwaga: szczegółowe rozkłady jazdy są zapisywane od 1 lutego 2025r. wyłącznie dla osób wspierających projekt Stacjownika!",
"journal-footer-text": "Szczegółowe dane o rozkładach jazdy do wygenerowania SRJP są zbierane od 1 lutego 2025r. wyłącznie dla maszynistów wspierających projekt Stacjownika!"
}
+17 -3
View File
@@ -1,8 +1,22 @@
import { createApp } from 'vue';
import { createApp, type Directive } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
import './style.css'
import './style.css';
import i18n from './i18n';
const pinia = createPinia();
createApp(App).use(pinia).mount('#app');
const clickOutsideDirective: Directive = {
mounted(el, binding) {
el.clickOutsideEvent = (event: Event) => {
if (!(el == event.target || el.contains(event.target))) {
binding.value();
}
};
document.addEventListener('click', el.clickOutsideEvent);
},
};
createApp(App).use(i18n).use(pinia).directive('click-outside', clickOutsideDirective).mount('#app');
+27
View File
@@ -0,0 +1,27 @@
import { useApiStore } from '../stores/api.store';
export function useVehicleMixin() {
const apiStore = useApiStore();
/**
* Gets loco load (obc. lok.) in tons - effectively train mass without locomotive or lone locomotive / unit mass
*/
function getLocoLoad(trainMass: number, stockString: string) {
if (!apiStore.vehiclesData) return trainMass;
const stockArray = stockString.split(';');
const headUnitsNames = stockArray.slice(0, 3).filter((v) => /-\d{3,}$/.test(v));
const headVehicleData = apiStore.vehiclesData.find((v) => v.name == headUnitsNames[0]);
if (!headVehicleData) return trainMass;
// 0t load for loco only
if (headVehicleData.type.startsWith("loco") && stockArray.length == 1) return 0;
else if (headVehicleData.type.startsWith("unit")) return trainMass;
return Math.min(trainMass, trainMass - headVehicleData.group.weight);
}
return { getLocoLoad };
}
+116
View File
@@ -0,0 +1,116 @@
import { defineStore } from 'pinia';
import {
DataStatus,
type ActiveDataResponse,
type SceneriesDataResponse,
type VehiclesDataResponse
} from '../types/api.types';
import type {
ActiveData,
JournalTimetableShort,
SceneryData,
VehicleData
} from '../types/common.types';
import { HttpClient } from '../http';
let activeDataInterval = -1;
// Base API URL
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('api', {
state() {
return {
client: new HttpClient(baseURL),
activeData: null as ActiveData | null,
sceneryData: null as SceneryData[] | null,
vehiclesData: null as VehicleData[] | null,
journalTimetablesData: null as JournalTimetableShort[] | null,
outdatedTimerId: -1,
isActiveDataOutdated: false,
apiDataStatus: DataStatus.LOADING,
journalDataStatus: DataStatus.SUCCESS,
connectionMode: 'online' as 'online' | 'offline'
};
},
actions: {
async setupAPIData() {
clearInterval(activeDataInterval);
try {
this.apiDataStatus = DataStatus.LOADING;
await Promise.all([
this.fetchSceneriesData(),
this.fetchVehiclesData(),
this.fetchActiveData()
]);
this.apiDataStatus = DataStatus.SUCCESS;
} catch (error) {
this.apiDataStatus = DataStatus.ERROR;
console.log('Data fetching error: ', error);
}
activeDataInterval = setInterval(() => {
this.fetchActiveData();
}, 25000);
},
async fetchActiveData() {
try {
const response = await this.client.get<ActiveDataResponse>('api/getActiveData');
this.activeData = response;
this.isActiveDataOutdated = false;
if (this.outdatedTimerId != -1) clearTimeout(this.outdatedTimerId);
this.outdatedTimerId = setTimeout(() => {
this.isActiveDataOutdated = true;
}, 60000);
} catch (error) {
throw error;
}
},
async fetchSceneriesData() {
try {
const response = await this.client.get<SceneriesDataResponse>('api/getSceneries');
this.sceneryData = response;
} catch (error) {
throw error;
}
},
async fetchVehiclesData() {
try {
const response = await this.client.get<VehiclesDataResponse>('api/getVehiclesData');
this.vehiclesData = response.vehicles.map((v) => ({
...v,
group: response.vehicleGroups.find((g) => g.id == v.vehicleGroupsId)!
}));
} catch (error) {
throw error;
}
}
}
});
+110 -33
View File
@@ -1,42 +1,119 @@
import { defineStore } from 'pinia';
import httpClient from '../utils/http';
import type { ActiveDataResponse, SceneriesDataResponse } from '../types/api.types';
import type { ActiveData, SceneryData } from '../types/common.types';
import { useApiStore } from './api.store';
import type {
ActiveTrain,
JournalTimetableDetailed,
TimetableData,
ViewMode
} from '../types/common.types';
import { getHeadUnits } from '../utils/trainUtils';
import { useVehicleMixin } from '../mixins/useVehicleMixin';
export const useGlobalStore = defineStore('global', {
state: () => ({
activeData: null as ActiveData | null,
sceneryData: null as SceneryData[] | null,
darkMode: false,
fullscreenMode: false,
viewMode: 'active' as ViewMode,
selectedTrainId: null as string | null,
selectedActiveTrain: null as ActiveTrain | null,
selectedStorageTimetable: null as TimetableData | null,
selectedJournalTimetable: null as JournalTimetableDetailed | null,
storageTimetables: {} as Record<number, TimetableData>,
timetableWarnings: [] as string[],
generatedDate: null as Date | null,
generatedMs: 0,
localTimetableSearch: '',
journalTimetableSearch: {
driverName: '',
date: '',
route: ''
},
showSettings: false,
isMigrationInfoOpen: false
}),
getters: {},
actions: {
async _fetchActiveData() {
try {
const response = (await httpClient.get<ActiveDataResponse>('/api/getActiveData')).data;
getters: {
activeTimetableTrains() {
const apiStore = useApiStore();
this.activeData = response;
} catch (error) {
console.error(error);
if (!apiStore.activeData) return [];
return apiStore.activeData.trains
.filter((train) => train.timetable)
.sort((t1, t2) => t1.driverName.localeCompare(t2.driverName, 'pl-PL'));
},
currentTimetableData(): TimetableData | null {
const vehicleUtils = useVehicleMixin();
if (this.viewMode == 'active') {
const selectedTrain = this.selectedActiveTrain;
if (!selectedTrain || !selectedTrain.timetable) return null;
return {
trainNo: selectedTrain.trainNo,
mass: vehicleUtils.getLocoLoad(selectedTrain.mass, selectedTrain.stockString),
length: selectedTrain.length,
driverId: selectedTrain.driverId,
driverName: selectedTrain.driverName,
category: selectedTrain.timetable.category,
hasDangerousCargo: selectedTrain.timetable.hasDangerousCargo,
hasExtraDeliveries: selectedTrain.timetable.hasExtraDeliveries,
warningNotes: selectedTrain.timetable.warningNotes,
path: selectedTrain.timetable.path,
route: selectedTrain.timetable.route,
trainMaxSpeed: selectedTrain.timetable.trainMaxSpeed,
timetableId: selectedTrain.timetable.timetableId,
stopListString: selectedTrain.timetable.stopList
.filter((stop) => stop.mainStop || /^podg|po|pe$/.test(stop.stopNameRAW))
.map(
(stop) =>
`${stop.arrivalLine ?? ''};${stop.arrivalTimestamp};${stop.stopNameRAW};${
stop.stopTime ? stop.stopTime + '_' + stop.stopType : ''
};${stop.mainStop};${stop.stopDistance};${stop.departureTimestamp};${
stop.departureLine ?? ''
}`
)
.join('~~'),
headUnits: getHeadUnits(selectedTrain.stockString)
};
} else if (this.viewMode == 'journal') {
const selectedTimetable = this.selectedJournalTimetable;
if (!selectedTimetable || !selectedTimetable.stopListString) return null;
return {
journalCreatedAt: new Date(selectedTimetable.createdAt).getTime(),
trainNo: selectedTimetable.trainNo,
mass: vehicleUtils.getLocoLoad(
selectedTimetable.stockMass,
selectedTimetable.stockString
),
length: selectedTimetable.stockLength,
driverId: selectedTimetable.driverId,
driverName: selectedTimetable.driverName,
category: selectedTimetable.trainCategoryCode,
hasDangerousCargo: selectedTimetable.hasDangerousCargo,
hasExtraDeliveries: selectedTimetable.hasExtraDeliveries,
warningNotes: selectedTimetable.warningNotes,
path: selectedTimetable.path,
route: selectedTimetable.route,
trainMaxSpeed: selectedTimetable.trainMaxSpeed,
timetableId: selectedTimetable.id,
stopListString: selectedTimetable.stopListString,
headUnits: getHeadUnits(selectedTimetable.stockString)
};
} else {
return this.selectedStorageTimetable;
}
},
async _fetchSceneriesData() {
try {
const response = (await httpClient.get<SceneriesDataResponse>('/api/getSceneries')).data;
this.sceneryData = response;
} catch (error) {
console.error(error);
}
},
setupData() {
this._fetchActiveData();
this._fetchSceneriesData();
setInterval(() => {
this._fetchActiveData();
}, 20000);
},
}
},
actions: {}
});
+83 -10
View File
@@ -2,25 +2,98 @@
@tailwind components;
@tailwind utilities;
:root {
color-scheme: dark;
}
body {
margin: 0;
display: flex;
min-width: 320px;
min-height: 100vh;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
width: 100%;
height: 100vh;
}
/* Scrollbar */
/* width */
::-webkit-scrollbar {
width: theme('size.2');
height: theme('size.2');
}
/* Track */
::-webkit-scrollbar-track {
background: theme('colors.stone.900');
}
/* Handle */
::-webkit-scrollbar-thumb {
background: theme('colors.stone.400');
border-radius: theme('borderRadius.md');
}
::-webkit-scrollbar-corner {
background: theme('colors.stone.900');
border-radius: 0 0 theme('borderRadius.md') 0;
}
/* Tooltips */
[data-tooltip] {
position: relative;
}
[data-tooltip] > .tooltip-content {
visibility: hidden;
background-color: theme('colors.inherit');
min-width: 125px;
color: inherit;
text-align: center;
border-radius: theme('borderRadius.md');
padding: 0.25rem 0.5rem;
position: absolute;
z-index: 1;
top: calc(100% + 0.5rem);
left: 50%;
transform: translateX(-50%);
}
[data-tooltip]:hover > .tooltip-content,
[data-tooltip]:focus-visible > .tooltip-content {
visibility: visible;
}
/* Animations */
.list-move,
.list-enter-active,
.list-leave-active {
transition: all 0.25s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-leave-active {
position: absolute;
width: 100%;
}
/* Print mode */
@media print {
:root {
color-scheme: light;
}
}
/* Animations */
.slide-anim-enter-active,
.slide-anim-leave-active {
transition: all 250ms ease-in-out;
transform: translateY(0);
}
.slide-anim-enter-from,
.slide-anim-leave-to {
transform: translateY(100%);
}
+53 -1
View File
@@ -1,5 +1,57 @@
import type { ActiveData, SceneryData } from './common.types';
import type {
ActiveData,
JournalTimetableShort,
SceneryData,
VehicleGroup,
VehicleRestrictions
} from './common.types';
/***
* API Data Status
* */
export enum DataStatus {
'INIT' = -1,
'LOADING' = 0,
'SUCCESS' = 1,
'ERROR' = 2
}
/***
* Active Data API
* */
export type ActiveDataResponse = ActiveData;
/***
* Sceneries API
* */
export type SceneriesDataResponse = SceneryData[];
/***
* Journal API
* */
export type JournalTimetablesShortResponse = JournalTimetableShort[];
/***
* Vehicles API
* */
export interface VehiclesDataResponse {
vehicles: VehicleDataAPI[];
vehicleGroups: VehicleGroupAPI[];
}
export interface VehicleDataAPI {
id: number;
name: string;
type: string;
cabinName?: string;
restrictions?: VehicleRestrictions;
vehicleGroupsId: number;
}
export interface VehicleGroupAPI extends VehicleGroup {}
+213
View File
@@ -1,3 +1,5 @@
export type ViewMode = 'active' | 'storage' | 'journal';
export interface ActiveData {
trains: ActiveTrain[];
activeSceneries: ActiveScenery[];
@@ -38,6 +40,7 @@ export interface ActiveTrainTimetable {
timetableId: number;
sceneries: string[];
path: string;
trainMaxSpeed: number;
}
export interface TimetableStop {
@@ -116,7 +119,217 @@ export interface SceneryRoute {
isInternal: boolean;
isRouteSBL: boolean;
routeSpeed: number;
routeSpeedExit?: number;
routeLength: number;
routeTracks: number;
hidden?: boolean;
realLineNo?: number;
}
export interface StopRow {
pointName: string;
pointKm: string;
isMain: boolean;
stopTime: number;
stopType: string;
scheduledArrivalDate: Date | null;
scheduledDepartureDate: Date | null;
arrivalLineNumber: string;
departureLineNumber: string;
driveTime: number;
abbrevs: string[];
sceneryName: string;
arrivalKm: string;
arrivalSpeedL: number;
arrivalSpeedP: number;
arrivalTracks: number;
departureKm: string;
departureSpeedL: number;
departureSpeedP: number;
departureTracks: number;
headUnits: string[];
stockVmax: number;
stockLength: number;
stockMass: number;
lastRowRef: StopRow | null;
}
export interface StopRowCZ {
pointName: string;
pointKm: string;
isMain: boolean;
stopTime: number;
stopType: string;
scheduledArrivalDate: Date | null;
scheduledDepartureDate: Date | null;
driveTime: number;
sceneryName: string;
arrivalSpeed: number;
departureSpeed: number;
arrivalTracks: number;
departureTracks: number;
headUnits: string[];
stockVmax: number;
stockLength: number;
stockMass: number;
arrivalDateStr: string;
departureDateStr: string;
}
export interface TimetablePathData {
sceneryName: string;
sceneryData: SceneryData | null;
arrivalLine: string;
departureLine: string;
arrivalLineData: SceneryRoute | null;
departureLineData: SceneryRoute | null;
}
export interface JournalTimetableShort {
id: number;
allStopsCount: number;
confirmedStopsCount: number;
createdAt: string;
beginDate: string;
driverId: number;
driverName: string;
route: string;
routeDistance: number;
currentDistance: number;
currentLocation: string[];
currentSceneryName: string;
currentSceneryHash: string;
driverLevel: number;
fulfilled: boolean;
terminated: boolean;
driverIsSupporter: boolean;
trainCategoryCode: string;
trainNo: number;
region: string;
hasDangerousCargo: boolean;
hasExtraDeliveries: boolean;
twr: boolean;
skr: boolean;
}
export interface JournalTimetableDetailed extends JournalTimetableShort {
id: number;
schemaVersion: string;
allStopsCount: number;
authorId: number;
authorName: string;
beginDate: string;
confirmedStopsCount: number;
currentDistance: number;
driverId: number;
driverName: string;
endDate: string;
fulfilled: boolean;
route: string;
routeDistance: number;
region: string;
sceneriesString: string;
scheduledBeginDate: string;
scheduledEndDate: string;
terminated: boolean;
timetableId: number;
trainCategoryCode: string;
trainNo: number;
twr: boolean;
skr: boolean;
stockString: string;
stockMass: number;
stockLength: number;
maxSpeed: number;
trainMaxSpeed: number;
hashesString: string;
currentSceneryName: string;
currentSceneryHash: any;
driverIsSupporter: boolean;
driverLevel: number;
createdAt: string;
updatedAt: string;
stockHistory: string[];
routeSceneries: string;
checkpointArrivals: any[];
checkpointDepartures: any[];
checkpointArrivalsScheduled: string[];
checkpointDeparturesScheduled: string[];
checkpointStopTypes: string[];
currentLocation: string[];
visitedSceneries: string[];
sceneryHashes: string[];
sceneryNames: string[];
checkpointComments: string[];
checkpointNames: string[];
path: string;
warningNotes: string;
hasDangerousCargo: boolean;
hasExtraDeliveries: boolean;
stopListString?: string;
}
export interface TimetableData {
trainNo: number;
mass: number;
length: number;
driverName: string;
driverId: number;
hasDangerousCargo: boolean;
hasExtraDeliveries: boolean;
warningNotes: string;
category: string;
route: string;
timetableId: number;
path: string;
trainMaxSpeed: number;
stopListString: string;
headUnits: string[];
savedTimestamp?: number;
journalCreatedAt?: number;
}
export interface VehicleData {
id: number;
name: string;
type: string;
group: VehicleGroup;
cabinName?: string;
restrictions?: VehicleRestrictions;
}
export interface VehicleRestrictions {
sponsorOnly?: number;
teamOnly?: boolean;
}
export interface VehicleGroup {
id: number;
name: string;
speed: number;
speedLoaded?: number;
speedLoco?: number;
length: number;
weight: number;
cargoTypes?: VehicleCargoType[];
locoProps?: VehicleLocoProps;
massSpeeds?: VehicleMassSpeeds;
}
export interface VehicleCargoType {
id: string;
weight: number;
}
export interface VehicleLocoProps {
coldStart: boolean;
doubleManned: boolean;
}
export interface VehicleMassSpeeds {
none: number;
cargo?: Record<string, number>;
passenger?: Record<string, number>;
}
+8
View File
@@ -0,0 +1,8 @@
const romanMonthDigits = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI', 'XII'];
export function parseTimetableRunDate(date: Date) {
return `${date.getDate()}.${romanMonthDigits[date.getMonth()]}.${date
.getFullYear()
.toString()
.slice(2)}`;
}
-8
View File
@@ -1,8 +0,0 @@
import axios from 'axios';
const httpClient = axios.create({
baseURL: 'https://stacjownik.spythere.eu',
timeout: 3000,
});
export default httpClient;
+45
View File
@@ -0,0 +1,45 @@
const unitNameCorrections: Record<string, string[]> = {
'2EN57': ['EN57', 'EN57'],
'201E': ['ET22'],
'4E': ['EU07'],
M62: ['ST44'],
CTLR4C: ['ST44']
};
export const getRegionNameById = (id: string) => {
switch (id) {
case 'eu':
return 'PL1';
case 'cae':
return 'PL2';
case 'us':
return 'CZE';
case 'usw':
return 'DE';
case 'ru':
return 'ENG';
default:
return 'PL1';
}
};
export function getHeadUnits(stockString: string) {
const stockList = stockString.split(';').slice(0, 3);
return stockList.reduce((acc, unitType, i) => {
if (i != 0 && !/-\d{3,}$/.test(unitType)) return acc;
const unitName = unitType.slice(0, unitType.indexOf('-'));
const correctedNames = unitNameCorrections[unitName] ?? [unitName];
acc.push(...correctedNames);
return acc;
}, [] as string[]);
}
+2 -1
View File
@@ -4,7 +4,8 @@ export default {
theme: {
extend: {},
},
darkMode: 'selector',
plugins: [],
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
}
+2 -3
View File
@@ -1,14 +1,13 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"types": ["vite/client", "vite-plugin-pwa/client"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/http.ts","./src/i18n.ts","./src/main.ts","./src/vite-env.d.ts","./src/mixins/useVehicleMixin.ts","./src/stores/api.store.ts","./src/stores/global.store.ts","./src/types/api.types.ts","./src/types/common.types.ts","./src/utils/dateUtils.ts","./src/utils/trainUtils.ts","./src/App.vue","./src/components/App/MainBottom.vue","./src/components/App/MainContainer.vue","./src/components/App/MigrateInfo.vue","./src/components/App/Navbar.vue","./src/components/App/SettingsCard.vue","./src/components/App/UpdatePrompt.vue","./src/components/Timetable/TimetableContainer.vue","./src/components/Timetable/TimetableContent.vue","./src/components/Timetable/TimetableContentCZ.vue","./src/components/Timetable/TimetableWarnings.vue","./src/components/TimetableSearch/ActiveSearchInput.vue","./src/components/TimetableSearch/JournalSearchInput.vue","./src/components/TimetableSearch/LocalSearchInput.vue","./src/components/TimetableSearch/SearchContainer.vue","./src/components/TimetableSearch/SearchModeActions.vue","./src/components/TimetableViews/ActiveDataView.vue","./src/components/TimetableViews/CurrentTimetableView.vue","./src/components/TimetableViews/JournalStorageView.vue","./src/components/TimetableViews/LocalStorageView.vue"],"version":"5.9.3"}
+1 -3
View File
@@ -1,6 +1,5 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
@@ -17,8 +16,7 @@
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.9.3"}
+32 -4
View File
@@ -1,7 +1,35 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
})
plugins: [
vue(),
VitePWA({
registerType: 'prompt',
workbox: {
disableDevLogs: true,
globPatterns: ['**/*.{js,css,html,png,svg,jpg,ico}'],
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/(getSceneries|getVehiclesData)/i,
handler: 'StaleWhileRevalidate',
options: {
expiration: {
maxAgeSeconds: 3600
},
cacheName: 'stacjownik-api-cache',
cacheableResponse: { statuses: [0, 200] }
}
}
]
},
devOptions: { enabled: false, suppressWarnings: true }
})
],
server: {
port: 5345
}
});
+3125 -460
View File
File diff suppressed because it is too large Load Diff