73 Commits

Author SHA1 Message Date
Spythere 0e888544c1 chore: added view mode menu and journal tab 2025-02-10 14:51:01 +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
46 changed files with 1871 additions and 438 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
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
+6
View File
@@ -11,6 +11,8 @@ node_modules
dist
dist-ssr
*.local
.env
.env.*
# Editor directories and files
.vscode/*
@@ -22,3 +24,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# api-mock
/api-mock/endpoints/
/api-mock/*.lock
+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', 'getVehicles'].map((endpointName) =>
fetchJSONEndpointData(
`https://stacjownik.spythere.eu/api/${endpointName}`,
`${endpointName}.json`
)
)
).then(() => {
console.log('Endpoints downloaded!');
});
}
main();
+28
View File
@@ -0,0 +1,28 @@
import express from 'express';
import path from 'path';
import { cwd } from 'process';
import cors from 'cors';
const app = express();
app.use(cors());
app.get('/api/getActiveData', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getActiveData.json'));
});
app.get('/api/getSceneries', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getSceneries.json'));
});
app.get('/api/getVehicles', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getVehicles.json'));
});
app.get('/api/getDonators', (_, res) => {
res.sendFile(path.join(cwd(), 'endpoints', 'getDonators.json'));
});
app.listen(3123, () => {
console.log('Mocking API server...');
});
+18
View File
@@ -0,0 +1,18 @@
{
"name": "api-mock",
"version": "1.0.0",
"description": "",
"type": "module",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"fetch": "node fetchEndpoints.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.3"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"hosting": {
"public": "dist",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
+29 -2
View File
@@ -1,10 +1,37 @@
<!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 >
<div id="app"></div>
+7 -3
View File
@@ -1,17 +1,21 @@
{
"name": "srjp-td2",
"private": true,
"version": "0.0.0",
"version": "1.0.2",
"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": {
"@heroicons/vue": "^2.2.0",
"axios": "^1.7.9",
"pinia": "^2.3.1",
"vue": "^3.5.13"
"vue": "^3.5.13",
"vue-i18n": "10"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
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 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

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

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+53 -373
View File
@@ -1,394 +1,74 @@
<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>
<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>
<div class="text-white min-h-screen bg-zinc-950">
<Navbar />
<MainContainer />
</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 { onMounted } from 'vue';
import { useApiStore } from './stores/api.store';
import { useGlobalStore } from './stores/global.store';
import { useI18n } from 'vue-i18n';
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;
onMounted(async () => {
setupLocale();
setupDarkMode();
loadStorageTimetables();
setupAfterPrintClose();
arrivalKm: string;
await apiStore.setupAPIData();
arrivalSpeed: number;
arrivalTracks: number;
const query = new URLSearchParams(window.location.search);
departureKm: string;
if (query.has('id')) {
const id = query.get('id')!;
departureSpeed: number;
departureTracks: number;
const queryTrain = apiStore.activeData?.trains.find((train) => train.id == id);
if (queryTrain) {
globalStore.selectedTrainId = id;
globalStore.selectedActiveTrain = queryTrain;
}
}
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[] = [];
function loadStorageTimetables() {
if (!window.localStorage.getItem('savedTimetables')) return;
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;
try {
globalStore.storageTimetables = JSON.parse(window.localStorage.getItem('savedTimetables')!);
} catch (error) {
alert('Ups! Coś poszło nie tak podczas pobierania zapisanych RJ!');
}
}
return stopRows;
},
},
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')!;
}
}
</script>
<style scoped>
table {
width: 100%;
height: 100%;
border-collapse: collapse;
}
.srjp-table {
min-width: 750px;
}
.no-bottom-border {
border-bottom-color: transparent;
}
@media print {
table {
page-break-inside: auto;
}
}
</style>
+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>
+15
View File
@@ -0,0 +1,15 @@
<template>
<main class="grid print:block p-3 mx-auto max-w-[800px] h-screen grid-rows-[auto_auto_1fr] gap-1">
<TimetableSelect />
<TimetableWarnings />
<TrainTimetable />
<!-- <MainBottom /> -->
</main>
</template>
<script setup lang="ts">
import TimetableSelect from '../Timetable/TimetableSelect.vue';
import TimetableWarnings from "../Timetable/TimetableWarnings.vue";
import TrainTimetable from '../Timetable/TrainTimetable.vue';
// import MainBottom from './MainBottom.vue';
</script>
+31
View File
@@ -0,0 +1,31 @@
<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" />
<b class="ml-2 text-lg"
>Rozkładownik TD2 <sup class="font-semibold text-zinc-300">{{ version }}</sup></b
>
</div>
<div>
<button class="bg-slate-600 p-1 px-2 rounded-md hover:bg-slate-500 flex items-center" @click="changeLang()">
<LanguageIcon class="size-5 inline-block align-middle 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 { LanguageIcon } from '@heroicons/vue/16/solid';
import { version } from '../../../package.json';
import { useI18n } from 'vue-i18n';
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 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>
+188
View File
@@ -0,0 +1,188 @@
<template>
<tbody>
<tr v-for="(row, i) in computedTimetable">
<td
class="text-center align-top border-l border-l-black dark:border-l-white"
:class="{
'border-t border-t-black dark:border-t-white': i != 0 && computedTimetable[i - 1].realLine != row.realLine,
'border-b border-b-black dark:border-b-white': i == computedTimetable.length - 1,
}"
>
{{ i == 0 || computedTimetable[i - 1].realLine != row.realLine ? row.realLine : '&nbsp;' }}
</td>
<td class="border border-black dark:border-white relative">
<div class="absolute top-0 left-0 w-full h-full p-0.5">
<table class="h-full w-full border-collapse">
<tbody>
<tr>
<td class="align-top">{{ row.arrivalKm == '0.000' ? '' : row.arrivalKm }}</td>
</tr>
<tr>
<td class="align-bottom">{{ row.departureKm == '0.000' ? '' : row.departureKm }}</td>
</tr>
</tbody>
</table>
</div>
</td>
<td
class="text-center align-top p-0 border-l-black dark:border-l-white relative"
:class="{
'border-t border-t-black dark:border-t-white': i != 0 && computedTimetable[i - 1].departureSpeed != row.arrivalSpeed,
'border-b border-b-black dark:border-b-white': i == computedTimetable.length - 1,
}"
colspan="2"
>
<div class="absolute top-0 left-0 w-full h-full">
<table class="h-full w-full border-collapse">
<tbody>
<tr class="align-top">
<td :colspan="row.arrivalTracks == 2 ? '1' : '2'" class="font-bold" width="35">
{{
i == 0 ||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
? row.arrivalSpeed
: '&nbsp; '
}}
</td>
<td v-if="row.arrivalTracks == 2" class="border-l border-l-black dark:border-l-white" width="35">
{{
i == 0 ||
computedTimetable[i - 1].departureSpeed != row.arrivalSpeed ||
computedTimetable[i - 1].departureTracks != row.arrivalTracks
? row.arrivalSpeed
: '&nbsp; '
}}
</td>
</tr>
<tr
:class="{
'border-t border-t-black dark:border-t-white align-top':
row.arrivalTracks != row.departureTracks || row.departureSpeed != row.arrivalSpeed,
}"
>
<td :colspan="row.departureTracks == 2 ? '1' : '2'" class="font-bold" width="35">
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : '&nbsp; ' }}
</td>
<td v-if="row.departureTracks == 2" class="border-l border-l-black dark:border-l-white" width="35">
{{ row.departureSpeed != row.arrivalSpeed || row.departureTracks != row.arrivalTracks ? row.departureSpeed : '&nbsp; ' }}
</td>
</tr>
</tbody>
</table>
</div>
</td>
<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>
<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 }">
{{
(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 h-full">
<td class="border-r-[1px] border-r-black dark: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>
</div>
</td>
<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>
<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>{{ row.stockMass }}</td>
</tr>
<tr>
<td>{{ row.stockLength }}</td>
</tr>
</tbody>
</table>
</div>
</td>
<td class="text-center border border-black dark:border-white" :class="{ 'text-stone-400 ': i > 0 }">{{ row.stockVmax }}</td>
</tr>
</tbody>
</template>
<script setup lang="ts">
import type { PropType } from 'vue';
import type { StopRow } from '../../types/common.types';
defineProps({
computedTimetable: {
type: Object as PropType<StopRow[]>,
required: true,
},
});
</script>
<style scoped>
@media print {
table {
page-break-after: auto;
}
tr {
page-break-inside: avoid;
page-break-after: auto;
}
thead {
display: table-header-group;
}
tr,
td {
border-color: theme('colors.black');
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<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="35" class="border border-black dark:border-white">V<sub>P</sub></th>
<th width="35" 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>
</template>
<style scoped>
@media print {
th, tr {
border-color: theme('colors.black');
}
}
</style>
@@ -0,0 +1,56 @@
<template>
<div class="text-white">
<!-- <div v-if="apiStore.journalTimetables == null">
<div class="font-bold text-xl">{{ $t('storage-empty-header') }}</div>
<div>{{ $t('storage-empty-info') }}</div>
</div> -->
<div class="font-bold text-2xl p-2 bg-zinc-800">DZIENNIK SRJP</div>
<div v-if="apiStore.journalTimetables == null" class="text-md mt-2 p-2 bg-zinc-900">
Wyszukaj gracza w polu powyżej, aby pokazać jego najnowsze SRJP.
<!-- <div class="text-sm text-red-400 mt-2 flex flex-wrap md:flex-nowrap justify-center items-center gap-1">
<ExclamationCircleIcon class="size-8 min-w-8" />
<span>
Rozkłady z danymi do wygenerowania SRJP dostępne tylko dla wspierających twórczość
<a class="underline hover:text-red-200 transition-colors" href="https://td2.info.pl/profile/?u=20777" target="_blank">@Spythere</a> dla
symulatora TD2!
</span>
</div> -->
</div>
<!-- <div class="font-bold text-xl p-2 bg-zinc-700 mb-3">{{ $t('storage-preview-title') }}</div> -->
<!-- <div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div> -->
<li v-for="timetable in apiStore.journalTimetables" 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.id)">
<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>
<!-- <button class="bg-zinc-900 p-2 hover:bg-zinc-800" @click="removeTimetable(timetable.timetableId)">
<TrashIcon class="size-5 text-white" />
</button> -->
</li>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useGlobalStore } from '../../stores/global.store';
import { useApiStore } from '../../stores/api.store';
import { onMounted } from 'vue';
import { ExclamationCircleIcon } from '@heroicons/vue/16/solid';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
const i18n = useI18n();
onMounted(() => {
// apiStore.fetchTimetableHistoryList();
});
function selectTimetable(timetableId: number) {}
</script>
<style scoped></style>
@@ -0,0 +1,247 @@
<template>
<div class="flex gap-2 mb-2">
<div class="relative" @focusin="isMenuOpen = true" @keydown.esc="isMenuOpen = false">
<button
class="p-1 rounded-md flex gap-2"
:class="{
'bg-zinc-800 hover:bg-zinc-700': isMenuOpen == false,
'bg-green-600 hover:bg-green-500': isMenuOpen == true,
}"
@click="isMenuOpen = !isMenuOpen"
>
<Bars3Icon class="size-6" />
</button>
<div class="fixed z-20 left-0 top-0 w-screen h-screen" v-if="isMenuOpen" @click="isMenuOpen = false"></div>
<div class="absolute z-30 top-full left-0 w-36 p-1 mt-2 flex flex-col gap-1 bg-zinc-600 rounded-md" v-if="isMenuOpen">
<button
class="p-1 rounded-md flex gap-2"
:class="{
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'active',
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'active',
}"
@click="toggleViewMode('active')"
>
<WifiIcon class="size-6" /> <span> Aktywne RJ</span>
</button>
<button
class="p-1 rounded-md flex gap-2"
:class="{
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'storage',
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'storage',
}"
@click="toggleViewMode('storage')"
>
<ArchiveBoxArrowDownIcon class="size-6" /> Zapisane RJ
</button>
<button
class="p-1 rounded-md flex gap-2"
:class="{
'bg-zinc-950 hover:bg-zinc-800': globalStore.viewMode != 'journal',
'bg-green-600 hover:bg-green-500': globalStore.viewMode == 'journal',
}"
@click="toggleViewMode('journal')"
>
<CloudArrowDownIcon class="size-6" /> Dziennik RJ
</button>
<!-- <button class="m-0 p-0" @focus="isMenuOpen = false"></button> -->
</div>
</div>
<select
name="trains"
id="trains-select"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:disabled="apiStore.activeDataStatus != DataStatus.SUCCESS"
v-model="globalStore.selectedTrainId"
v-if="globalStore.viewMode == 'active'"
@change="selectTrain"
>
<option :value="null" disabled>
{{ apiStore.activeDataStatus == 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>
<input
type="text"
v-if="globalStore.viewMode == 'storage'"
v-model="globalStore.storageTimetableSearch"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:placeholder="$t('train-search-placeholder')"
/>
<div v-if="globalStore.viewMode == 'journal'" class="w-full relative">
<input
type="text"
v-model="globalStore.journalTimetableSearch"
class="bg-zinc-800 p-1 rounded-md print:hidden w-full"
:placeholder="$t('journal-search-placeholder')"
@keydown.enter="fetchJournalTrain"
/>
<div class="absolute top-0 right-0">
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700">
<MagnifyingGlassIcon class="text-white size-6" />
</button>
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700">
<XMarkIcon class="text-white size-6" />
</button>
</div>
</div>
<button class="bg-zinc-800 p-1 rounded-md hover:bg-zinc-700" @click="toggleDarkMode">
<MoonIcon v-if="globalStore.darkMode" class="text-white size-6" />
<SunIcon v-else class="text-white size-6" />
</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"
>
<PrinterIcon class="text-white size-6" />
</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"
>
<ArrowDownTrayIcon class="text-white size-6" />
</button>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { useApiStore } from '../../stores/api.store';
import { DataStatus } from '../../types/api.types';
import { useGlobalStore } from '../../stores/global.store';
import {
PrinterIcon,
MoonIcon,
SunIcon,
ArchiveBoxArrowDownIcon,
ArrowDownTrayIcon,
CloudArrowDownIcon,
WifiIcon,
XMarkIcon,
MagnifyingGlassIcon,
Bars3Icon,
} from '@heroicons/vue/16/solid';
import { getRegionNameById } from '../../utils/trainUtils';
import type { TimetableData, ViewMode } from '../../types/common.types';
// Stores
const apiStore = useApiStore();
const globalStore = useGlobalStore();
const isMenuOpen = ref(false);
// Computed
const isTimetableSaved = computed(() => {
if (!globalStore.currentTimetableData) return false;
return Object.keys(globalStore.storageTimetables).includes(`${globalStore.currentTimetableData.timetableId}`);
});
// Methods
function selectTrain() {
if (!apiStore.activeData) return;
globalStore.selectedActiveTrain = globalStore.activeTimetableTrains.find((train) => train.id == globalStore.selectedTrainId) ?? null;
if (globalStore.selectedActiveTrain != null) {
globalStore.generatedDate = new Date();
}
}
function fetchJournalTrain(e: Event) {
e.preventDefault();
console.log(globalStore.journalTimetableSearch);
}
function toggleViewMode(viewMode: ViewMode) {
if (viewMode == globalStore.viewMode) {
switch (viewMode) {
case 'active':
globalStore.selectedActiveTrain = null;
break;
case 'storage':
globalStore.selectedStorageTimetable = null;
break;
case 'journal':
globalStore.selectedJournalTimetable = null;
break;
default:
break;
}
}
isMenuOpen.value = false;
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 refreshData() {
// apiStore.fetchActiveData();
// selectTrain();
// }
</script>
@@ -0,0 +1,73 @@
<template>
<div class="text-white">
<div v-if="globalStore.selectedStorageTimetable == null && Object.keys(globalStore.storageTimetables).length == 0">
<div class="font-bold text-2xl p-1 bg-zinc-800">{{ $t('storage-empty-header') }}</div>
<div class="text-md mt-2 p-2 bg-zinc-900">{{ $t('storage-empty-info') }}</div>
</div>
<div v-else>
<div class="font-bold text-2xl p-2 bg-zinc-800 mb-3">{{ $t('storage-preview-title') }}</div>
<div class="font-bold p-2 bg-zinc-800 mb-3" v-if="filteredTimetables.length == 0">{{ $t('storage-preview-empty') }}</div>
<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">#{{ timetable.timetableId }} &bull; {{ new Date(timetable.savedTimestamp!).toLocaleString() }}</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)">
<TrashIcon class="size-5 text-white" />
</button>
</li>
</div>
</div>
</template>
<script setup lang="ts">
import { TrashIcon } from '@heroicons/vue/16/solid';
import { useGlobalStore } from '../../stores/global.store';
import type { TimetableData } from '../../types/common.types';
import { useI18n } from 'vue-i18n';
import { computed } from 'vue';
const globalStore = useGlobalStore();
const i18n = useI18n();
const filteredTimetables = computed(() => {
let timetables = Object.values(globalStore.storageTimetables);
if (globalStore.storageTimetableSearch.length != 0)
timetables = timetables.filter((st) =>
`${st.timetableId} ${st.driverName} ${st.route} ${st.category} ${st.trainNo}`
.toLocaleLowerCase()
.includes(globalStore.storageTimetableSearch.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) {}
}
</script>
<style scoped></style>
@@ -0,0 +1,57 @@
<template>
<div class="my-2 print:hidden" v-if="globalStore.currentTimetableData?.savedTimestamp">
<div class="flex gap-2">
<div class="flex items-center gap-2 bg-zinc-900 p-1 w-full">
<div>
<InformationCircleIcon class="size-5" />
</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.savedTimestamp).toLocaleString() }}</b>
</template>
</i18n-t>
</div>
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="removeTimetable(globalStore.currentTimetableData.timetableId)">
<TrashIcon class="text-white size-6" />
</button>
<button class="font-bold bg-zinc-900 p-1 hover:bg-zinc-800" @click="globalStore.selectedStorageTimetable = null">
<ArrowUturnLeftIcon class="text-white size-6" />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ArrowUturnLeftIcon, InformationCircleIcon, TrashIcon } from '@heroicons/vue/16/solid';
import { useGlobalStore } from '../../stores/global.store';
import { useI18n } from 'vue-i18n';
import type { TimetableData } from '../../types/common.types';
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>
+252
View File
@@ -0,0 +1,252 @@
<template>
<div
:class="{ dark: globalStore.darkMode }"
v-if="globalStore.currentTimetableData != null"
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"
>
<div>
<div class="p-1 font-bold w-max">
{{ globalStore.currentTimetableData.category }} {{ globalStore.currentTimetableData.trainNo }} {{ $t('headers.relation') }}
{{ globalStore.currentTimetableData.route.replace('|', ' - ') }}
</div>
<table class="table-fixed mt-2 w-full border-collapse" v-if="computedTimetableRows.length > 0">
<TimetableHeader />
<TimetableBody :computed-timetable="computedTimetableRows" />
</table>
</div>
</div>
<div class="overflow-auto text-center font-bold text-zinc-400 p-1 min-h-full" v-else>
<div v-if="globalStore.viewMode == 'active'">
<div class="text-xl p-2 bg-zinc-900">{{ $t('train-select-info') }}</div>
</div>
<TimetableStorage v-else-if="globalStore.viewMode == 'storage'" />
<TimetableJournal v-else-if="globalStore.viewMode == 'journal'" />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
``;
import { useApiStore } from '../../stores/api.store';
import { useGlobalStore } from '../../stores/global.store';
import TimetableBody from './TimetableBody.vue';
import TimetableHeader from './TimetableHeader.vue';
import type { SceneryRoute, StopRow, TimetablePathData } from '../../types/common.types';
import TimetableStorage from './TimetableStorage.vue';
import TimetableJournal from './TimetableJournal.vue';
const globalStore = useGlobalStore();
const apiStore = useApiStore();
// Tymczasowa tabelka z posterunkami APO
const apoNames = ['Stary Kisielin, pe', 'Czerwony Dwór, pe', 'Szczejkowice, pe'];
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: StopRow[] = [];
let currentPathIndex = 0;
let currentPath = timetablePath[0];
let lastDepartureTimestamp = 0;
let arrivalKm = 0,
arrivalSpeed = 0,
arrivalTracks = 0,
departureSpeed = 0,
departureTracks = 2,
realLineNo = 0,
abbrevs = [] as string[];
if (currentPath.departureLineData) {
departureSpeed = currentPath.departureLineData.routeSpeed;
departureTracks = currentPath.departureLineData.routeTracks;
arrivalSpeed = currentPath.departureLineData.routeSpeed;
arrivalTracks = currentPath.departureLineData.routeTracks;
realLineNo = currentPath.departureLineData?.realLineNo ?? 0;
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) {
arrivalKm = stop.stopDistance;
if (currentPath.arrivalLineData) {
arrivalSpeed = currentPath.arrivalLineData.routeSpeed;
arrivalTracks = currentPath.arrivalLineData.routeTracks;
realLineNo = currentPath.arrivalLineData.realLineNo ?? 0;
abbrevs = getAbbrevs(currentPath.arrivalLineData);
}
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 = internalRouteInfo.routeSpeed;
departureSpeed = internalRouteInfo.routeSpeed;
realLineNo = internalRouteInfo.realLineNo ?? realLineNo;
abbrevs = getAbbrevs(internalRouteInfo);
correctedDepartureTracks = internalRouteInfo.routeTracks;
departureTracks = internalRouteInfo.routeTracks;
if (stopRows.length == 0) {
arrivalSpeed = departureSpeed;
arrivalTracks = departureTracks;
}
}
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,
realLine: realLineNo == 0 ? '' : realLineNo.toString(),
driveTime: lastDepartureTimestamp ? stop.arrivalTimestamp - lastDepartureTimestamp : 0,
abbrevs: [...pointAbbrevs, ...abbrevs],
arrivalKm: arrivalKm.toFixed(3),
departureKm: stop.stopDistance.toFixed(3),
arrivalSpeed: arrivalSpeed,
arrivalTracks: arrivalTracks,
departureSpeed: departureSpeed,
departureTracks: departureTracks,
headUnits: timetableData.headUnits,
stockVmax,
stockLength,
stockMass,
};
// console.debug(stop.stopNameRAW, stop.departureLine);
arrivalKm = stop.stopDistance;
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
for (let i = stopRows.length - 1; i > 0; i--) {
if (currentPath.departureLineData) {
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
stopRows[i].departureSpeed = currentPath.departureLineData.routeSpeed;
stopRows[i].realLine = currentPath.departureLineData.realLineNo?.toString() ?? '';
if (stopRows[i].isMain || stopRows[i].pointName.endsWith(', podg')) {
stopRows[i].departureSpeed = currentPath.departureLineData.routeSpeed;
stopRows[i].departureTracks = currentPath.departureLineData.routeTracks;
abbrevs = getAbbrevs(currentPath.departureLineData);
stopRows[i].abbrevs = abbrevs;
break;
}
stopRows[i].arrivalSpeed = currentPath.departureLineData.routeSpeed;
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: 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>
+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;
+27
View File
@@ -0,0 +1,27 @@
{
"data-loading-text": "Loading data...",
"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)",
"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"
},
"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",
"delete-timetable-confirm": "Are you sure that you want to delete this timetable?",
"journal-search-placeholder": "Driver nickname"
}
+27
View File
@@ -0,0 +1,27 @@
{
"data-loading-text": "Ładowanie danych...",
"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)",
"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"
},
"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óć",
"delete-timetable-confirm": "Czy na pewno chcesz usunąć ten rozkład jazdy z archiwum?",
"journal-search-placeholder": "Nick maszynisty"
}
+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');
+101
View File
@@ -0,0 +1,101 @@
import type { AxiosInstance } from 'axios';
import axios from 'axios';
import { defineStore } from 'pinia';
import { DataStatus, type ActiveDataResponse, type JournalTimetableShortResponse, type SceneriesDataResponse } from '../types/api.types';
import type { ActiveData, JournalTimetableShort, SceneryData } from '../types/common.types';
export const useApiStore = defineStore('api', {
state() {
return {
client: null as AxiosInstance | null,
activeData: null as ActiveData | null,
sceneryData: null as SceneryData[] | null,
journalTimetables: null as JournalTimetableShort[] | null,
outdatedTimerId: -1,
isActiveDataOutdated: false,
activeDataStatus: DataStatus.LOADING,
};
},
actions: {
async setupAPIData() {
if (this.client != null) return;
let baseURL = 'https://stacjownik.spythere.eu';
switch (import.meta.env.VITE_API_MODE) {
case 'development':
baseURL = 'http://localhost:3001';
break;
case 'mocking':
baseURL = 'http://localhost:3123';
break;
default:
break;
}
this.client = axios.create({
baseURL,
});
this.fetchSceneriesData();
await this.fetchActiveData();
setInterval(() => {
this.fetchActiveData();
}, 25000);
},
async fetchActiveData() {
try {
const response = (await this.client!.get<ActiveDataResponse>('/api/getActiveData')).data;
this.activeData = response;
this.activeDataStatus = DataStatus.SUCCESS;
this.isActiveDataOutdated = false;
if (this.outdatedTimerId != -1) clearTimeout(this.outdatedTimerId);
this.outdatedTimerId = setTimeout(() => {
this.isActiveDataOutdated = true;
}, 60000);
} catch (error) {
console.error(error);
}
},
async fetchSceneriesData() {
try {
const response = (await this.client!.get<SceneriesDataResponse>('/api/getSceneries')).data;
this.sceneryData = response;
} catch (error) {
console.error(error);
}
},
async fetchTimetableHistoryList() {
try {
const response = (
await this.client!.get<JournalTimetableShortResponse>('/api/getTimetables', {
params: {
driverName: 'Spythere',
returnType: 'short',
hasStopsDetails: 1
},
})
).data;
this.journalTimetables = response;
// this.sceneryData = response;
} catch (error) {
console.error(error);
}
},
},
});
+69 -28
View File
@@ -1,42 +1,83 @@
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, TimetableData, ViewMode } from '../types/common.types';
import { unitNameCorrections } from '../utils/trainUtils';
export const useGlobalStore = defineStore('global', {
state: () => ({
activeData: null as ActiveData | null,
sceneryData: null as SceneryData[] | null,
darkMode: false,
viewMode: 'active' as ViewMode,
selectedTrainId: null as string | null,
selectedActiveTrain: null as ActiveTrain | null,
selectedStorageTimetable: null as TimetableData | null,
selectedJournalTimetable: null,
storageTimetables: {} as Record<number, TimetableData>,
timetableWarnings: [] as string[],
generatedDate: null as Date | null,
generatedMs: 0,
storageTimetableSearch: '',
journalTimetableSearch: '',
showSettings: 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'));
},
async _fetchSceneriesData() {
try {
const response = (await httpClient.get<SceneriesDataResponse>('/api/getSceneries')).data;
currentTimetableData(): TimetableData | null {
if (this.viewMode == 'active') {
const selectedTrain = this.selectedActiveTrain;
this.sceneryData = response;
} catch (error) {
console.error(error);
}
},
if (!selectedTrain || !selectedTrain.timetable) return null;
setupData() {
this._fetchActiveData();
this._fetchSceneriesData();
return {
trainNo: selectedTrain.trainNo,
mass: selectedTrain.mass,
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: selectedTrain.stockString
.split(';')
.slice(0, 3)
.filter((s, i) => i == 0 || /-\d+$/.test(s))
.map((s) => {
const unitName = s.slice(0, s.indexOf('-'));
setInterval(() => {
this._fetchActiveData();
}, 20000);
return unitNameCorrections[unitName] ?? unitName;
}),
};
} else if (this.viewMode == 'storage') {
const selectedStorageTimetable = this.selectedStorageTimetable;
return selectedStorageTimetable;
} else return null;
},
},
actions: {},
});
+72 -10
View File
@@ -2,23 +2,85 @@
@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;
+10 -1
View File
@@ -1,5 +1,14 @@
import type { ActiveData, SceneryData } from './common.types';
import type { ActiveData, JournalTimetableDetailed, JournalTimetableShort, SceneryData } from './common.types';
export type ActiveDataResponse = ActiveData;
export type SceneriesDataResponse = SceneryData[];
export type JournalTimetableShortResponse = JournalTimetableShort[];
export type JournalTimetableDetailedResponse = JournalTimetableDetailed[];
export enum DataStatus {
'LOADING' = 0,
'SUCCESS' = 1,
'ERROR' = 2,
}
+141
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 {
@@ -119,4 +122,142 @@ export interface SceneryRoute {
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;
realLine: string;
driveTime: number;
abbrevs: string[];
sceneryName: string;
arrivalKm: string;
arrivalSpeed: number;
arrivalTracks: number;
departureKm: string;
departureSpeed: number;
departureTracks: number;
headUnits: string[];
stockVmax: number;
stockLength: number;
stockMass: number;
}
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[];
hidden: boolean;
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;
}
-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;
+29
View File
@@ -0,0 +1,29 @@
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 const unitNameCorrections: Record<string, string> = {
'2EN57': 'EN57',
'201E': 'ET22',
'4E': 'EU07',
M62: 'ST44',
CTLR4C: 'ST44',
};
+1
View File
@@ -4,6 +4,7 @@ export default {
theme: {
extend: {},
},
darkMode: 'selector',
plugins: [],
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
}
+3
View File
@@ -4,4 +4,7 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 5345
}
})
+37 -2
View File
@@ -157,6 +157,32 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz#34aa0b52d0fbb1a654b596acfa595f0c7b77a77b"
integrity sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==
"@heroicons/vue@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@heroicons/vue/-/vue-2.2.0.tgz#d81f14eed448eec9859849ed63facd3f29bca2b3"
integrity sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==
"@intlify/core-base@10.0.5":
version "10.0.5"
resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-10.0.5.tgz#c4d992381f8c3a50c79faf67be3404b399c3be28"
integrity sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==
dependencies:
"@intlify/message-compiler" "10.0.5"
"@intlify/shared" "10.0.5"
"@intlify/message-compiler@10.0.5":
version "10.0.5"
resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-10.0.5.tgz#4eeace9f4560020d5e5d77f32bed7755e71d8efd"
integrity sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==
dependencies:
"@intlify/shared" "10.0.5"
source-map-js "^1.0.2"
"@intlify/shared@10.0.5":
version "10.0.5"
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-10.0.5.tgz#1b46ca8b541f03508fe28da8f34e4bb85506d6bc"
integrity sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
@@ -403,7 +429,7 @@
de-indent "^1.0.2"
he "^1.2.0"
"@vue/devtools-api@^6.6.3":
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.3":
version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
@@ -1181,7 +1207,7 @@ signal-exit@^4.0.1:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
source-map-js@^1.2.0, source-map-js@^1.2.1:
source-map-js@^1.0.2, source-map-js@^1.2.0, source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -1345,6 +1371,15 @@ vue-demi@^0.14.10:
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
vue-i18n@10:
version "10.0.5"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-10.0.5.tgz#fdf4e6c7b669e80cfa3a12ed9625e2b46671cdf0"
integrity sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==
dependencies:
"@intlify/core-base" "10.0.5"
"@intlify/shared" "10.0.5"
"@vue/devtools-api" "^6.5.0"
vue-tsc@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.2.0.tgz#dd06c56636f760d7534b7a7a0f6669ba93c217b8"