Compare commits

..

88 Commits

Author SHA1 Message Date
Spythere a459fdf178 Merge pull request #93 from Spythere/development
v1.25.0
2024-06-09 23:40:54 +02:00
Spythere 4e7fba89ee chore: improved stop label information 2024-06-09 00:58:45 +02:00
Spythere 6084e5876d chore: changed default history mode 2024-06-08 21:38:05 +02:00
Spythere 44f548c7b7 chore: scenery history locales 2024-06-08 21:37:28 +02:00
Spythere 59a5fbe5ac chore: adjusted to new version of API vehicles data 2024-06-08 20:53:22 +02:00
Spythere c252213ed9 hotfix 2024-06-07 18:31:20 +02:00
Spythere fb56378f18 chore: redesigned scenery history tables 2024-06-07 16:44:09 +02:00
Spythere e9635eae06 chore: redesigned train schedule list 2024-06-06 17:11:52 +02:00
Spythere 1fc98a8f99 chore: added test data mocks 2024-06-06 14:41:54 +02:00
Spythere c9de1a48ce chore: scenery timetables history translation; layout fixes 2024-06-06 14:19:17 +02:00
Spythere fee9774f88 chore: layout fixes 2024-06-06 14:12:21 +02:00
Spythere 7c974e8d0e bump: 1.25.0 2024-06-06 14:04:07 +02:00
Spythere c84fbbcf42 chore: added scenery timetables history modes 2024-06-05 20:03:05 +02:00
Spythere 45af649505 chore: changes in scenery view layout 2024-06-05 16:01:17 +02:00
Spythere 6c1e00d002 chore: layout & design fixes 2024-06-04 15:57:17 +02:00
Spythere 69ff85cfb1 chore: added route electrification indicators in train schedule 2024-06-03 22:26:58 +02:00
Spythere bdc2ca784c chore: missing translations 2024-06-03 21:37:33 +02:00
Spythere dbd73d448d chore: added active train's rolling stock vmax 2024-06-03 20:09:15 +02:00
Spythere 26b1ec246d chore: added extra data to vehicles tooltip 2024-06-03 18:10:45 +02:00
Spythere 8190dfa2cb chore: fetching & caching vehicles data information 2024-06-03 01:31:31 +02:00
Spythere 44df685606 Merge pull request #92 from Spythere/development
v1.24.4
2024-05-30 14:38:04 +02:00
Spythere 785a42b849 hotfix: detecting user timetable status at checkpoints 2024-05-30 14:29:09 +02:00
Spythere ccfcca8728 hotfix: scenery timetable duplicating 2024-05-30 14:24:18 +02:00
Spythere d9a7ba122c Merge pull request #91 from Spythere/development
v1.24.3
2024-05-26 01:44:45 +02:00
Spythere bf8d4a9ef4 chore: global font sizing; chore: train modal dvh 2024-05-25 18:06:01 +02:00
Spythere 6ea1e91d1d hotfix: card positioning 2024-05-25 17:57:25 +02:00
Spythere 813b557455 chore: improved card positioning 2024-05-25 17:55:18 +02:00
Spythere 834b14da69 fix: card dvh 2024-05-25 17:26:27 +02:00
Spythere c809b2146d chore: locale update 2024-05-25 17:12:19 +02:00
Spythere 33b98ca313 chore: added text color for active filters info 2024-05-25 17:11:28 +02:00
Spythere bcb9c63cb0 chore: reactive hiding body scroll on modal 2024-05-25 17:05:41 +02:00
Spythere 17d77a80d8 bump: 1.24.3 2024-05-25 16:02:40 +02:00
Spythere 65b159f8fd fix: scenery timetable duplicates; fix: not opening train modal for queries 2024-05-25 16:02:20 +02:00
Spythere 063d5283e4 Merge pull request #90 from Spythere/development
v1.24.2
2024-05-24 13:56:39 +02:00
Spythere 29de1b3c4b chore: scenery view layout 2024-05-24 13:52:42 +02:00
Spythere f0c02bf12e chore: pwa adjustments 2024-05-24 13:43:29 +02:00
Spythere 8aa23468b3 chore: changed station stats median to avg 2024-05-23 15:53:18 +02:00
Spythere 4c1fcf710b refactor: global modals to cards 2024-05-23 15:01:30 +02:00
Spythere a529d6e9eb chore: changed no stations message 2024-05-23 14:08:42 +02:00
Spythere 9fc602e08f chore: filters improvements 2024-05-22 15:41:33 +02:00
Spythere 56e40bd84b bump: version (1.24.2) 2024-05-21 16:17:41 +02:00
Spythere a5b5df7452 refactor: restructured station filters 2024-05-21 16:17:23 +02:00
Spythere 1a8da02ced chore: checkpoints detection fix 2024-05-19 23:42:06 +02:00
Spythere 7e75fa2516 chore: checkpoints hotfix 2024-05-19 23:12:07 +02:00
Spythere 3ed2c09184 chore: checkpoints filtering 2024-05-19 23:05:57 +02:00
Spythere 6901c3d2b4 chore: hotfix 2024-05-19 22:30:21 +02:00
Spythere 8417754403 refactor: optimization of train schedules 2024-05-19 19:50:01 +02:00
Spythere de5c57181a Merge pull request #89 from Spythere/development
v1.24.1
2024-05-16 23:43:39 +02:00
Spythere d91d4cc6a8 fix: station stats spawn count regions 2024-05-16 23:42:35 +02:00
Spythere 9a5fd4d670 chore: version bump 2024-05-16 23:29:56 +02:00
Spythere 4202a55673 chore: updated pwa strategies 2024-05-16 21:36:16 +02:00
Spythere 5181e8f4af chore: fix journal refresh date visibility 2024-05-16 20:06:02 +02:00
Spythere e117f62fcb chore: added station filters (scenery types); pwa adjustments 2024-05-16 19:59:43 +02:00
Spythere e0036bf969 chore: filters & stats fixes 2024-05-15 18:40:42 +02:00
Spythere 1f457d6389 Merge pull request #88 from Spythere/development
hotfix: minor adjustments for new simulator version (2024.1.1)
2024-05-13 15:05:28 +02:00
Spythere eb5b94c9f6 chore: vehicle images hotfixes 2024-05-13 15:02:15 +02:00
Spythere 328e8c0573 chore: fixed stock fallback thumbnnail 2024-05-13 14:54:21 +02:00
Spythere 9f58ae5428 Merge pull request #87 from Spythere/development
hotfix: modal positioning
2024-05-12 15:23:30 +02:00
Spythere ebd0eeb8c4 hotfix: modal positioning 2024-05-12 15:22:03 +02:00
Spythere fa656c2f26 Merge pull request #86 from Spythere/development
v1.24.0
2024-05-12 15:14:22 +02:00
Spythere 0cc3a12d1d fix: modal responsiveness 2024-05-12 14:55:35 +02:00
Spythere 392a6437f8 feature: current users tooltip 2024-05-09 17:19:22 +02:00
Spythere 122532f0ed chore: general fixes 2024-05-09 16:40:53 +02:00
Spythere 366ff91f60 hotfix: update modal 2024-05-08 20:12:07 +02:00
Spythere a0496736dd chore: modals update 2024-05-08 20:04:41 +02:00
Spythere f974120e87 fix: lock files 2024-05-08 18:42:33 +02:00
Spythere abd8b8178b chore: vue deep selector 2024-05-08 16:42:04 +02:00
Spythere f1fcde8459 feat: update modal 2024-05-08 16:41:14 +02:00
Spythere b3289d6aab chore: region dropdown fixes 2024-05-08 15:16:20 +02:00
Spythere 6481a4a3b0 chore: design improvements 2024-05-08 15:10:40 +02:00
Spythere 05dc268526 fix: spawns detection 2024-05-06 18:18:15 +02:00
Spythere 669acc98d2 chore: station stats translation 2024-05-06 18:16:30 +02:00
Spythere 3371b661c2 fix: ufactor calc 2024-05-06 17:53:07 +02:00
Spythere 871b2c0221 feature: open spawns tooltip 2024-05-06 17:36:23 +02:00
Spythere d366a877a4 refactor: popups -> tooltips 2024-05-06 16:37:56 +02:00
Spythere 405aab96bd feature: stations stats 2024-05-05 13:34:43 +02:00
Spythere f29c160000 fix: lock files 2024-05-04 14:47:30 +02:00
Spythere a2de0e2030 refactor: types & performance 2024-05-04 14:43:34 +02:00
Spythere 7dd1c06f3f chore: accessibility of filters 2024-05-03 19:29:10 +02:00
Spythere ff041b9aaf bump(version): 1.24.0 2024-05-03 19:02:49 +02:00
Spythere 4782dba444 feat(app): added min route speed & max route speed station filters 2024-05-03 19:02:16 +02:00
Spythere d6b8d032d6 fix(app): improved data fetching scheduler 2024-05-03 19:02:13 +02:00
Spythere c16616330c chore(packages): update & cleanup 2024-05-03 18:01:54 +02:00
Spythere 57cec8bfe7 chore: pwa adjustments 2024-05-03 17:49:54 +02:00
Spythere 6bea340e19 chore(pwa): changed sceneries cache to cachefirst 2024-05-01 19:37:51 +02:00
Spythere c181cf7e64 fix(workflows): release color 2024-04-27 01:11:38 +02:00
Spythere 8e4ae64cd3 chore(workflows): added release discord webhook notification 2024-04-15 15:13:22 +02:00
Spythere 5750490f01 refactor: journals 2024-04-08 23:21:50 +02:00
99 changed files with 15070 additions and 25863 deletions
@@ -0,0 +1,17 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "15844367"
footer_title: "Changelog - Stacjownik"
footer_timestamp: true
+2828 -9512
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.23.1",
"version": "1.25.0",
"private": true,
"scripts": {
"dev": "vite",
@@ -14,11 +14,9 @@
"dependencies": {
"core-js": "^3.32.2",
"dotenv": "^16.3.1",
"firebase": "^10.4.0",
"howler": "^2.2.4",
"pinia": "^2.1.6",
"sass": "^1.67.0",
"socket.io-client": "^4.7.4",
"showdown": "^2.1.0",
"vue": "^3.3.4",
"vue-i18n": "^9.4.1",
"vue-router": "^4.2.4"
@@ -26,7 +24,8 @@
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2",
"@vite-pwa/assets-generator": "^0.0.10",
"@types/showdown": "^2.0.6",
"@vite-pwa/assets-generator": "^0.2.4",
"@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
@@ -37,7 +36,7 @@
"prettier": "^3.0.3",
"typescript": "^5.2.2",
"vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5",
"vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11"
},
"browserslist": [
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

+62 -68
View File
@@ -1,6 +1,11 @@
<template>
<div class="app_container" v-cloak>
<PopUp />
<div class="app_container">
<UpdateCard
:is-update-card-open="isUpdateCardOpen"
@toggle-card="() => (isUpdateCardOpen = false)"
/>
<Tooltip />
<transition name="modal-anim">
<keep-alive>
@@ -22,7 +27,10 @@
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
<button class="btn--text" @click="() => (isUpdateCardOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
<br />
<a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
@@ -36,19 +44,21 @@
<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '.././package.json';
import { version } from '.././package.json';
import { Status } from './typings/common';
import { useMainStore } from './store/mainStore';
import popupMixin from './mixins/popupMixin';
import { useApiStore } from './store/apiStore';
import { useTooltipStore } from './store/tooltipStore';
import Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateCard from './components/App/UpdateCard.vue';
import StorageManager from './managers/storageManager';
import PopUp from './components/PopUp/PopUp.vue';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common';
const STORAGE_VERSION_KEY = 'app_version';
@@ -58,19 +68,22 @@ export default defineComponent({
StatusIndicator,
AppHeader,
TrainModal,
PopUp
UpdateCard,
Tooltip
},
mixins: [popupMixin],
data: () => ({
VERSION: version,
store: useMainStore(),
apiStore: useApiStore(),
tooltipStore: useTooltipStore(),
isUpdateCardOpen: false,
currentLang: 'pl',
releaseURL: '',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
nextUpdateTime: 0
}),
created() {
@@ -78,41 +91,52 @@ export default defineComponent({
},
async mounted() {
window.addEventListener('focus', () => {
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
this.apiStore.fetchActiveData();
});
window.addEventListener('mousemove', (e: MouseEvent) => this.handlePopUpEvents(e));
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
},
methods: {
init() {
this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling();
this.checkAppVersion();
this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
checkAppVersion() {
if (import.meta.env.DEV) {
this.store.isNewUpdate = true;
return;
update(t: number) {
if (t >= this.nextUpdateTime) {
this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.update);
},
async checkAppVersion() {
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
if (storageVersion === undefined || storageVersion != version) {
this.store.isNewUpdate = true;
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
if (!releaseData) return;
this.store.appUpdate = {
version,
changelog: releaseData.body,
releaseURL: releaseData.html_url
};
this.isUpdateCardOpen =
storageVersion != version || import.meta.env.VITE_UPDATE_TEST === 'test';
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
}
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
},
setupOfflineHandling() {
@@ -137,27 +161,6 @@ export default defineComponent({
this.apiStore.connectToAPI();
},
handlePopUpEvents(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-popup-key'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.store.popUpData.key != null) this.hidePopUp();
return;
}
const popupComponentKey = targetEl.getAttribute('data-popup-key');
const popupContent = targetEl.getAttribute('data-popup-content');
if (popupComponentKey && popupContent) this.showPopUp(e, popupComponentKey, popupContent);
else if (this.store.popUpData.key != null) this.hidePopUp();
this.store.mousePos.x = e.pageX;
this.store.mousePos.y = e.pageY;
},
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
@@ -165,21 +168,6 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang);
},
async setReleaseURL() {
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
if (!releaseData) return;
this.releaseURL = releaseData.html_url;
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
return;
}
},
loadLang() {
const storageLang = StorageManager.getStringValue('lang');
@@ -192,7 +180,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString();
if (naviLanguage.includes('en')) {
if (naviLanguage.startsWith('en')) {
this.changeLang('en');
return;
}
@@ -222,7 +210,7 @@ export default defineComponent({
overflow-x: hidden;
@include smallScreen() {
font-size: calc(0.65rem + 0.8vw);
font-size: calc(0.65rem + 0.85vw);
}
@include screenLandscape() {
@@ -238,6 +226,7 @@ export default defineComponent({
min-height: 100vh;
overflow: hidden;
position: relative;
}
.app_main {
@@ -255,10 +244,15 @@ export default defineComponent({
}
// FOOTER
footer.app_footer {
.app_footer {
max-width: 100%;
padding: 0.5em;
button {
display: inline-block;
padding: 0.1em;
}
img {
width: 1.1em;
vertical-align: text-bottom;
-11
View File
@@ -29,11 +29,6 @@
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span>
<!-- <span class="g-tooltip">
<b class="text--primary">{{ factorU }}U</b>
<div class="content">Test</div>
</span> -->
<span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span>
<img src="/images/icon-train.svg" alt="icon train" />
@@ -103,12 +98,6 @@ export default defineComponent({
return this.store.activeSceneryList.filter(
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
).length;
},
factorU() {
return this.onlineDispatchersCount == 0
? '-'
: (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
}
},
components: { StatusIndicator, Clock, RegionDropdown }
+123
View File
@@ -0,0 +1,123 @@
<template>
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<button class="btn btn--action" ref="confirm-btn" @click="toggleCard(false)">
{{ $t('update.confirm') }}
</button>
<p class="bottom-info">
{{ $t('update.info-1') }}
<br />
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import { Converter } from 'showdown';
import Card from '../Global/Card.vue';
const converter = new Converter();
export default defineComponent({
components: { Card },
props: {
isUpdateCardOpen: {
type: Boolean,
required: true
}
},
emits: ['toggleCard'],
data() {
return {
mainStore: useMainStore(),
version: version
};
},
watch: {
isUpdateCardOpen(val: boolean) {
this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
});
}
},
computed: {
htmlChangelog() {
if (this.mainStore.appUpdate == null) return '';
return converter.makeHtml(this.mainStore.appUpdate.changelog);
}
},
methods: {
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/variables';
::v-deep(h1) {
text-align: center;
color: $accentCol;
}
::v-deep(h2) {
padding: 0.25em 0;
border-bottom: 1px solid #aaa;
}
::v-deep(ul) {
list-style: initial;
padding: 1em;
line-height: 1.5em;
}
.content {
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
padding: 1em;
min-height: 700px;
overflow: auto;
text-align: justify;
max-width: 700px;
}
.no-features {
text-align: center;
}
button {
margin: 0 auto;
padding: 0.5em 0.75em;
font-size: 1.1em;
}
p.bottom-info {
text-align: center;
color: #ccc;
}
a {
text-decoration: underline;
}
</style>
-133
View File
@@ -1,133 +0,0 @@
<template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
<div class="modal_content">
<div>
<h1 style="margin-bottom: 0.5em">{{ $t('update.title') }}</h1>
<h2 class="text--primary">{{ $t('update.version', [version]) }}</h2>
<hr class="separator" />
</div>
<div class="features-list">
<h2>Nowości i zmiany:</h2>
<ul>
<li v-for="content in localeChangesArray" :key="content">{{ content }}</li>
</ul>
</div>
<div class="modal_actions">
<button class="btn--action">Przyjąłem!</button>
<p>Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony!</p>
<!-- <div class="actions-checkboxes">
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
</div> -->
</div>
</div>
</AnimatedModal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json';
import AnimatedModal from '../Global/AnimatedModal.vue';
export default defineComponent({
components: { AnimatedModal },
data() {
return {
mainStore: useMainStore(),
version: version
};
},
computed: {
localeChangesArray() {
return this.$t('update.content').split('\n');
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
}
});
</script>
<style lang="scss" scoped>
.modal_content {
font-size: 1.2em;
text-align: center;
padding: 1em;
height: 80vh;
min-height: 550px;
display: grid;
grid-template-rows: auto 1fr auto;
gap: 0.5em;
}
hr.separator {
margin: 0.5em 0;
padding: 0;
height: 3px;
background-color: #fff;
}
.features-list {
margin-top: 0.5em;
overflow: auto;
ul {
text-align: left;
list-style: '\21D2 ';
padding: 1em;
}
li {
margin: 0.5em 0;
}
}
.modal_actions {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
button {
font-weight: bold;
padding: 0.35em;
}
p {
font-size: 0.9em;
}
}
.actions-checkboxes {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1em;
label {
font-size: 0.9em;
}
label > input {
margin-right: 0.5em;
}
}
</style>
-101
View File
@@ -1,101 +0,0 @@
<template>
<transition name="modal-anim" tag="div" class="modal">
<div class="body" v-if="isOpen">
<div class="background" @click="toggleModal(false)"></div>
<div class="wrapper" ref="wrapper" tabindex="0">
<slot></slot>
</div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
emits: ['toggleModal'],
props: {
isOpen: Boolean
},
data() {
return {
store: useMainStore()
};
},
watch: {
isOpen(v) {
this.$nextTick(() => {
if (v) (this.$refs['wrapper'] as HTMLElement).focus();
else (this.store.modalLastClickedTarget as HTMLElement)?.focus();
});
}
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.body {
position: fixed;
top: 0;
left: 0;
z-index: 200;
width: 100vw;
height: 100vh;
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #333333;
width: 95%;
max-width: 800px;
max-height: 95vh;
& > :slotted(div) {
max-height: 95vh;
}
}
@include smallScreen {
.wrapper {
top: 0;
transform: translate(-50%, 1em);
max-height: 90vh;
& > :slotted(div) {
max-height: 90vh;
}
}
}
</style>
+93
View File
@@ -0,0 +1,93 @@
<template>
<transition name="modal-anim" tag="div">
<div class="card" v-if="isOpen">
<div class="card-background" @click="toggleCard(false)"></div>
<div class="card-body" tabindex="0">
<slot></slot>
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
emits: ['toggleCard'],
props: {
isOpen: Boolean
},
data() {
return {
store: useMainStore()
};
},
watch: {
isOpen(v) {
this.$nextTick(() => {
if (v == false) (this.store.modalLastClickedTarget as HTMLElement)?.focus();
});
}
},
methods: {
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.card {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
}
.card-background {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.55);
}
.card-body {
position: relative;
margin: 1em;
max-height: 95vh;
max-height: 95dvh;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
overflow: auto;
}
@include smallScreen {
.card {
align-items: flex-start;
}
}
</style>
@@ -1,12 +1,7 @@
<template>
<AnimatedModal
class="donation-modal"
:isOpen="isModalOpen"
@toggleModal="toggleModal"
@keydown.esc="toggleModal(false)"
>
<div class="modal_content">
<div class="modal_main">
<Card :isOpen="isCardOpen" @toggleCard="toggleCard" @keydown.esc="toggleCard(false)">
<div class="body">
<div class="content">
<h1 v-html="$t('donations.header')"></h1>
<div class="donators-slider" v-if="donatorList.length != 0">
<span v-html="$t('donations.donator-title', { count: donatorList.length })"></span>
@@ -61,18 +56,19 @@
</i>
</div>
<div class="modal_actions">
<div class="actions">
<a
class="modal-action a-button btn--image coffee"
class="action a-button btn--image coffee"
href="https://buycoffee.to/spythere"
target="_blank"
ref="action"
>
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
{{ $t('donations.action-buycoffee') }}
</a>
<a
class="modal-action a-button btn--image paypal"
class="action a-button btn--image paypal"
href="https://www.paypal.com/donate/?hosted_button_id=EDB3SKFAHXFTW"
target="_blank"
>
@@ -80,32 +76,36 @@
{{ $t('donations.action-paypal') }}
</a>
<button class="modal-action btn--image exit" @click="toggleModal(false)">
<button class="action btn--image exit" @click="toggleCard(false)">
<img src="/images/icon-exit.svg" alt="dollar donation icon" />
{{ $t('donations.action-exit') }}
</button>
</div>
</div>
</AnimatedModal>
</Card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AnimatedModal from './AnimatedModal.vue';
import { useApiStore } from '../../store/apiStore';
import Card from './Card.vue';
export default defineComponent({
components: { AnimatedModal },
components: { Card },
props: {
isModalOpen: Boolean
isCardOpen: Boolean
},
emits: ['toggleModal'],
emits: ['toggleCard'],
watch: {
isModalOpen(b: boolean) {
this.running = b;
isCardOpen(val: boolean) {
this.running = val;
this.lastUpdate = Date.now();
this.$nextTick(() => {
if (val) (this.$refs['action'] as HTMLElement).focus();
});
}
},
@@ -133,8 +133,8 @@ export default defineComponent({
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
toggleCard(value: boolean) {
this.$emit('toggleCard', value);
},
runUpdate() {
@@ -152,53 +152,53 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
.modal_content {
.body {
display: grid;
grid-template-rows: 1fr auto;
gap: 1em;
font-size: 1.1em;
& > div {
padding: 1em;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
max-width: 820px;
}
.modal_main {
.content {
overflow: auto;
overflow-x: hidden;
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
padding: 1em;
}
.modal_actions {
img {
max-height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
h1 {
font-size: 1.95em;
text-align: center;
}
p {
text-align: justify;
}
a.discord {
text-decoration: underline;
}
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em;
padding: 1em;
form button {
width: 100%;
}
}
.modal_actions > .modal-action {
.actions > .action {
&.paypal {
$btnColor: #254069;
+4 -11
View File
@@ -65,12 +65,12 @@ export default defineComponent({
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
this.store.region =
regionsJSON.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
) ?? regionsJSON[0];
}
}
}
@@ -139,15 +139,10 @@ button.selected-region {
color: paleturquoise;
font-weight: bold;
padding: 0.1em 0.5em;
&:focus {
background-color: #262626;
}
span {
margin-right: 10px;
}
}
.content {
@@ -197,6 +192,8 @@ li.option {
}
label {
width: 100%;
padding: 0.5em 0;
position: relative;
display: inline-block;
@@ -207,10 +204,6 @@ li.option {
background-color: #333333f2;
}
padding: 0.5em 0;
width: 100%;
cursor: pointer;
}
}
+30 -27
View File
@@ -8,9 +8,12 @@
<img
class="traction-only"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
/^EN/.test(computedStockList[0]) ? 'rb' : ''
}.png`"
:src="
getVehicleThumbnailURL(
computedStockList[0].split(':')[0],
/^EN/.test(computedStockList[0]) ? 'rb' : ''
)
"
@error="onImageError($event, computedStockList[0])"
width="300"
height="60"
@@ -20,18 +23,18 @@
<ul v-else>
<li v-for="(stockName, i) in computedStockList" :key="i">
<p>
{{ stockName.split(':')[0].split('_').splice(0, 2).join(' ') }}
{{ stockName.split(':')[1] }}
{{ stockName.split(':')[0].split('_').splice(0, 3).join(' ') }}
<div v-if="stockName.split(':')[1]">({{ stockName.split(':')[1] }})</div>
</p>
<span>
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : ''
}.png`"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName"
:src="
getVehicleThumbnailURL(stockName.split(':')[0], /^EN/.test(stockName) ? 'rb' : '')
"
@error="onImageError($event, stockName)"
@click.stop="() => {}"
width="400"
@@ -41,10 +44,10 @@
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@@ -53,10 +56,10 @@
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
:src="getVehicleThumbnailURL(stockName, 's')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
"
@@ -65,10 +68,10 @@
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
:src="getVehicleThumbnailURL(stockName, 'ra')"
@error="
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
"
@@ -110,20 +113,20 @@ export default defineComponent({
},
methods: {
getVehicleThumbnailURL(locoType: string, suffix?: string) {
return `https://static.spythere.eu/thumbnails/${locoType}${suffix}.png`;
},
onImageError(event: Event, stockName: string) {
let fallbackName = '';
const isLoco = /.-\d{3}/.test(stockName);
if (isLoco) {
fallbackName += 'loco-';
fallbackName += /^\d?EN\d{2}/.test(stockName)
? 'ezt'
: /^SN\d{2}/.test(stockName)
? 'szt'
: /^\d?E/.test(stockName)
? 'e'
: 's';
if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
else fallbackName = 'loco-s';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
@@ -43,7 +43,7 @@
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="isDonator(historyItem.dispatcherName)"
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
@@ -128,13 +128,13 @@ import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin],
mixins: [dateMixin, styleMixin],
props: {
dispatcherHistory: {
@@ -159,6 +159,7 @@ export default defineComponent({
return {
Status,
store: useMainStore(),
apiStore: useApiStore(),
regions
};
},
@@ -9,7 +9,7 @@
ref="button"
>
<img src="/images/icon-filter2.svg" alt="Open filters" />
{{ $t('options.filters') }} [F]
[F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
@@ -301,6 +301,6 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/dropdown.scss';
@import '../../styles/dropdown_filters.scss';
@import '../../styles/dropdown';
@import '../../styles/dropdown_filters';
</style>
@@ -17,7 +17,34 @@
</div>
<div v-else>
<TimetableHistoryList :timetableHistory="timetableHistory" />
<ul class="journal-list">
<transition-group name="list-anim">
<li
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<!-- Extra -->
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
<AddDataButton
:list="timetableHistory"
@@ -37,17 +64,29 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType, ref } from 'vue';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common';
import { API } from '../../../typings/api';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableDetails from './TimetableDetails.vue';
export default defineComponent({
components: { Loading, AddDataButton, TimetableHistoryList },
components: {
Loading,
AddDataButton,
TimetableDetails,
TimetableGeneral,
TimetableStatus,
TimetableStops
},
props: {
timetableHistory: {
@@ -73,6 +112,15 @@ export default defineComponent({
Status,
store: useMainStore()
};
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
}
});
</script>
@@ -80,4 +128,15 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss';
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
}
</style>
@@ -0,0 +1,195 @@
<template>
<div>
<div class="details-actions">
<button class="btn--action">
<b>{{ $t('journal.stock-info') }}</b>
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
</button>
</div>
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { StockList },
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.details-body {
margin-top: 0.5em;
}
.details-actions {
display: flex;
button img {
height: 1.25em;
}
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
@include smallScreen() {
.stock-specs {
justify-content: center;
}
.details-actions {
justify-content: center;
}
}
</style>
@@ -1,173 +0,0 @@
<template>
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { StockList },
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-extra {
margin-top: 0.5em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen() {
justify-content: center;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
</style>
@@ -24,7 +24,7 @@
</strong>
<strong
v-if="isDonator(timetable.driverName)"
v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator"
:title="$t('donations.driver-message')"
>
@@ -57,18 +57,18 @@
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
<button
v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable"
class="btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('journal.timetable-online-button') }}
<img src="/images/icon-train.svg" alt="train icon" />
<b>{{ $t('journal.timetable-online-button') }}</b>
</button>
</span>
</div>
@@ -81,10 +81,16 @@ import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin, donatorMixin],
mixins: [dateMixin, modalTrainMixin, styleMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: {
timetable: {
@@ -97,15 +103,15 @@ export default defineComponent({
showTimetable(timetable: API.TimetableHistory.Data, target: EventTarget | null) {
if (timetable?.terminated) return;
this.selectModalTrain(timetable.driverName + timetable.trainNo.toString(), target);
this.selectModalTrainById(`${timetable.driverName}${timetable.trainNo}`, target);
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
@import '../../../styles/responsive';
@import '../../../styles/badge';
.item-general {
display: flex;
@@ -117,8 +123,22 @@ export default defineComponent({
margin-bottom: 0.5em;
}
.info-date {
margin-right: 0.5em;
.general-train {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.general-time {
display: flex;
align-items: center;
gap: 0.5em;
}
.badges {
@@ -143,24 +163,12 @@ export default defineComponent({
}
}
.general-train {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.btn-timetable {
display: inline-block;
padding: 0.1em 0.4em;
margin-left: 0.5em;
display: flex;
padding: 0.2em 0.5em;
img {
vertical-align: top;
height: 1.25em;
}
}
@@ -21,7 +21,7 @@
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<button class="btn--option btn--show">
<button class="btn--action btn--show">
{{ $t('journal.stock-info') }}
<img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
@@ -66,9 +66,9 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/JournalSection.scss';
@import '../../../styles/variables';
@import '../../../styles/responsive';
@import '../../../styles/JournalSection';
.btn--show {
display: flex;
+3 -1
View File
@@ -6,7 +6,9 @@ export namespace Journal {
| 'search-train'
| 'search-date'
| 'search-dispatcher'
| 'search-issuedFrom';
| 'search-issuedFrom'
| 'search-terminatingAt'
| 'search-via';
export type TimetableSearchType = {
[key in TimetableSearchKey]: string;
@@ -1,85 +0,0 @@
<template>
<div class="popup-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="store.popUpData.key"
@load="onImageLoad"
@error="onImageError"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${store.popUpData.content}--300px.jpg`"
/>
<div class="vehicle-name" v-if="imageState != 'error'">
{{ store.popUpData.content.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
store: useMainStore(),
imageState: 'loading'
};
},
mounted() {
this.imageState = 'loading';
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
},
onImageError(e: Event) {
this.imageState = 'error';
(e.target as HTMLElement).style.display = 'none';
}
}
});
</script>
<style lang="scss" scoped>
.popup-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
width: 300px;
min-height: 200px;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em;
border-radius: 0.5em;
}
.loading-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
img {
width: 100%;
height: auto;
}
.vehicle-name {
text-align: center;
margin-top: 0.5em;
color: #ccc;
text-wrap: wrap;
}
</style>
@@ -1,31 +1,18 @@
<template>
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="scenery-dispatchers-history">
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded && historyList.length == 0" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
</div>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.dispatchers-history-hash') }}</th>
<th>{{ $t('scenery.dispatchers-history-dispatcher') }}</th>
<th>{{ $t('scenery.dispatchers-history-level') }}</th>
<th>{{ $t('scenery.dispatchers-history-rate') }}</th>
<th>{{ $t('scenery.dispatchers-history-date') }}</th>
</thead>
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>#{{ historyItem.stationHash }}</td>
<td>
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b>{{ historyItem.dispatcherName }}</b>
</router-link>
</td>
<td>
<div v-else class="history-list">
<div v-for="historyItem in historyList" :key="historyItem.id">
<span>
<span class="text--grayed" style="margin-right: 10px">
#{{ historyItem.stationHash }}
</span>
<b
v-if="historyItem.dispatcherLevel !== null"
class="level-badge dispatcher"
@@ -35,55 +22,67 @@
>
{{ historyItem.dispatcherLevel >= 2 ? historyItem.dispatcherLevel : 'L' }}
</b>
<b style="margin-left: 5px">
<router-link
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
{{ historyItem.dispatcherName }}
</router-link>
</b>
<b v-else>?</b>
</td>
<td class="text--primary">
<b>{{ historyItem.dispatcherRate }}</b>
</td>
<td style="min-width: 300px">
<div v-if="historyItem.timestampTo">
<div>
<span>
{{ $t('scenery.dispatcher-rate') }}
<b class="text--primary"> {{ historyItem.dispatcherRate }}</b>
</span>
|
<span>
{{ $t('scenery.dispatcher-status-changes') }}
<b class="text--primary">{{ historyItem.statusHistory.length }}</b>
</span>
</div>
</span>
<span>
<span v-if="historyItem.timestampTo">
<b>{{ $d(historyItem.timestampFrom) }}</b>
{{ timestampToString(historyItem.timestampFrom) }}
- {{ timestampToString(historyItem.timestampTo) }} ({{
calculateDuration(historyItem.currentDuration)
}})
</div>
</span>
<div class="dispatcher-online" v-else>
<span class="dispatcher-online" v-else>
{{ $t('journal.online-since') }}
<b>{{ timestampToString(historyItem.timestampFrom) }}</b>
({{ calculateDuration(historyItem.currentDuration) }})
</div>
</td>
</tr>
</tbody>
</table>
</section>
</span>
</span>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'SceneryDispatchersHistory',
mixins: [dateMixin, styleMixin, listObserverMixin],
mixins: [dateMixin, styleMixin],
components: { Loading },
props: {
station: {
@@ -152,8 +151,43 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.scenery-dispatchers-history {
height: 100%;
overflow: auto;
display: grid;
gap: 0.5em;
grid-template-rows: auto 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.history-list > div {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.75em;
}
.level-badge {
margin: 0 auto;
text-align: center;
display: inline-block;
line-height: 1.6em;
}
.dispatcher-online {
@@ -161,13 +195,10 @@ export default defineComponent({
}
@include smallScreen {
.history-list {
font-size: 1.1em;
}
.list-item {
align-items: center;
.history-list > div {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
</style>
../../store/storeTypes
+1 -2
View File
@@ -14,8 +14,7 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station';
import { ActiveScenery } from '../../store/typings';
import { ActiveScenery, Station } from '../../typings/common';
export default defineComponent({
props: {
+2 -8
View File
@@ -72,7 +72,7 @@
<div class="info-lists">
<!-- user list -->
<SceneryInfoUserList :onlineScenery="onlineScenery" />
<SceneryInfoUserList :onlineScenery="onlineScenery" :station="station" />
<!-- spawn list -->
<SceneryInfoSpawnList :onlineScenery="onlineScenery" />
@@ -89,8 +89,7 @@ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station';
import { ActiveScenery } from '../../store/typings';
import { ActiveScenery, Station } from '../../typings/common';
export default defineComponent({
components: {
@@ -125,11 +124,6 @@ h3.section-header {
align-items: center;
font-size: 1.2em;
img {
width: 1.1em;
margin-left: 0.5em;
}
}
.info-lists {
@@ -14,7 +14,7 @@
>
<span
class="text--donator"
v-if="isDonator(onlineScenery.dispatcherName)"
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')"
>
{{ onlineScenery.dispatcherName }}
@@ -49,11 +49,18 @@ import dateMixin from '../../../mixins/dateMixin';
import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin';
import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { ActiveScenery } from '../../../store/typings';
import donatorMixin from '../../../mixins/donatorMixin';
import { ActiveScenery } from '../../../typings/common';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, donatorMixin],
mixins: [styleMixin, dateMixin, routerMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: {
onlineScenery: {
type: Object as PropType<ActiveScenery>,
@@ -24,8 +24,8 @@
:title="
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
"
v-html="getControlTypeAbbrev(station?.generalInfo.controlType)"
>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span>
<img
@@ -88,12 +88,11 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import stationInfoMixin from '../../../mixins/stationInfoMixin';
import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station';
import { Station } from '../../../typings/common';
export default defineComponent({
mixins: [stationInfoMixin, styleMixin],
mixins: [styleMixin],
props: {
station: {
type: Object as PropType<Station>
@@ -104,6 +103,7 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../../styles/icons.scss';
.info-icons {
display: flex;
justify-content: center;
@@ -111,6 +111,7 @@ export default defineComponent({
margin: 1em;
}
.icon-info {
display: flex;
justify-content: center;
@@ -52,7 +52,7 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station';
import { Station } from '../../../typings/common';
export default defineComponent({
props: {
@@ -8,7 +8,7 @@
<transition-group name="spawns-anim" tag="ul">
<li
class="badge spawn badge-none"
class="badge badge-none"
v-if="!onlineScenery || onlineScenery.spawns.length == 0"
key="no-spawns"
>
@@ -16,13 +16,13 @@
</li>
<li
class="badge spawn"
class="badge spawn-badge"
v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + onlineScenery?.dispatcherName + i"
:data-electrified="spawn.isElectrified"
>
<span class="spawn_name">{{ spawn.spawnName }}</span>
<span class="spawn_length">{{ spawn.spawnLength }}m</span>
<span class="name">{{ spawn.spawnName }}</span>
<span class="length">{{ spawn.spawnLength }}m</span>
</li>
</transition-group>
</section>
@@ -30,7 +30,7 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import { ActiveScenery } from '../../../store/typings';
import { ActiveScenery } from '../../../typings/common';
export default defineComponent({
props: {
@@ -59,19 +59,6 @@ ul {
position: relative;
}
.spawn {
color: white;
&_length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
}
}
.spawns-anim {
&-move,
&-enter-active,
@@ -1,83 +0,0 @@
<template>
<section class="info-stats" :class="!station.onlineInfo ? 'no-stats' : ''">
<span class="likes">
<img src="/images/icon-like" alt="Likes count icon" />
<span>{{ station.onlineInfo?.dispatcherRate || '0' }}</span>
</span>
<span class="users">
<img src="/images/icon-user" alt="Users count icon" />
<span>{{ station.onlineInfo?.currentUsers || '0' }}</span>
/
<span>{{ station.onlineInfo?.maxUsers || '0' }}</span>
</span>
<span class="spawns">
<img src="/images/icon-spawn" alt="Spawns count icon" />
<span>{{ station.onlineInfo?.spawns.length || '0' }}</span>
</span>
<span class="schedules">
<img src="/images/icon-timetable" alt="Timetables count icon" />
<span>
<span style="color: #eee">{{ station.onlineInfo?.scheduledTrains?.length || '0' }}</span>
/
<span style="color: #bbb"
>{{
station.onlineInfo?.scheduledTrains?.filter((train) => train.stopInfo.confirmed)
.length || '0'
}}
</span>
</span>
</span>
</section>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station';
export default defineComponent({
props: {
station: {
type: Object as PropType<Station>,
required: true
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
.info-stats {
padding: 1rem 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
font-size: 1.65em;
&.no-stats {
opacity: 0.5;
}
& > span {
display: flex;
align-items: center;
margin: 0.3em;
}
.likes,
.spawns {
color: $accentCol;
}
span > img {
width: 1.2em;
margin-right: 0.5em;
}
}
</style>
@@ -13,13 +13,13 @@
</li>
<li
v-for="train in onlineScenery?.stationTrains"
v-for="{ train, status } in stationTrains"
class="badge user"
:class="train.stopStatus"
:key="train.trainId"
tabindex="0"
@click.prevent="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
:key="train.id"
:data-status="status"
@click.prevent="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
>
<span class="user_train">{{ train.trainNo }}</span>
<span class="user_name">{{ train.driverName }}</span>
@@ -32,7 +32,9 @@
import { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery } from '../../../store/typings';
import { ActiveScenery, Station, StopStatus } from '../../../typings/common';
import { getTrainStopStatus } from '../utils';
import { useMainStore } from '../../../store/mainStore';
export default defineComponent({
mixins: [routerMixin, modalTrainMixin],
@@ -41,6 +43,40 @@ export default defineComponent({
onlineScenery: {
type: Object as PropType<ActiveScenery>,
required: false
},
station: {
type: Object as PropType<Station>
}
},
data() {
return {
mainStore: useMainStore()
};
},
computed: {
stationTrains() {
if (!this.onlineScenery) return;
const name = this.station?.generalInfo?.checkpoints[0] ?? this.onlineScenery.name;
return this.onlineScenery.stationTrains.map((train) => {
const stop = train.timetableData?.followingStops.find(
(stop) =>
stop.stopNameRAW.toLowerCase() == name.toLowerCase() ||
this.station?.generalInfo?.checkpoints.includes(stop.stopNameRAW)
);
const status = stop
? getTrainStopStatus(stop, train.currentStationName, this.onlineScenery!.name)
: 'no-timetable';
return {
train,
status
};
});
}
}
});
@@ -74,31 +110,31 @@ ul {
-webkit-transition: background-color 200ms;
}
&.no-timetable .user_train {
&[data-status='no-timetable'] .user_train {
background-color: $no-timetable;
}
&.departed > &_train {
&[data-status='departed'] > &_train {
background-color: $departed;
}
&.stopped > &_train {
&[data-status='stopped'] > &_train {
background-color: $stopped;
}
&.online > &_train {
&[data-status='online'] > &_train {
background-color: $online;
}
&.terminated > &_train {
&[data-status='terminated'] > &_train {
background-color: $terminated;
}
&.disconnected > &_train {
&[data-status='disconnected'] > &_train {
background-color: $disconnected;
}
&.offline {
&[data-status='offline'] {
background: firebrick;
pointer-events: none;
}
+130 -77
View File
@@ -14,14 +14,6 @@
</span>
<span class="header_links" v-if="station">
<!-- <a
:href="`https://pragotron-td2.web.app/board?name=${station.name}`"
target="_blank"
:title="$t('scenery.pragotron-link')"
>
<img src="/images/icon-pragotron.svg" alt="icon-pragotron" />
</a> -->
<a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" />
</a>
@@ -47,8 +39,8 @@
<div class="timetable-list">
<transition-group name="list-anim">
<div
v-if="apiStore.dataStatuses.connection == 0 && sceneryTimetables.length == 0"
style="padding-bottom: 5em"
v-if="apiStore.dataStatuses.connection == 0 && computedScheduledTrains.length == 0"
key="list-loading"
>
<Loading />
@@ -56,7 +48,7 @@
<span
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0 && !onlineScenery"
v-else-if="sceneryTimetables.length == 0 && !onlineScenery"
key="list-offline"
>
{{ $t('scenery.offline') }}
@@ -64,7 +56,7 @@
<div
class="timetable-item empty"
v-else-if="computedScheduledTrains.length == 0"
v-else-if="sceneryTimetables.length == 0"
key="list-no-timetables"
>
{{ $t('scenery.no-timetables') }}
@@ -73,59 +65,56 @@
<div
class="timetable-item"
v-else
v-for="scheduledTrain in computedScheduledTrains"
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
v-for="(row, i) in sceneryTimetables"
:key="row.train.id + i"
tabindex="0"
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
@click.prevent.stop="selectModalTrain(row.train, $event.currentTarget)"
@keydown.enter.prevent="selectModalTrain(row.train, $event.currentTarget)"
>
<span class="timetable-general">
<span class="general-info">
<span class="info-number">
<strong>{{ scheduledTrain.category }}</strong>
{{ scheduledTrain.trainNo }}
<strong>{{ row.train.timetableData!.category }}</strong>
{{ row.train.trainNo }}
<span
v-if="scheduledTrain.stopInfo.comments"
:title="scheduledTrain.stopInfo.comments"
>
<span v-if="row.checkpointStop.comments" :title="row.checkpointStop.comments">
<img src="/images/icon-warning.svg" />
</span>
</span>
&nbsp;|&nbsp;
<span>
{{ scheduledTrain.driverName }}
{{ row.train.driverName }}
</span>
<div class="info-route">
<strong>{{ scheduledTrain.beginsAt }} - {{ scheduledTrain.terminatesAt }}</strong>
<strong>{{ row.train.timetableData!.route.replace('|', ' - ') }}</strong>
</div>
<ScheduledTrainStatus :scheduledTrain="scheduledTrain" />
<ScheduledTrainStatus :sceneryTimetableRow="row" />
</span>
</span>
<span class="timetable-schedule">
<span class="schedule-arrival">
<span class="arrival-time begins" v-if="scheduledTrain.stopInfo.beginsHere">
<span class="arrival-time begins" v-if="row.checkpointStop.beginsHere">
{{ $t('timetables.begins') }}
</span>
<span class="arrival-time" v-else>
<div v-if="scheduledTrain.stopInfo.arrivalDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.arrivalTimestamp) }}</span>
<div v-if="row.checkpointStop.arrivalDelay == 0">
<span>{{ timestampToString(row.checkpointStop.arrivalTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.arrivalTimestamp)
timestampToString(row.checkpointStop.arrivalTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.arrivalRealTimestamp) }}
({{ scheduledTrain.stopInfo.arrivalDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.arrivalDelay }})
{{ timestampToString(row.checkpointStop.arrivalRealTimestamp) }}
({{ row.checkpointStop.arrivalDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.arrivalDelay }})
</span>
</div>
</span>
@@ -133,41 +122,39 @@
<span class="schedule-stop">
<span class="stop-connection">
{{ scheduledTrain.arrivingLine }}
{{ row.arrivingLine }}
</span>
<span class="stop-time">
{{ scheduledTrain.stopInfo.stopTime || '' }}
{{
scheduledTrain.stopInfo.stopTime ? scheduledTrain.stopInfo.stopType || 'pt' : ''
}}
{{ row.checkpointStop.stopTime || '' }}
{{ row.checkpointStop.stopTime ? row.checkpointStop.stopType || 'pt' : '' }}
</span>
<span class="stop-connection">
{{ scheduledTrain.departureLine }}
{{ row.departureLine }}
</span>
</span>
<span class="schedule-departure">
<span class="departure-time terminates" v-if="scheduledTrain.stopInfo.terminatesHere">
<span class="departure-time terminates" v-if="row.checkpointStop.terminatesHere">
{{ $t('timetables.terminates') }}
</span>
<span class="departure-time" v-else>
<div v-if="scheduledTrain.stopInfo.departureDelay == 0">
<span>{{ timestampToString(scheduledTrain.stopInfo.departureTimestamp) }}</span>
<div v-if="row.checkpointStop.departureDelay == 0">
<span>{{ timestampToString(row.checkpointStop.departureTimestamp) }}</span>
</div>
<div v-else>
<div>
<s style="margin-right: 0.2em" class="text--grayed">{{
timestampToString(scheduledTrain.stopInfo.departureTimestamp)
timestampToString(row.checkpointStop.departureTimestamp)
}}</s>
</div>
<span>
{{ timestampToString(scheduledTrain.stopInfo.departureRealTimestamp) }}
({{ scheduledTrain.stopInfo.departureDelay > 0 ? '+' : ''
}}{{ scheduledTrain.stopInfo.departureDelay }})
{{ timestampToString(row.checkpointStop.departureRealTimestamp) }}
({{ row.checkpointStop.departureDelay > 0 ? '+' : ''
}}{{ row.checkpointStop.departureDelay }})
</span>
</div>
</span>
@@ -186,12 +173,13 @@ import { useRoute } from 'vue-router';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station';
import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { ActiveScenery } from '../../store/typings';
import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
import { getTrainStopStatus, stopStatusPriority } from './utils';
export default defineComponent({
name: 'SceneryTimetable',
@@ -213,10 +201,6 @@ export default defineComponent({
listOpen: false
}),
mounted() {
this.loadSelectedOption();
},
activated() {
this.loadSelectedOption();
},
@@ -229,9 +213,7 @@ export default defineComponent({
const mainStore = useMainStore();
const chosenCheckpoint = ref(
props.station?.generalInfo?.checkpoints?.length == 0
? ''
: props.station?.generalInfo?.checkpoints[0] ?? null
props.station?.generalInfo?.checkpoints[0] ?? props.station?.name ?? ''
);
return {
@@ -250,27 +232,105 @@ export default defineComponent({
return url;
},
computedScheduledTrains() {
sceneryTimetables(): SceneryTimetableRow[] {
if (!this.station) return [];
if (!this.onlineScenery) return [];
return (
this.onlineScenery?.scheduledTrains
?.filter(
(train) =>
train.checkpointName.toLocaleLowerCase() ==
(this.chosenCheckpoint || this.station!.name).toLocaleLowerCase() &&
train.region == this.mainStore.region.id
)
.sort((a, b) => {
if (a.stopStatusID > b.stopStatusID) return 1;
if (a.stopStatusID < b.stopStatusID) return -1;
return this.onlineScenery.scheduledTrains
.filter(
(ct) =>
ct.train.region == this.mainStore.region.id &&
this.chosenCheckpoint &&
ct.checkpointStop.stopNameRAW.toLowerCase() == this.chosenCheckpoint.toLowerCase()
)
.map((ct) => {
const trainStopStatus = getTrainStopStatus(
ct.checkpointStop,
ct.train.currentStationName,
this.station!.name
);
if (a.stopInfo.arrivalTimestamp > b.stopInfo.arrivalTimestamp) return 1;
if (a.stopInfo.arrivalTimestamp < b.stopInfo.arrivalTimestamp) return -1;
const trainStopIndex =
ct.train.timetableData?.followingStops.findIndex(
(stop) => stop.stopName == ct.checkpointStop.stopName
) ?? -1;
return a.stopInfo.departureTimestamp > b.stopInfo.departureTimestamp ? 1 : -1;
}) || []
);
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
if (trainStopIndex > -1 && ct.train.timetableData?.followingStops !== undefined) {
for (let i = trainStopIndex; i >= 0; i--) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!prevStationName &&
i <= trainStopIndex - 1
)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.arrivalLine != null &&
!arrivingLine &&
!/-|_|it|sbl/gi.test(stop.arrivalLine)
) {
arrivingLine = stop.arrivalLine;
prevDepartureLine =
ct.train.timetableData.followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < ct.train.timetableData.followingStops.length; i++) {
const stop = ct.train.timetableData.followingStops[i];
if (
/strong|podg\.|pe\./g.test(stop.stopName) &&
!nextStationName &&
i > trainStopIndex
)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (
stop.departureLine &&
!departureLine &&
!/-|_|it|sbl/gi.test(stop.departureLine)
) {
departureLine = stop.departureLine;
nextArrivalLine = ct.train.timetableData.followingStops[i + 1]?.arrivalLine || null;
}
}
}
return {
checkpointStop: ct.checkpointStop,
train: ct.train,
prevDepartureLine,
nextArrivalLine,
departureLine,
arrivingLine,
prevStationName,
nextStationName,
status: trainStopStatus
};
})
.sort((a, b) => {
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) < 0)
return -1;
if (stopStatusPriority.indexOf(a.status) - stopStatusPriority.indexOf(b.status) > 0)
return 1;
if (a.checkpointStop.arrivalTimestamp > b.checkpointStop.arrivalTimestamp) return 1;
if (a.checkpointStop.arrivalTimestamp < b.checkpointStop.arrivalTimestamp) return -1;
return a.checkpointStop.departureTimestamp > b.checkpointStop.departureTimestamp ? 1 : -1;
});
}
},
@@ -414,13 +474,6 @@ export default defineComponent({
width: 100%;
}
.g-tooltip > .content {
z-index: 100;
color: white;
left: 110%;
}
img {
width: 1.1em;
}
@@ -1,69 +1,97 @@
<template>
<!-- WIP -->
<!-- <div class="top-filters">
<button class="btn btn--option">ROZPOCZYNA BIEG</button>
<button class="btn btn--option">PRZEZ</button>
<button class="btn btn--option">KOŃCZY BIEG</button>
</div> -->
<section class="scenery-table-section">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<div class="no-history" v-else-if="historyList.length == 0">
{{ $t('scenery.history-list-empty') }}
<div class="scenery-timetables-history">
<div class="history-modes">
<button
class="btn btn--option"
v-for="mode in historyModeList"
:key="mode"
:class="{ checked: checkedHistoryMode == mode }"
@click="checkHistoryMode(mode)"
>
{{ $t(`scenery.timetable-${mode}`) }}
</button>
</div>
<table class="scenery-history-table" v-else>
<thead>
<th>{{ $t('scenery.timetables-history-id') }}</th>
<th>{{ $t('scenery.timetables-history-number') }}</th>
<th>{{ $t('scenery.timetables-history-route') }}</th>
<th>{{ $t('scenery.timetables-history-driver') }}</th>
<th>{{ $t('scenery.timetables-history-author') }}</th>
<th>{{ $t('scenery.timetables-history-date') }}</th>
</thead>
<div class="history-wrapper">
<Loading v-if="dataStatus != DataStatus.Loaded" />
<tbody>
<tr v-for="historyItem in historyList" :key="historyItem.id">
<td>
<router-link :to="`/journal/timetables?search-train=%23${historyItem.id}`">
#{{ historyItem.id }}
</router-link>
</td>
<td>
<b class="text--primary">{{ historyItem.trainCategoryCode }}</b> <br />
{{ historyItem.trainNo }}
</td>
<td>{{ historyItem.route.replace('|', ' -> ') }}</td>
<td>
<router-link :to="`/journal/timetables?search-driver=${historyItem.driverName}`">
{{ historyItem.driverName }}
</router-link>
</td>
<div v-else-if="historyList.length == 0" class="no-history">
{{ $t('scenery.history-list-empty') }}
</div>
<td>
<router-link
v-if="historyItem.authorName"
:to="`/journal/timetables?search-dispatcher=${historyItem.authorName}`"
>{{ historyItem.authorName }}
</router-link>
<i v-else>{{ $t('scenery.timetable-author-unknown') }}</i>
</td>
<td>
<b>{{ localeDay(historyItem.beginDate, $i18n.locale) }}</b>
{{ localeTime(historyItem.beginDate, $i18n.locale) }}
</td>
</tr>
</tbody>
</table>
</section>
<div v-else class="history-list">
<div v-for="timetableHistory in historyList" :key="timetableHistory.id">
<span>
<div>
<span
class="timetable-status-indicator"
:data-terminated="timetableHistory.terminated"
:data-fulfilled="timetableHistory.fulfilled"
>
&ofcir;
</span>
#{{ timetableHistory.id }} |
<b class="text--primary">{{ timetableHistory.trainCategoryCode }}</b>
{{ timetableHistory.trainNo }}
{{ timetableHistory.route.replace('|', ' &Rightarrow; ') }}
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
<div class="text--grayed">
<span>
{{ $t('scenery.timetable-issued-date') }}
<b>
{{
localeDateTime(
timetableHistory.createdAt > timetableHistory.beginDate
? timetableHistory.beginDate
: timetableHistory.createdAt,
$i18n.locale
)
}}
</b></span
>
<span v-if="timetableHistory.authorName">
{{ $t('scenery.timetable-issued-by') }}
<b>
<router-link
:to="`/journal/timetables?search-dispatcher=${timetableHistory.authorName}`"
>
{{ timetableHistory.authorName }}
</router-link>
</b>
</span>
<span>
{{ $t('scenery.timetable-issued-for') }}
<b>
<router-link
:to="`/journal/timetables?search-driver=${timetableHistory.driverName}`"
>
{{ timetableHistory.driverName }}
</router-link>
</b>
</span>
</div>
</span>
<button
@click="
navigateTo(`/journal/timetables`, {
'search-train': `#${timetableHistory.id}`
})
"
>
<img src="/images/icon-back.svg" alt="icon navigate to timetable" />
</button>
</div>
</div>
</div>
<div class="bottom-info">
<button class="btn btn--option" v-if="historyList.length > 0" @click="navigateToHistory()">
{{ $t('scenery.bottom-info') }}
</button>
</div>
</div>
</template>
@@ -71,17 +99,19 @@
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
import routerMixin from '../../mixins/routerMixin';
import { useMainStore } from '../../store/mainStore';
const historyModeList = ['via', 'issuedFrom', 'terminatingAt'] as const;
type HistoryMode = (typeof historyModeList)[number];
export default defineComponent({
name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin],
mixins: [dateMixin, routerMixin],
props: {
station: {
type: Object as PropType<Station>
@@ -94,9 +124,14 @@ export default defineComponent({
data() {
return {
historyList: [] as API.TimetableHistory.Response,
historyModeList,
apiStore: useApiStore(),
mainStore: useMainStore(),
dataStatus: Status.Data.Loading,
DataStatus: Status.Data
DataStatus: Status.Data,
checkedHistoryMode: 'via' as HistoryMode
};
},
@@ -106,17 +141,22 @@ export default defineComponent({
methods: {
async fetchAPIData() {
if (!this.station && !this.onlineScenery) {
const stationName = this.$route.query['station'];
if (!stationName) {
this.historyList = [];
this.dataStatus = Status.Data.Loaded;
return;
}
const requestFilters: Record<string, any> = {};
requestFilters[this.checkedHistoryMode] = stationName.toString();
requestFilters.countLimit = 30;
try {
const response: API.TimetableHistory.Response = await (
await this.apiStore.client!.get('api/getTimetables', {
params: {
issuedFrom: this.station?.name || this.onlineScenery?.name
}
params: requestFilters
})
).data;
@@ -128,11 +168,17 @@ export default defineComponent({
}
},
checkHistoryMode(mode: HistoryMode) {
this.checkedHistoryMode = mode;
this.dataStatus = Status.Data.Loading;
this.fetchAPIData();
},
navigateToHistory() {
this.$router.push({
path: '/journal/timetables',
query: {
'search-issuedFrom': this.station?.name || this.onlineScenery?.name
[`search-${this.checkedHistoryMode}`]: this.station?.name || this.onlineScenery?.name
}
});
}
@@ -145,13 +191,66 @@ export default defineComponent({
@import '../../styles/responsive.scss';
@import '../../styles/sceneryViewTables.scss';
.top-filters {
.scenery-timetables-history {
height: 100%;
overflow: auto;
display: grid;
gap: 1em;
grid-template-rows: auto 1fr 40px;
}
.history-wrapper {
position: relative;
overflow: auto;
}
.history-modes {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 0.5em;
padding: 0.25em;
button {
padding: 0.5em;
padding: 0.35em;
min-width: 120px;
}
}
.history-list {
display: flex;
flex-direction: column;
gap: 0.5em;
text-align: left;
}
.history-list > div {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5em;
background-color: #2b2b2b;
line-height: 1.5em;
}
.history-list > div > button > img {
width: 2em;
transform: rotate(180deg);
}
.timetable-status-indicator {
&[data-fulfilled='true'] {
color: lightgreen;
}
&[data-terminated='false'] {
color: lightblue;
}
&[data-terminated='true'][data-fulfilled='false'] {
color: lightcoral;
}
}
</style>
@@ -1,7 +1,7 @@
<template>
<div class="general-status">
<span
:class="computedScheduledTrain.stopStatus"
:class="computedScheduledTrain.status"
:title="computedScheduledTrain.stopStatusDescription"
>
{{ computedScheduledTrain.stopStatusIndicator }}
@@ -11,25 +11,21 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../store/typings';
interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string;
stopStatusDescription: string;
}
import { StopStatus } from '../../typings/common';
import { SceneryTimetableRow } from './typings';
export default defineComponent({
props: {
scheduledTrain: {
type: Object as PropType<ScheduledTrain>,
sceneryTimetableRow: {
type: Object as PropType<SceneryTimetableRow>,
required: true
}
},
computed: {
computedScheduledTrain(): ScheduledTrainComp {
const { prevDepartureLine, prevStationName, stopStatus, nextArrivalLine, nextStationName } =
this.scheduledTrain;
computedScheduledTrain() {
const { prevDepartureLine, prevStationName, nextArrivalLine, nextStationName, status } =
this.sceneryTimetableRow;
const prevDepartureIndicator = prevDepartureLine
? `(${prevDepartureLine}) ${prevStationName}`
@@ -41,7 +37,7 @@ export default defineComponent({
let stopStatusDescription = '',
stopStatusIndicator = '';
switch (stopStatus) {
switch (status) {
case StopStatus.ARRIVING:
stopStatusIndicator = `${this.$t('timetables.from')}: ${prevDepartureIndicator}`;
stopStatusDescription = this.$t('timetables.desc-arriving', {
@@ -56,7 +52,7 @@ export default defineComponent({
? `${this.$t('timetables.to')}: ${nextArrivalIndicator}`
: `${this.$t('timetables.desc-end')}`;
stopStatusDescription = nextArrivalLine
? this.$t(`timetables.desc-${stopStatus}`, { nextStationName, nextArrivalLine })
? this.$t(`timetables.desc-${status}`, { nextStationName, nextArrivalLine })
: '';
break;
@@ -85,7 +81,7 @@ export default defineComponent({
break;
}
return {
...this.scheduledTrain,
...this.sceneryTimetableRow,
stopStatusDescription,
stopStatusIndicator
};
+13
View File
@@ -0,0 +1,13 @@
import { StopStatus, Train, TrainStop } from '../../typings/common';
export interface SceneryTimetableRow {
checkpointStop: TrainStop;
train: Train;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
departureLine: string | null;
arrivingLine: string | null;
prevStationName: string | null;
nextStationName: string | null;
status: StopStatus;
}
+42
View File
@@ -0,0 +1,42 @@
import { StopStatus, TrainStop } from '../../typings/common';
export const stopStatusPriority = [
StopStatus.ONLINE,
StopStatus.STOPPED,
StopStatus.DEPARTED,
StopStatus.ARRIVING,
StopStatus.DEPARTED_AWAY,
StopStatus.TERMINATED
];
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
if (stopInfo.terminatesHere && stopInfo.confirmed) {
return StopStatus.TERMINATED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
return StopStatus.DEPARTED;
}
if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
return StopStatus.DEPARTED_AWAY;
}
if (currentStationName == sceneryName && !stopInfo.stopped) {
return StopStatus.ONLINE;
}
if (currentStationName == sceneryName && stopInfo.stopped) {
return StopStatus.STOPPED;
}
if (currentStationName != sceneryName) {
return StopStatus.ARRIVING;
}
return StopStatus.ONLINE;
}
+9 -16
View File
@@ -15,7 +15,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
interface FilterOption {
id: string;
@@ -40,15 +39,9 @@ export default defineComponent({
emits: ['update:optionValue'],
setup() {
return {
filterStore: useStationFiltersStore()
};
},
watch: {
'option.value'() {
this.filterStore.changeFilterValue(this.option.name, !this.option.value);
// this.filterStore.changeFilterValue(this.option.name, !this.option.value);
}
},
@@ -56,17 +49,17 @@ export default defineComponent({
handleDbClick(e: Event) {
e.preventDefault();
this.filterStore.lastClickedFilterId = this.option.id;
// this.filterStore.lastClickedFilterId = this.option.id;
// this.option.value = true;
this.$emit('update:optionValue', true);
this.filterStore.inputs.options
.filter((option) => {
return option.section == this.option.section && option.id != this.option.id;
})
.forEach((option) => {
option.value = !this.option.value;
});
// this.filterStore.inputs.options
// .filter((option) => {
// return option.section == this.option.section && option.id != this.option.id;
// })
// .forEach((option) => {
// option.value = !this.option.value;
// });
}
}
});
+207 -130
View File
@@ -1,10 +1,10 @@
<template>
<section class="filter-card" v-click-outside="closeCard" @keydown.esc="closeCard">
<div class="card_controls">
<button class="btn--filled btn--image" @click="toggleCard">
<button class="card-button btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
{{ $t('options.filters') }} [F]
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
<p>[F] {{ $t('options.filters') }}</p>
<span class="active-indicator" v-if="changedFilters.length != 0"></span>
</button>
<label for="scenery-search">
@@ -28,34 +28,50 @@
</div>
<transition name="card-anim">
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl">
<div class="card_content">
<div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content" @scroll="onScroll" ref="cardContentRef">
<div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p>
<div class="changed-filters" :data-active="changedFilters.length > 0">
<template v-if="changedFilters.length > 0">
{{ $t('filters.changed-filters-count') }} <b>{{ changedFilters.length }}</b>
</template>
<template v-else>{{ $t('filters.no-changed-filters') }}</template>
</div>
<section class="card_options">
<div
class="option-section"
v-for="section in filterStore.inputs.optionSections"
:key="section"
v-for="(sectionFilters, sectionKey) in filtersSections"
:key="sectionKey"
>
<h3 class="text--primary">
{{ $t(`filters.sections.${section}`) }}
<button @click="filterStore.resetSectionOptions(section)">RESET</button>
<span class="active-indicator" v-if="!areSectionFiltersDefault(sectionKey)"></span>
{{ $t(`filters.sections.${sectionKey}`) }}
<button @click="resetSectionFilters(sectionKey)">RESET</button>
</h3>
<hr />
<div class="section-inputs">
<FilterOption
v-for="(option, i) in filterStore.inputs.options.filter(
(o) => o.section == section
)"
v-model:optionValue="option.value"
:option="option"
:key="i"
/>
<div class="section-filters">
<label
v-for="filterKey in sectionFilters"
@click="() => (filters[filterKey] = !filters[filterKey])"
@dblclick="setSingleSectionFilter(sectionKey, filterKey)"
:for="filterKey"
>
<input
:checked="filters[filterKey]"
v-model="filters[filterKey]"
type="checkbox"
:class="sectionKey"
:name="filterKey"
/>
<span>
{{ $t(`filters.${filterKey}`) }}
</span>
</label>
</div>
</div>
</section>
@@ -68,29 +84,29 @@
<span>{{
minimumHours == 0
? $t('filters.now')
: minimumHours < 8
? minimumHours + $t('filters.hour')
: $t('filters.no-limit')
: minimumHours < 7
? minimumHours + $t('filters.hour')
: $t('filters.no-limit')
}}</span>
<button class="btn--action" @click="addHour">+</button>
</span>
</section>
<datalist id="authors">
<option v-for="(author, i) in authors" :key="i" :value="author"></option>
</datalist>
<section class="card_authors-search">
<h3 class="section-header">{{ $t('filters.authors-search') }}</h3>
<datalist id="authors" name="authors">
<option v-for="(author, i) in authorsHint" :key="i" :value="author"></option>
</datalist>
<form action="javascript:void(0);" @submit="handleAuthorsInput">
<input
type="text"
id="author"
list="authors"
name="authors"
v-model="authors"
:placeholder="$t('filters.authors-placeholder')"
v-model="authorsInputValue"
@focus="preventKeyDown = true"
@blur="preventKeyDown = false"
/>
@@ -100,18 +116,18 @@
</section>
<section class="card_sliders">
<div class="slider" v-for="(slider, i) in filterStore.inputs.sliders" :key="i">
<div class="slider" v-for="(slider, i) in initSliders" :key="i">
<input
class="slider-input"
type="range"
:name="slider.name"
:name="slider.id"
:id="slider.id"
:min="slider.minRange"
:max="slider.maxRange"
v-model="slider.value"
@change="handleInput"
:step="slider.step"
v-model="filters[slider.id]"
/>
<span class="slider-value">{{ slider.value }}</span>
<span class="slider-value">{{ filters[slider.id] }}</span>
<div class="slider-content">
{{ $t(`filters.sliders.${slider.id}`) }}
</div>
@@ -132,11 +148,11 @@
<button
class="btn--action"
:disabled="changedFilters.length == 0"
:data-disabled="changedFilters.length == 0"
@click="resetFilters"
:disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault"
>
{{ $t('filters.reset') }}
[R] {{ $t('filters.reset') }}
</button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div>
@@ -150,48 +166,76 @@
import { defineComponent, inject } from 'vue';
import keyMixin from '../../mixins/keyMixin';
import routerMixin from '../../mixins/routerMixin';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import FilterOption from './FilterOption.vue';
import StorageManager from '../../managers/storageManager';
import {
filtersSections,
initSliders,
initFilters,
getChangedFilters
} from '../../managers/stationFilterManager';
import { StationFilterSection } from '../../managers/stationFilterManager';
import { computed } from 'vue';
import { watch } from 'vue';
const STORAGE_KEY = 'options_saved';
export default defineComponent({
components: { FilterOption },
mixins: [keyMixin, routerMixin],
data: () => ({
saveOptions: false,
STORAGE_KEY: 'options_saved',
authorsInputValue: '',
filtersSections,
initSliders,
minimumHours: 0,
authors: '',
currentRegion: { id: '', value: '' },
delayInputTimer: -1,
chosenSearchScenery: ''
chosenSearchScenery: '',
scrollTop: 0,
lastFocusedEl: null as HTMLElement | null
}),
setup() {
const isVisible = inject('isFilterCardVisible');
const store = useMainStore();
const filterStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const changedFilters = computed(() => getChangedFilters(filters));
// Save filters to persistent storage
watch(filters, (value) => {
if (!StorageManager.isRegistered(STORAGE_KEY)) return;
Object.keys(value).forEach((filterKey) => {
StorageManager.setValue(filterKey, filters[filterKey]);
});
});
return {
isVisible,
store,
filterStore
filters,
changedFilters
};
},
mounted() {
this.saveOptions = StorageManager.isRegistered(this.STORAGE_KEY);
this.saveOptions = StorageManager.isRegistered(STORAGE_KEY);
if (StorageManager.isRegistered('onlineFromHours') && this.saveOptions) {
this.minimumHours = StorageManager.getNumericValue('onlineFromHours');
this.changeNumericFilterValue('onlineFromHours', this.minimumHours);
}
this.currentRegion = this.store.region;
@@ -210,7 +254,7 @@ export default defineComponent({
return true;
},
authors() {
authorsHint() {
return this.store.stationList
.reduce((acc, station) => {
station.generalInfo?.authors?.forEach((author) => {
@@ -236,7 +280,10 @@ export default defineComponent({
isVisible(value: boolean) {
this.$nextTick(() => {
if (value) (this.$refs['cardEl'] as HTMLDivElement).focus();
if (value) {
(this.$refs['cardRef'] as HTMLDivElement).focus();
(this.$refs['cardContentRef'] as HTMLDivElement).scrollTop = this.scrollTop;
}
});
}
},
@@ -247,61 +294,67 @@ export default defineComponent({
this.isVisible = !this.isVisible;
},
handleInput(e: Event) {
const target = e.target as HTMLInputElement;
this.filterStore.changeFilterValue(target.name, target.value);
if (this.saveOptions) StorageManager.setStringValue(target.name, target.value);
onScroll(e: Event) {
this.scrollTop = (e.target as HTMLElement).scrollTop;
},
handleAuthorsInput() {
this.filterStore.changeFilterValue('authors', this.authorsInputValue);
if (this.saveOptions) StorageManager.setStringValue('authors', this.authorsInputValue);
},
changeNumericFilterValue(name: string, value: number, saveToStorage = false) {
this.filterStore.changeFilterValue(name, value);
if (this.saveOptions && saveToStorage) StorageManager.setNumericValue(name, value);
this.filters['authors'] = this.authors;
// if (this.saveOptions) StorageManager.setStringValue('authors', target.value);
},
subHour() {
this.minimumHours = this.minimumHours < 1 ? 8 : this.minimumHours - 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours < 1 ? 7 : this.minimumHours - 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
addHour() {
this.minimumHours = this.minimumHours > 7 ? 0 : this.minimumHours + 1;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.minimumHours = this.minimumHours > 6 ? 0 : this.minimumHours + 1;
this.filters['onlineFromHours'] = this.minimumHours;
},
saveFilters() {
this.saveOptions = !this.saveOptions;
if (!this.saveOptions) {
StorageManager.unregisterStorage(this.STORAGE_KEY);
StorageManager.unregisterStorage(STORAGE_KEY);
return;
}
StorageManager.registerStorage(this.STORAGE_KEY);
StorageManager.registerStorage(STORAGE_KEY);
this.filterStore.inputs.options.forEach((option) =>
StorageManager.setBooleanValue(option.name, !option.value)
);
this.filterStore.inputs.sliders.forEach((slider) =>
StorageManager.setNumericValue(slider.name, slider.value)
);
Object.keys(this.filters).forEach((filterKey) => {
StorageManager.setValue(filterKey, this.filters[filterKey]);
});
},
resetFilters() {
this.authorsInputValue = '';
// Reset local model values
this.minimumHours = 0;
this.changeNumericFilterValue('onlineFromHours', this.minimumHours, true);
this.filterStore.resetFilters();
this.authors = '';
// Reset global filters
Object.keys(this.filters).forEach((filterKey) => {
this.filters[filterKey] = (initFilters as any)[filterKey];
});
},
areSectionFiltersDefault(sectionKey: StationFilterSection) {
return filtersSections[sectionKey].every((filterKey) => {
return this.filters[filterKey] == initFilters[filterKey];
});
},
resetSectionFilters(sectionKey: StationFilterSection) {
filtersSections[sectionKey].forEach((filterKey) => {
this.filters[filterKey] = initFilters[filterKey];
});
},
setSingleSectionFilter(sectionKey: StationFilterSection, chosenKey: string) {
filtersSections[sectionKey].forEach((filterKey) => {
if (filterKey != chosenKey) this.filters[filterKey] = initFilters[filterKey];
});
},
closeCard() {
@@ -316,9 +369,10 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/card.scss';
@import '../../styles/animations.scss';
@import '../../styles/responsive';
@import '../../styles/card';
@import '../../styles/animations';
@import '../../styles/variables';
h3.section-header {
text-align: center;
@@ -335,6 +389,15 @@ h3.section-header {
padding: 0.5em;
}
.changed-filters {
background-color: #111;
padding: 0.5em;
&[data-active='true'] {
color: lightgreen;
}
}
.card_controls {
display: flex;
gap: 0.5em;
@@ -363,28 +426,6 @@ h3.section-header {
text-align: center;
}
.card_regions {
display: flex;
justify-content: center;
label > input {
display: none;
}
label > span {
padding: 0.25em 0.5em;
margin: 0 0.25em;
cursor: pointer;
background-color: gray;
&.checked {
background-color: seagreen;
}
}
}
.card_timestamp {
display: flex;
flex-direction: column;
@@ -430,24 +471,63 @@ h3.section-header {
}
}
.card_actions {
width: 100%;
padding: 0.5em;
.section-filters {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.filter-option {
max-width: 50%;
margin: 0 auto;
.section-filters > label {
position: relative;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
span {
cursor: pointer;
display: inline-block;
width: 100%;
text-align: center;
padding: 0.25em;
font-weight: bold;
background-color: forestgreen;
}
span:hover {
background-color: #22aa22;
}
input[type='checkbox'] {
cursor: pointer;
position: absolute;
opacity: 0;
&:checked + span {
background-color: #444;
&:hover {
background-color: #555;
}
}
&:focus-visible + span {
outline: 1px solid $accentCol;
}
}
}
.card_actions {
padding: 0.5em;
.action-buttons {
display: flex;
gap: 0.5em;
width: 100%;
margin-top: 0.5em;
button {
width: 50%;
width: 100%;
margin: 0 auto;
padding: 0.5em;
@@ -471,35 +551,18 @@ h3.section-header {
}
}
.section-inputs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5em;
margin: 1em 0;
}
.quick-actions div {
display: flex;
margin: 1em 0;
gap: 1em;
}
.slider {
display: flex;
align-items: center;
gap: 0.25em;
margin-bottom: 1em;
&-value {
color: $accentCol;
margin-right: 0.5em;
padding: 0.1em 0.2em;
}
&-content {
flex-grow: 2;
}
&-input {
-webkit-appearance: none;
appearance: none;
@@ -508,7 +571,6 @@ h3.section-header {
outline: none;
min-width: 25%;
max-width: 120px;
&:focus-visible ~ * {
color: gold;
@@ -578,4 +640,19 @@ h3.section-header {
}
}
}
@include smallScreen {
.card_controls > button.card-button > p {
display: none;
}
.slider {
flex-wrap: wrap;
justify-content: center;
&-input {
width: 90%;
}
}
}
</style>
@@ -0,0 +1,212 @@
<template>
<div class="station-stats">
<div class="separator" />
<div class="stats-row">
<div>
<span
>{{ $t('station-stats.u-factor') }}
<a
href="https://td2.info.pl/dyskusje/wspolczynnik-ugla-czy-to-ma-sens/msg81011/#msg81011"
target="_blank"
:data-tooltip="$t('station-stats.u-factor-tooltip')"
>(?)</a
>:
</span>
<b class="u-factor" :style="calculateFactorStyle()">
{{ uFactor.toFixed(2) }}
</b>
</div>
<div>
&bull;
{{ $t('station-stats.avg-timetable-count') }}
<b>{{ avgTimetableCount.toFixed(2) }}</b>
</div>
<div>
&bull;
{{ $t('station-stats.single-track-count') }}
<b>{{ trackCount.oneWay }}</b> (<b>{{ trackCount.oneWayElectric }} </b>)
</div>
<div>
&bull;
{{ $t('station-stats.double-track-count') }}
<b>{{ trackCount.twoWay }}</b>
(<b>{{ trackCount.twoWayElectric }} </b>)
</div>
<div>
&bull; {{ $t('station-stats.cross-sceneries') }} <b>{{ trackCount.crossTrack }}</b> (<b
>{{ trackCount.crossTrackElectric }} </b
>)
</div>
<div>
&bull;
{{ $t('station-stats.open-spawns') }} <b>{{ spawnCount.passenger }}</b> - PAS /
<b>{{ spawnCount.freight }}</b> - TOW / <b>{{ spawnCount.loco }}</b> - LUZ /
<b>{{ spawnCount.all }}</b> - ALL
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
export default defineComponent({
data() {
return {
mainStore: useMainStore()
};
},
methods: {
calculateFactorStyle() {
if (this.uFactor == 0) return '';
const norm = this.uFactor == 0 ? 1 : Math.max(Math.min(this.uFactor / 2, 1), 0);
const lerp = 120 * norm;
return `color: hsl(${lerp}, 100%, 60%)`;
}
},
computed: {
uFactor() {
const activeDispatchers = this.mainStore.activeSceneryList.filter(
(scenery) => scenery.region == this.mainStore.region.id && scenery.dispatcherId != -1
);
const activeTrains = this.mainStore.trainList.filter(
(train) => train.region == this.mainStore.region.id
);
return activeDispatchers.length != 0 ? activeTrains.length / activeDispatchers.length : 0;
},
avgTimetableCount() {
const regionSceneries = this.mainStore.activeSceneryList.filter((sc) => {
return sc.region == this.mainStore.region.id;
});
const timetableCountSum = regionSceneries.reduce((acc, sc) => {
acc += sc.scheduledTrainCount.all;
return acc;
}, 0);
if (regionSceneries.length == 0) return 0;
return timetableCountSum / regionSceneries.length;
},
trackCount() {
return this.mainStore.allStationInfo
.filter(
(st) =>
st.onlineInfo?.dispatcherId != -1 &&
st.onlineInfo?.region == this.mainStore.region.id &&
st.generalInfo?.routes
)
.reduce(
(acc, st) => {
const { routes } = st.generalInfo!;
if (
routes.single.filter((r) => !r.isInternal).length > 0 &&
routes.double.filter((r) => !r.isInternal).length > 0
) {
acc.crossTrack++;
if (
routes.single.some((r) => r.isElectric) &&
routes.double.some((r) => r.isElectric)
)
acc.crossTrackElectric++;
}
[...routes.single, ...routes.double].forEach((r) => {
if (r.isInternal) return;
acc[r.routeTracks == 2 ? 'twoWay' : 'oneWay'] += 1;
if (r.isElectric) acc[r.routeTracks == 2 ? 'twoWayElectric' : 'oneWayElectric'] += 1;
});
return acc;
},
{
oneWay: 0,
oneWayElectric: 0,
twoWay: 0,
twoWayElectric: 0,
crossTrack: 0,
crossTrackElectric: 0
}
);
},
spawnCount() {
return this.mainStore.activeSceneryList.reduce(
(acc, scenery) => {
if (scenery.region != this.mainStore.region.id) return acc;
scenery.spawns.forEach((spawn) => {
if (/EZT|POS|OSOB/i.test(spawn.spawnName)) acc['passenger'] += 1;
if (/TOW/i.test(spawn.spawnName)) acc['freight'] += 1;
if (/LUZ|SM/i.test(spawn.spawnName)) acc['loco'] += 1;
if (/ALL/i.test(spawn.spawnName)) acc['all'] += 1;
});
return acc;
},
{ passenger: 0, freight: 0, loco: 0, all: 0 }
);
}
}
});
</script>
<style lang="scss" scoped>
.separator {
width: 100%;
height: 2px;
margin: 0.5em 0;
background-color: #aaa;
}
.station-stats {
text-align: center;
color: #ddd;
}
.stats-row {
display: flex;
justify-content: center;
flex-wrap: wrap;
text-wrap: pretty;
gap: 0.25em;
margin-top: 0.25em;
}
.u-factor {
[data-factor-low='true'] {
color: #ddd;
}
[data-factor-mediocre='true'] {
color: lightgreen;
}
[data-factor-high='true'] {
color: greenyellow;
}
[data-factor-highest='true'] {
color: rgb(22, 245, 22);
}
}
</style>
+339 -335
View File
@@ -1,363 +1,376 @@
<template>
<section class="station_table">
<transition name="status-anim" mode="out-in">
<div class="table_wrapper" :key="apiStore.dataStatuses.connection">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/>
<table v-else-if="filteredStationList.length > 0">
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="station in filteredStationList"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</td>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationCard"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<tbody>
<tr
v-for="station in stations"
:class="{ 'last-selected': lastSelectedStationName == station.name }"
:key="station.name"
@click.left="setScenery(station.name)"
@click.right="openForumSite($event, station.generalInfo?.url)"
@keydown.enter="setScenery(station.name)"
@keydown.space="openForumSite($event, station.generalInfo?.url)"
tabindex="0"
<td class="station-dispatcher-exp">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
{{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<td class="station-level">
<span v-if="station.generalInfo">
<span
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/>
</td>
<td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName">
<b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
@click.stop="openDonationModal"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<td class="station-dispatcher-exp">
<span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
"
>
{{
station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp
}}
</span>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
</div>
</td>
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
>
</span>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td class="station-users" :class="{ inactive: !station.onlineInfo }">
<span class="text--primary">{{ station.onlineInfo?.currentUsers ?? '-' }}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td class="station-spawns" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${
station.generalInfo.routes.singleElectrifiedNames.length
}`"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
<span
v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.single-track-routes-other')}${
station.generalInfo.routes.singleOtherNames.length
}`"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
{{ station.generalInfo.routes.singleOtherNames.length }}
</span>
</div>
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
<td class="station-tracks">
<div v-if="station.generalInfo">
<span
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
class="track catenary"
:title="`${$t('sceneries.info.double-track-routes-catenary')}${
station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && stations.length == 0"
/>
<span
v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<div class="no-stations" v-else-if="stations.length == 0">
{{ $t('sceneries.no-stations') }}
</div>
<td class="station-info">
<span
v-if="station.generalInfo?.signalType"
class="scenery-icon icon-info"
:class="station.generalInfo?.controlType.replace('+', '-')"
:title="
$t('sceneries.info.control-type') +
$t(`controls.${station.generalInfo?.controlType}`)
"
>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span>
<img
v-if="station.generalInfo?.signalType"
class="icon-info"
:src="`/images/icon-${station.generalInfo.signalType}.svg`"
:alt="station.generalInfo.signalType"
:title="
$t('sceneries.info.signals-type') + $t(`signals.${station.generalInfo.signalType}`)
"
/>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<img
v-if="!station.generalInfo"
class="icon-info"
src="/images/icon-unknown.svg"
alt="icon-unknown"
:title="$t('sceneries.info.unknown')"
/>
</td>
<td
class="station-users"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="UsersTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.stationTrains ?? [])"
>
<span class="text--primary">{{
station.onlineInfo?.stationTrains?.length ?? '-'
}}</span>
/
<span class="text--primary">{{ station.onlineInfo?.maxUsers ?? '-' }}</span>
</td>
<td class="station-likes" :class="{ inactive: !station.onlineInfo }">
<span>{{ station.onlineInfo?.dispatcherRate ?? '-' }}</span>
</td>
<td
class="station-spawns"
:class="{ inactive: !station.onlineInfo }"
data-tooltip-type="SpawnsTooltip"
:data-tooltip-content="JSON.stringify(station.onlineInfo?.spawns ?? [])"
>
<span>{{ station.onlineInfo?.spawns.length ?? '-' }}</span>
</td>
<td
class="station-schedules all"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
<div class="no-stations" v-else>
<div>
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
>)
</div>
</transition>
<div class="text--primary" v-if="getChangedFilters(filters).length != 0">
{{ $t('sceneries.active-filters') }}
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin';
import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import { defineComponent, inject, computed } from 'vue';
import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common';
import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin';
import styleMixin from '../../mixins/styleMixin';
import { useApiStore } from '../../store/apiStore';
import popupMixin from '../../mixins/popupMixin';
import { useMainStore } from '../../store/mainStore';
import { Status } from '../../typings/common';
import { useTooltipStore } from '../../store/tooltipStore';
import { getChangedFilters } from '../../managers/stationFilterManager';
import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils';
export default defineComponent({
props: {
stations: {
type: Array as PropType<Station[]>,
required: true
}
},
emits: ['toggleDonationCard'],
emits: ['toggleDonationModal'],
components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin, popupMixin],
mixins: [styleMixin, dateMixin],
data: () => ({
headIconsIds,
headIds,
lastSelectedStationName: ''
lastSelectedStationName: '',
getChangedFilters
}),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
}
},
setup() {
const mainStore = useMainStore();
const apiStore = useApiStore();
const tooltipStore = useTooltipStore();
const stationFiltersStore = useStationFiltersStore();
const filters = inject('StationsView_filters') as Record<string, any>;
const activeSorter = inject('StationsView_activeSorter') as ActiveSorter;
const filteredStationList = computed(() =>
mainStore.allStationInfo
.filter((station) => filterStations(station, filters))
.sort((a, b) => sortStations(a, b, activeSorter))
);
return {
Status: Status.Data,
stationFiltersStore,
mainStore,
apiStore
apiStore,
tooltipStore,
filters,
filteredStationList,
activeSorter
};
},
methods: {
setScenery(name: string) {
const station = this.stations.find((station) => station.name === name);
const station = this.filteredStationList.find((station) => station.name === name);
if (!station) return;
this.lastSelectedStationName = station.name;
this.tooltipStore.hide();
this.$router.push({
name: 'SceneryView',
@@ -368,10 +381,10 @@ export default defineComponent({
});
},
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
openDonationCard(e: Event) {
this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target;
this.hidePopUp();
this.tooltipStore.hide();
},
openForumSite(e: Event, url: string | undefined) {
@@ -380,10 +393,14 @@ export default defineComponent({
window.open(url, '_blank');
},
changeSorter(headerName: HeadIdsTypes) {
changeSorter(headerName: HeadIdsType) {
if (headerName == 'general') return;
this.stationFiltersStore.changeSorter(headerName);
if (headerName == this.activeSorter.headerName)
this.activeSorter.dir = -1 * this.activeSorter.dir;
else this.activeSorter.dir = 1;
this.activeSorter.headerName = headerName;
}
}
});
@@ -396,33 +413,20 @@ export default defineComponent({
$rowCol: #424242;
.change-anim {
&-enter-active,
&-leave-active {
transition: opacity 100ms ease-in;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
.table_wrapper {
.station_table {
height: 80vh;
max-height: 2000px;
min-height: 700px;
overflow: auto;
font-weight: 500;
height: 90vh;
min-height: 550px;
}
.no-stations {
text-align: center;
font-size: 1.5em;
font-size: 1.25em;
padding: 1em;
margin: 1em 0;
background: #333;
background: #1a1a1a;
line-height: 1.5em;
}
table {
+25 -48
View File
@@ -5,52 +5,29 @@ export interface FilterOption {
defaultValue: boolean;
}
export interface Filter {
[key: string]: boolean | number | string;
default: boolean;
notDefault: boolean;
real: boolean;
fictional: boolean;
SPK: boolean;
SCS: boolean;
SPE: boolean;
SUP: boolean;
noSUP: boolean;
ASDEK: boolean;
noASDEK: boolean;
ręczne: boolean;
'ręczne+SPK': boolean;
'ręczne+SCS': boolean;
mechaniczne: boolean;
'mechaniczne+SPK': boolean;
'mechaniczne+SCS': boolean;
SBL: boolean;
PBL: boolean;
współczesna: boolean;
kształtowa: boolean;
historyczna: boolean;
mieszana: boolean;
minLevel: number;
maxLevel: number;
minOneWayCatenary: number;
minOneWay: number;
minTwoWayCatenary: number;
minTwoWay: number;
'no-1track': boolean;
'no-2track': boolean;
'include-selected': boolean;
free: boolean;
occupied: boolean;
nonPublic: boolean;
unavailable: boolean;
abandoned: boolean;
endingStatus: boolean;
afkStatus: boolean;
noSpaceStatus: boolean;
unavailableStatus: boolean;
unsignedStatus: boolean;
authors: string;
onlineFromHours: number;
withActiveTimetables: boolean;
withoutActiveTimetables: boolean;
export const headIds = [
'station',
'min-lvl',
'status',
'dispatcher',
'dispatcher-lvl',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
'timetableConfirmed'
] as const;
export type HeadIdsType = (typeof headIds)[number] | (typeof headIconsIds)[number];
export interface ActiveSorter {
headerName: HeadIdsType;
dir: number;
}
+275
View File
@@ -0,0 +1,275 @@
import { ActiveSorter } from '../../components/StationsView/typings';
import { ActiveScenery, StationGeneralInfo, Status } from '../../typings/common';
import { Station } from '../../typings/common';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
const filtersAssociations: Record<string, string> = {
mechaniczne: 'mechanical',
ręczne: 'manual',
'mechaniczne+SPK': 'SPK-M',
'ręczne+SPK': 'SPK-R',
'mechaniczne+SCS': 'SCS-M',
'ręczne+SCS': 'SCS-R',
współczesna: 'modern',
historyczna: 'historical',
kształtowa: 'semaphores',
mieszana: 'mixed'
};
function filterStatusSection(
filters: Record<string, any>,
{ dispatcherStatus, dispatcherTimestamp }: ActiveScenery
) {
return (
(filters['endingStatus'] && dispatcherStatus == Status.ActiveDispatcher.ENDING) ||
(filters['unavailableStatus'] &&
(dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE ||
dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN)) ||
(filters['afkStatus'] && dispatcherStatus == Status.ActiveDispatcher.AFK) ||
(filters['noSpaceStatus'] && dispatcherStatus == Status.ActiveDispatcher.NO_SPACE) ||
(filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE) ||
(filters['onlineFromHours'] > 0 &&
(dispatcherTimestamp ?? 0) <= Date.now() + filters['onlineFromHours'] * 3600000)
);
}
function filterTimetablesSection(filters: Record<string, any>, station: Station) {
return (
(filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0)) ||
(filters['withActiveTimetables'] &&
station.onlineInfo &&
(station.onlineInfo.scheduledTrainCount.all != 0 ||
station.onlineInfo.dispatcherStatus == Status.ActiveDispatcher.FREE))
);
}
function filterAccessibilitySection(filters: Record<string, any>, station: Station) {
if (
filters['nonPublic'] &&
(!station.generalInfo || station.generalInfo.availability == 'nonPublic')
)
return true;
if (!station.generalInfo) return false;
const { availability } = station.generalInfo;
return (
(filters['unavailable'] && availability == 'unavailable' && !station.onlineInfo) ||
(filters['abandoned'] && availability == 'abandoned' && !station.onlineInfo) ||
(filters['default'] && availability == 'default') ||
(filters['notDefault'] &&
availability != 'default' &&
availability != 'abandoned' &&
availability != 'unavailable')
);
}
function filterRealitySection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (filters['real'] && generalInfo.lines) || (filters['fictional'] && !generalInfo.lines);
}
function filterProgramsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
(filters['SUP'] && generalInfo.SUP) ||
(filters['noSUP'] && !generalInfo.SUP) ||
(filters['ASDEK'] && generalInfo.ASDEK) ||
(filters['noASDEK'] && !generalInfo.ASDEK)
);
}
function filterControlsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.controlType] == true ||
filters[filtersAssociations[generalInfo.controlType]] == true
);
}
function filterSignalsSection(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters[generalInfo.signalType] == true ||
filters[filtersAssociations[generalInfo.signalType]] == true ||
(filters['SBL'] && generalInfo.routes.sblNames.length > 0) ||
(filters['PBL'] && generalInfo.routes.sblNames.length == 0)
);
}
function filterStationType(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const singleTracks = generalInfo.routes.single.filter((r) => !r.isInternal);
const doubleTracks = generalInfo.routes.double.filter((r) => !r.isInternal);
let isJunction = singleTracks.length > 0 && doubleTracks.length > 0;
return (filters['junction'] && isJunction) || (filters['nonJunction'] && !isJunction);
}
function filterSliderValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
const { availability, reqLevel, routes } = generalInfo;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
return (
filters['minLevel'] > reqLevel + (otherAvailability ? 1 : 0) ||
filters['maxLevel'] < reqLevel + (otherAvailability ? 1 : 0) ||
filters['minVmax'] > routes.maxRouteSpeed ||
filters['maxVmax'] < routes.minRouteSpeed ||
(filters['no-1track'] && routes.single.length != 0) ||
(filters['no-2track'] && routes.double.length != 0) ||
filters['minOneWayCatenary'] > routes.singleElectrifiedNames.length ||
filters['minOneWay'] > routes.singleOtherNames.length ||
filters['minTwoWayCatenary'] > routes.doubleElectrifiedNames.length ||
filters['minTwoWay'] > routes.doubleOtherNames.length
);
}
function filterInputValues(filters: Record<string, any>, generalInfo: StationGeneralInfo) {
return (
filters['authors'].length > 3 &&
!generalInfo.authors
?.map((a) => a.toLocaleLowerCase())
.includes(filters['authors'].toLocaleLowerCase())
);
}
export const sortStations = (a: Station, b: Station, sorter: ActiveSorter) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Record<string, any>) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
// Scenery Timetables section
if (filterTimetablesSection(filters, station)) return false;
// Scenery Accessibility section
if (filterAccessibilitySection(filters, station)) return false;
// Scenery Status section
if (station.onlineInfo && filterStatusSection(filters, station.onlineInfo)) return false;
if (station.generalInfo) {
// Scenery Reality section
if (filterRealitySection(filters, station.generalInfo)) return false;
// Scenery Additional Programs section
if (filterProgramsSection(filters, station.generalInfo)) return false;
// Scenery Controls section
if (filterControlsSection(filters, station.generalInfo)) return false;
// Scenery Signalling section(s)
if (filterSignalsSection(filters, station.generalInfo)) return false;
// Scenery Station Type section
if (filterStationType(filters, station.generalInfo)) return false;
// Scenery sliders
if (filterSliderValues(filters, station.generalInfo)) return false;
// Scenery Authors section
if (filterInputValues(filters, station.generalInfo)) return false;
}
return true;
};
@@ -1,24 +1,24 @@
<template>
<div class="popup-content">
<span>{{ store.popUpData.content }}</span>
<div class="tooltip-content">
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
.tooltip-content {
display: flex;
justify-content: center;
align-items: center;
@@ -1,25 +1,25 @@
<template>
<div class="popup-content">
<div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="" />
<span>{{ store.popUpData.content }}</span>
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
.tooltip-content {
gap: 0.5em;
padding: 0.5em;
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="spawns.length != 0">
<span v-for="(spawn, i) in spawns">
<template v-if="i > 0"> | </template>
<b>{{ spawn.spawnName }}</b> ({{ spawn.spawnLength }}m)
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { ScenerySpawn } from '../../typings/common';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
},
computed: {
spawns() {
if (this.tooltipStore.content == '') return [];
const parsedSpawns = JSON.parse(this.tooltipStore.content) as ScenerySpawn[];
return parsedSpawns ?? [];
}
}
});
</script>
<style scoped>
.tooltip-content {
width: 300px;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa;
}
</style>
@@ -1,55 +1,55 @@
<template>
<div class="popup" v-show="store.popUpData.key" ref="preview">
<component v-if="store.popUpData.key" :is="store.popUpData.key" />
<div class="tooltip" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import DonatorPopUp from './DonatorPopUp.vue';
import TrainCommentsPopUp from './TrainCommentsPopUp.vue';
import VehiclePreviewPopUp from './VehiclePreviewPopUp.vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp },
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
},
watch: {
'store.mousePos': {
'tooltipStore.mousePos': {
deep: true,
handler(val: typeof this.store.mousePos) {
// [x, y]
handler(val: [number, number]) {
this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0px',
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val.x <= boxWidth / 2) {
if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val.x >= clientWidth - boxWidth / 2) {
} else if (val[0] >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%';
translateX = '-100%';
} else {
previewEl.style.left = `${val.x}px`;
previewEl.style.left = `${val[0]}px`;
translateX = '-50%';
}
previewEl.style.top = `${val.y}px`;
previewEl.style.top = `${val[1]}px`;
const isOutside =
val.y + previewEl.getBoundingClientRect().height + 30 >=
val[1] + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)';
@@ -62,7 +62,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.popup {
.tooltip {
position: absolute;
z-index: 250;
max-width: 400px;
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="trains.length != 0">
<span v-for="(train, i) in trains">
<template v-if="i > 0"> | </template>
<b>{{ train.trainNo }}</b> {{ train.driverName }}
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { Train } from '../../typings/common';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
},
computed: {
trains() {
if (this.tooltipStore.content == '') return [];
const parsedTrains = JSON.parse(this.tooltipStore.content) as Train[];
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
}
}
});
</script>
<style scoped>
.tooltip-content {
width: 300px;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa;
}
</style>
@@ -0,0 +1,135 @@
<template>
<div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="tooltipStore.type"
@load="onImageLoad"
@error="onImageError"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://static.spythere.eu/images/${vehicleName}--300px.jpg`"
/>
<div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name">
{{ vehicleName.replace(/_/g, ' ') }}
<span v-if="vehicleCargo">({{ vehicleCargo.id }})</span>
</div>
<div class="vehicle-props" v-if="vehicleData">
{{ vehicleData.group.speed }}km/h &bull; {{ vehicleData.group.length }}m &bull;
{{ (vehicleData.group.weight / 1000).toFixed(1) }}t
<span v-if="vehicleCargo">(+{{ (vehicleCargo.weight / 1000).toFixed(1) }}t)</span>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore(),
apiStore: useApiStore(),
imageState: 'loading'
};
},
mounted() {
this.imageState = 'loading';
},
watch: {
'tooltipStore.type'(prev, val) {
if (prev != val) this.imageState = 'loading';
}
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
},
onImageError(e: Event) {
this.imageState = 'error';
(e.target as HTMLElement).style.display = 'none';
}
},
computed: {
vehicleName() {
return this.tooltipStore.content.split(':')[0];
},
vehicleData() {
return this.apiStore.vehiclesData?.find((v) => v.name == this.vehicleName);
},
vehicleCargo() {
return this.vehicleData?.group.cargoTypes?.find(
(c) => c.id == this.tooltipStore.content.split(':')[1]
);
}
// vehicleProps() {
// const vehicleDataArray = this.apiStore.vehiclesData?.vehicleList.find(
// ([name]) => name === this.vehicleName
// );
// if (!vehicleDataArray) return null;
// return (
// this.apiStore.vehiclesData!.vehicleProps.find((v) => v.type == vehicleDataArray[1]) ?? null
// );
// }
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
width: 300px;
min-height: 200px;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
padding: 0.5em;
border-radius: 0.5em;
}
.loading-info {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
img {
width: 100%;
height: auto;
}
.vehicle-name {
text-align: center;
margin-top: 0.5em;
text-wrap: wrap;
}
.vehicle-props {
color: #ccc;
}
.error-placeholder {
height: 176px;
}
</style>
+33 -21
View File
@@ -1,5 +1,8 @@
<template>
<span class="stop-label" :data-sbl="stop.isSBL">
<span
class="stop-label"
:data-minor="stop.isSBL || (stop.nameRaw.endsWith(', po.') && !stop.duration)"
>
<span class="name" v-html="stop.nameHtml"></span>
<span
@@ -9,12 +12,13 @@
stop.arrivalDelay > 0 && stop.status != 'unconfirmed'
? 'delayed'
: stop.arrivalDelay < 0 && stop.status != 'unconfirmed'
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
? 'preponed'
: stop.arrivalDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
p.
<span v-if="stop.arrivalDelay != 0 && stop.status != 'unconfirmed'">
<s>{{ timestampToString(stop.arrivalScheduled) }}</s>
{{ timestampToString(stop.arrivalReal) }}
@@ -29,17 +33,17 @@
<span
v-if="
stop.duration ||
(stop.status == 'stopped' &&
stop.position != 'begin' &&
stop.departureDelay != stop.arrivalDelay)
(stop.status == 'stopped' && stop.position != 'begin' && stop.departureDelay > 0)
"
class="date stop"
:data-stop-types="stop.type.replace(', ', '-')"
:data-stop-status="
stop.departureDelay - stop.arrivalDelay > 0 && !stop.duration ? 'delayed' : ''
"
:data-stop-status="stop.departureDelay > 0 && !stop.duration ? 'delayed' : ''"
>
{{ stop.duration || stop.departureDelay - stop.arrivalDelay }}
{{
stop.duration == 0 && stop.departureDelay > 0
? stop.departureDelay - stop.arrivalDelay
: stop.duration
}}
{{ stop.type == '' ? 'pt' : stop.type }}
</span>
@@ -53,13 +57,16 @@
stop.departureDelay > 0 && stop.status == 'confirmed'
? 'delayed'
: stop.departureDelay < 0 && stop.status == 'confirmed'
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
? 'preponed'
: stop.departureDelay == 0 && stop.status == 'confirmed'
? 'on-time'
: ''
"
>
<span v-if="stop.departureDelay != 0 && stop.status == 'confirmed'">
o.
<span
v-if="stop.departureDelay != 0 && (stop.status == 'confirmed' || stop.status == 'stopped')"
>
<s>{{ timestampToString(stop.departureScheduled) }}</s>
{{ timestampToString(stop.departureReal) }}
@@ -96,14 +103,14 @@ $delayedClr: salmon;
$dateClr: #525151;
$stopExchangeClr: #db8e29;
$stopDefaultClr: #252525;
$stopNameClr: #22a8d1;
$stopNameClr: #303030;
.stop-label {
display: flex;
flex-wrap: wrap;
align-items: center;
&[data-sbl='true'] {
&[data-minor='true'] {
.date {
display: none;
}
@@ -117,6 +124,7 @@ $stopNameClr: #22a8d1;
.name {
background: $stopNameClr;
border-radius: 0.5em 0 0 0.5em;
padding: 0.3em 0.5em;
display: flex;
@@ -130,6 +138,10 @@ $stopNameClr: #22a8d1;
.date {
background: $dateClr;
padding: 0.3em 0.5em;
&:last-child {
border-radius: 0 0.5em 0.5em 0;
}
}
.stop {
@@ -150,7 +162,7 @@ $stopNameClr: #22a8d1;
.departure {
&[data-status='delayed'] {
s {
color: #999;
color: #ccc;
}
span {
@@ -160,7 +172,7 @@ $stopNameClr: #22a8d1;
&[data-status='preponed'] {
s {
color: #999;
color: #ccc;
}
span {
+29 -5
View File
@@ -45,8 +45,8 @@
<div class="train-driver">
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.driver-message')"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
@@ -74,8 +74,8 @@
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<span
v-if="getSceneriesWithComments(train.timetableData).length > 0"
data-popup-key="TrainCommentsPopUp"
:data-popup-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
data-tooltip-type="BaseTooltip"
:data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData
)})`"
>
@@ -131,6 +131,18 @@
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h
<span v-if="stockSpeedLimit != Infinity">
&bull;
<em
class="text--grayed"
style="text-decoration: underline dotted"
tabindex="0"
:data-tooltip="$t('trains.vmax-tooltip')"
>
{{ stockSpeedLimit }} km/h
</em>
</span>
</div>
</div>
@@ -163,12 +175,12 @@
import { defineComponent } from 'vue';
import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
@@ -191,6 +203,18 @@ export default defineComponent({
};
},
computed: {
stockSpeedLimit() {
return this.train.stockList.reduce((acc, stockName) => {
const vehicleSpeed =
this.apiStore.vehiclesData?.find((v) => v.name == stockName.split(':')[0])?.group.speed ??
300;
return Math.min(vehicleSpeed, acc);
}, 300);
}
},
methods: {
navigateToJournal() {
this.$router.push({
+18 -29
View File
@@ -1,7 +1,7 @@
<template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-content" ref="content" tabindex="0">
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
@@ -13,7 +13,7 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
import Train from '../../scripts/interfaces/Train';
import { Train } from '../../typings/common';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
@@ -21,7 +21,7 @@ export default defineComponent({
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
}
},
@@ -29,8 +29,15 @@ export default defineComponent({
chosenTrain(train: Train | undefined) {
this.$nextTick(() => {
if (train) {
document.body.classList.add('no-scroll');
const contentEl = this.$refs['content'] as HTMLElement;
contentEl.focus();
} else {
(this.store.modalLastClickedTarget as any)?.focus();
setTimeout(() => {
document.body.classList.remove('no-scroll');
}, 90);
}
});
}
@@ -40,20 +47,6 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/responsive.scss';
@import '../../styles/card.scss';
.top-info-bar-anim {
&-enter-active,
&-leave-active {
transition: all 150ms ease-in-out;
}
&-enter-from,
&-leave-to {
transform: translate(-50%, -50%) scale(0.8);
opacity: 0;
}
}
.train-modal {
position: fixed;
@@ -61,17 +54,19 @@ export default defineComponent({
left: 0;
width: 100%;
height: 100%;
color: white;
z-index: 200;
display: flex;
justify-content: center;
align-items: flex-start;
text-align: left;
}
.modal_background {
.modal-background {
position: absolute;
top: 0;
left: 0;
@@ -83,14 +78,14 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55);
}
.modal_content {
.modal-content {
position: relative;
overflow-y: scroll;
margin-top: 1em;
width: 95vw;
max-height: 96vh;
max-height: 95vh;
max-height: 95dvh;
margin-top: 1em;
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
@@ -105,10 +100,4 @@ export default defineComponent({
}
}
}
@include smallScreen {
.modal_content {
max-height: 85vh;
}
}
</style>
+1 -5
View File
@@ -4,7 +4,7 @@
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-filter2.svg" alt="Open filters icon" />
{{ $t('options.filters') }} [F]
[F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
@@ -81,7 +81,6 @@
</div>
<div class="filter-actions">
<div></div>
<button class="btn--action" @click="resetAllFilters">
{{ $t('options.filter-reset') }}
</button>
@@ -223,9 +222,6 @@ export default defineComponent({
.filter-actions {
display: flex;
gap: 0.5em;
width: 100%;
margin-top: 1em;
> * {
+85 -42
View File
@@ -14,7 +14,6 @@
:data-stop-type="stop.type"
:data-minor-stop-active="stop.isActive"
:data-last-confirmed="stop.isLastConfirmed"
x
>
<span class="stop_info">
<span class="distance">
@@ -48,26 +47,46 @@
<span
v-if="
stop.departureLine &&
stop.departureLine == scheduleStops[i + 1]?.arrivalLine &&
!/sbl/gi.test(stop.departureLine)
scheduleStops[i + 1] != undefined &&
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.departureLine)
"
>
{{ stop.departureLine }}
</span>
<span v-else-if="stop.departureLine && !/sbl/gi.test(stop.departureLine)">
<div>{{ stop.departureLine }}</div>
<div
class="scenery-change-name"
v-if="
i < scheduleStops.length - 1 &&
stop.sceneryName != scheduleStops[i + 1].sceneryName
"
>
{{ scheduleStops[i + 1].sceneryName }}
<div class="scenery-route">
<span>{{ stop.departureLine }}</span>
<span v-if="stop.departureLineInfo">
| {{ stop.departureLineInfo.routeSpeed }}
<span v-if="stop.departureLineInfo.isElectric"></span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span>
</div>
<div>
{{ scheduleStops[i + 1].arrivalLine }}
<div
v-if="stop.sceneryName != scheduleStops[i + 1]?.sceneryName"
class="scenery-change-name"
>
<span>{{ scheduleStops[i + 1].sceneryName }}</span>
<span v-if="stop.departureLineInfo?.routeTracks == 1"> &UpDownArrow;</span>
<span v-else> &UpArrowDownArrow;</span>
</div>
<div class="scenery-route">
<span> {{ scheduleStops[i + 1].arrivalLine }}</span>
<span v-if="scheduleStops[i + 1].arrivalLineInfo">
| {{ scheduleStops[i + 1].arrivalLineInfo!.routeSpeed }}
<span v-if="scheduleStops[i + 1].arrivalLineInfo!.isElectric"></span>
<img
v-else
src="/images/icon-we4a.png"
:title="$t('trains.we4a-tooltip')"
width="12"
/>
</span>
</div>
</span>
</div>
@@ -81,11 +100,11 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train';
import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import { StationRoutesInfo, Train } from '../../typings/common';
export interface TrainScheduleStop {
nameHtml: string;
@@ -111,12 +130,16 @@ export interface TrainScheduleStop {
isSBL: boolean;
sceneryName: string | null;
sceneryHash: string;
distance: number;
arrivalLine: string | null;
departureLine: string | null;
arrivalLineInfo?: StationRoutesInfo;
departureLineInfo?: StationRoutesInfo;
isExternal: boolean;
comments: string | null;
}
@@ -146,13 +169,25 @@ export default defineComponent({
return (
this.train.timetableData?.followingStops.map((stop, i, arr) => {
if (
const isExternal =
i > 0 &&
stop.arrivalLine &&
stop.arrivalLine != arr[i - 1].departureLine &&
!/sbl/gi.test(stop.arrivalLine)
)
currentSceneryIndex++;
stop.arrivalLine != null &&
(stop.arrivalLine != arr[i - 1].departureLine ||
(stop.arrivalLine == arr[i - 1].departureLine &&
!/-|_|(^it\d+)|(^sbl)/gi.test(stop.arrivalLine)));
if (isExternal) currentSceneryIndex++;
const sceneryName = this.train.timetableData!.sceneryNames[currentSceneryIndex];
const sceneryInfo = this.apiStore.sceneryData.find((st) => st.name == sceneryName);
const arrivalLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.arrivalLine
);
const departureLineInfo = sceneryInfo?.routesInfo.find(
(r) => r.routeName == stop.departureLine
);
return {
nameHtml: stop.stopName,
@@ -174,14 +209,18 @@ export default defineComponent({
arrivalLine: stop.arrivalLine,
departureLine: stop.departureLine,
arrivalLineInfo: arrivalLineInfo,
departureLineInfo: departureLineInfo,
isExternal,
type: stop.stopType,
distance: stop.stopDistance,
isActive: this.activeMinorStops.includes(i),
isLastConfirmed: this.lastConfirmed === i && !stop.terminatesHere,
isSBL: /sbl/gi.test(stop.stopName),
position: stop.beginsHere ? 'begin' : stop.terminatesHere ? 'end' : 'en-route',
sceneryHash: '',
sceneryName: this.train.timetableData!.sceneryNames[currentSceneryIndex],
sceneryName,
status: stop.confirmed ? 'confirmed' : stop.stopped ? 'stopped' : 'unconfirmed'
};
}) ?? []
@@ -483,22 +522,26 @@ $blinkAnim: 0.5s ease-in-out alternate infinite blink;
}
}
.bottom-line-info {
.scenery-change-name {
position: relative;
margin: 0.25em 0;
.scenery-route {
img {
vertical-align: middle;
}
}
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
.scenery-change-name {
position: relative;
margin: 0.25em 0;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
&::before {
content: '';
position: absolute;
height: 2px;
width: 30px;
background-color: #aaa;
top: 50%;
right: calc(100% + 5px);
transform: translate(0, -50%);
}
}
</style>
+7 -19
View File
@@ -8,17 +8,18 @@
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
{{ $t('trains.no-trains') }}
{{ $t('trains.no-trains') }} (region: <b>{{ store.region.name }}</b
>)
</div>
<transition-group name="list-anim" tag="ul">
<li
class="train-row"
v-for="train in trains"
:key="train.trainId"
:key="train.id"
tabindex="0"
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
@click.stop="selectModalTrain(train, $event.currentTarget)"
@keydown.enter="selectModalTrain(train, $event.currentTarget)"
>
<TrainInfo :train="train" :extended="false" />
</li>
@@ -30,11 +31,10 @@
<script lang="ts">
import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import Train from '../../scripts/interfaces/Train';
import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue';
import { Status } from '../../typings/common';
import { Status, Train } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
@@ -77,17 +77,6 @@ export default defineComponent({
return Status.Data.Loaded;
}
},
activated() {
const query = this.$route.query;
if (query.trainNo && query.driverName) {
this.searchedDriver = query.driverName.toString();
this.searchedTrain = query.trainNo.toString();
setTimeout(() => {
this.selectModalTrain(query.driverName! + query.trainNo!.toString());
}, 20);
}
}
});
</script>
@@ -109,8 +98,7 @@ export default defineComponent({
text-align: center;
padding: 1em 0;
font-size: 1.5em;
font-size: 1.25em;
background: #1a1a1a;
}
File diff suppressed because it is too large Load Diff
-326
View File
@@ -1,330 +1,4 @@
{
"optionSections": [
"status",
"timetables",
"reality",
"package-access",
"access",
"control",
"blockades",
"signals",
"addons"
],
"options": [
{
"id": "real",
"name": "real",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "fictional",
"name": "fictional",
"section": "reality",
"value": true,
"defaultValue": true
},
{
"id": "default",
"name": "default",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "not-default",
"name": "notDefault",
"section": "package-access",
"value": true,
"defaultValue": true
},
{
"id": "non-public",
"name": "nonPublic",
"section": "access",
"value": true,
"defaultValue": true
},
{
"id": "unavailable",
"name": "unavailable",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "abandoned",
"name": "abandoned",
"section": "access",
"value": false,
"defaultValue": false
},
{
"id": "SPK",
"name": "SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS",
"name": "SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPE",
"name": "SPE",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-M",
"name": "mechaniczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-M",
"name": "mechaniczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "mechanical",
"name": "mechaniczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SPK-R",
"name": "ręczne+SPK",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SCS-R",
"name": "ręczne+SCS",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "manual",
"name": "ręczne",
"section": "control",
"value": true,
"defaultValue": true
},
{
"id": "SUP",
"name": "SUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noSUP",
"name": "noSUP",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "ASDEK",
"name": "ASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "noASDEK",
"name": "noASDEK",
"section": "addons",
"value": true,
"defaultValue": true
},
{
"id": "SBL",
"name": "SBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "PBL",
"name": "PBL",
"section": "blockades",
"value": true,
"defaultValue": true
},
{
"id": "modern",
"name": "współczesna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "semaphores",
"name": "kształtowa",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "mixed",
"name": "mieszana",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "historical",
"name": "historyczna",
"section": "signals",
"value": true,
"defaultValue": true
},
{
"id": "free",
"name": "free",
"section": "status",
"value": false,
"defaultValue": false
},
{
"id": "occupied",
"name": "occupied",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "endingStatus",
"name": "endingStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "afkStatus",
"name": "afkStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "noSpaceStatus",
"name": "noSpaceStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "unavailableStatus",
"name": "unavailableStatus",
"section": "status",
"value": true,
"defaultValue": true
},
{
"id": "withActiveTimetables",
"name": "withActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
},
{
"id": "withoutActiveTimetables",
"name": "withoutActiveTimetables",
"section": "timetables",
"value": true,
"defaultValue": true
}
],
"sliders": [
{
"id": "min-lvl",
"name": "minLevel",
"minRange": 0,
"maxRange": 20,
"value": 0,
"defaultValue": 0
},
{
"id": "max-lvl",
"name": "maxLevel",
"minRange": 0,
"maxRange": 20,
"value": 20,
"defaultValue": 20
},
{
"id": "routes-1t-cat",
"name": "minOneWayCatenary",
"minRange": 0,
"maxRange": 5,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-1t-other",
"name": "minOneWay",
"minRange": 0,
"maxRange": 5,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-cat",
"name": "minTwoWayCatenary",
"minRange": 0,
"maxRange": 5,
"value": 0,
"defaultValue": 0
},
{
"id": "routes-2t-other",
"name": "minTwoWay",
"minRange": 0,
"maxRange": 5,
"value": 0,
"defaultValue": 0
}
],
"modes": [
{
"id": "include-selected",
"name": "include-selected",
"section": "mode",
"value": true,
"defaultValue": true
},
{
"id": "save",
"name": "save",
"section": "mode",
"value": true,
"defaultValue": true
}
],
"regions": [
{
"id": "eu",
+68 -35
View File
@@ -27,11 +27,11 @@
"SKR": "Train with exceeded gauge"
},
"update": {
"title": "Stacjownik app update!",
"version": "Version {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "context tooltips when hovering over project sponsors and timetable comment warnings\nvehicle image preview when hovering over its thumbnail in the active timetable card view and timetable journal\nlink to the driver's timetable history in the active timetable card view\nlink to the driver's active timetable card view in the timetable journal (available for online trains only)\nnew update card with version changelog"
"title": "Stacjownik update!",
"confirm": "ROGER THAT!",
"no-data": "No data about the latest app update has been found",
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
},
"app": {
"sceneries": "SCENERIES",
@@ -81,7 +81,20 @@
"ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK",
"mechaniczne+SCS": "levers + SCS"
"mechaniczne+SCS": "levers + SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
},
"status": {
"online": "UNTIL ",
@@ -99,8 +112,8 @@
"filters": "FILTERS",
"donate": "DONATE",
"search-button": "Search",
"reset-button": "Reset",
"search-button": "SEARCH",
"reset-button": "RESET",
"sort-title": "SORT BY:",
"filter-title": "FILTER BY:",
@@ -112,7 +125,9 @@
"search-dispatcher": "Dispatcher name",
"search-station": "Scenery name",
"search-author": "Timetable author name",
"search-issuedFrom": "Origin scenery name",
"search-issuedFrom": "Issuing scenery name",
"search-via": "Via scenery name",
"search-terminatingAt": "Terminating scenery name",
"search-timetables-date": "Timetable date (UTC+2 / CEST)",
"search-dispatchers-date": "Service date (UTC+2 / CEST)",
"search-date": "Date (UTC+2 / CEST)",
@@ -161,17 +176,22 @@
"sections": {
"quick": "QUICK FILTERS",
"stationType": "STATION TYPE",
"reality": "SCENERY REALITY",
"package-access": "IN-GAME AVAILABILITY",
"packageAccess": "IN-GAME AVAILABILITY",
"access": "GENERAL AVAILABILITY",
"control": "CONTROLS",
"signals": "SIGNALLING",
"addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS",
"timetables": "ACTIVE TIMETABLES"
"timetables": "ACTIVE TIMETABLES",
"spawns": "OPEN SPAWNS"
},
"changed-filters-count": "Changed filters:",
"no-changed-filters": "No changed filters",
"all-available": "ALL AVAILABLE",
"all-free": "CURRENTLY FREE",
@@ -182,11 +202,11 @@
"title": "STATION FILTERS",
"default": "IN-GAME",
"not-default": "ADDITIONAL",
"notDefault": "ADDITIONAL",
"real": "REAL",
"fictional": "FICTIONAL",
"unavailable": "UNSUPPORTED",
"non-public": "NON-PUBLIC",
"nonPublic": "NON-PUBLIC",
"abandoned": "ABANDONED",
"SPK": "SPK",
@@ -196,7 +216,6 @@
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
@@ -219,14 +238,20 @@
"withActiveTimetables": "ACTIVE",
"withoutActiveTimetables": "NO ACTIVE",
"junction": "JUNCTIONS",
"nonJunction": "OTHER",
"sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
"routes-1t-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
"minLevel": "MIN. REQUIRED DISPATCHER LEVEL",
"maxLevel": "MAX. REQUIRED DISPATCHER LEVEL",
"minVmax": "MIN. SCENERY ROUTE SPEED",
"maxVmax": "MAX. SCENERY ROUTE SPEED",
"minOneWayCatenary": "MIN. CATENARY SINGLE TRACK ROUTES",
"minOneWay": "MIN. OTHER SINGLE TRACK ROUTES",
"minTwoWayCatenary": "MIN. CATENARY DOUBLE TRACK ROUTES",
"minTwoWay": "MIN. OTHER DOUBLE TRACK ROUTES"
},
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
"authors-placeholder": "Enter the author nickname...",
"authors-button-title": "Search",
@@ -277,7 +302,17 @@
"single-track-routes-other": "Not electrified single-track routes count: "
},
"no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..."
"scenery-search": "Search for scenery...",
"active-filters": "Attention! You got active filters!"
},
"station-stats": {
"u-factor": "U-factor",
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
"avg-timetable-count": "Average count of scenery timetables:",
"single-track-count": "Single track routes:",
"double-track-count": "Double track routes:",
"cross-sceneries": "Cross-track sceneries (1-track <-> 2-track)",
"open-spawns": "Open spawns:"
},
"trains": {
"no-trains": "No trains to show here!",
@@ -296,6 +331,9 @@
"current-signal": "at signal",
"current-track": "on track",
"vmax-tooltip": "Maximum train speed based on rolling stock vehicles - braked weight is not included",
"we4a-tooltip": "Non-electrified track",
"delayed": "Delayed: ",
"preponed": "Ahead of schedule: ",
"on-time": "On time",
@@ -367,7 +405,7 @@
"minutes": "{value} min | {value} mins",
"seconds": "{value} s",
"stock-info": "EXTRA INFO",
"stock-info": "DETAILS",
"stock-length": "Length",
"stock-mass": "Mass",
"stock-max-speed": "Max. speed",
@@ -458,21 +496,16 @@
"option-timetables-history": "Timetables history PL1",
"option-dispatchers-history": "Dispatchers history PL1",
"timetable-author-title": "Issued by",
"timetable-author-unknown": "Author unknown",
"timetable-via": "ALL TIMETABLES",
"timetable-issuedFrom": "BEGINS HERE",
"timetable-terminatingAt": "TERMINATES HERE",
"timetables-history-id": "ID",
"timetables-history-number": "Number",
"timetables-history-route": "Route",
"timetables-history-driver": "Driver",
"timetables-history-author": "TT author",
"timetables-history-date": "Date",
"timetable-issued-date": "Issued",
"timetable-issued-by": " by:",
"timetable-issued-for": " for driver:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dispatcher",
"dispatchers-history-level": "Level",
"dispatchers-history-rate": "Rate",
"dispatchers-history-date": "Service date",
"dispatcher-rate": "Rate:",
"dispatcher-status-changes": "Status changes:",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!",
+66 -34
View File
@@ -28,10 +28,10 @@
},
"update": {
"title": "Aktualizacja Stacjownika!",
"version": "Wersja {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "dymki kontekstowe po najechaniu kursorem na m.in. sponsorów projektu i uwagi eksploatacyjne\npodgląd pojazdu po najechaniu kursorem na jego miniaturkę w karcie aktywnego rozkładu jazdy oraz dzienniku RJ\nodnośnik do historii RJ maszynisty w widoku karty aktywnego rozkładu jazdy\nodnośnik do karty aktywnego rozkładu jazdy maszynisty w dzienniku (dostępny tylko dla pociągów online)\nnowa karta ze zmianami w aktualizacji"
"confirm": "PRZYJĄŁEM!",
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
},
"app": {
"sceneries": "SCENERIE",
@@ -77,7 +77,20 @@
"ręczne+SCS": "ręczne z SCS",
"mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne z SPK",
"mechaniczne+SCS": "mechaniczne z SCS"
"mechaniczne+SCS": "mechaniczne z SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
},
"status": {
"online": "DO ",
@@ -95,8 +108,8 @@
"filters": "FILTRY",
"donate": "WESPRZYJ",
"search-button": "Szukaj",
"reset-button": "Zresetuj",
"search-button": "SZUKAJ",
"reset-button": "ZRESETUJ",
"sort-title": "SORTUJ WG:",
"filter-title": "FILTRUJ WG:",
@@ -109,6 +122,8 @@
"search-station": "Nazwa scenerii",
"search-author": "Nick autora rozkładu jazdy",
"search-issuedFrom": "Sceneria początkowa",
"search-via": "Przez scenerię",
"search-terminatingAt": "Sceneria końcowa",
"search-timetables-date": "Data rozkładu jazdy (UTC+2 / CEST)",
"search-dispatchers-date": "Data służby (UTC+2 / CEST)",
"search-date": "Data (UTC+2 / CEST)",
@@ -158,17 +173,22 @@
"sections": {
"quick": "SZYBKIE FILTRY",
"stationType": "RODZAJ STACJI",
"reality": "FIKCYJNOŚĆ SCENERII",
"package-access": "DOSTĘPNOŚĆ W PACZCE",
"packageAccess": "DOSTĘPNOŚĆ W PACZCE",
"access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY",
"addons": "SZCZEGÓŁY",
"blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE",
"timetables": "AKTYWNE ROZKŁADY JAZDY"
"timetables": "AKTYWNE ROZKŁADY JAZDY",
"spawns": "OTWARTE SPAWNY"
},
"changed-filters-count": "Zmienione filtry:",
"no-changed-filters": "Brak zmienionych filtrów",
"all-available": "WSZYSTKIE DOSTĘPNE",
"all-free": "WSZYSTKIE WOLNE",
@@ -179,11 +199,11 @@
"title": "FILTRUJ STACJE",
"default": "DOMYŚLNA",
"not-default": "POZA PACZKĄ",
"notDefault": "POZA PACZKĄ",
"real": "REALNA",
"fictional": "FIKCYJNA",
"unavailable": "NIEDOSTĘPNA",
"non-public": "NIEPUBLICZNA",
"nonPublic": "NIEPUBLICZNA",
"abandoned": "WYCOFANA",
"SPK": "SPK",
@@ -209,20 +229,24 @@
"semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA",
"historical": "HISTORYCZNA",
"free": "WOLNA",
"occupied": "ZAJĘTA",
"withActiveTimetables": "AKTYWNE",
"withoutActiveTimetables": "BEZ AKTYWNYCH",
"junction": "WĘZŁOWE",
"nonJunction": "INNE",
"sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"routes-2t-other": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
"minLevel": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"maxLevel": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
"minVmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
"maxVmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
"minOneWayCatenary": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"minOneWay": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"minTwoWayCatenary": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
"minTwoWay": "SZLAKI DWUTOROWE NIEZELEKTR. (MINIMUM)"
},
"authors-search": "SZUKAJ AUTORA (uwzględnia inne filtry):",
@@ -272,7 +296,17 @@
"single-track-routes-other": "Liczba niezelektryfikowanych szlaków jednotorowych: "
},
"no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..."
"scenery-search": "Wyszukaj scenerię...",
"active-filters": "Uwaga! Masz obecnie aktywne filtry!"
},
"station-stats": {
"u-factor": "Współczynnik Ugla",
"u-factor-tooltip": "(?) Współczynnik ruchu na serwerze (liczba maszynistów online dzielona na liczbę dyżurnych ruchu)",
"avg-timetable-count": "Średnia liczba rozkładów jazdy na sceneriach:",
"single-track-count": "Szlaki jednotorowe:",
"double-track-count": "Szlaki dwutorowe:",
"cross-sceneries": "Scenerie przejściowe (1-tor <-> 2-tor):",
"open-spawns": "Otwarte spawny:"
},
"trains": {
"no-trains": "Brak pociągów do wyświetlenia!",
@@ -283,6 +317,9 @@
"current-signal": "przy semaforze",
"current-track": "na szlaku",
"vmax-tooltip": "Maksymalna prędkość na podstawie pojazdów w składzie - nie bierze pod uwagę masy hamowania",
"we4a-tooltip": "Szlak niezelektryfikowany",
"delayed": "Opóźniony: ",
"preponed": "Przed czasem: ",
"on-time": "Planowo",
@@ -352,7 +389,7 @@
"timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE",
"stock-info": "SZCZEGÓŁY",
"stock-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Prędkość maks.",
@@ -442,21 +479,16 @@
"option-timetables-history": "Historia rozkładów PL1",
"option-dispatchers-history": "Historia dyżurów PL1",
"timetable-author-title": "Wydany przez",
"timetable-author-unknown": "Autor nieznany",
"timetable-via": "WSZYSTKIE RJ",
"timetable-issuedFrom": "ROZPOCZYNA BIEG",
"timetable-terminatingAt": "KOŃCZY BIEG",
"timetables-history-id": "ID",
"timetables-history-number": "Numer",
"timetables-history-route": "Trasa",
"timetables-history-driver": "Maszynista",
"timetables-history-author": "Autor RJ",
"timetables-history-date": "Data",
"timetable-issued-date": "Wystawiony",
"timetable-issued-by": " przez:",
"timetable-issued-for": " dla maszynisty:",
"dispatchers-history-hash": "Hash",
"dispatchers-history-dispatcher": "Dyżurny",
"dispatchers-history-level": "Poziom",
"dispatchers-history-rate": "Ocena",
"dispatchers-history-date": "Data służby",
"dispatcher-rate": "Ocena:",
"dispatcher-status-changes": "Zmiany statusów:",
"req-level": "ogólnodostępna | minimum {lvl} poziom dyżurnego | minimum {lvl} poziom dyżurnego",
"history-list-empty": "Brak historii dla tej scenerii!",
+7 -2
View File
@@ -5,10 +5,15 @@ import router from './router';
import i18n from './i18n';
import { createPinia } from 'pinia';
import useCustomSW from './mixins/useCustomSW';
import { registerSW } from 'virtual:pwa-register';
// Service worker
useCustomSW();
registerSW({
immediate: true,
onNeedRefresh() {
console.log('Needs refresh!');
}
});
const clickOutsideDirective: Directive = {
mounted(el, binding) {
+119
View File
@@ -0,0 +1,119 @@
import StorageManager from './storageManager';
export const sections = [
'status',
'timetables',
'reality',
'packageAccess',
'stationType',
'access',
'control',
'blockades',
'signals',
'addons'
] as const;
export const initFilters = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
manual: false,
'SPK-R': false,
'SCS-R': false,
mechanical: false,
'SPK-M': false,
'SCS-M': false,
modern: false,
semaphores: false,
historical: false,
mixed: false,
SBL: false,
PBL: false,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
junction: false,
nonJunction: false,
maxVmax: 200,
minVmax: 0,
onlineFromHours: 0,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
authors: ''
};
export const initSliders = [
{ id: 'maxVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minVmax', minRange: 0, maxRange: 200, step: 10 },
{ id: 'minLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'maxLevel', minRange: 0, maxRange: 20, step: 1 },
{ id: 'minOneWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minOneWay', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWayCatenary', minRange: 0, maxRange: 5, step: 1 },
{ id: 'minTwoWay', minRange: 0, maxRange: 5, step: 1 }
];
export type StationFilter = keyof typeof initFilters;
export type StationFilterSection = (typeof sections)[number];
export const filtersSections: Record<StationFilterSection, StationFilter[]> = {
status: ['free', 'occupied', 'endingStatus', 'afkStatus', 'noSpaceStatus', 'unavailableStatus'],
timetables: ['withActiveTimetables', 'withoutActiveTimetables'],
reality: ['real', 'fictional'],
packageAccess: ['default', 'notDefault'],
stationType: ['junction', 'nonJunction'],
access: ['nonPublic', 'unavailable', 'abandoned'],
addons: ['SUP', 'ASDEK', 'noSUP', 'noASDEK'],
control: ['SPK', 'SCS', 'SPE', 'SPK-M', 'SCS-M', 'mechanical', 'SPK-R', 'SCS-R', 'manual'],
blockades: ['SBL', 'PBL'],
signals: ['modern', 'semaphores', 'mixed', 'historical']
};
export function setupFilters(currentFilters: Record<string, any>) {
if (!StorageManager.isRegistered('options_saved')) return;
Object.keys(currentFilters).forEach((filterKey) => {
const savedValue = StorageManager.getValue(filterKey);
if (savedValue != null) {
if (typeof currentFilters[filterKey] == 'boolean')
currentFilters[filterKey] = savedValue === 'true';
else if (typeof currentFilters[filterKey] == 'number')
currentFilters[filterKey] = Number(savedValue);
else currentFilters[filterKey] = savedValue.toString();
}
});
}
export function getChangedFilters(currentFilters: Record<string, any>): string[] {
return (
Object.keys(currentFilters).filter(
(filterKey) =>
currentFilters[filterKey] !== initFilters[filterKey as keyof typeof initFilters]
) ?? []
);
}
+4
View File
@@ -34,6 +34,10 @@ export default class StorageManager {
window.localStorage.removeItem(key);
}
static getValue(key: string) {
return window.localStorage.getItem(key);
}
static getBooleanValue(key: string): boolean {
return window.localStorage.getItem(key) === 'true' ? true : false;
}
+1 -2
View File
@@ -1,6 +1,5 @@
import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings';
import Train from '../scripts/interfaces/Train';
import { TrainStop } from '../store/typings';
import { Train, TrainStop } from '../typings/common';
function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1;
-16
View File
@@ -1,16 +0,0 @@
import { defineComponent } from 'vue';
import { useApiStore } from '../store/apiStore';
export default defineComponent({
data() {
return {
apiStore: useApiStore()
};
},
methods: {
isDonator(name: string) {
return this.apiStore.donatorsData.includes(name);
}
}
});
-27
View File
@@ -1,27 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].intersectionRatio > 0.5) actionFunction();
},
{ threshold: 0.2 }
);
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
}
}
});
+12 -10
View File
@@ -1,28 +1,30 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({
data() {
return {
store: useMainStore()
store: useMainStore(),
tooltipStore: useTooltipStore()
};
},
methods: {
selectModalTrain(trainId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = trainId;
document.body.classList.add('no-scroll');
selectModalTrain(train: Train, target?: EventTarget | null) {
this.store.chosenModalTrainId = train.modalId;
if (target) this.store.modalLastClickedTarget = target;
},
selectModalTrainById(modalId: string, target?: EventTarget | null) {
this.store.chosenModalTrainId = modalId;
if (target) this.store.modalLastClickedTarget = target;
},
closeModal() {
this.store.chosenModalTrainId = undefined;
this.store.popUpData.key = null;
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
this.tooltipStore.hide();
}
}
});
-27
View File
@@ -1,27 +0,0 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { PopUpType, popupKeys } from '../store/typings';
const isPopUp = (v: any): v is PopUpType => popupKeys.includes(v);
export default defineComponent({
data() {
return {
store: useMainStore()
};
},
methods: {
showPopUp(e: MouseEvent, componentKey: string, value?: string) {
if (!isPopUp(componentKey)) return;
this.store.popUpData['key'] = componentKey;
this.store.popUpData['content'] = value ?? '';
},
hidePopUp() {
this.store.popUpData['key'] = null;
this.store.popUpData['content'] = '';
}
}
});
-24
View File
@@ -1,24 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getControlTypeAbbrev(controlType: string) {
switch (controlType) {
case 'mechaniczne':
return 'M';
case 'SCS-SPK':
return 'S/S';
case 'ręczne':
return 'R';
case 'mechaniczne+SPK':
return 'M';
case 'ręczne+SPK':
return 'R';
case 'mechaniczne+SCS':
return 'M';
default:
return controlType;
}
}
}
});
+3 -4
View File
@@ -1,6 +1,5 @@
import { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train';
import { TrainStop } from '../store/typings';
import { Train, TrainStop } from '../typings/common';
export default defineComponent({
data: () => ({
@@ -51,8 +50,8 @@ export default defineComponent({
return diffMins < 1
? this.$t('trains.last-seen-now')
: diffMins < 2
? this.$t('trains.last-seen-min')
: this.$t('trains.last-seen-ago', { minutes: diffMins });
? this.$t('trains.last-seen-min')
: this.$t('trains.last-seen-ago', { minutes: diffMins });
},
displayTrainPosition(train: Train) {
-13
View File
@@ -1,13 +0,0 @@
import { useRegisterSW } from 'virtual:pwa-register/vue';
export default () => {
const { needRefresh, updateServiceWorker, offlineReady } = useRegisterSW({
immediate: true
});
return {
needRefresh,
updateServiceWorker,
offlineReady
};
};
+1 -1
View File
@@ -58,7 +58,7 @@ const routes: Array<RouteRecordRaw> = [
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.name == 'SceneryView' && from.name !== to.name && from.query['view'] === undefined)
return { el: `.app_main` };
return { el: `.app_main`, top: -15 };
if (savedPosition) return savedPosition;
},
-21
View File
@@ -1,21 +0,0 @@
export const headIds = [
'station',
'min-lvl',
'status',
'dispatcher',
'dispatcher-lvl',
'routes-single',
'routes-double',
'general'
] as const;
export const headIconsIds = [
'user',
'like',
'spawn',
'timetableAll',
'timetableUnconfirmed',
'timetableConfirmed'
] as const;
export type HeadIdsTypes = (typeof headIds)[number] | (typeof headIconsIds)[number];
-34
View File
@@ -1,34 +0,0 @@
import { Availability, ActiveScenery } from '../../store/typings';
import { StationRoutes } from './StationRoutes';
export default interface Station {
name: string;
generalInfo?: {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
onlineInfo?: ActiveScenery;
}
-12
View File
@@ -1,12 +0,0 @@
import { StationRoutesInfo } from '../../store/typings';
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
singleElectrifiedNames: string[];
singleOtherNames: string[];
doubleElectrifiedNames: string[];
doubleOtherNames: string[];
sblNames: string[];
}
-38
View File
@@ -1,38 +0,0 @@
import { TrainStop } from '../../store/typings';
export default interface Train {
trainId: string;
mass: number;
length: number;
speed: number;
signal: string;
distance: number;
connectedTrack: string;
driverId: number;
trainNo: number;
driverName: string;
driverLevel: number;
currentStationName: string;
currentStationHash: string;
locoType: string;
online: boolean;
lastSeen: number;
region: string;
stockList: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: {
timetableId: number;
category: string;
route: string;
followingStops: TrainStop[];
TWR: boolean;
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
};
}
-233
View File
@@ -1,233 +0,0 @@
import { Filter } from '../../components/StationsView/typings';
import { Status } from '../../typings/common';
import { HeadIdsTypes } from '../data/stationHeaderNames';
import Station from '../interfaces/Station';
const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN,
Status.ActiveDispatcher.INVALID,
Status.ActiveDispatcher.NOT_LOGGED_IN,
Status.ActiveDispatcher.UNAVAILABLE,
Status.ActiveDispatcher.AFK,
Status.ActiveDispatcher.ENDING,
Status.ActiveDispatcher.NO_SPACE,
undefined
];
export const sortStations = (
a: Station,
b: Station,
sorter: { headerName: HeadIdsTypes; dir: number }
) => {
let diff = 0;
switch (sorter.headerName) {
case 'station':
return sorter.dir == 1 ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name);
case 'min-lvl':
diff = (a.generalInfo?.reqLevel || 0) - (b.generalInfo?.reqLevel || 0);
break;
case 'status':
diff =
(a.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(a.onlineInfo?.dispatcherStatus)) -
(b.onlineInfo?.dispatcherTimestamp ??
dispatcherStatusPriority.indexOf(b.onlineInfo?.dispatcherStatus));
break;
case 'dispatcher':
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') >
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return sorter.dir;
if (
(a.onlineInfo?.dispatcherName.toLowerCase() || '') <
(b.onlineInfo?.dispatcherName.toLowerCase() || '')
)
return -sorter.dir;
break;
case 'dispatcher-lvl':
diff = (a.onlineInfo?.dispatcherExp || 0) - (b.onlineInfo?.dispatcherExp || 0);
break;
case 'routes-single':
diff =
(a.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.single.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'routes-double':
diff =
(a.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1) -
(b.generalInfo?.routes.double.filter((r) => !r.hidden && !r.isInternal).length ?? -1);
break;
case 'user':
diff =
(b.onlineInfo ? b.onlineInfo.currentUsers : -1) -
(a.onlineInfo ? a.onlineInfo.currentUsers : -1);
break;
case 'like':
diff =
(a.onlineInfo ? a.onlineInfo.dispatcherRate : -Infinity) -
(b.onlineInfo ? b.onlineInfo.dispatcherRate : -Infinity);
break;
case 'spawn':
diff =
(a.onlineInfo ? a.onlineInfo.spawns.length : -1) -
(b.onlineInfo ? b.onlineInfo.spawns.length : -1);
break;
case 'timetableConfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.confirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.confirmed ?? -1);
break;
case 'timetableUnconfirmed':
diff =
(a.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1) -
(b.onlineInfo?.scheduledTrainCount.unconfirmed ?? -1);
break;
case 'timetableAll':
diff =
(a.onlineInfo?.scheduledTrainCount.all ?? -1) -
(b.onlineInfo?.scheduledTrainCount.all ?? -1);
break;
default:
break;
}
if (diff != 0) return Math.sign(diff) * sorter.dir;
return a.name.localeCompare(b.name);
};
export const filterStations = (station: Station, filters: Filter) => {
if (filters['free'] && (!station.onlineInfo || station.onlineInfo.dispatcherId == -1))
return false;
if (station.onlineInfo) {
const { dispatcherStatus } = station.onlineInfo;
const excludeEnding =
dispatcherStatus == Status.ActiveDispatcher.ENDING && filters['endingStatus'];
const excludeNotSigned =
(dispatcherStatus == Status.ActiveDispatcher.NOT_LOGGED_IN ||
dispatcherStatus == Status.ActiveDispatcher.UNAVAILABLE) &&
filters['unavailableStatus'];
const excludeAFK = dispatcherStatus == Status.ActiveDispatcher.AFK && filters['afkStatus'];
const excludeNoSpace =
dispatcherStatus == Status.ActiveDispatcher.NO_SPACE && filters['noSpaceStatus'];
const excludeOccupied = filters['occupied'] && dispatcherStatus != Status.ActiveDispatcher.FREE;
const excludeActiveTTs =
(dispatcherStatus == Status.ActiveDispatcher.FREE ||
station.onlineInfo.scheduledTrainCount.all != 0) &&
filters['withActiveTimetables'];
if (
excludeEnding ||
excludeAFK ||
excludeNoSpace ||
excludeNotSigned ||
excludeOccupied ||
excludeActiveTTs
)
return false;
if (
filters['onlineFromHours'] > 0 &&
dispatcherStatus <= Date.now() + filters['onlineFromHours'] * 3600000
)
return false;
}
const excludeNoActiveTTs =
filters['withoutActiveTimetables'] &&
(!station.onlineInfo || station.onlineInfo.scheduledTrainCount.all == 0);
if (excludeNoActiveTTs) return false;
if (
(station.generalInfo?.availability == 'nonPublic' || !station.generalInfo) &&
filters['nonPublic']
)
return false;
if (station.generalInfo) {
const { routes, availability, controlType, lines, reqLevel, signalType, SUP, ASDEK, authors } =
station.generalInfo;
if (availability == 'unavailable' && filters['unavailable'] && !station.onlineInfo)
return false;
if (availability == 'abandoned' && filters['abandoned'] && !station.onlineInfo) return false;
if (availability == 'default' && filters['default']) return false;
if (
availability != 'default' &&
filters['notDefault'] &&
!(availability == 'abandoned' || availability == 'unavailable')
)
return false;
if (filters['real'] && lines) return false;
if (filters['fictional'] && !lines) return false;
const otherAvailability =
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) return false;
if (
filters['no-1track'] &&
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
)
return false;
if (
filters['no-2track'] &&
(routes.doubleElectrifiedNames.length != 0 || routes.doubleOtherNames.length != 0)
)
return false;
if (routes.singleElectrifiedNames.length < filters['minOneWayCatenary']) return false;
if (routes.singleOtherNames.length < filters['minOneWay']) return false;
if (routes.doubleElectrifiedNames.length < filters['minTwoWayCatenary']) return false;
if (routes.doubleOtherNames.length < filters['minTwoWay']) return false;
if (filters[controlType]) return false;
if (filters[signalType]) return false;
if (filters['SUP'] && SUP) return false;
if (filters['noSUP'] && !SUP) return false;
if (filters['ASDEK'] && ASDEK) return false;
if (filters['noASDEK'] && !ASDEK) return false;
if (filters['SBL'] && routes.sblNames.length > 0) return false;
if (filters['PBL'] && routes.sblNames.length == 0) return false;
if (
filters['authors'].length > 3 &&
!authors?.map((a) => a.toLocaleLowerCase()).includes(filters['authors'].toLocaleLowerCase())
)
return false;
}
return true;
};
+46 -28
View File
@@ -4,20 +4,17 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
MOCK = 2
}
export const useApiStore = defineStore('apiStore', {
state: () => ({
dataStatuses: {
connection: Status.Data.Loading,
sceneries: Status.Data.Loading
sceneries: Status.Data.Loading,
vehicles: Status.Data.Loading
},
activeData: undefined as API.ActiveData.Response | undefined,
vehiclesData: undefined as API.Vehicles.Response | undefined,
donatorsData: [] as API.Donators.Response,
sceneryData: [] as StationJSONData[],
@@ -54,21 +51,22 @@ export const useApiStore = defineStore('apiStore', {
// Static data
this.fetchDonatorsData();
this.fetchStationsGeneralInfo();
// Active data schedueler
this.fetchActiveData();
this.setupActiveDataFetcher();
},
async setupActiveDataFetcher() {
if (this.activeDataScheduler) return;
this.activeDataScheduler = window.setInterval(() => {
this.fetchActiveData();
}, 25000);
this.fetchVehiclesInfo();
},
async fetchActiveData() {
if (import.meta.env.VITE_API_ACTIVE_DATA_MODE == 'mocking') {
import('../../tests/data/getActiveData.json').then((data) => {
console.warn('activeData: mocking mode');
this.activeData = data.default as API.ActiveData.Response;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded;
});
return;
}
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try {
@@ -77,8 +75,6 @@ export const useApiStore = defineStore('apiStore', {
this.activeData = response.data;
this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded;
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
} catch (error) {
this.dataStatuses.connection = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania danych online:', error);
@@ -96,17 +92,39 @@ export const useApiStore = defineStore('apiStore', {
},
async fetchStationsGeneralInfo() {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
try {
const sceneryData: StationJSONData[] = (
await this.client!.get<StationJSONData[]>('api/getSceneries')
).data;
if (!sceneryData) {
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
} catch (error) {
this.dataStatuses.sceneries = Status.Data.Error;
return;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o sceneriach:', error);
}
},
this.dataStatuses.sceneries = Status.Data.Loaded;
this.sceneryData = sceneryData;
async fetchVehiclesInfo() {
// if (import.meta.env.VITE_API_VEHICLES_MODE == 'mocking') {
// import('../../tests/data/vehicles.json').then((data) => {
// console.warn('vehicles.json: mocking mode');
// this.vehiclesData = data.default;
// this.dataStatuses.vehicles = Status.Data.Loaded;
// });
// return;
// }
try {
const response = await this.client!.get<API.Vehicles.Response>('api/getVehicles');
this.vehiclesData = response.data;
this.dataStatuses.vehicles = response.data ? Status.Data.Loaded : Status.Data.Warning;
} catch (error) {
this.dataStatuses.vehicles = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error);
}
}
}
});
+127 -51
View File
@@ -1,21 +1,27 @@
import { defineStore } from 'pinia';
import Train from '../scripts/interfaces/Train';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { parseSpawns } from './utils';
import { ActiveScenery, ScheduledTrain, StoreState } from './typings';
import { Status } from '../typings/common';
import Station from '../scripts/interfaces/Station';
import {
ActiveScenery,
CheckpointTrain,
Station,
StationRoutes,
Status,
Train
} from '../typings/common';
import { useApiStore } from './apiStore';
import { StationRoutes } from '../scripts/interfaces/StationRoutes';
import { MainStoreState } from './typings';
export const useMainStore = defineStore('store', {
const checkpointsTrains: Map<string, CheckpointTrain[]> = new Map();
const sceneriesTrains: Map<string, Train[]> = new Map();
export const useMainStore = defineStore('mainStore', {
state: () =>
({
region: { id: 'eu', value: 'PL1' },
region: { id: 'eu', value: 'PL1', name: 'PL1' },
isOffline: false,
isNewUpdate: false,
appUpdate: null,
dispatcherStatsName: '',
dispatcherStatsStatus: Status.Data.Initialized,
@@ -26,16 +32,16 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined,
modalLastClickedTarget: null,
mousePos: { x: 0, y: 0 },
popUpData: { key: null, content: '' }
}) as StoreState,
modalLastClickedTarget: null
}) as MainStoreState,
getters: {
trainList(): Train[] {
const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online)
.map((train) => {
@@ -53,8 +59,9 @@ export const useMainStore = defineStore('store', {
sceneryHash
) ?? [];
return {
trainId: train.driverName + train.trainNo.toString(),
const trainObj = {
id: train.id,
modalId: `${train.driverName}${train.trainNo}`, // simplified id for train modal
trainNo: train.trainNo,
mass: train.mass,
@@ -93,9 +100,37 @@ export const useMainStore = defineStore('store', {
}
: undefined
} as Train;
// Sceneries trains map
if (sceneriesTrains.has(train.currentStationName)) {
sceneriesTrains.set(train.currentStationName, [
...sceneriesTrains.get(train.currentStationName)!,
trainObj
]);
} else sceneriesTrains.set(train.currentStationName, [trainObj]);
// Checkpoints trains map
timetable?.stopList.forEach((stop, i) => {
if (/strong|podg\.|pe\./.test(stop.stopName)) {
const checkpointTrain: CheckpointTrain = {
train: trainObj,
checkpointStop: stop
};
if (checkpointsTrains.has(stop.stopNameRAW.toLowerCase())) {
checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [
...checkpointsTrains.get(stop.stopNameRAW.toLowerCase())!,
checkpointTrain
]);
} else checkpointsTrains.set(stop.stopNameRAW.toLowerCase(), [checkpointTrain]);
}
});
return trainObj;
});
},
// computed active sceneries
activeSceneryList(state): ActiveScenery[] {
const apiStore = useApiStore();
@@ -130,13 +165,14 @@ export const useMainStore = defineStore('store', {
dispatcherId: -1,
dispatcherExp: -1,
dispatcherIsSupporter: false,
scheduledTrains: [],
stationTrains: [],
dispatcherStatus: Status.ActiveDispatcher.FREE,
dispatcherTimestamp: -1,
isOnline: false,
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -156,8 +192,8 @@ export const useMainStore = defineStore('store', {
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
? Date.now() + 25500000
: scenery.dispatcherStatus > 5
? scenery.dispatcherStatus
: null;
? scenery.dispatcherStatus
: null;
list.push({
name: scenery.stationName,
@@ -176,8 +212,9 @@ export const useMainStore = defineStore('store', {
isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -195,42 +232,45 @@ export const useMainStore = defineStore('store', {
const station = this.stationList.find((s) => s.name === scenery.name);
const scheduledTrains = getScheduledTrains(
this.trainList,
station?.generalInfo,
scenery.name,
scenery.region
);
let checkpointsSet: Set<string> = new Set();
const stationTrains = getStationTrains(
this.trainList,
scheduledTrains,
this.region.id,
scenery.name
);
// Add checkpoints to active scenery data
checkpointsSet.add(scenery.name.toLowerCase());
// Remove checkpoint duplicates
const uniqueScheduledTrains = scheduledTrains.reduce(
(uniqueList, sTrain) =>
uniqueList.find((v) => v.trainId === sTrain.trainId)
? uniqueList
: [...uniqueList, sTrain],
[] as ScheduledTrain[]
);
station?.generalInfo?.checkpoints.forEach((cpName) => {
checkpointsSet.add(cpName.toLowerCase());
});
scenery.scheduledTrains = scheduledTrains;
scenery.stationTrains = stationTrains;
const checkpoints = Array.from(checkpointsSet);
scenery.scheduledTrainCount = {
all: uniqueScheduledTrains.length,
confirmed: uniqueScheduledTrains.filter((train) => train.stopInfo.confirmed).length,
unconfirmed: uniqueScheduledTrains.filter((train) => !train.stopInfo.confirmed).length
};
scenery.stationTrains =
sceneriesTrains.get(scenery.name)?.filter((sc) => sc.region == this.region.id) ?? [];
const uniqueTrainIds: string[] = [];
checkpoints.forEach((cp) => {
const scheduledTrains = checkpointsTrains.get(cp.toLowerCase());
if (!scheduledTrains) return;
scheduledTrains.forEach(({ train, checkpointStop }) => {
scenery.scheduledTrains.push({ train, checkpointStop });
if (uniqueTrainIds.includes(train.id) || train.region != this.region.id) return;
scenery.scheduledTrainCount.all += 1;
if (checkpointStop.confirmed) scenery.scheduledTrainCount.confirmed++;
else scenery.scheduledTrainCount.unconfirmed++;
uniqueTrainIds.push(train.id);
});
});
}
return allActiveSceneries;
},
// computed station data
stationList(): Station[] {
const apiStore = useApiStore();
@@ -248,6 +288,13 @@ export const useMainStore = defineStore('store', {
if (!route.isInternal) acc[routesKey].push(route.routeName);
if (route.isRouteSBL) acc['sblNames'].push(route.routeName);
acc.minRouteSpeed =
acc.minRouteSpeed == 0
? route.routeSpeed
: Math.min(route.routeSpeed, acc.minRouteSpeed);
acc.maxRouteSpeed = Math.max(route.routeSpeed, acc.maxRouteSpeed);
acc[tracksKey].push(route);
return acc;
@@ -259,7 +306,9 @@ export const useMainStore = defineStore('store', {
double: [],
doubleElectrifiedNames: [],
doubleOtherNames: [],
sblNames: []
sblNames: [],
minRouteSpeed: 0,
maxRouteSpeed: 0
} as StationRoutes
);
@@ -270,10 +319,37 @@ export const useMainStore = defineStore('store', {
...scenery,
authors: scenery.authors?.split(',').map((a) => a.trim()),
routes: routes,
checkpoints: scenery.checkpoints?.split(';') ?? []
checkpoints:
scenery.checkpoints && scenery.checkpoints.trim().length > 0
? scenery.checkpoints.split(';')
: []
}
};
});
},
allStationInfo(): Station[] {
const onlineUnsavedStations = this.activeSceneryList
.filter(
(scenery) =>
this.stationList.findIndex((st) => st.name == scenery.name) == -1 &&
scenery.region == this.region.id
)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...this.stationList.map((st) => ({
...st,
onlineInfo: this.activeSceneryList.find(
(os) => os.name == st.name && os.region == this.region.id
)
}))
];
}
}
});
-160
View File
@@ -1,160 +0,0 @@
import { defineStore } from 'pinia';
import inputData from '../data/options.json';
import { useMainStore } from './mainStore';
import { filterStations, sortStations } from '../scripts/utils/stationFilterUtils';
import { HeadIdsTypes } from '../scripts/data/stationHeaderNames';
import StorageManager from '../managers/storageManager';
import { Filter } from '../components/StationsView/typings';
const filterInitStates: Filter = {
default: false,
notDefault: false,
real: false,
fictional: false,
SPK: false,
SCS: false,
SPE: false,
SUP: false,
noSUP: false,
ASDEK: false,
noASDEK: false,
ręczne: false,
'ręczne+SPK': false,
'ręczne+SCS': false,
mechaniczne: false,
'mechaniczne+SPK': false,
'mechaniczne+SCS': false,
współczesna: false,
kształtowa: false,
historyczna: false,
mieszana: false,
SBL: false,
PBL: false,
minLevel: 0,
maxLevel: 20,
minOneWayCatenary: 0,
minOneWay: 0,
minTwoWayCatenary: 0,
minTwoWay: 0,
'include-selected': false,
'no-1track': false,
'no-2track': false,
free: true,
occupied: false,
ending: false,
nonPublic: false,
unavailable: true,
abandoned: true,
afkStatus: false,
endingStatus: false,
noSpaceStatus: false,
unavailableStatus: false,
unsignedStatus: false,
withActiveTimetables: false,
withoutActiveTimetables: false,
authors: '',
onlineFromHours: 0
};
export const useStationFiltersStore = defineStore('stationFiltersStore', {
state() {
return {
inputs: inputData,
filters: { ...filterInitStates },
sorterActive: { headerName: 'station' as HeadIdsTypes, dir: 1 },
lastClickedFilterId: ''
};
},
getters: {
areFiltersAtDefault: (state) => {
return Object.keys(state.filters).every((f) => state.filters[f] === filterInitStates[f]);
},
filteredStationList: (state) => {
const store = useMainStore();
const savedStationNames = store.stationList.map((s) => s.name);
const onlineUnsavedStations = store.activeSceneryList
.filter((os) => !savedStationNames.includes(os.name) && os.region == store.region.id)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...store.stationList.map((station) => ({
...station,
// append to 'onlineInfo' object for filtering legacy reasons - to optimize later (hopefully)
onlineInfo: store.activeSceneryList.find(
(os) => os.name == station.name && os.region == store.region.id
)
}))
]
.filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive));
}
},
actions: {
setupFilters() {
if (!StorageManager.isRegistered('options_saved')) return;
this.inputs.options.forEach((option) => {
if (!StorageManager.isRegistered(option.name)) return;
const savedValue = StorageManager.getBooleanValue(option.name);
this.filters[option.name] = savedValue;
option.value = !savedValue;
});
this.inputs.sliders.forEach((slider) => {
if (!StorageManager.isRegistered(slider.name)) return;
const savedValue = StorageManager.getNumericValue(slider.name);
this.filters[slider.name] = savedValue;
slider.value = savedValue;
});
},
changeFilterValue(name: string, value: any) {
this.filters[name] = value;
if (StorageManager.isRegistered('options_saved')) StorageManager.setValue(name, value);
},
resetFilters() {
this.filters = { ...filterInitStates };
this.inputs.options.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
this.inputs.sliders.forEach((slider) => {
slider.value = slider.defaultValue;
StorageManager.setNumericValue(slider.name, slider.defaultValue);
});
},
resetSectionOptions(section: string) {
this.inputs.options
.filter((option) => option.section == section)
.forEach((option) => {
option.value = option.defaultValue;
StorageManager.setBooleanValue(option.name, !option.defaultValue);
});
},
changeSorter(headerName: HeadIdsTypes) {
if (headerName == this.sorterActive.headerName)
this.sorterActive.dir = -1 * this.sorterActive.dir;
else this.sorterActive.dir = 1;
this.sorterActive.headerName = headerName;
}
}
});
+56
View File
@@ -0,0 +1,56 @@
import { defineStore } from 'pinia';
const isTooltip = (v: any): v is TooltipType => tooltipKeys.includes(v);
export const tooltipKeys = [
'DonatorTooltip',
'BaseTooltip',
'VehiclePreviewTooltip',
'SpawnsTooltip',
'UsersTooltip'
] as const;
export type TooltipType = (typeof tooltipKeys)[number];
export const useTooltipStore = defineStore('tooltipStore', {
state: () => ({
mousePos: [0, 0],
type: null as TooltipType | null,
content: ''
}),
actions: {
show(_e: MouseEvent, type: string, value?: string) {
if (!isTooltip(type)) return;
this.type = type;
this.content = value ?? '';
},
hide() {
this.type = null;
this.content = '';
},
handle(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-tooltip-type'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.type != null) this.hide();
return;
}
const tooltipType = targetEl.getAttribute('data-tooltip-type');
const tooltipContent = targetEl.getAttribute('data-tooltip-content');
if (tooltipType && tooltipContent) this.show(e, tooltipType, tooltipContent);
else if (this.type != null) this.hide();
this.mousePos[0] = e.pageX;
this.mousePos[1] = e.pageY;
}
}
});
+4 -134
View File
@@ -1,20 +1,10 @@
import { API } from '../typings/api';
import { Status } from '../typings/common';
import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export const popupKeys = ['DonatorPopUp', 'TrainCommentsPopUp', 'VehiclePreviewPopUp'] as const;
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type PopUpType = (typeof popupKeys)[number];
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface StoreState {
region: { id: string; value: string };
export interface MainStoreState {
region: { id: string; value: string; name: string };
isOffline: boolean;
isNewUpdate: boolean;
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
@@ -22,19 +12,6 @@ export interface StoreState {
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
modalLastClickedTarget: EventTarget | null;
mousePos: { x: number; y: number };
popUpData: { key: PopUpType | null; content: string };
}
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface StationJSONData {
@@ -62,110 +39,3 @@ export interface StationJSONData {
availability: Availability;
}
export interface ActiveScenery {
name: string;
hash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawns: { spawnName: string; spawnLength: number; isElectrified: boolean }[];
dispatcherName: string;
dispatcherRate: number;
dispatcherId: number;
dispatcherExp: number;
dispatcherIsSupporter: boolean;
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
unconfirmed: number;
};
}
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number | null;
}
+11 -184
View File
@@ -1,12 +1,4 @@
import Station from '../scripts/interfaces/Station';
import Train from '../scripts/interfaces/Train';
import { ScheduledTrain, StationTrain, StopStatus, TrainStop } from './typings';
export function getLocoURL(locoType: string): string {
return `https://rj.td2.info.pl/dist/img/thumbnails/${
locoType.includes('EN') ? locoType + 'rb' : locoType
}.png`;
}
import { ScenerySpawn, ScenerySpawnType } from '../typings/common';
export function getStatusTimestamp(stationStatus: any): number {
if (!stationStatus) return -2;
@@ -31,7 +23,7 @@ export function getStatusTimestamp(stationStatus: any): number {
return -1;
}
export function parseSpawns(spawnString: string | null) {
export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
if (!spawnString) return [];
if (spawnString === 'NO_SPAWN') return [];
@@ -41,183 +33,18 @@ export function parseSpawns(spawnString: string | null) {
const spawnLength = parseInt(spawnArray[2]);
const isElectrified = spawnArray[3] == 'True';
return { spawnName, spawnLength, isElectrified };
let spawnType: ScenerySpawnType = /EZT|POS|OSOB|PAS/i.test(spawnName)
? 'passenger'
: /TOW/i.test(spawnName)
? 'freight'
: /LUZ/i.test(spawnName)
? 'loco'
: 'all';
return { spawnName, spawnLength, isElectrified, spawnType };
});
}
export function getTimestamp(date: string | null): number {
return date ? new Date(date).getTime() : 0;
}
export function getTrainStopStatus(
stopInfo: TrainStop,
currentStationName: string,
sceneryName: string
) {
let stopStatus = StopStatus.ARRIVING,
stopLabel = '',
stopStatusID = -1;
if (stopInfo.terminatesHere && stopInfo.confirmed) {
stopStatus = StopStatus.TERMINATED;
stopLabel = 'Skończył bieg';
stopStatusID = 5;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName == sceneryName) {
stopStatus = StopStatus.DEPARTED;
stopLabel = 'Odprawiony';
stopStatusID = 2;
} else if (!stopInfo.terminatesHere && stopInfo.confirmed && currentStationName != sceneryName) {
stopStatus = StopStatus.DEPARTED_AWAY;
stopLabel = 'Odjechał';
stopStatusID = 4;
} else if (currentStationName == sceneryName && !stopInfo.stopped) {
stopStatus = StopStatus.ONLINE;
stopLabel = 'Na stacji';
stopStatusID = 0;
} else if (currentStationName == sceneryName && stopInfo.stopped) {
stopStatus = StopStatus.STOPPED;
stopLabel = 'Postój';
stopStatusID = 1;
} else if (currentStationName != sceneryName) {
stopStatus = StopStatus.ARRIVING;
stopLabel = 'W drodze';
stopStatusID = 3;
}
return { stopStatus, stopLabel, stopStatusID };
}
export function getCheckpointTrain(
train: Train,
trainStopIndex: number,
sceneryName: string
): ScheduledTrain {
const timetable = train.timetableData!;
const followingStops = timetable.followingStops;
const trainStop = followingStops[trainStopIndex];
const trainStopStatus = getTrainStopStatus(trainStop, train.currentStationName, sceneryName);
let prevStationName = '',
nextStationName = '';
let departureLine: string | null = null;
let arrivingLine: string | null = null;
let prevDepartureLine: string | null = null,
nextArrivalLine: string | null = null;
for (let i = trainStopIndex; i >= 0; i--) {
const stop = followingStops[i];
if (/strong|podg\.|pe\./g.test(stop.stopName) && !prevStationName && i <= trainStopIndex - 1)
prevStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (stop.arrivalLine != null && !arrivingLine && !/-|_|it|sbl/gi.test(stop.arrivalLine)) {
arrivingLine = stop.arrivalLine;
prevDepartureLine = followingStops[i - 1]?.departureLine || null;
}
}
for (let i = trainStopIndex; i < followingStops.length; i++) {
const stop = followingStops[i];
if (/strong|podg\.|pe\./g.test(stop.stopName) && !nextStationName && i > trainStopIndex)
nextStationName = stop.stopNameRAW.replace(/,.*/g, '');
if (stop.departureLine && !departureLine && !/-|_|it|sbl/gi.test(stop.departureLine)) {
departureLine = stop.departureLine;
nextArrivalLine = followingStops[i + 1]?.arrivalLine || null;
}
}
return {
checkpointName: trainStop.stopNameRAW,
trainNo: train.trainNo,
trainId: train.trainId,
signal: train.signal,
connectedTrack: train.connectedTrack,
driverName: train.driverName,
driverId: train.driverId,
currentStationName: train.currentStationName,
currentStationHash: train.currentStationHash,
category: timetable.category,
beginsAt: timetable.followingStops[0].stopNameRAW,
terminatesAt: timetable.followingStops[timetable.followingStops.length - 1].stopNameRAW,
nextStationName,
prevStationName,
stopInfo: trainStop,
stopLabel: trainStopStatus.stopLabel,
stopStatus: trainStopStatus.stopStatus,
stopStatusID: trainStopStatus.stopStatusID,
region: train.region,
arrivingLine: arrivingLine,
departureLine: departureLine,
nextArrivalLine,
prevDepartureLine
};
}
export function getScheduledTrains(
trainList: Train[],
stationGeneralInfo: Station['generalInfo'],
stationName: string,
region: string
// sceneryData: API.ActiveSceneries.Data,
): ScheduledTrain[] {
// stationGeneralInfo?.checkpoints.forEach((cp) => (cp.scheduledTrains.length = 0));
return trainList.reduce((acc: ScheduledTrain[], train) => {
if (!train.timetableData) return acc;
if (train.region != region) return acc;
const timetable = train.timetableData;
if (!timetable.sceneryNames.includes(stationName)) return acc;
const checkpoints = [stationName];
if (stationGeneralInfo?.checkpoints) checkpoints.push(...stationGeneralInfo.checkpoints);
const checkpointScheduledTrains: ScheduledTrain[] = [];
for (let i = 0; i < timetable.followingStops.length; i++) {
if (
new RegExp(`^(${checkpoints.join('|')})$`, 'i').test(
timetable.followingStops[i].stopNameRAW
)
) {
checkpointScheduledTrains.push(getCheckpointTrain(train, i, stationName));
}
}
acc.push(...checkpointScheduledTrains);
return acc;
}, []) as ScheduledTrain[];
}
export function getStationTrains(
trainList: Train[],
scheduledTrainList: ScheduledTrain[],
region: string,
stationName: string
): StationTrain[] {
return trainList
.filter(
(train) =>
train?.region === region && train.online && train.currentStationName === stationName
)
.map((train) => ({
driverName: train.driverName,
driverId: train.driverId,
trainNo: train.trainNo,
trainId: train.trainId,
stopStatus:
scheduledTrainList.find((st) => st.trainNo === train.trainNo)?.stopStatus || 'no-timetable'
}));
}
+1 -1
View File
@@ -4,7 +4,7 @@
.list_wrapper {
overflow-y: auto;
height: 90vh;
min-height: 550px;
min-height: 650px;
margin-top: 0.5em;
position: relative;
+13
View File
@@ -101,3 +101,16 @@
background-color: #be3728;
}
}
.spawn-badge {
color: white;
.length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > .name {
background-color: #007599;
}
}
+33 -2
View File
@@ -121,8 +121,6 @@ input {
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
a {
@@ -213,6 +211,7 @@ a.a-button {
&.btn--action {
background-color: #424242;
border-radius: 0.25em;
font-weight: bold;
&:hover {
background-color: #555;
@@ -229,6 +228,10 @@ a.a-button {
background-color: #3c3c3c;
}
&:hover {
background-color: #555;
}
}
&.btn--image {
@@ -284,6 +287,27 @@ a.a-button {
}
}
// Basic tooltip
[data-tooltip] {
cursor: help;
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(0, -50%);
content: attr(data-tooltip);
color: white;
background-color: #333;
box-shadow: 0 0 5px 2px #aaa;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.5em;
max-width: 300px;
z-index: 100;
}
@include smallScreen {
::-webkit-scrollbar {
width: 0.5em;
@@ -297,4 +321,11 @@ a.a-button {
background-color: #777;
}
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
transform: translate(-50%, 2em);
left: 50%;
width: 100%;
}
}
+3 -7
View File
@@ -1,11 +1,7 @@
.scenery-table-section {
position: relative;
height: 100%;
overflow-y: scroll;
}
table.scenery-history-table {
width: 100%;
table-layout: fixed;
min-width: 900px;
border-collapse: collapse;
thead {
@@ -25,7 +21,7 @@ table.scenery-history-table {
td {
padding: 0.75em;
border-bottom: solid 5px #111;
border-bottom: solid 5px #181818;
}
}
+8 -3
View File
@@ -1,4 +1,4 @@
import { Status } from './common';
import { Status, VehicleData } from './common';
export enum APIDataStatus {
OK = 'OK',
@@ -19,6 +19,7 @@ export namespace API {
apiStatuses?: APIStatuses;
}
}
export namespace DispatcherHistory {
export type Response = Data[];
@@ -38,6 +39,7 @@ export namespace API {
stationName: string;
timestampFrom: number;
timestampTo?: number;
statusHistory: string[];
}
}
@@ -128,8 +130,8 @@ export namespace API {
export type Response = Data[];
export interface Data {
id: string;
trainNo: number;
mass: number;
length: number;
speed: number;
@@ -161,7 +163,6 @@ export namespace API {
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
@@ -317,6 +318,10 @@ export namespace API {
export namespace Donators {
export type Response = string[];
}
export namespace Vehicles {
export type Response = VehicleData[];
}
}
export namespace GithubAPI {
+199
View File
@@ -1,3 +1,15 @@
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export namespace Status {
export enum ActiveDispatcher {
FREE = -3,
@@ -20,3 +32,190 @@ export namespace Status {
Warning = 3
}
}
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface Train {
id: string;
modalId: string;
mass: number;
length: number;
speed: number;
signal: string;
distance: number;
connectedTrack: string;
driverId: number;
trainNo: number;
driverName: string;
driverLevel: number;
currentStationName: string;
currentStationHash: string;
locoType: string;
online: boolean;
lastSeen: number;
region: string;
stockList: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: {
timetableId: number;
category: string;
route: string;
followingStops: TrainStop[];
TWR: boolean;
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
};
}
export interface Station {
name: string;
generalInfo?: StationGeneralInfo;
onlineInfo?: ActiveScenery;
}
export interface StationGeneralInfo {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
}
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
singleElectrifiedNames: string[];
singleOtherNames: string[];
doubleElectrifiedNames: string[];
doubleOtherNames: string[];
sblNames: string[];
minRouteSpeed: number;
maxRouteSpeed: number;
}
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface ActiveScenery {
name: string;
hash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawns: ScenerySpawn[];
dispatcherName: string;
dispatcherRate: number;
dispatcherId: number;
dispatcherExp: number;
dispatcherIsSupporter: boolean;
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains: Train[];
scheduledTrains: CheckpointTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
unconfirmed: number;
};
}
export interface ScenerySpawn {
spawnName: string;
spawnLength: number;
isElectrified: boolean;
spawnType: ScenerySpawnType;
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: number;
stopped: number;
stopTime: number | null;
}
export interface CheckpointTrain {
checkpointStop: TrainStop;
train: Train;
}
// Vehicles Data
export interface VehicleData {
id: number;
name: string;
type: string;
cabinName: string | null;
restrictions: Record<string, any> | null;
vehicleGroupsId: number;
group: VehiclesGroup;
}
export interface VehiclesGroup {
id: number;
name: string;
speed: number;
length: number;
weight: number;
cargoTypes: VehicleCargo[] | null;
locoProps: {
coldStart: boolean;
doubleManned: boolean;
} | null;
}
export interface VehicleCargo {
id: string;
weight: number;
}
+3 -2
View File
@@ -17,8 +17,9 @@
<JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
<div class="journal_refreshed-date">
{{ $t('journal.data-refreshed-at') }}:
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
</div>
<div class="list_wrapper" @scroll="handleScroll">
+24 -7
View File
@@ -17,8 +17,9 @@
<JournalStats :statsButtons="statsButtons" />
</div>
<div class="journal_refreshed-date" v-if="dataRefreshedAt">
{{ $t('journal.data-refreshed-at') }}: {{ dataRefreshedAt.toLocaleString($i18n.locale) }}
<div class="journal_refreshed-date">
{{ $t('journal.data-refreshed-at') }}:
{{ dataRefreshedAt?.toLocaleString($i18n.locale) ?? '---' }}
</div>
<div class="list_wrapper" @scroll="handleScroll">
@@ -125,6 +126,8 @@ interface TimetablesQueryParams {
dateTo?: string;
issuedFrom?: string;
terminatingAt?: string;
via?: string;
countFrom?: number;
countLimit?: number;
@@ -205,6 +208,8 @@ export default defineComponent({
'search-driver': '',
'search-dispatcher': '',
'search-issuedFrom': '',
'search-via': '',
'search-terminatingAt': '',
'search-date': ''
} as Journal.TimetableSearchType);
@@ -298,11 +303,17 @@ export default defineComponent({
},
setOptions(options: { [key: string]: string }) {
this.searchersValues['search-date'] = options['search-date'] ?? '';
this.searchersValues['search-driver'] = options['search-driver'] ?? '';
this.searchersValues['search-train'] = options['search-train'] ?? '';
this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
Object.keys(this.searchersValues).forEach((v) => {
this.searchersValues[v as Journal.TimetableSearchKey] = options[v] ?? '';
});
// this.searchersValues['search-date'] = options['search-date'] ?? '';
// this.searchersValues['search-driver'] = options['search-driver'] ?? '';
// this.searchersValues['search-train'] = options['search-train'] ?? '';
// this.searchersValues['search-dispatcher'] = options['search-dispatcher'] ?? '';
// this.searchersValues['search-issuedFrom'] = options['search-issuedFrom'] ?? '';
// this.searchersValues['search-via'] = options['search-via'] ?? '';
// this.searchersValues['search-terminatingAt'] = options['search-terminatingAt'] ?? '';
this.sorterActive.id =
(options['sorter-active'] as Journal.TimetableSorterKey) ?? 'timetableId';
@@ -346,6 +357,8 @@ export default defineComponent({
const authorName = this.searchersValues['search-dispatcher'].trim() || undefined;
const dateFrom = this.searchersValues['search-date'].trim() || undefined;
const issuedFrom = this.searchersValues['search-issuedFrom'].trim() || undefined;
const via = this.searchersValues['search-via'].trim() || undefined;
const terminatingAt = this.searchersValues['search-terminatingAt'].trim() || undefined;
let dateTo: string | undefined = undefined;
@@ -417,6 +430,10 @@ export default defineComponent({
queryParams['authorName'] = authorName;
queryParams['dateFrom'] = dateFrom;
queryParams['dateTo'] = dateTo;
queryParams['issuedFrom'] = issuedFrom;
queryParams['terminatingAt'] = terminatingAt;
queryParams['via'] = via;
queryParams['issuedFrom'] = issuedFrom;
queryParams['sortBy'] =
this.sorterActive.id != 'timetableId' ? this.sorterActive.id : undefined;
+16 -30
View File
@@ -22,8 +22,8 @@
v-for="(viewMode, i) in viewModes"
:key="i"
class="btn btn--option"
:class="{ checked: currentMode == viewMode.component }"
@click="setViewMode(viewMode.component)"
:data-checked="currentMode == viewMode.component"
>
{{ $t(viewMode.id) }}
</button>
@@ -121,10 +121,6 @@ export default defineComponent({
Status: Status.Data
}),
// activated() {
// this.loadSelectedCheckpoint();
// },
setup() {
const route = useRoute();
@@ -200,7 +196,7 @@ button.back-btn {
display: inline-block;
font-size: 1.5em;
font-size: 1.25em;
button {
margin: 1em auto;
@@ -215,11 +211,10 @@ button.back-btn {
position: relative;
width: 100%;
max-width: var(--max-container-width);
min-height: 100vh;
width: 100%;
margin: 1rem 0;
padding: 1rem 0;
text-align: center;
&[data-timetable-only='true'] {
@@ -228,30 +223,27 @@ button.back-btn {
}
}
.scenery-left {
.scenery-left,
.scenery-right {
position: relative;
overflow: auto;
background-color: #181818;
padding: 1em 0.5em;
height: 95vh;
min-height: 750px;
max-height: 1000px;
overflow: auto;
height: calc(100vh - 0.5em);
min-height: 800px;
max-height: 2000px;
}
.scenery-left {
display: flex;
flex-direction: column;
}
.scenery-right {
background: #181818;
padding: 1em 0.5em;
height: 95vh;
min-height: 750px;
max-height: 1000px;
display: grid;
grid-template-rows: auto 1fr auto;
grid-template-rows: auto 1fr;
gap: 1em;
}
@@ -261,18 +253,12 @@ button.back-btn {
.info-actions {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 0.75em;
.btn {
button {
padding: 0.5em;
box-shadow: 0 0 10px 4px #242424;
&[data-checked='true'] {
color: var(--clr-primary);
}
}
}
+30 -46
View File
@@ -11,16 +11,17 @@
<button
class="btn-donation btn--image"
ref="btn"
@click="isDonationModalOpen = true"
@focus="isDonationModalOpen = false"
@click="isDonationCardOpen = true"
@focus="isDonationCardOpen = false"
>
<img src="/images/icon-dollar.svg" alt="dollar donation icon" />
<span>{{ $t('donations.button-title') }}</span>
</button>
</div>
<Donation :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
<StationTable :stations="computedStationList" @toggleDonationModal="toggleDonationModal" />
<DonationCard :is-card-open="isDonationCardOpen" @toggle-card="toggleDonationCard" />
<StationTable @toggle-donation-card="toggleDonationCard" />
<StationStats />
</div>
</section>
</template>
@@ -29,41 +30,47 @@
import { defineComponent } from 'vue';
import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore';
import Donation from '../components/Global/Donation.vue';
import DonationCard from '../components/Global/DonationCard.vue';
import StationStats from '../components/StationsView/StationStats.vue';
import { initFilters, setupFilters } from '../managers/stationFilterManager';
import { reactive } from 'vue';
import { provide } from 'vue';
import { ActiveSorter } from '../components/StationsView/typings';
import { onMounted } from 'vue';
const filterInitStates = { ...initFilters };
export default defineComponent({
components: {
StationTable,
StationFilterCard,
Donation
StationStats,
DonationCard
},
data: () => ({
filterCardOpen: false,
modalHidden: true,
STORAGE_KEY: 'options_saved',
focusedStationName: '',
filterStore: useStationFiltersStore(),
store: useMainStore(),
isDonationCardOpen: false,
isDonationModalOpen: false
mainStore: useMainStore()
}),
mounted() {
this.filterStore.setupFilters();
},
setup() {
const filters = reactive(filterInitStates);
const activeSorter = reactive({ headerName: 'station', dir: 1 }) as ActiveSorter;
computed: {
computedStationList() {
return this.filterStore.filteredStationList;
}
provide('StationsView_filters', filters);
provide('StationsView_activeSorter', activeSorter);
onMounted(() => {
setupFilters(filters);
});
},
methods: {
toggleDonationModal(value: boolean) {
this.isDonationModalOpen = value;
toggleDonationCard(value: boolean) {
this.isDonationCardOpen = value;
}
}
});
@@ -73,30 +80,6 @@ export default defineComponent({
@import '../styles/variables.scss';
@import '../styles/responsive.scss';
@keyframes blinkAnim {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
.indicator-anim {
&-enter-active,
&-leave-active {
transition: all 0.25s ease-in-out;
}
&-enter,
&-leave-to {
transform: translateY(100%);
opacity: 0;
}
}
.stations-view {
position: relative;
display: flex;
@@ -114,6 +97,7 @@ export default defineComponent({
.stations-options {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
margin-bottom: 0.5em;
+2 -2
View File
@@ -20,11 +20,11 @@ import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch }
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train';
import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager';
import TrainStats from '../components/TrainsView/TrainStats.vue';
import { Train } from '../typings/common';
export default defineComponent({
components: {
@@ -109,7 +109,7 @@ export default defineComponent({
this.$nextTick(() => {
if (this.trainId) {
this.selectModalTrain(this.trainId);
this.selectModalTrainById(this.trainId);
}
});
}
+4 -2
View File
@@ -6,8 +6,10 @@ declare module '*.vue' {
export default component;
}
interface ImportMetaEnv {
readonly VITE_APP_API_DEV: string;
readonly VITE_APP_WS_DEV: string;
readonly VITE_API_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_VEHICLES_MODE: 'production' | 'mocking' | 'development';
readonly VITE_API_ACTIVE_DATA_MODE: 'production' | 'mocking' | 'development';
readonly VITE_UPDATE_TEST: 'test' | 'production';
}
interface ImportMeta {
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
["kowbojYT","matseb","peterminecraft333","MIlanSVK_SimRailCZ","kierownik_z_ulicy","luk31as","pppatryk123","Kryszakos","MilyPan","paweld","Isitkiwi","Krisoy007","zeswaq","robcioRK","Ugulele","Spanky","KapitanKoza","Kuba6396","BravuraLion","trichlor","jasieleczeq","trannelgamer","tommy001","Waffel","krytaqu","NadrazakHonza","zordem","Ludolog"]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24 -21
View File
@@ -7,49 +7,43 @@ export default defineConfig({
port: 5001,
open: true
},
preview: {
port: 4001,
open: true
},
publicDir: 'public',
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['/images/*.png', '/fonts/*.woff', '/fonts/*.woff2'],
includeAssets: ['/images/*.{png,svg,jpg}', '/fonts/*.{woff,woff2}'],
workbox: {
disableDevLogs: true,
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'),
urlPattern:
/^https:\/\/stacjownik.spythere.eu\/api\/(getVehicles|getDonators|getSceneries)/i,
handler: 'NetworkFirst',
options: {
cacheName: 'sceneries-cache',
cacheName: 'stacjownik-api-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: new RegExp('^https://raw.githubusercontent.com/Spythere/api/*', 'i'),
handler: 'NetworkFirst',
urlPattern: /^https:\/\/static.spythere.eu\/.*/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'github-api-cache',
cacheName: 'spythere-static-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
},
cacheableResponse: {
statuses: [0, 200, 404]
expiration: {
maxEntries: 100
}
}
}
@@ -60,5 +54,14 @@ export default defineConfig({
suppressWarnings: true
}
})
]
],
build: {
rollupOptions: {
output: {
entryFileNames: 'app-[name].js',
assetFileNames: 'app-[name].css',
chunkFileNames: 'chunk-[name].js'
}
}
}
});
+1964 -2600
View File
File diff suppressed because it is too large Load Diff