Compare commits

..

80 Commits

Author SHA1 Message Date
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
Spythere 3ef27e1d69 Merge pull request #85 from Spythere/development
Wersja 1.23.1
2024-04-01 13:00:28 +02:00
Spythere f53993c717 hotfix 2024-03-31 21:55:33 +02:00
Spythere 235c16e30f train modal 2024-03-31 21:37:14 +02:00
Spythere c3533f07ad literówka 2024-03-30 17:48:34 +01:00
Spythere d05579c5ee popupy 2024-03-30 13:24:39 +01:00
Spythere c8f53c2f06 hotfixy designu 2024-03-30 00:18:54 +01:00
Spythere b44f88ebcd src miniaturek 2024-03-29 23:37:26 +01:00
Spythere 7805d1350c responsywność 2024-03-29 23:35:56 +01:00
Spythere b17bd19433 zmiana położenia przycisku RJ ONLINE w dzienniku 2024-03-29 23:23:14 +01:00
Spythere c12a6cbacd zmiana rozłożenia elementów w modalu aktywnego pociągu 2024-03-29 23:21:15 +01:00
Spythere ba650238db poprawki rozmieszczenia popupu 2024-03-29 23:04:08 +01:00
Spythere d5ec9919e2 update modal (wip) 2024-03-29 20:34:56 +01:00
88 changed files with 7949 additions and 14793 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.0",
"version": "1.24.4",
"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": [
+63 -76
View File
@@ -1,10 +1,15 @@
<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>
<TrainModal v-if="store.chosenModalTrainId" />
<TrainModal />
</keep-alive>
</transition>
@@ -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>
@@ -34,21 +42,23 @@
</template>
<script lang="ts">
import { defineComponent, watch } from 'vue';
import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '.././package.json';
import { version } from '.././package.json';
import { Status } from './typings/common';
import { useMainStore } from './store/mainStore';
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';
import { usePopupStore } from './store/popupStore';
const STORAGE_VERSION_KEY = 'app_version';
@@ -58,18 +68,22 @@ export default defineComponent({
StatusIndicator,
AppHeader,
TrainModal,
PopUp
UpdateCard,
Tooltip
},
data: () => ({
VERSION: version,
store: useMainStore(),
apiStore: useApiStore(),
popupStore: usePopupStore(),
tooltipStore: useTooltipStore(),
isUpdateCardOpen: false,
currentLang: 'pl',
releaseURL: '',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
nextUpdateTime: 0
}),
created() {
@@ -77,69 +91,52 @@ export default defineComponent({
},
async mounted() {
window.addEventListener('focus', () => {
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
this.apiStore.fetchActiveData();
});
// popup handling
window.addEventListener('mousemove', (e: MouseEvent) => {
e.stopPropagation();
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-popup-key'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.popupStore.currentPopupComponent != null) this.popupStore.onPopUpHide();
return;
}
const popupComponentKey = targetEl.getAttribute('data-popup-key');
const popupContent = targetEl.getAttribute('data-popup-content');
if (popupComponentKey && popupContent)
this.popupStore.onPopUpShow(e, popupComponentKey, popupContent);
else if (this.popupStore.currentPopupComponent != null) this.popupStore.onPopUpHide();
});
watch(
() => this.store.blockScroll,
(value) => {
if (value) document.body.classList.add('no-scroll');
else document.body.classList.remove('no-scroll');
}
);
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() {
@@ -171,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');
@@ -228,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() {
@@ -243,7 +225,7 @@ export default defineComponent({
grid-template-columns: 100%;
min-height: 100vh;
position: relative;
overflow: hidden;
}
.app_main {
@@ -261,10 +243,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>
-48
View File
@@ -1,48 +0,0 @@
<template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
<div class="modal_content">
<h1 class="header">Aktualizacja Stacjownika</h1>
<h2>wersja {{ version }}</h2>
<b>Co nowego?</b>
<p>
<ul>
<li>test</li>
</ul>
</p>
</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
};
},
methods: {
toggleModal(value: boolean) {
this.$emit('toggleModal', value);
}
}
});
</script>
<style lang="scss" scoped>
.modal_content {
text-align: center;
padding: 1em;
height: 80vh;
min-height: 550px;
}
</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;
}
}
+28 -25
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"
@@ -27,11 +30,11 @@
<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.split(':')[0]"
: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')"
>
@@ -34,15 +34,6 @@
<strong v-else>
{{ timetable.driverName }}
</strong>
<button
v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('journal.timetable-online-button') }}
</button>
</span>
<span class="general-time">
@@ -66,10 +57,19 @@
!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--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="train icon" />
<b>{{ $t('journal.timetable-online-button') }}</b>
</button>
</span>
</div>
</template>
@@ -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,22 +163,12 @@ export default defineComponent({
}
}
.general-train {
cursor: pointer;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 0.25em;
}
.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;
}
}
@@ -5,7 +5,7 @@
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click.stop.prevent="showExtraInfo.value = !showExtraInfo.value"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
@@ -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;
-55
View File
@@ -1,55 +0,0 @@
<template>
<div class="popup" v-show="popupStore.currentPopupComponent" ref="preview">
<component v-if="popupStore.currentPopupComponent" :is="popupStore.currentPopupComponent" />
</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 { usePopupStore } from '../../store/popupStore';
export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp },
data() {
return {
popupStore: usePopupStore()
};
},
watch: {
'popupStore.popupPosition': {
deep: true,
handler(val: typeof this.popupStore.popupPosition) {
const previewEl = this.$refs['preview'] as HTMLElement;
previewEl.style.top = `${val.y}px`;
previewEl.style.left = `${val.x}px`;
previewEl.style.transform = 'translateY(1.5rem)';
this.$nextTick(() => {
const isOutside =
val.y + previewEl.getBoundingClientRect().height > window.innerHeight + window.scrollY;
// previewEl.style.transform = `translate(-${~~((val.x / window.innerWidth) * 100)}%, calc(${isOutside ? '-100% - 1.5rem' : '1.5rem'}))`;
previewEl.style.transform = `translate(-${~~((val.x / window.innerWidth) * 100)}%, calc(${
isOutside ? '-100% - 1.5rem' : '1.5rem'
}))`;
});
}
}
}
});
</script>
<style lang="scss" scoped>
.popup {
position: absolute;
z-index: 250;
max-width: 400px;
text-align: center;
}
</style>
@@ -72,18 +72,15 @@
<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: {
+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 -3
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: {
@@ -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;
}
@@ -71,17 +71,14 @@
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';
export default defineComponent({
name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin],
mixins: [dateMixin],
props: {
station: {
type: Object as PropType<Station>
@@ -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>
+327 -323
View File
@@ -1,365 +1,379 @@
<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"
>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/>
<img
class="sort-icon"
v-if="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.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="sorterActive.headerName == headerName"
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</span>
</th>
</tr>
</thead>
<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"
<div class="table_wrapper" v-else-if="filteredStationList.length > 0">
<table>
<thead>
<tr>
<th
v-for="headerName in headIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-text"
:class="headerName"
>
<td class="station-name" :class="station.generalInfo?.availability">
<b v-if="station.generalInfo?.project" style="color: salmon">{{
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<span class="header_wrapper">
<div v-html="$t(`sceneries.headers.${headerName}`)"></div>
<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"
<img
class="sort-icon"
v-if="activeSorter.headerName == headerName"
:src="`/images/icon-arrow-${activeSorter.dir == 1 ? 'asc' : 'desc'}.svg`"
alt="sort icon"
/>
</td>
</span>
</th>
<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>
<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}`)"
/>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<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>
<td class="station-dispatcher-exp">
<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.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
:style="
calculateExpStyle(
station.onlineInfo.dispatcherExp,
station.onlineInfo.dispatcherIsSupporter
)
v-if="
station.generalInfo.reqLevel > -1 &&
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{
station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp
}}
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</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-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</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>
<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')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</td>
</span>
</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>
<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>
<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">
<td class="station-tracks">
<div v-if="station.generalInfo">
<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)"
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>
<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}`)
"
/>
<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>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<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>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<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>
<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-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}`)
"
>
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }}
</td>
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span>
<td
class="station-schedules unconfirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }}
</td>
<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}`)
"
/>
<td
class="station-schedules confirmed"
style="width: 30px"
:class="{ inactive: !station.onlineInfo }"
>
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }}
</td>
</tr>
</tbody>
</table>
<img
v-if="station.generalInfo?.SUP"
class="icon-info"
src="/images/icon-SUP.svg"
alt="SUP (RASP-UZK)"
:title="$t('sceneries.info.SUP')"
/>
<Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && stations.length == 0"
/>
<img
v-if="station.generalInfo?.ASDEK"
class="icon-info"
src="/images/icon-ASDEK.svg"
alt="dSAT ASDEK"
:title="$t('sceneries.info.ASDEK')"
/>
<div class="no-stations" v-else-if="stations.length == 0">
{{ $t('sceneries.no-stations') }}
</div>
<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>
<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 { usePopupStore } from '../../store/popupStore';
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],
mixins: [styleMixin, dateMixin],
data: () => ({
headIconsIds,
headIds,
lastSelectedStationName: ''
lastSelectedStationName: '',
getChangedFilters
}),
computed: {
sorterActive() {
return this.stationFiltersStore.sorterActive;
}
},
setup() {
const mainStore = useMainStore();
const apiStore = useApiStore();
const popupStore = usePopupStore();
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,
popupStore
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',
@@ -370,10 +384,10 @@ export default defineComponent({
});
},
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
openDonationCard(e: Event) {
this.$emit('toggleDonationCard', true);
this.mainStore.modalLastClickedTarget = e.target;
this.popupStore.currentPopupComponent = null;
this.tooltipStore.hide();
},
openForumSite(e: Event, url: string | undefined) {
@@ -382,10 +396,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;
}
}
});
@@ -398,33 +416,19 @@ 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;
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>{{ popupStore.currentPopupContent }}</span>
<div class="tooltip-content">
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore()
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>{{ popupStore.currentPopupContent }}</span>
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore()
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>
+74
View File
@@ -0,0 +1,74 @@
<template>
<div class="tooltip" v-show="tooltipStore.type" ref="preview">
<component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
export default defineComponent({
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
data() {
return {
tooltipStore: useTooltipStore()
};
},
watch: {
'tooltipStore.mousePos': {
deep: true,
// [x, y]
handler(val: [number, number]) {
this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val[0] >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%';
translateX = '-100%';
} else {
previewEl.style.left = `${val[0]}px`;
translateX = '-50%';
}
previewEl.style.top = `${val[1]}px`;
const isOutside =
val[1] + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)';
previewEl.style.transform = `translate(${translateX}, ${translateY})`;
});
}
}
}
});
</script>
<style lang="scss" scoped>
.tooltip {
position: absolute;
z-index: 250;
max-width: 400px;
text-align: center;
}
</style>
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="trains.length != 0">
<span v-for="(train, i) in trains">
<template v-if="i > 0"> | </template>
<b>{{ train.trainNo }}</b> {{ train.driverName }}
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { 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>
@@ -1,5 +1,5 @@
<template>
<div class="popup-content">
<div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
@@ -7,30 +7,31 @@
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="popupStore.currentPopupContent"
v-if="tooltipStore.type"
@load="onImageLoad"
@error="onImageError"
@click="popupStore.onPopUpHide"
width="300"
height="176"
class="rounded-md w-full h-auto"
:src="`https://spythere.github.io/api/td2/images/${popupStore.currentPopupContent}--300px.jpg`"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
/>
<div class="vehicle-name" v-if="imageState != 'error'">
{{ popupStore.currentPopupContent.replace(/_/g, ' ') }}
<div v-if="imageState == 'error'" class="error-placeholder"></div>
<div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { usePopupStore } from '../../store/popupStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
popupStore: usePopupStore(),
tooltipStore: useTooltipStore(),
imageState: 'loading'
};
},
@@ -39,6 +40,12 @@ export default defineComponent({
this.imageState = 'loading';
},
watch: {
'tooltipStore.type'(prev, val) {
if (prev != val) this.imageState = 'loading';
}
},
methods: {
onImageLoad() {
this.imageState = 'loaded';
@@ -54,9 +61,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.popup-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
.tooltip-content {
width: 300px;
min-height: 200px;
background-color: #333;
@@ -83,4 +88,8 @@ img {
color: #ccc;
text-wrap: wrap;
}
.error-placeholder {
height: 176px;
}
</style>
+133 -91
View File
@@ -1,57 +1,71 @@
<template>
<div class="train-info">
<div class="train-info" :data-extended="extended">
<section class="train-general">
<div class="general-info">
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }}
</span>
<span
class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
>
<span class="train-badge twr" v-if="train.timetableData?.TWR" :title="$t('general.TWR')">
TWR
<div class="general-top-bar">
<div>
<b class="warning-timeout" v-if="train.isTimeout" :title="$t('trains.timeout')">?</b>
<span class="timetable-id" v-if="train.timetableData">
#{{ train.timetableData.timetableId }}
</span>
<span class="train-badge skr" v-if="train.timetableData?.SKR" :title="$t('general.SKR')">
SKR
</span>
</span>
<strong>
<span v-if="train.timetableData" class="text--primary"
>{{ train.timetableData.category }}&nbsp;</span
<span
class="timetable-warnings"
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<span
class="train-badge twr"
v-if="train.timetableData?.TWR"
:title="$t('general.TWR')"
>
TWR
</span>
<span
class="train-badge skr"
v-if="train.timetableData?.SKR"
:title="$t('general.SKR')"
>
SKR
</span>
</span>
<div class="train-driver">
<strong>
<span v-if="train.timetableData" class="text--primary"
>{{ train.timetableData.category }}&nbsp;</span
>
<span class="train-number">{{ train.trainNo }}</span>
</strong>
<span>&bull;</span>
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.driver-message')"
class="level-badge driver"
:style="calculateExpStyle(train.driverLevel, train.isSupporter)"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
{{ train.driverLevel < 2 ? 'L' : `${train.driverLevel}` }}
</b>
<span v-else>{{ train.driverName }}</span>
<div class="train-driver">
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
</b>
<button
class="btn--image btn--action btn-timetable"
@click="navigateToJournal"
v-if="extended"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('trains.journal-button') }}
<span v-else>{{ train.driverName }}</span>
</div>
</div>
<div v-if="extended">
<button class="btn-timetable btn--image btn--action" @click="navigateToJournal">
<img src="/images/icon-train.svg" alt="train icon" />
<span>
{{ $t('trains.journal-button') }}
</span>
</button>
<button class="btn-exit btn--image btn--action" @click="closeModal">
<img src="/images/icon-exit.svg" alt="modal exit icon" />
</button>
</div>
</div>
@@ -60,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
)})`"
>
@@ -79,7 +93,7 @@
</div>
<div class="general-status">
<div class="timetable-progress" v-if="train.timetableData">
<div class="status-timetable-progress" v-if="train.timetableData">
<ProgressBar :progressPercent="confirmedPercentage(train.timetableData.followingStops)" />
<span class="progress-distance">
@@ -103,12 +117,29 @@
</div>
</div>
<div class="driver_position text--grayed" style="margin-top: 0.25em">
<div class="general-stats" v-if="extended">
<div>
<img src="/images/icon-length.svg" alt="length icon" />
{{ train.length }}m
</div>
<div>
<img src="/images/icon-mass.svg" alt="mass icon" />
{{ (train.mass / 1000).toFixed(1) }}t
</div>
<div>
<img src="/images/icon-speed.svg" alt="speed icon" />
{{ train.speed }} km/h
</div>
</div>
<div class="text--grayed" style="margin-top: 0.25em">
{{ displayTrainPosition(train) }}
</div>
</section>
<section class="train-stats">
<section class="train-stats" v-if="!extended">
<StockList :trainStockList="train.stockList" :tractionOnly="true" />
<div>
@@ -132,13 +163,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 { usePopupStore } from '../../store/popupStore';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
@@ -157,8 +187,7 @@ export default defineComponent({
data() {
return {
store: useMainStore(),
apiStore: useApiStore(),
popupStore: usePopupStore()
apiStore: useApiStore()
};
},
@@ -203,6 +232,10 @@ export default defineComponent({
grid-template-columns: 2fr 1fr;
grid-template-rows: 1fr;
&[data-extended='true'] {
grid-template-columns: 1fr;
}
padding: 1em;
background-color: #1a1a1a;
@@ -214,12 +247,6 @@ export default defineComponent({
vertical-align: text-bottom;
}
.btn-timetable {
display: inline-block;
padding: 0.25em;
margin-left: 0.5em;
}
.timetable-id {
color: #d2d2d2;
}
@@ -243,14 +270,29 @@ export default defineComponent({
font-size: 0.8em;
}
.general-info {
.general-top-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5em;
gap: 0.25em;
margin-right: 1.5em;
& > div {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
}
}
.btn-timetable {
padding: 0.25em;
}
.btn-exit {
padding: 0.25em;
}
.general-status {
display: flex;
align-items: center;
@@ -259,6 +301,27 @@ export default defineComponent({
gap: 0.25em;
}
.general-stats {
display: flex;
gap: 0.5em;
flex-wrap: wrap;
& > div {
display: flex;
align-items: center;
gap: 0.25em;
}
img {
width: 1.5em;
}
}
.general-timetable {
display: flex;
align-items: center;
}
.status-badges {
display: flex;
flex-wrap: wrap;
@@ -270,17 +333,7 @@ export default defineComponent({
}
}
.general-timetable {
display: flex;
align-items: center;
}
.timetable-warnings {
display: flex;
gap: 0.25em;
}
.timetable-progress {
.status-timetable-progress {
display: flex;
align-items: center;
flex-wrap: wrap;
@@ -290,30 +343,19 @@ export default defineComponent({
margin-right: 0.25em;
}
.timetable-warnings {
display: flex;
gap: 0.25em;
}
@include smallScreen() {
.train-info {
grid-template-columns: 1fr;
gap: 1em 0;
text-align: center;
}
.general-info,
.general-status,
.general-timetable {
justify-content: center;
}
.timetable-progress {
justify-content: center;
}
.comments {
flex-direction: column;
justify-content: center;
img {
margin: 0 0 0.5em 0;
}
.btn-timetable > span {
display: none;
}
}
</style>
+31 -53
View File
@@ -1,11 +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">
<button class="btn exit" @click="closeModal">
<img src="/images/icon-exit.svg" alt="close card" />
</button>
<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>
@@ -17,54 +13,40 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
import { Train } from '../../typings/common';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
mixins: [modalTrainMixin],
activated() {
const contentEl = this.$refs['content'] as HTMLElement;
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.modalId == this.store.chosenModalTrainId);
}
},
this.$nextTick(() => {
contentEl.focus();
});
watch: {
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);
}
});
}
}
});
</script>
<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;
}
}
.exit {
position: absolute;
top: 0;
right: 0;
margin: 0.5em 1em;
padding: 0.25em;
z-index: 201;
img {
width: 1.5rem;
vertical-align: middle;
}
}
.train-modal {
position: fixed;
@@ -72,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;
@@ -94,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;
@@ -116,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;
> * {
+1 -1
View File
@@ -81,11 +81,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 { Train } from '../../typings/common';
export interface TrainScheduleStop {
nameHtml: string;
+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;
}
+39
View File
@@ -4,6 +4,7 @@
"timetables",
"reality",
"package-access",
"station-type",
"access",
"control",
"blockades",
@@ -61,6 +62,20 @@
"value": false,
"defaultValue": false
},
{
"id": "junction",
"name": "junction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "nonJunction",
"name": "nonJunction",
"section": "station-type",
"value": true,
"defaultValue": true
},
{
"id": "SPK",
"name": "SPK",
@@ -265,6 +280,7 @@
"name": "minLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 0,
"defaultValue": 0
},
@@ -273,14 +289,34 @@
"name": "maxLevel",
"minRange": 0,
"maxRange": 20,
"step": 1,
"value": 20,
"defaultValue": 20
},
{
"id": "min-vmax",
"name": "minVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 0,
"defaultValue": 0
},
{
"id": "max-vmax",
"name": "maxVmax",
"minRange": 0,
"maxRange": 200,
"step": 10,
"value": 200,
"defaultValue": 200
},
{
"id": "routes-1t-cat",
"name": "minOneWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
@@ -289,6 +325,7 @@
"name": "minOneWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
@@ -297,6 +334,7 @@
"name": "minTwoWayCatenary",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
},
@@ -305,6 +343,7 @@
"name": "minTwoWay",
"minRange": 0,
"maxRange": 5,
"step": 1,
"value": 0,
"defaultValue": 0
}
+57 -23
View File
@@ -26,6 +26,13 @@
"TWR": "High risk freight train",
"SKR": "Train with exceeded gauge"
},
"update": {
"title": "Stacjownik update!",
"confirm": "ROGER THAT!",
"no-data": "No data about the latest app update has been found",
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
},
"app": {
"sceneries": "SCENERIES",
"trains": "TRAINS",
@@ -41,13 +48,7 @@
"footer": {
"discord": "Stacjownik Discord server"
},
"update": {
"title": "New version of the app is available!",
"paragraph1": "Enjoy the application and may the green signal be with you!",
"release-link": "Click here to browse version changelog (GitHub)",
"confirm-button": "UPDATE NOW",
"later-button": "LATER"
},
"vehicle-preview": {
"loading": "Loading preview...",
"error": "Oops! The vehicle preview seems to be missing! :/"
@@ -80,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 ",
@@ -98,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:",
@@ -160,17 +174,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",
@@ -181,11 +200,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",
@@ -195,7 +214,6 @@
"SCS-R": "SCS + MANUAL",
"SCS-M": "SCS + MECH.",
"SPE": "SPE",
"manual": "MANUAL",
"mechanical": "MECHANICAL",
@@ -218,14 +236,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",
@@ -276,7 +300,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!",
@@ -366,7 +400,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",
+57 -18
View File
@@ -4,7 +4,7 @@
"header": "Grosza daj Stacjownikowi!",
"donator-title": "Projekt ma już ponad <b>{count}</b> wspierających, w tym:",
"p1": "<b>Hej o7!</b> Z tej strony Spythere, twórca Stacjownika, Pojazdownika oraz kilku innych aplikacji wspomagających rozgrywkę symulatora Train Driver 2!",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdeczne zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2": "{b1} to narzędzie całkowicie darmowe, tworzone i rozwijane dla społeczności symulatora TD2 nieprzerwanie od 2020 roku. Jednakże, część projektu jest podtrzymywana wyłącznie dzięki mojemu prywatnemu wkładowi finansowemu. Funkcje takie jak {b2} czy też {b3} działający na moim {link} (na który serdecznie zapraszam) muszą działać na wydzielonym serwerze, gdzie będą mogły zbierać i przetwarzać dane, aby następnie pokazać je na stronie.",
"p2-b1": "Stacjownik",
"p2-b2": "Dziennik",
"p2-b3": "Stacjobot",
@@ -26,6 +26,13 @@
"TWR": "Towar niebezpieczny wysokiego ryzyka",
"SKR": "Przekroczona skrajnia"
},
"update": {
"title": "Aktualizacja Stacjownika!",
"confirm": "PRZYJĄŁEM!",
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
},
"app": {
"sceneries": "SCENERIE",
"trains": "POCIĄGI",
@@ -70,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 ",
@@ -88,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:",
@@ -151,17 +171,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",
@@ -172,11 +197,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",
@@ -202,20 +227,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):",
@@ -265,7 +294,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!",
@@ -345,7 +384,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.",
+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);
}
}
});
+11 -17
View File
@@ -1,36 +1,30 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { usePopupStore } from '../store/popupStore';
import { useTooltipStore } from '../store/tooltipStore';
import { Train } from '../typings/common';
export default defineComponent({
data() {
return {
store: useMainStore(),
popupStore: usePopupStore()
tooltipStore: useTooltipStore()
};
},
computed: {
chosenTrain() {
return this.store.trainList.find((train) => train.trainId == this.store.chosenModalTrainId);
}
},
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.popupStore.currentPopupComponent = null;
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
document.body.classList.remove('no-scroll');
}, 150);
this.tooltipStore.hide();
}
}
});
-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
};
};
-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;
};
-17
View File
@@ -4,9 +4,6 @@ import { Status } from '../typings/common';
import { StationJSONData } from './typings';
import axios, { AxiosInstance } from 'axios';
// Update seconds cron for active data scheduler
const UPDATE_SECONDS = [3, 23, 43];
export enum APIMode {
PRODUCTION = 0,
DEV = 1,
@@ -57,18 +54,6 @@ 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);
},
async fetchActiveData() {
@@ -80,8 +65,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);
+128 -49
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,14 +32,16 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined,
blockScroll: false,
modalLastClickedTarget: null
}) as StoreState,
}) as MainStoreState,
getters: {
trainList(): Train[] {
const apiStore = useApiStore();
checkpointsTrains.clear();
sceneriesTrains.clear();
return (apiStore.activeData?.trains ?? [])
.filter((train) => train.timetable || train.online)
.map((train) => {
@@ -45,13 +53,15 @@ export const useMainStore = defineStore('store', {
const sceneryNames =
train.timetable?.sceneries?.map(
(sceneryHash) =>
this.activeSceneryList.find((st) => st.hash === sceneryHash)?.name ??
apiStore.activeData?.activeSceneries?.find((st) => st.stationHash === sceneryHash)
?.stationName ??
apiStore.sceneryData.find((sd) => sd.hash === sceneryHash)?.name ??
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,
@@ -90,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();
@@ -127,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,
@@ -153,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,
@@ -173,8 +212,9 @@ export const useMainStore = defineStore('store', {
isOnline: scenery.isOnline == 1,
scheduledTrains: [],
stationTrains: [],
scheduledTrains: [],
scheduledTrainCount: {
all: 0,
confirmed: 0,
@@ -192,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();
@@ -245,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;
@@ -256,7 +306,9 @@ export const useMainStore = defineStore('store', {
double: [],
doubleElectrifiedNames: [],
doubleOtherNames: [],
sblNames: []
sblNames: [],
minRouteSpeed: 0,
maxRouteSpeed: 0
} as StationRoutes
);
@@ -267,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
)
}))
];
}
}
});
-37
View File
@@ -1,37 +0,0 @@
import { defineStore } from 'pinia';
export const popupKeys = ['DonatorPopUp', 'TrainCommentsPopUp', 'VehiclePreviewPopUp'] as const;
export type PopUp = (typeof popupKeys)[number];
const isPopUp = (v: any): v is PopUp => popupKeys.includes(v);
export const usePopupStore = defineStore('popupStore', {
state: () => ({
popupPosition: { x: 0, y: 0 },
currentPopupComponent: null as PopUp | null,
currentPopupContent: '',
donatorPopupVisible: false
}),
actions: {
onPopUpShow(e: MouseEvent, componentKey: string, value?: string) {
if (!isPopUp(componentKey)) return;
this.popupPosition.x = e.pageX;
this.popupPosition.y = e.pageY;
this.currentPopupComponent = componentKey;
this.currentPopupContent = value ?? '';
},
onPopUpMove(e: MouseEvent) {
this.popupPosition.x = e.pageX;
this.popupPosition.y = e.pageY;
},
onPopUpHide() {
this.currentPopupComponent = null;
this.currentPopupContent = '';
}
}
});
-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 -131
View File
@@ -1,39 +1,19 @@
import { API } from '../typings/api';
import { Status } from '../typings/common';
import { Availability, CheckpointTrain, StationRoutesInfo, Status } from '../typings/common';
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface StoreState {
region: { id: string; value: string };
export interface MainStoreState {
region: { id: string; value: string; name: string };
isOffline: boolean;
isNewUpdate: boolean;
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
driverStatsData?: API.DriverStats.Response;
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
blockScroll: boolean;
modalLastClickedTarget: EventTarget | null;
}
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface StationJSONData {
name: string;
abbr: string;
@@ -59,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
View File
@@ -6,6 +6,7 @@
height: 90vh;
min-height: 550px;
margin-top: 0.5em;
position: relative;
padding-right: 0.2em;
}
+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;
}
}
+23 -2
View File
@@ -55,6 +55,8 @@ body {
-webkit-font-smoothing: antialiased !important;
overflow-y: scroll;
overflow-x: hidden;
position: relative;
&.no-scroll {
overflow-y: hidden;
@@ -119,8 +121,6 @@ input {
height: 7px;
background-color: lightgreen;
border-radius: 50%;
margin-left: 10px;
}
a {
@@ -211,6 +211,7 @@ a.a-button {
&.btn--action {
background-color: #424242;
border-radius: 0.25em;
font-weight: bold;
&:hover {
background-color: #555;
@@ -296,3 +297,23 @@ a.a-button {
}
}
}
// Basic tooltip
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translate(10px, -50%);
content: attr(data-tooltip);
color: white;
background-color: #171717;
border-radius: 0.5em;
padding: 0.5em;
margin: 0 0.25em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: help;
}
+1 -2
View File
@@ -128,8 +128,8 @@ export namespace API {
export type Response = Data[];
export interface Data {
id: string;
trainNo: number;
mass: number;
length: number;
speed: number;
@@ -161,7 +161,6 @@ export namespace API {
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
+169
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,160 @@ 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;
}
+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">
+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">
+3 -3
View File
@@ -200,7 +200,7 @@ button.back-btn {
display: inline-block;
font-size: 1.5em;
font-size: 1.25em;
button {
margin: 1em auto;
@@ -233,7 +233,7 @@ button.back-btn {
background-color: #181818;
padding: 1em 0.5em;
height: 95vh;
height: 100vh;
min-height: 750px;
max-height: 1000px;
overflow: auto;
@@ -246,7 +246,7 @@ button.back-btn {
background: #181818;
padding: 1em 0.5em;
height: 95vh;
height: 100vh;
min-height: 750px;
max-height: 1000px;
+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);
}
});
}
+25 -22
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'),
handler: 'NetworkFirst',
urlPattern: /^https:\/\/stacjownik.spythere.eu\/api\/getSceneries/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'sceneries-cache',
cacheName: 'spythere-sceneries-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: new RegExp('^https://raw.githubusercontent.com/Spythere/api/*', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'github-api-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
urlPattern: /^https:\/\/static.spythere.eu\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'images-cache',
cacheName: 'spythere-static-cache',
cacheableResponse: {
statuses: [0, 200]
},
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
},
cacheableResponse: {
statuses: [0, 200, 404]
maxAgeSeconds: 60 * 60 * 8
}
}
}
@@ -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