mirror of
https://github.com/Spythere/stacjownik.git
synced 2026-05-03 05:18:11 +00:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f457d6389 | |||
| eb5b94c9f6 | |||
| 328e8c0573 | |||
| 9f58ae5428 | |||
| ebd0eeb8c4 | |||
| fa656c2f26 | |||
| 0cc3a12d1d | |||
| 392a6437f8 | |||
| 122532f0ed | |||
| 366ff91f60 | |||
| a0496736dd | |||
| f974120e87 | |||
| abd8b8178b | |||
| f1fcde8459 | |||
| b3289d6aab | |||
| 6481a4a3b0 | |||
| 05dc268526 | |||
| 669acc98d2 | |||
| 3371b661c2 | |||
| 871b2c0221 | |||
| d366a877a4 | |||
| 405aab96bd | |||
| f29c160000 | |||
| a2de0e2030 | |||
| 7dd1c06f3f | |||
| ff041b9aaf | |||
| 4782dba444 | |||
| d6b8d032d6 | |||
| c16616330c | |||
| 57cec8bfe7 | |||
| 6bea340e19 | |||
| c181cf7e64 | |||
| 8e4ae64cd3 | |||
| 5750490f01 | |||
| 3ef27e1d69 | |||
| f53993c717 | |||
| 235c16e30f | |||
| c3533f07ad | |||
| d05579c5ee | |||
| c8f53c2f06 | |||
| b44f88ebcd | |||
| 7805d1350c | |||
| b17bd19433 | |||
| c12a6cbacd | |||
| ba650238db | |||
| d5ec9919e2 | |||
| 20cd393e05 | |||
| 31e65c09d6 | |||
| fb2348e774 | |||
| 1ec75bda70 | |||
| 6b6b837dde | |||
| 66a02d76bd | |||
| c7162dbd14 | |||
| 1cfe073bab | |||
| e3b72c81ea | |||
| 5552995564 | |||
| 623d5dd2ce |
@@ -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
|
||||
Generated
+2828
-9512
File diff suppressed because it is too large
Load Diff
+5
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stacjownik",
|
||||
"version": "1.22.2",
|
||||
"version": "1.24.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -14,11 +14,9 @@
|
||||
"dependencies": {
|
||||
"core-js": "^3.32.2",
|
||||
"dotenv": "^16.3.1",
|
||||
"firebase": "^10.4.0",
|
||||
"howler": "^2.2.4",
|
||||
"pinia": "^2.1.6",
|
||||
"sass": "^1.67.0",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"showdown": "^2.1.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.4.1",
|
||||
"vue-router": "^4.2.4"
|
||||
@@ -26,7 +24,8 @@
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@types/node": "^20.6.2",
|
||||
"@vite-pwa/assets-generator": "^0.0.10",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@vite-pwa/assets-generator": "^0.2.4",
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
@@ -37,7 +36,7 @@
|
||||
"prettier": "^3.0.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-pwa": "^0.16.5",
|
||||
"vite-plugin-pwa": "^0.20.0",
|
||||
"vue-tsc": "^1.8.11"
|
||||
},
|
||||
"browserslist": [
|
||||
|
||||
+64
-50
@@ -1,8 +1,15 @@
|
||||
<template>
|
||||
<div class="app_container" v-cloak>
|
||||
<div class="app_container">
|
||||
<UpdateModal
|
||||
:update-modal-open="isUpdateModalOpen"
|
||||
@toggle-modal="() => (isUpdateModalOpen = false)"
|
||||
/>
|
||||
|
||||
<Tooltip />
|
||||
|
||||
<transition name="modal-anim">
|
||||
<keep-alive>
|
||||
<TrainModal v-if="store.chosenModalTrainId" />
|
||||
<TrainModal />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
|
||||
@@ -20,7 +27,10 @@
|
||||
©
|
||||
<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="() => (isUpdateModalOpen = true)">
|
||||
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
|
||||
</button>
|
||||
|
||||
<br />
|
||||
<a href="https://discord.gg/x2mpNN3svk">
|
||||
<img src="/images/icon-discord.png" alt="" /> <b>{{ $t('footer.discord') }}</b>
|
||||
@@ -32,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 { 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 { useMainStore } from './store/mainStore';
|
||||
|
||||
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 UpdateModal from './components/App/UpdateModal.vue';
|
||||
|
||||
import StorageManager from './managers/storageManager';
|
||||
import { useApiStore } from './store/apiStore';
|
||||
import { Status } from './typings/common';
|
||||
|
||||
const STORAGE_VERSION_KEY = 'app_version';
|
||||
|
||||
@@ -55,17 +67,23 @@ export default defineComponent({
|
||||
Clock,
|
||||
StatusIndicator,
|
||||
AppHeader,
|
||||
TrainModal
|
||||
TrainModal,
|
||||
UpdateModal,
|
||||
Tooltip
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
VERSION: version,
|
||||
store: useMainStore(),
|
||||
apiStore: useApiStore(),
|
||||
tooltipStore: useTooltipStore(),
|
||||
|
||||
isUpdateModalOpen: false,
|
||||
|
||||
currentLang: 'pl',
|
||||
releaseURL: '',
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
|
||||
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
|
||||
|
||||
nextUpdateTime: 0
|
||||
}),
|
||||
|
||||
created() {
|
||||
@@ -73,47 +91,52 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
window.addEventListener('focus', () => {
|
||||
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
|
||||
|
||||
this.apiStore.fetchActiveData();
|
||||
});
|
||||
|
||||
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.isUpdateModalOpen =
|
||||
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() {
|
||||
@@ -145,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');
|
||||
|
||||
@@ -217,6 +225,7 @@ export default defineComponent({
|
||||
grid-template-columns: 100%;
|
||||
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app_main {
|
||||
@@ -234,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;
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<template>
|
||||
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
|
||||
<div class="modal_content">
|
||||
<h1 class="header">Aktualizacja Stacjownika</h1>
|
||||
<h2>wersja {{ version }}</h2>
|
||||
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
|
||||
<div class="modal-content">
|
||||
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
|
||||
|
||||
<b>Co nowego?</b>
|
||||
<p>
|
||||
<ul>
|
||||
<li>test</li>
|
||||
</ul>
|
||||
<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="toggleModal(false)">
|
||||
{{ $t('update.confirm') }}
|
||||
</button>
|
||||
|
||||
<p class="bottom-info">
|
||||
{{ $t('update.info-1') }}
|
||||
<br />
|
||||
<span v-html="$t('update.info-2')"></span>
|
||||
</p>
|
||||
</div>
|
||||
</AnimatedModal>
|
||||
@@ -18,11 +23,24 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { version } from '../../../package.json';
|
||||
import { Converter } from 'showdown';
|
||||
|
||||
import AnimatedModal from '../Global/AnimatedModal.vue';
|
||||
|
||||
const converter = new Converter();
|
||||
|
||||
export default defineComponent({
|
||||
components: { AnimatedModal },
|
||||
|
||||
props: {
|
||||
updateModalOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['toggleModal'],
|
||||
|
||||
data() {
|
||||
return {
|
||||
mainStore: useMainStore(),
|
||||
@@ -30,6 +48,25 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
updateModalOpen(val: boolean) {
|
||||
this.$nextTick(() => {
|
||||
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
htmlChangelog() {
|
||||
if (this.mainStore.appUpdate == null) return '';
|
||||
|
||||
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
|
||||
console.log(x);
|
||||
|
||||
return x;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleModal(value: boolean) {
|
||||
this.$emit('toggleModal', value);
|
||||
@@ -39,10 +76,46 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.modal_content {
|
||||
::v-deep(h1) {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep(h2) {
|
||||
padding: 0.25em 0;
|
||||
}
|
||||
|
||||
::v-deep(ul) {
|
||||
list-style: inside;
|
||||
padding: 0.5em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 0.5em;
|
||||
padding: 1em;
|
||||
height: 80vh;
|
||||
min-height: 550px;
|
||||
min-height: 700px;
|
||||
overflow: auto;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<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">
|
||||
<transition name="modal-anim" tag="div">
|
||||
<div class="modal" v-if="isOpen">
|
||||
<div class="modal-background" @click="toggleModal(false)"></div>
|
||||
<div class="modal-wrapper" ref="wrapper" tabindex="0">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
|
||||
@@ -30,8 +30,7 @@ export default defineComponent({
|
||||
watch: {
|
||||
isOpen(v) {
|
||||
this.$nextTick(() => {
|
||||
if (v) (this.$refs['wrapper'] as HTMLElement).focus();
|
||||
else (this.store.modalLastClickedTarget as HTMLElement)?.focus();
|
||||
if (v == false) (this.store.modalLastClickedTarget as HTMLElement)?.focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -47,17 +46,17 @@ export default defineComponent({
|
||||
<style lang="scss" scoped>
|
||||
@import '../../styles/responsive.scss';
|
||||
|
||||
.body {
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
|
||||
width: 100vw;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.background {
|
||||
.modal-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -69,33 +68,21 @@ export default defineComponent({
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
.modal-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;
|
||||
z-index: 210;
|
||||
overflow: auto;
|
||||
max-height: 95vh;
|
||||
|
||||
& > :slotted(div) {
|
||||
max-height: 95vh;
|
||||
}
|
||||
}
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
|
||||
@include smallScreen {
|
||||
.wrapper {
|
||||
top: 0;
|
||||
transform: translate(-50%, 1em);
|
||||
max-height: 90vh;
|
||||
|
||||
& > :slotted(div) {
|
||||
max-height: 90vh;
|
||||
}
|
||||
width: 95vw;
|
||||
max-width: 850px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
class="modal-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') }}
|
||||
@@ -103,9 +104,13 @@ export default defineComponent({
|
||||
emits: ['toggleModal'],
|
||||
|
||||
watch: {
|
||||
isModalOpen(b: boolean) {
|
||||
this.running = b;
|
||||
isModalOpen(val: boolean) {
|
||||
this.running = val;
|
||||
this.lastUpdate = Date.now();
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (val) (this.$refs['action'] as HTMLElement).focus();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,12 @@
|
||||
</p>
|
||||
|
||||
<img
|
||||
class="traction-only"
|
||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${computedStockList[0].split(':')[0]}${
|
||||
/^EN/.test(computedStockList[0]) ? 'rb' : ''
|
||||
}.png`"
|
||||
@error="onImageError($event, computedStockList[0])"
|
||||
width="400"
|
||||
width="300"
|
||||
height="60"
|
||||
/>
|
||||
</div>
|
||||
@@ -25,39 +26,53 @@
|
||||
|
||||
<span>
|
||||
<img
|
||||
:data-mouseover="stockName"
|
||||
data-tooltip-type="VehiclePreviewTooltip"
|
||||
:data-tooltip-content="stockName.split(':')[0]"
|
||||
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
|
||||
/^EN/.test(stockName) ? 'rb' : ''
|
||||
}.png`"
|
||||
@error="onImageError($event, stockName)"
|
||||
@click.stop="() => {}"
|
||||
width="400"
|
||||
height="60"
|
||||
/>
|
||||
|
||||
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
|
||||
<img
|
||||
:data-mouseover="stockName"
|
||||
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`"
|
||||
@error="
|
||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
|
||||
"
|
||||
@click.stop="() => {}"
|
||||
/>
|
||||
|
||||
<img
|
||||
class="train-thumbnail"
|
||||
:data-mouseover="stockName"
|
||||
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`"
|
||||
@error="
|
||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-s.png')
|
||||
"
|
||||
@click.stop="() => {}"
|
||||
/>
|
||||
|
||||
<img
|
||||
class="train-thumbnail"
|
||||
:data-mouseover="stockName"
|
||||
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`"
|
||||
@error="
|
||||
(event) => ((event.target as HTMLImageElement).src = '/images/icon-loco-ezt-ra.png')
|
||||
"
|
||||
@click.stop="() => {}"
|
||||
/>
|
||||
<!-- /// -->
|
||||
</span>
|
||||
@@ -101,14 +116,10 @@ export default defineComponent({
|
||||
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);
|
||||
|
||||
@@ -139,6 +150,7 @@ export default defineComponent({
|
||||
ul > li > span {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -147,6 +159,10 @@ img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
img.traction-only {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<div class="item-general">
|
||||
<span
|
||||
class="general-train"
|
||||
tabindex="0"
|
||||
@click.stop="showTimetable(timetable, $event.currentTarget)"
|
||||
@keydown.enter="showTimetable(timetable, $event.currentTarget)"
|
||||
>
|
||||
<span class="general-train">
|
||||
<span class="text--grayed">#{{ timetable.id }}</span>
|
||||
|
||||
<span class="badges" v-if="timetable.skr || timetable.twr">
|
||||
@@ -29,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')"
|
||||
>
|
||||
@@ -62,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>
|
||||
@@ -77,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: {
|
||||
@@ -100,8 +110,8 @@ export default defineComponent({
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '../../../styles/responsive.scss';
|
||||
@import '../../../styles/badge.scss';
|
||||
@import '../../../styles/responsive';
|
||||
@import '../../../styles/badge';
|
||||
|
||||
.item-general {
|
||||
display: flex;
|
||||
@@ -113,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 {
|
||||
@@ -139,13 +163,13 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.general-train {
|
||||
cursor: pointer;
|
||||
.btn-timetable {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.2em 0.5em;
|
||||
|
||||
img {
|
||||
height: 1.25em;
|
||||
}
|
||||
}
|
||||
|
||||
@include smallScreen {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
@@ -32,7 +32,7 @@
|
||||
import { PropType, defineComponent } from 'vue';
|
||||
import modalTrainMixin from '../../../mixins/modalTrainMixin';
|
||||
import routerMixin from '../../../mixins/routerMixin';
|
||||
import { ActiveScenery } from '../../../store/typings';
|
||||
import { ActiveScenery } from '../../../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [routerMixin, modalTrainMixin],
|
||||
|
||||
@@ -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>
|
||||
@@ -74,7 +66,7 @@
|
||||
class="timetable-item"
|
||||
v-else
|
||||
v-for="scheduledTrain in computedScheduledTrains"
|
||||
:key="scheduledTrain.trainId"
|
||||
:key="scheduledTrain.trainId + scheduledTrain.stopInfo.arrivalTimestamp"
|
||||
tabindex="0"
|
||||
@click.prevent.stop="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
@keydown.enter.prevent="selectModalTrain(scheduledTrain.trainId, $event.currentTarget)"
|
||||
@@ -186,12 +178,11 @@ 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';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SceneryTimetable',
|
||||
@@ -414,13 +405,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>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { ScheduledTrain, StopStatus } from '../../store/typings';
|
||||
import { ScheduledTrain, StopStatus } from '../../typings/common';
|
||||
|
||||
interface ScheduledTrainComp extends ScheduledTrain {
|
||||
stopStatusIndicator: string;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="card_controls">
|
||||
<button class="btn--filled btn--image" @click="toggleCard">
|
||||
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" />
|
||||
{{ $t('options.filters') }} [F]
|
||||
[F] {{ $t('options.filters') }}
|
||||
<span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
|
||||
</button>
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
</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>
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
minimumHours == 0
|
||||
? $t('filters.now')
|
||||
: minimumHours < 8
|
||||
? minimumHours + $t('filters.hour')
|
||||
: $t('filters.no-limit')
|
||||
? minimumHours + $t('filters.hour')
|
||||
: $t('filters.no-limit')
|
||||
}}</span>
|
||||
<button class="btn--action" @click="addHour">+</button>
|
||||
</span>
|
||||
@@ -108,6 +108,7 @@
|
||||
:id="slider.id"
|
||||
:min="slider.minRange"
|
||||
:max="slider.maxRange"
|
||||
:step="slider.step"
|
||||
v-model="slider.value"
|
||||
@change="handleInput"
|
||||
/>
|
||||
@@ -136,7 +137,7 @@
|
||||
: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>
|
||||
@@ -170,7 +171,10 @@ export default defineComponent({
|
||||
currentRegion: { id: '', value: '' },
|
||||
|
||||
delayInputTimer: -1,
|
||||
chosenSearchScenery: ''
|
||||
chosenSearchScenery: '',
|
||||
|
||||
scrollTop: 0,
|
||||
lastFocusedEl: null as HTMLElement | null
|
||||
}),
|
||||
|
||||
setup() {
|
||||
@@ -236,7 +240,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,6 +254,10 @@ export default defineComponent({
|
||||
this.isVisible = !this.isVisible;
|
||||
},
|
||||
|
||||
onScroll(e: Event) {
|
||||
this.scrollTop = (e.target as HTMLElement).scrollTop;
|
||||
},
|
||||
|
||||
handleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
|
||||
@@ -431,23 +442,16 @@ h3.section-header {
|
||||
}
|
||||
|
||||
.card_actions {
|
||||
width: 100%;
|
||||
padding: 0.5em;
|
||||
|
||||
.filter-option {
|
||||
max-width: 50%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
width: 100%;
|
||||
|
||||
margin-top: 0.5em;
|
||||
|
||||
button {
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0.5em;
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="station-stats">
|
||||
<div class="separator" />
|
||||
<div>
|
||||
{{ $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
|
||||
>:
|
||||
<b :style="calculateFactorStyle()">
|
||||
{{ uFactor.toFixed(2) }}
|
||||
</b>
|
||||
| {{ $t('station-stats.avg-timetable-count') }}
|
||||
<b>{{ avgTimetableCount.toFixed(2) }}</b>
|
||||
</div>
|
||||
<div>
|
||||
{{ $t('station-stats.single-track-count') }}
|
||||
<b>{{ trackCount.oneWayElectric }}</b> {{ $t('station-stats.electrified') }} /
|
||||
<b>{{ trackCount.oneWayOther }}</b> {{ $t('station-stats.not-electrified') }} |
|
||||
{{ $t('station-stats.double-track-count') }} <b>{{ trackCount.twoWayElectric }}</b>
|
||||
{{ $t('station-stats.electrified') }} / <b>{{ trackCount.twoWayOther }}</b>
|
||||
{{ $t('station-stats.not-electrified') }} | {{ $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>
|
||||
</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 scheduledTrainsTotal = this.mainStore.activeSceneryList.reduce<number>((acc, sc) => {
|
||||
if (sc.region != this.mainStore.region.id) return acc;
|
||||
|
||||
acc += sc.scheduledTrainCount.all;
|
||||
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return this.mainStore.activeSceneryList.length != 0
|
||||
? scheduledTrainsTotal / this.mainStore.activeSceneryList.length
|
||||
: 0;
|
||||
},
|
||||
|
||||
trackCount() {
|
||||
return this.mainStore.allStationInfo
|
||||
.filter(
|
||||
(st) =>
|
||||
st.onlineInfo?.dispatcherId != -1 &&
|
||||
st.onlineInfo?.region == this.mainStore.region.id &&
|
||||
st.generalInfo?.routes
|
||||
)
|
||||
.reduce(
|
||||
(acc, st) => {
|
||||
[...st.generalInfo!.routes.single, ...st.generalInfo!.routes.double].forEach((r) => {
|
||||
if (r.isInternal) return;
|
||||
|
||||
const keyName: keyof typeof acc = `${r.routeTracks == 2 ? 'twoWay' : 'oneWay'}${r.isElectric ? 'Electric' : 'Other'}`;
|
||||
|
||||
acc[keyName] += 1;
|
||||
});
|
||||
|
||||
return acc;
|
||||
},
|
||||
{ oneWayElectric: 0, oneWayOther: 0, twoWayElectric: 0, twoWayOther: 0 }
|
||||
);
|
||||
},
|
||||
|
||||
spawnCount() {
|
||||
return this.mainStore.activeSceneryList.reduce(
|
||||
(acc, scenery) => {
|
||||
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;
|
||||
}
|
||||
|
||||
[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>
|
||||
@@ -1,328 +1,330 @@
|
||||
<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 && displayedStations.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="displayedStations.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="sorterActive.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${sorterActive.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)"
|
||||
:title="$t('donations.dispatcher-message')"
|
||||
@click.stop="openDonationModal"
|
||||
>
|
||||
<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="sorterActive.headerName == headerName"
|
||||
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
|
||||
alt="sort icon"
|
||||
/>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<td class="station-dispatcher-exp">
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="station in displayedStations"
|
||||
: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="openDonationModal"
|
||||
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>
|
||||
</div>
|
||||
</transition>
|
||||
<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>
|
||||
{{ $t('sceneries.no-stations') }} (region: <b>{{ mainStore.region.name }}</b
|
||||
>)
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import { defineComponent } 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 StationStatusBadge from '../Global/StationStatusBadge.vue';
|
||||
import { Status } from '../../typings/common';
|
||||
import { Station, Status } from '../../typings/common';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
stations: {
|
||||
type: Array as PropType<Station[]>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['toggleDonationModal'],
|
||||
components: { Loading, StationStatusBadge },
|
||||
mixins: [styleMixin, dateMixin, stationInfoMixin],
|
||||
mixins: [styleMixin, dateMixin],
|
||||
|
||||
data: () => ({
|
||||
headIconsIds,
|
||||
@@ -333,28 +335,37 @@ export default defineComponent({
|
||||
computed: {
|
||||
sorterActive() {
|
||||
return this.stationFiltersStore.sorterActive;
|
||||
},
|
||||
|
||||
displayedStations() {
|
||||
return this.stationFiltersStore.filteredStationList;
|
||||
}
|
||||
},
|
||||
|
||||
setup() {
|
||||
const mainStore = useMainStore();
|
||||
const apiStore = useApiStore();
|
||||
const tooltipStore = useTooltipStore();
|
||||
|
||||
const stationFiltersStore = useStationFiltersStore();
|
||||
|
||||
return {
|
||||
Status: Status.Data,
|
||||
stationFiltersStore,
|
||||
mainStore,
|
||||
apiStore
|
||||
apiStore,
|
||||
tooltipStore
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
setScenery(name: string) {
|
||||
const station = this.stations.find((station) => station.name === name);
|
||||
const station = this.displayedStations.find((station) => station.name === name);
|
||||
|
||||
if (!station) return;
|
||||
|
||||
this.lastSelectedStationName = station.name;
|
||||
this.tooltipStore.hide();
|
||||
|
||||
this.$router.push({
|
||||
name: 'SceneryView',
|
||||
@@ -368,6 +379,7 @@ export default defineComponent({
|
||||
openDonationModal(e: Event) {
|
||||
this.$emit('toggleDonationModal', true);
|
||||
this.mainStore.modalLastClickedTarget = e.target;
|
||||
this.tooltipStore.hide();
|
||||
},
|
||||
|
||||
openForumSite(e: Event, url: string | undefined) {
|
||||
@@ -392,23 +404,11 @@ 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: 550px;
|
||||
overflow: auto;
|
||||
font-weight: 500;
|
||||
height: 90vh;
|
||||
min-height: 550px;
|
||||
}
|
||||
|
||||
.no-stations {
|
||||
@@ -416,9 +416,8 @@ $rowCol: #424242;
|
||||
font-size: 1.5em;
|
||||
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
|
||||
background: #333;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
@@ -36,6 +36,8 @@ export interface Filter {
|
||||
minOneWay: number;
|
||||
minTwoWayCatenary: number;
|
||||
minTwoWay: number;
|
||||
minVmax: number;
|
||||
maxVmax: number;
|
||||
'no-1track': boolean;
|
||||
'no-2track': boolean;
|
||||
'include-selected': boolean;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div class="tooltip-content">
|
||||
<span>{{ tooltipStore.content }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tooltipStore: useTooltipStore()
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
width: 100%;
|
||||
background-color: #333;
|
||||
box-shadow: 0 0 5px 2px #aaa;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="tooltip-content">
|
||||
<img src="/images/icon-diamond.svg" alt="" />
|
||||
<span>{{ tooltipStore.content }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { useTooltipStore } from '../../store/tooltipStore';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tooltipStore: useTooltipStore()
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-content {
|
||||
gap: 0.5em;
|
||||
|
||||
padding: 0.5em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
width: 100%;
|
||||
|
||||
background-color: #333;
|
||||
box-shadow: 0 0 10px 2px #aaa;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
height: 1em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 { StationTrain } 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 StationTrain[];
|
||||
return (parsedTrains ?? []).sort((a, b) => a.trainNo - b.trainNo);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tooltip-content {
|
||||
width: 300px;
|
||||
|
||||
padding: 0.25em 0.5em;
|
||||
border-radius: 0.25em;
|
||||
|
||||
width: 100%;
|
||||
background-color: #1b1b1b;
|
||||
box-shadow: 0 0 5px 2px #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="tooltip-content">
|
||||
<div v-if="imageState == 'loading'" class="loading-info">
|
||||
{{ $t('vehicle-preview.loading') }}
|
||||
</div>
|
||||
|
||||
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
|
||||
|
||||
<img
|
||||
v-if="tooltipStore.type"
|
||||
@load="onImageLoad"
|
||||
@error="onImageError"
|
||||
width="300"
|
||||
height="176"
|
||||
class="rounded-md w-full h-auto"
|
||||
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
|
||||
/>
|
||||
|
||||
<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 { useTooltipStore } from '../../store/tooltipStore';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
tooltipStore: useTooltipStore(),
|
||||
imageState: 'loading'
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.imageState = 'loading';
|
||||
},
|
||||
|
||||
watch: {
|
||||
'tooltipStore.type'(prev, val) {
|
||||
if (prev != val) this.imageState = 'loading';
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onImageLoad() {
|
||||
this.imageState = 'loaded';
|
||||
},
|
||||
|
||||
onImageError(e: Event) {
|
||||
this.imageState = 'error';
|
||||
|
||||
(e.target as HTMLElement).style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tooltip-content {
|
||||
width: 300px;
|
||||
min-height: 200px;
|
||||
background-color: #333;
|
||||
box-shadow: 0 0 10px 2px #aaa;
|
||||
|
||||
padding: 0.5em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
.loading-info {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.vehicle-name {
|
||||
text-align: center;
|
||||
margin-top: 0.5em;
|
||||
color: #ccc;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.error-placeholder {
|
||||
height: 176px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,60 +1,86 @@
|
||||
<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 }} </span
|
||||
<span
|
||||
class="timetable-warnings"
|
||||
v-if="train.timetableData?.TWR || train.timetableData?.SKR"
|
||||
>
|
||||
<span class="train-number">{{ train.trainNo }}</span>
|
||||
</strong>
|
||||
<span>•</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 }} </span
|
||||
>
|
||||
<span class="train-number">{{ train.trainNo }}</span>
|
||||
</strong>
|
||||
<span>•</span>
|
||||
<b
|
||||
v-if="apiStore.donatorsData.includes(train.driverName)"
|
||||
:title="$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>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="general-timetable" v-if="train.timetableData">
|
||||
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
|
||||
<img
|
||||
<span
|
||||
v-if="getSceneriesWithComments(train.timetableData).length > 0"
|
||||
class="image-warning"
|
||||
src="/images/icon-warning.svg"
|
||||
:title="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
|
||||
data-tooltip-type="BaseTooltip"
|
||||
:data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
|
||||
train.timetableData
|
||||
)})`"
|
||||
/>
|
||||
>
|
||||
<img class="image-warning" src="/images/icon-warning.svg" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<hr style="margin: 0.25em 0" />
|
||||
@@ -67,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">
|
||||
@@ -91,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>
|
||||
@@ -120,14 +163,15 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import styleMixin from '../../mixins/styleMixin';
|
||||
import trainInfoMixin from '../../mixins/trainInfoMixin';
|
||||
import Train from '../../scripts/interfaces/Train';
|
||||
import ProgressBar from '../Global/ProgressBar.vue';
|
||||
import { useMainStore } from '../../store/mainStore';
|
||||
import { useApiStore } from '../../store/apiStore';
|
||||
import StockList from '../Global/StockList.vue';
|
||||
import modalTrainMixin from '../../mixins/modalTrainMixin';
|
||||
import { Train } from '../../typings/common';
|
||||
|
||||
export default defineComponent({
|
||||
mixins: [trainInfoMixin, styleMixin],
|
||||
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
|
||||
components: { ProgressBar, StockList },
|
||||
|
||||
props: {
|
||||
@@ -136,8 +180,7 @@ export default defineComponent({
|
||||
required: true
|
||||
},
|
||||
extended: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
|
||||
@@ -146,6 +189,19 @@ export default defineComponent({
|
||||
store: useMainStore(),
|
||||
apiStore: useApiStore()
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
navigateToJournal() {
|
||||
this.$router.push({
|
||||
path: '/journal/timetables',
|
||||
query: {
|
||||
'search-driver': this.train.driverName
|
||||
}
|
||||
});
|
||||
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -156,8 +212,8 @@ export default defineComponent({
|
||||
|
||||
.image-warning {
|
||||
height: 1em;
|
||||
|
||||
margin-left: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.train-stats {
|
||||
@@ -176,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;
|
||||
@@ -210,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;
|
||||
@@ -226,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;
|
||||
@@ -237,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;
|
||||
@@ -257,32 +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;
|
||||
|
||||
font-size: 1.15em;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<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>
|
||||
|
||||
<TrainInfo :train="chosenTrain" :extended="false" ref="trainInfo" />
|
||||
<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>
|
||||
</div>
|
||||
@@ -17,17 +13,27 @@ 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.trainId == this.store.chosenModalTrainId);
|
||||
}
|
||||
},
|
||||
|
||||
this.$nextTick(() => {
|
||||
contentEl.focus();
|
||||
});
|
||||
watch: {
|
||||
chosenTrain(train: Train | undefined) {
|
||||
this.$nextTick(() => {
|
||||
if (train) {
|
||||
const contentEl = this.$refs['content'] as HTMLElement;
|
||||
contentEl.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -49,23 +55,6 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
top: 0;
|
||||
@@ -82,7 +71,7 @@ export default defineComponent({
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.modal_background {
|
||||
.modal-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -94,14 +83,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;
|
||||
|
||||
background-color: #1a1a1a;
|
||||
box-shadow: 0 0 15px 10px #0e0e0e;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<transition name="status-anim" mode="out-in" tag="div" class="train-table">
|
||||
<div :key="apiStore.dataStatuses.connection">
|
||||
<div class="table-info" key="offline" v-if="store.isOffline">
|
||||
<div class="table-warning" key="offline" v-if="store.isOffline">
|
||||
{{ $t('app.offline') }}
|
||||
</div>
|
||||
|
||||
<Loading v-else-if="apiStore.dataStatuses.connection == Status.Loading" key="loading" />
|
||||
|
||||
<div class="table-info" key="no-trains" v-else-if="trains.length == 0">
|
||||
<div class="table-warning" key="no-trains" v-else-if="trains.length == 0">
|
||||
{{ $t('trains.no-trains') }}
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
@click.stop="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
@keydown.enter="selectModalTrain(train.trainId, $event.currentTarget)"
|
||||
>
|
||||
<TrainInfo :train="train" />
|
||||
<TrainInfo :train="train" :extended="false" />
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
@@ -30,11 +30,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({
|
||||
@@ -105,7 +104,7 @@ export default defineComponent({
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.table-info {
|
||||
.table-warning {
|
||||
text-align: center;
|
||||
|
||||
padding: 1em 0;
|
||||
|
||||
@@ -265,6 +265,7 @@
|
||||
"name": "minLevel",
|
||||
"minRange": 0,
|
||||
"maxRange": 20,
|
||||
"step": 1,
|
||||
"value": 0,
|
||||
"defaultValue": 0
|
||||
},
|
||||
@@ -273,14 +274,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 +310,7 @@
|
||||
"name": "minOneWay",
|
||||
"minRange": 0,
|
||||
"maxRange": 5,
|
||||
"step": 1,
|
||||
"value": 0,
|
||||
"defaultValue": 0
|
||||
},
|
||||
@@ -297,6 +319,7 @@
|
||||
"name": "minTwoWayCatenary",
|
||||
"minRange": 0,
|
||||
"maxRange": 5,
|
||||
"step": 1,
|
||||
"value": 0,
|
||||
"defaultValue": 0
|
||||
},
|
||||
@@ -305,6 +328,7 @@
|
||||
"name": "minTwoWay",
|
||||
"minRange": 0,
|
||||
"maxRange": 5,
|
||||
"step": 1,
|
||||
"value": 0,
|
||||
"defaultValue": 0
|
||||
}
|
||||
|
||||
+45
-10
@@ -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,12 +48,10 @@
|
||||
"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! :/"
|
||||
},
|
||||
"data-status": {
|
||||
"S1-offline": "<b>S1 signal</b> <br> The app is working in offline mode!",
|
||||
@@ -76,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 ",
|
||||
@@ -164,7 +182,8 @@
|
||||
"addons": "ADDITIONAL PROGRAMS",
|
||||
"blockades": "BLOCK SIGNALLING",
|
||||
"status": "ONLINE STATUS",
|
||||
"timetables": "ACTIVE TIMETABLES"
|
||||
"timetables": "ACTIVE TIMETABLES",
|
||||
"spawns": "OPEN SPAWNS"
|
||||
},
|
||||
|
||||
"all-available": "ALL AVAILABLE",
|
||||
@@ -217,11 +236,14 @@
|
||||
"sliders": {
|
||||
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
|
||||
"max-lvl": "MAX. REQUIRED DISPATCHER LEVEL",
|
||||
"min-vmax": "MIN. SCENERY ROUTE SPEED",
|
||||
"max-vmax": "MAX. SCENERY ROUTE SPEED",
|
||||
"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"
|
||||
},
|
||||
|
||||
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
|
||||
"authors-placeholder": "Enter the author nickname...",
|
||||
"authors-button-title": "Search",
|
||||
@@ -274,6 +296,16 @@
|
||||
"no-stations": "No stations to show here!",
|
||||
"scenery-search": "Search for scenery..."
|
||||
},
|
||||
"station-stats": {
|
||||
"u-factor": "U-factor",
|
||||
"u-factor-tooltip": "(?) Current server traffic factor (driver count divided by dispatcher count)",
|
||||
"avg-timetable-count": "Average timetable count for one dispatcher:",
|
||||
"single-track-count": "Available single track routes:",
|
||||
"double-track-count": "Available double track routes:",
|
||||
"electrified": "(electrified)",
|
||||
"not-electrified": "(not electr.)",
|
||||
"open-spawns": "Open spawns:"
|
||||
},
|
||||
"trains": {
|
||||
"no-trains": "No trains to show here!",
|
||||
"loading": "Loading train data...",
|
||||
@@ -315,7 +347,9 @@
|
||||
"last-seen-ago": "since {minutes} minutes",
|
||||
|
||||
"scenery-offline": "Offline ride",
|
||||
"timeout": "An error occured while trying to refresh SWDR timetable data!"
|
||||
"timeout": "An error occured while trying to refresh SWDR timetable data!",
|
||||
|
||||
"journal-button": "DRIVER'S JOURNAL"
|
||||
},
|
||||
"train-stats": {
|
||||
"stats-button": "STATISTICS",
|
||||
@@ -351,6 +385,7 @@
|
||||
"timetable-active": "ACTIVE",
|
||||
"timetable-fulfilled": "FULFILLED",
|
||||
"timetable-abandoned": "ABANDONED",
|
||||
"timetable-online-button": "ONLINE TIMETABLE",
|
||||
|
||||
"online-since": "ONLINE SINCE",
|
||||
"duty-lasted": "The duty lasted",
|
||||
@@ -359,7 +394,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",
|
||||
|
||||
+46
-7
@@ -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",
|
||||
@@ -38,6 +45,10 @@
|
||||
"footer": {
|
||||
"discord": "Serwer Discord Stacjownika"
|
||||
},
|
||||
"vehicle-preview": {
|
||||
"loading": "Ładowanie podglądu...",
|
||||
"error": "Ups! Nie znaleziono podglądu pojazdu! :/"
|
||||
},
|
||||
"data-status": {
|
||||
"S1-offline": "<b>Sygnał S1</b> <br> Aplikacja działa w trybie offline!",
|
||||
"S1a-connection": "<b>Sygnał S1a</b> <br> Błąd podczas próby połączenia się z API Stacjownika!",
|
||||
@@ -66,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 ",
|
||||
@@ -152,10 +176,11 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
"all-available": "WSZYSTKIE DOSTĘPNE",
|
||||
@@ -198,7 +223,6 @@
|
||||
"semaphores": "KSZTAŁTOWA",
|
||||
"mixed": "MIESZANA",
|
||||
"historical": "HISTORYCZNA",
|
||||
|
||||
"free": "WOLNA",
|
||||
"occupied": "ZAJĘTA",
|
||||
|
||||
@@ -208,6 +232,8 @@
|
||||
"sliders": {
|
||||
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"max-lvl": "MAKS. WYMAGANY POZIOM DYŻURNEGO",
|
||||
"min-vmax": "MIN. PRĘDKOŚĆ SZLAKOWA",
|
||||
"max-vmax": "MAKS. PRĘDKOŚĆ SZLAKOWA",
|
||||
"routes-1t-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
|
||||
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
|
||||
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
|
||||
@@ -263,6 +289,16 @@
|
||||
"no-stations": "Brak stacji do wyświetlenia!",
|
||||
"scenery-search": "Wyszukaj scenerię..."
|
||||
},
|
||||
"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 dyżurnego:",
|
||||
"single-track-count": "Dostępne szlaki jednotorowe:",
|
||||
"double-track-count": "Dostępne szlaki dwutorowe:",
|
||||
"electrified": "(zelektr.)",
|
||||
"not-electrified": "(niezelektr.)",
|
||||
"open-spawns": "Otwarte spawny:"
|
||||
},
|
||||
"trains": {
|
||||
"no-trains": "Brak pociągów do wyświetlenia!",
|
||||
"loading": "Pobieranie danych o pociągach...",
|
||||
@@ -295,7 +331,9 @@
|
||||
|
||||
"scenery-offline": "Przejazd offline",
|
||||
|
||||
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR"
|
||||
"timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR",
|
||||
|
||||
"journal-button": "DZIENNIK MASZYNISTY"
|
||||
},
|
||||
"train-stats": {
|
||||
"stats-button": "STATYSTYKI",
|
||||
@@ -337,8 +375,9 @@
|
||||
"timetable-active": "AKTYWNY",
|
||||
"timetable-fulfilled": "WYPEŁNIONY",
|
||||
"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.",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,19 +1,15 @@
|
||||
import { defineComponent } from 'vue';
|
||||
import { useMainStore } from '../store/mainStore';
|
||||
import { useTooltipStore } from '../store/tooltipStore';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
store: useMainStore()
|
||||
store: useMainStore(),
|
||||
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;
|
||||
@@ -23,6 +19,7 @@ export default defineComponent({
|
||||
|
||||
closeModal() {
|
||||
this.store.chosenModalTrainId = undefined;
|
||||
this.tooltipStore.hide();
|
||||
|
||||
setTimeout(() => {
|
||||
(this.store.modalLastClickedTarget as any)?.focus();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[];
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Filter } from '../../components/StationsView/typings';
|
||||
import { Status } from '../../typings/common';
|
||||
import { HeadIdsTypes } from '../data/stationHeaderNames';
|
||||
import Station from '../interfaces/Station';
|
||||
import { Station } from '../../typings/common';
|
||||
|
||||
const dispatcherStatusPriority = [
|
||||
Status.ActiveDispatcher.UNKNOWN,
|
||||
@@ -68,8 +68,8 @@ export const sortStations = (
|
||||
|
||||
case 'user':
|
||||
diff =
|
||||
(b.onlineInfo ? b.onlineInfo.currentUsers : -1) -
|
||||
(a.onlineInfo ? a.onlineInfo.currentUsers : -1);
|
||||
(b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
|
||||
(a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
|
||||
break;
|
||||
|
||||
case 'like':
|
||||
@@ -189,9 +189,11 @@ export const filterStations = (station: Station, filters: Filter) => {
|
||||
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['minVmax'] > station.generalInfo.routes.maxRouteSpeed) return false;
|
||||
if (filters['maxVmax'] < station.generalInfo.routes.minRouteSpeed) return false;
|
||||
|
||||
if (
|
||||
filters['no-1track'] &&
|
||||
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
|
||||
|
||||
+2
-17
@@ -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,31 +54,19 @@ 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() {
|
||||
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
|
||||
|
||||
try {
|
||||
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
|
||||
|
||||
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
|
||||
|
||||
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);
|
||||
|
||||
+57
-16
@@ -1,21 +1,24 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import Train from '../scripts/interfaces/Train';
|
||||
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
|
||||
|
||||
import { ActiveScenery, ScheduledTrain, StoreState } from './typings';
|
||||
|
||||
import { Status } from '../typings/common';
|
||||
import Station from '../scripts/interfaces/Station';
|
||||
import {
|
||||
ActiveScenery,
|
||||
ScheduledTrain,
|
||||
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', {
|
||||
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,9 +29,8 @@ export const useMainStore = defineStore('store', {
|
||||
|
||||
chosenModalTrainId: undefined,
|
||||
|
||||
blockScroll: false,
|
||||
modalLastClickedTarget: null
|
||||
}) as StoreState,
|
||||
}) as MainStoreState,
|
||||
|
||||
getters: {
|
||||
trainList(): Train[] {
|
||||
@@ -45,7 +47,8 @@ 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
|
||||
) ?? [];
|
||||
@@ -93,6 +96,7 @@ export const useMainStore = defineStore('store', {
|
||||
});
|
||||
},
|
||||
|
||||
// computed active sceneries
|
||||
activeSceneryList(state): ActiveScenery[] {
|
||||
const apiStore = useApiStore();
|
||||
|
||||
@@ -107,7 +111,10 @@ export const useMainStore = defineStore('store', {
|
||||
if (
|
||||
acc.findIndex((v) => v.name == name && v.region == train.region) != -1 ||
|
||||
apiStore.activeData?.activeSceneries?.findIndex(
|
||||
(sc) => sc.stationName === name && sc.region == train.region
|
||||
(sc) =>
|
||||
sc.stationName === name &&
|
||||
sc.region == train.region &&
|
||||
Date.now() - sc.lastSeen < 1000 * 60 * 2
|
||||
) != -1
|
||||
)
|
||||
return acc;
|
||||
@@ -150,8 +157,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,
|
||||
@@ -225,6 +232,7 @@ export const useMainStore = defineStore('store', {
|
||||
return allActiveSceneries;
|
||||
},
|
||||
|
||||
// computed station data
|
||||
stationList(): Station[] {
|
||||
const apiStore = useApiStore();
|
||||
|
||||
@@ -242,6 +250,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;
|
||||
@@ -253,7 +268,9 @@ export const useMainStore = defineStore('store', {
|
||||
double: [],
|
||||
doubleElectrifiedNames: [],
|
||||
doubleOtherNames: [],
|
||||
sblNames: []
|
||||
sblNames: [],
|
||||
minRouteSpeed: 0,
|
||||
maxRouteSpeed: 0
|
||||
} as StationRoutes
|
||||
);
|
||||
|
||||
@@ -268,6 +285,30 @@ export const useMainStore = defineStore('store', {
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
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
|
||||
)
|
||||
}))
|
||||
];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -52,9 +52,9 @@ const filterInitStates: Filter = {
|
||||
unsignedStatus: false,
|
||||
withActiveTimetables: false,
|
||||
withoutActiveTimetables: false,
|
||||
|
||||
maxVmax: 200,
|
||||
minVmax: 0,
|
||||
authors: '',
|
||||
|
||||
onlineFromHours: 0
|
||||
};
|
||||
|
||||
@@ -75,26 +75,8 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
|
||||
|
||||
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
|
||||
)
|
||||
}))
|
||||
]
|
||||
return store.allStationInfo
|
||||
.filter((station) => filterStations(station, state.filters))
|
||||
.sort((a, b) => sortStations(a, b, state.sorterActive));
|
||||
}
|
||||
|
||||
@@ -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
-137
@@ -1,45 +1,19 @@
|
||||
import { API } from '../typings/api';
|
||||
import { 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 };
|
||||
import { Availability, StationRoutesInfo, Status } from '../typings/common';
|
||||
|
||||
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;
|
||||
@@ -65,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;
|
||||
}
|
||||
|
||||
+20
-5
@@ -1,6 +1,13 @@
|
||||
import Station from '../scripts/interfaces/Station';
|
||||
import Train from '../scripts/interfaces/Train';
|
||||
import { ScheduledTrain, StationTrain, StopStatus, TrainStop } from './typings';
|
||||
import {
|
||||
TrainStop,
|
||||
StopStatus,
|
||||
Train,
|
||||
ScheduledTrain,
|
||||
Station,
|
||||
StationTrain,
|
||||
ScenerySpawn,
|
||||
ScenerySpawnType
|
||||
} from '../typings/common';
|
||||
|
||||
export function getLocoURL(locoType: string): string {
|
||||
return `https://rj.td2.info.pl/dist/img/thumbnails/${
|
||||
@@ -31,7 +38,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,7 +48,15 @@ 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 };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
height: 90vh;
|
||||
min-height: 550px;
|
||||
margin-top: 0.5em;
|
||||
position: relative;
|
||||
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$animDuration: 150ms;
|
||||
$animDuration: 95ms;
|
||||
$animType: ease-in-out;
|
||||
|
||||
// List animation
|
||||
@@ -72,7 +72,6 @@ $animType: ease-in-out;
|
||||
|
||||
&-enter-from,
|
||||
&-leave-to {
|
||||
transform: translateY(-25%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,3 +101,16 @@
|
||||
background-color: #be3728;
|
||||
}
|
||||
}
|
||||
|
||||
.spawn-badge {
|
||||
color: white;
|
||||
|
||||
.length {
|
||||
background-color: #404040;
|
||||
color: #cfcfcf;
|
||||
}
|
||||
|
||||
&[data-electrified='true'] > .name {
|
||||
background-color: #007599;
|
||||
}
|
||||
}
|
||||
|
||||
+24
-1
@@ -55,6 +55,8 @@ body {
|
||||
-webkit-font-smoothing: antialiased !important;
|
||||
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
|
||||
&.no-scroll {
|
||||
overflow-y: hidden;
|
||||
@@ -211,6 +213,7 @@ a.a-button {
|
||||
&.btn--action {
|
||||
background-color: #424242;
|
||||
border-radius: 0.25em;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
background-color: #555;
|
||||
@@ -234,7 +237,7 @@ a.a-button {
|
||||
padding: 0.35em 0.75em;
|
||||
|
||||
img {
|
||||
width: 1.5em;
|
||||
width: 1.35em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@@ -296,3 +299,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
-1
@@ -128,8 +128,8 @@ export namespace API {
|
||||
export type Response = Data[];
|
||||
|
||||
export interface Data {
|
||||
id: string;
|
||||
trainNo: number;
|
||||
|
||||
mass: number;
|
||||
length: number;
|
||||
speed: number;
|
||||
|
||||
@@ -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,204 @@ export namespace Status {
|
||||
Warning = 3
|
||||
}
|
||||
}
|
||||
|
||||
export interface RegionCounters {
|
||||
stationCount: number;
|
||||
trainsCount: number;
|
||||
timetablesCount: number;
|
||||
}
|
||||
|
||||
export interface Train {
|
||||
id: string;
|
||||
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[];
|
||||
};
|
||||
}
|
||||
|
||||
export 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;
|
||||
}
|
||||
|
||||
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?: StationTrain[];
|
||||
scheduledTrains?: ScheduledTrain[];
|
||||
scheduledTrainCount: {
|
||||
all: number;
|
||||
confirmed: number;
|
||||
unconfirmed: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ScenerySpawn {
|
||||
spawnName: string;
|
||||
spawnLength: number;
|
||||
isElectrified: boolean;
|
||||
spawnType: ScenerySpawnType;
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
+10
-40
@@ -19,8 +19,9 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Donation :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
|
||||
<StationTable :stations="computedStationList" @toggleDonationModal="toggleDonationModal" />
|
||||
<DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
|
||||
<StationTable @toggleDonationModal="toggleDonationModal" />
|
||||
<StationStats />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -31,36 +32,29 @@ 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 DonationModal from '../components/Global/DonationModal.vue';
|
||||
import StationStats from '../components/StationsView/StationStats.vue';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
StationTable,
|
||||
StationFilterCard,
|
||||
Donation
|
||||
StationStats,
|
||||
DonationModal
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
filterCardOpen: false,
|
||||
modalHidden: true,
|
||||
STORAGE_KEY: 'options_saved',
|
||||
focusedStationName: '',
|
||||
filterStore: useStationFiltersStore(),
|
||||
store: useMainStore(),
|
||||
isDonationModalOpen: false,
|
||||
|
||||
isDonationModalOpen: false
|
||||
filterStore: useStationFiltersStore(),
|
||||
store: useMainStore()
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.filterStore.setupFilters();
|
||||
},
|
||||
|
||||
computed: {
|
||||
computedStationList() {
|
||||
return this.filterStore.filteredStationList;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleDonationModal(value: boolean) {
|
||||
this.isDonationModalOpen = value;
|
||||
@@ -73,30 +67,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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+33
-28
@@ -12,46 +12,42 @@ export default defineConfig({
|
||||
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',
|
||||
options: {
|
||||
cacheName: '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,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'images-cache',
|
||||
expiration: {
|
||||
maxEntries: 100,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
|
||||
},
|
||||
cacheName: 'spythere-sceneries-cache',
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp('^https://rj.td2.info.pl/dist/img/thumbnails/*', 'i'),
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'swdr-images-cache',
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200, 404]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp('^https://static.spythere.eu/images/*', 'i'),
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'spythere-images-cache',
|
||||
cacheableResponse: {
|
||||
statuses: [0, 200]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -60,5 +56,14 @@ export default defineConfig({
|
||||
suppressWarnings: true
|
||||
}
|
||||
})
|
||||
]
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'app-[name].js',
|
||||
assetFileNames: 'app-[name].css',
|
||||
chunkFileNames: 'chunk-[name].js'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user