Merge pull request #86 from Spythere/development

v1.24.0
This commit is contained in:
Spythere
2024-05-12 15:14:22 +02:00
committed by GitHub
72 changed files with 6464 additions and 13510 deletions
@@ -0,0 +1,17 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "15844367"
footer_title: "Changelog - Stacjownik"
footer_timestamp: true
+2828 -9512
View File
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -1,6 +1,6 @@
{
"name": "stacjownik",
"version": "1.23.1",
"version": "1.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": [
+59 -66
View File
@@ -1,6 +1,11 @@
<template>
<div class="app_container" v-cloak>
<PopUp />
<div class="app_container">
<UpdateModal
:update-modal-open="isUpdateModalOpen"
@toggle-modal="() => (isUpdateModalOpen = false)"
/>
<Tooltip />
<transition name="modal-anim">
<keep-alive>
@@ -22,7 +27,10 @@
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} |
<a :href="releaseURL" target="_blank">v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}</a>
<button class="btn--text" @click="() => (isUpdateModalOpen = true)">
v{{ VERSION }}{{ isOnProductionHost ? '' : 'dev' }}
</button>
<br />
<a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
@@ -36,19 +44,21 @@
<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';
import { version } from '.././package.json';
import { version } from '.././package.json';
import { Status } from './typings/common';
import { useMainStore } from './store/mainStore';
import popupMixin from './mixins/popupMixin';
import { useApiStore } from './store/apiStore';
import { useTooltipStore } from './store/tooltipStore';
import Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.vue';
import Tooltip from './components/Tooltip/Tooltip.vue';
import UpdateModal from './components/App/UpdateModal.vue';
import StorageManager from './managers/storageManager';
import PopUp from './components/PopUp/PopUp.vue';
import { useApiStore } from './store/apiStore';
import { Status } from './typings/common';
const STORAGE_VERSION_KEY = 'app_version';
@@ -58,19 +68,22 @@ export default defineComponent({
StatusIndicator,
AppHeader,
TrainModal,
PopUp
UpdateModal,
Tooltip
},
mixins: [popupMixin],
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() {
@@ -78,41 +91,52 @@ export default defineComponent({
},
async mounted() {
window.addEventListener('focus', () => {
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
this.apiStore.fetchActiveData();
});
window.addEventListener('mousemove', (e: MouseEvent) => this.handlePopUpEvents(e));
window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
},
methods: {
init() {
this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling();
this.checkAppVersion();
this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
},
checkAppVersion() {
if (import.meta.env.DEV) {
this.store.isNewUpdate = true;
return;
update(t: number) {
if (t >= this.nextUpdateTime) {
this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
}
window.requestAnimationFrame(this.update);
},
async checkAppVersion() {
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
if (storageVersion === undefined || storageVersion != version) {
this.store.isNewUpdate = true;
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
StorageManager.setStringValue(STORAGE_VERSION_KEY, version);
if (!releaseData) return;
this.store.appUpdate = {
version,
changelog: releaseData.body,
releaseURL: releaseData.html_url
};
this.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() {
@@ -137,27 +161,6 @@ export default defineComponent({
this.apiStore.connectToAPI();
},
handlePopUpEvents(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-popup-key'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.store.popUpData.key != null) this.hidePopUp();
return;
}
const popupComponentKey = targetEl.getAttribute('data-popup-key');
const popupContent = targetEl.getAttribute('data-popup-content');
if (popupComponentKey && popupContent) this.showPopUp(e, popupComponentKey, popupContent);
else if (this.store.popUpData.key != null) this.hidePopUp();
this.store.mousePos.x = e.pageX;
this.store.mousePos.y = e.pageY;
},
changeLang(lang: string) {
this.$i18n.locale = lang;
this.currentLang = lang;
@@ -165,21 +168,6 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang);
},
async setReleaseURL() {
try {
const releaseData = await (
await axios.get('https://api.github.com/repos/Spythere/stacjownik/releases/latest')
).data;
if (!releaseData) return;
this.releaseURL = releaseData.html_url;
} catch (error) {
console.error(`Wystąpił błąd podczas pobierania danych z API GitHuba: ${error}`);
return;
}
},
loadLang() {
const storageLang = StorageManager.getStringValue('lang');
@@ -255,10 +243,15 @@ export default defineComponent({
}
// FOOTER
footer.app_footer {
.app_footer {
max-width: 100%;
padding: 0.5em;
button {
display: inline-block;
padding: 0.1em;
}
img {
width: 1.1em;
vertical-align: text-bottom;
-11
View File
@@ -29,11 +29,6 @@
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span>
<!-- <span class="g-tooltip">
<b class="text--primary">{{ factorU }}U</b>
<div class="content">Test</div>
</span> -->
<span class="text--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span>
<img src="/images/icon-train.svg" alt="icon train" />
@@ -103,12 +98,6 @@ export default defineComponent({
return this.store.activeSceneryList.filter(
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
).length;
},
factorU() {
return this.onlineDispatchersCount == 0
? '-'
: (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
}
},
components: { StatusIndicator, Clock, RegionDropdown }
+70 -82
View File
@@ -1,36 +1,20 @@
<template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal">
<div class="modal_content">
<div>
<h1 style="margin-bottom: 0.5em">{{ $t('update.title') }}</h1>
<h2 class="text--primary">{{ $t('update.version', [version]) }}</h2>
<hr class="separator" />
</div>
<AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
<div class="modal-content">
<h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<div class="features-list">
<h2>Nowości i zmiany:</h2>
<ul>
<li v-for="content in localeChangesArray" :key="content">{{ content }}</li>
</ul>
</div>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<div class="modal_actions">
<button class="btn--action">Przyjąłem!</button>
<button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)">
{{ $t('update.confirm') }}
</button>
<p>Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony!</p>
<!-- <div class="actions-checkboxes">
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
</div> -->
</div>
<p class="bottom-info">
{{ $t('update.info-1') }}
<br />
<span v-html="$t('update.info-2')"></span>
</p>
</div>
</AnimatedModal>
</template>
@@ -39,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(),
@@ -51,9 +48,22 @@ export default defineComponent({
};
},
watch: {
updateModalOpen(val: boolean) {
this.$nextTick(() => {
if (val) (this.$refs['confirm-btn'] as HTMLElement).focus();
});
}
},
computed: {
localeChangesArray() {
return this.$t('update.content').split('\n');
htmlChangelog() {
if (this.mainStore.appUpdate == null) return '';
const x = converter.makeHtml(this.mainStore.appUpdate.changelog);
console.log(x);
return x;
}
},
@@ -66,68 +76,46 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.modal_content {
font-size: 1.2em;
::v-deep(h1) {
text-align: center;
padding: 1em;
height: 80vh;
min-height: 550px;
}
::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;
}
hr.separator {
margin: 0.5em 0;
padding: 0;
height: 3px;
background-color: #fff;
}
.features-list {
margin-top: 0.5em;
padding: 1em;
min-height: 700px;
overflow: auto;
ul {
text-align: left;
list-style: '\21D2 ';
padding: 1em;
}
li {
margin: 0.5em 0;
}
text-align: justify;
}
.modal_actions {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
button {
font-weight: bold;
padding: 0.35em;
}
p {
font-size: 0.9em;
}
.no-features {
text-align: center;
}
.actions-checkboxes {
display: flex;
flex-wrap: wrap;
justify-content: center;
button {
margin: 0 auto;
padding: 0.5em 0.75em;
font-size: 1.1em;
}
gap: 1em;
p.bottom-info {
text-align: center;
color: #ccc;
}
label {
font-size: 0.9em;
}
label > input {
margin-right: 0.5em;
}
a {
text-decoration: underline;
}
</style>
+17 -21
View File
@@ -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,28 +68,25 @@ 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;
max-height: 95vh;
z-index: 210;
& > :slotted(div) {
background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e;
width: 95vw;
max-width: 850px;
max-height: 95vh;
}
}
@include smallScreen {
.wrapper {
top: 0;
transform: translate(-50%, 1em);
.modal-wrapper {
max-height: 90vh;
& > :slotted(div) {
@@ -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();
});
}
},
+4 -11
View File
@@ -65,12 +65,12 @@ export default defineComponent({
immediate: true,
handler(regionQuery: string) {
if (regionQuery) {
this.store.region.id =
this.store.region =
regionsJSON.find(
(reg) =>
reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu';
) ?? regionsJSON[0];
}
}
}
@@ -139,15 +139,10 @@ button.selected-region {
color: paleturquoise;
font-weight: bold;
padding: 0.1em 0.5em;
&:focus {
background-color: #262626;
}
span {
margin-right: 10px;
}
}
.content {
@@ -197,6 +192,8 @@ li.option {
}
label {
width: 100%;
padding: 0.5em 0;
position: relative;
display: inline-block;
@@ -207,10 +204,6 @@ li.option {
background-color: #333333f2;
}
padding: 0.5em 0;
width: 100%;
cursor: pointer;
}
}
+12 -12
View File
@@ -27,8 +27,8 @@
<span>
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
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`"
@@ -41,8 +41,8 @@
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
@@ -53,8 +53,8 @@
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error="
@@ -65,8 +65,8 @@
<img
:data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp"
:data-popup-content="stockName.split(':')[0]"
data-tooltip-type="VehiclePreviewTooltip"
:data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error="
@@ -120,10 +120,10 @@ export default defineComponent({
fallbackName += /^\d?EN\d{2}/.test(stockName)
? 'ezt'
: /^SN\d{2}/.test(stockName)
? 'szt'
: /^\d?E/.test(stockName)
? 'e'
: 's';
? 'szt'
: /^\d?E/.test(stockName)
? 'e'
: 's';
} else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
@@ -43,7 +43,7 @@
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
>
<b
v-if="isDonator(historyItem.dispatcherName)"
v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator"
:title="$t('donations.dispatcher-message')"
>
@@ -128,13 +128,13 @@ import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({
components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin],
mixins: [dateMixin, styleMixin],
props: {
dispatcherHistory: {
@@ -159,6 +159,7 @@ export default defineComponent({
return {
Status,
store: useMainStore(),
apiStore: useApiStore(),
regions
};
},
@@ -9,7 +9,7 @@
ref="button"
>
<img src="/images/icon-filter2.svg" alt="Open filters" />
{{ $t('options.filters') }} [F]
[F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
@@ -17,7 +17,34 @@
</div>
<div v-else>
<TimetableHistoryList :timetableHistory="timetableHistory" />
<ul class="journal-list">
<transition-group name="list-anim">
<li
v-for="{ timetable, showExtraInfo } in computedTimetableHistory"
class="journal_item"
:key="timetable.id"
@click="showExtraInfo.value = !showExtraInfo.value"
>
<div class="journal_item-info">
<!-- General -->
<TimetableGeneral :timetable="timetable" />
<!-- Route -->
<span class="item-route">
<b>{{ timetable.route.replace('|', ' - ') }}</b>
</span>
<hr />
<!-- Stops -->
<TimetableStops :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<!-- Extra -->
<TimetableDetails :timetable="timetable" :showExtraInfo="showExtraInfo.value" />
</div>
</li>
</transition-group>
</ul>
<AddDataButton
:list="timetableHistory"
@@ -37,17 +64,29 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { defineComponent, PropType, ref } from 'vue';
import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common';
import { API } from '../../../typings/api';
import TimetableGeneral from './TimetableGeneral.vue';
import TimetableStops from './TimetableStops.vue';
import TimetableStatus from './TimetableStatus.vue';
import TimetableDetails from './TimetableDetails.vue';
export default defineComponent({
components: { Loading, AddDataButton, TimetableHistoryList },
components: {
Loading,
AddDataButton,
TimetableDetails,
TimetableGeneral,
TimetableStatus,
TimetableStops
},
props: {
timetableHistory: {
@@ -73,6 +112,15 @@ export default defineComponent({
Status,
store: useMainStore()
};
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
}
});
</script>
@@ -80,4 +128,15 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss';
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
}
</style>
@@ -0,0 +1,195 @@
<template>
<div>
<div class="details-actions">
<button class="btn--action">
<b>{{ $t('journal.stock-info') }}</b>
<img :src="`/images/icon-arrow-${showExtraInfo ? 'asc' : 'desc'}.svg`" alt="Arrow icon" />
</button>
</div>
<div class="details-body" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { StockList },
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.details-body {
margin-top: 0.5em;
}
.details-actions {
display: flex;
button img {
height: 1.25em;
}
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
@include smallScreen() {
.stock-specs {
justify-content: center;
}
.details-actions {
justify-content: center;
}
}
</style>
@@ -1,173 +0,0 @@
<template>
<div class="item-extra" v-if="timetable.stockString && timetable.stockMass && showExtraInfo">
<hr />
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.dispatcher-name') }}</span>
<span>{{ timetable.authorName }}</span>
</span>
</div>
<div class="stock-specs">
<span class="badge">
<span>{{ $t('journal.stock-max-speed') }}</span>
<span>{{ timetable.maxSpeed }}km/h</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-length') }}</span>
<span>
{{
currentHistoryIndex == 0
? timetable.stockLength
: stockHistory[currentHistoryIndex].stockLength || timetable.stockLength
}}m
</span>
</span>
<span class="badge">
<span>{{ $t('journal.stock-mass') }}</span>
<span>
{{
Math.floor(
(currentHistoryIndex == 0
? timetable.stockMass!
: stockHistory[currentHistoryIndex].stockMass || timetable.stockMass) / 1000
)
}}t
</span>
</span>
</div>
<!-- Historia zmian w składzie -->
<div class="stock-history" v-if="stockHistory.length > 1">
<button
v-for="(sh, i) in stockHistory"
:key="i"
class="btn--action"
:data-checked="i == currentHistoryIndex"
@click.stop="currentHistoryIndex = i"
>
{{ sh.updatedAt }}
</button>
</div>
<StockList
:trainStockList="
(currentHistoryIndex == 0
? timetable.stockString
: stockHistory[currentHistoryIndex].stockString
).split(';')
"
/>
</div>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import StockList from '../../Global/StockList.vue';
import { API } from '../../../typings/api';
export default defineComponent({
components: { StockList },
props: {
showExtraInfo: {
type: Boolean,
required: true
},
timetable: {
type: Object as PropType<API.TimetableHistory.Data>,
required: true
}
},
data() {
return {
currentHistoryIndex: 0
};
},
computed: {
stockHistory() {
return this.timetable.stockHistory
.slice()
.reverse()
.map((h) => {
const historyData = h.split('@');
return {
updatedAt: new Date(Number(historyData[0])).toLocaleTimeString(this.$i18n.locale, {
hour: '2-digit',
minute: '2-digit'
}),
stockString: historyData[1],
stockMass: Number(historyData[2]) || undefined,
stockLength: Number(historyData[3]) || undefined
};
});
}
},
methods: {
onImageError(e: Event) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = '/images/icon-unknown.png';
}
}
});
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/badge.scss';
.item-extra {
margin-top: 0.5em;
}
.stock-history {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 1em;
button[data-checked='true'] {
color: $accentCol;
}
}
.stock-specs {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
.badge {
margin: 0;
span:last-child {
color: black;
background-color: $accentCol;
}
}
@include smallScreen() {
justify-content: center;
}
}
ul.stock-list {
display: flex;
align-items: flex-end;
overflow: auto;
padding-bottom: 0.5em;
li > div {
margin: 1em 0;
text-align: center;
color: #aaa;
font-size: 0.9em;
}
}
</style>
@@ -24,7 +24,7 @@
</strong>
<strong
v-if="isDonator(timetable.driverName)"
v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator"
:title="$t('donations.driver-message')"
>
@@ -57,18 +57,18 @@
!timetable.terminated
? $t('journal.timetable-active')
: timetable.fulfilled
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}}
</b>
<button
v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable"
class="btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)"
>
<img src="/images/icon-train.svg" alt="" />
{{ $t('journal.timetable-online-button') }}
<img src="/images/icon-train.svg" alt="train icon" />
<b>{{ $t('journal.timetable-online-button') }}</b>
</button>
</span>
</div>
@@ -81,10 +81,16 @@ import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin, donatorMixin],
mixins: [dateMixin, modalTrainMixin, styleMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: {
timetable: {
@@ -104,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;
@@ -117,8 +123,22 @@ export default defineComponent({
margin-bottom: 0.5em;
}
.info-date {
margin-right: 0.5em;
.general-train {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.general-time {
display: flex;
align-items: center;
gap: 0.5em;
}
.badges {
@@ -143,24 +163,12 @@ export default defineComponent({
}
}
.general-train {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.25em;
cursor: pointer;
line-height: 2;
}
.btn-timetable {
display: inline-block;
padding: 0.1em 0.4em;
margin-left: 0.5em;
display: flex;
padding: 0.2em 0.5em;
img {
vertical-align: top;
height: 1.25em;
}
}
@@ -21,7 +21,7 @@
<!-- Status -->
<TimetableStatus :timetable="timetable" />
<button class="btn--option btn--show">
<button class="btn--action btn--show">
{{ $t('journal.stock-info') }}
<img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
@@ -66,9 +66,9 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../../styles/variables.scss';
@import '../../../styles/responsive.scss';
@import '../../../styles/JournalSection.scss';
@import '../../../styles/variables';
@import '../../../styles/responsive';
@import '../../../styles/JournalSection';
.btn--show {
display: flex;
@@ -72,18 +72,15 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api';
import { Status } from '../../typings/common';
import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore';
export default defineComponent({
name: 'SceneryDispatchersHistory',
mixins: [dateMixin, styleMixin, listObserverMixin],
mixins: [dateMixin, styleMixin],
components: { Loading },
props: {
station: {
+1 -2
View File
@@ -14,8 +14,7 @@
<script lang="ts">
import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station';
import { ActiveScenery } from '../../store/typings';
import { ActiveScenery, Station } from '../../typings/common';
export default defineComponent({
props: {
+1 -2
View File
@@ -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>
@@ -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>
+296 -301
View File
@@ -1,330 +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)"
@click.stop="openDonationModal"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.dispatcher-message')"
>
<img src="/images/icon-diamond.svg" alt="" />
{{ station.onlineInfo.dispatcherName }}
</b>
<th
v-for="headerName in headIconsIds"
:key="headerName"
@click="changeSorter(headerName)"
class="header-image"
:class="headerName"
>
<span class="header_wrapper">
<img
:src="`/images/icon-${headerName}.svg`"
:alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else>
{{ station.onlineInfo.dispatcherName }}
</div>
</span>
</td>
<img
class="sort-icon"
v-if="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 popupMixin from '../../mixins/popupMixin';
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, popupMixin],
mixins: [styleMixin, dateMixin],
data: () => ({
headIconsIds,
@@ -335,12 +335,17 @@ 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();
@@ -348,16 +353,19 @@ export default defineComponent({
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',
@@ -371,7 +379,7 @@ export default defineComponent({
openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true);
this.mainStore.modalLastClickedTarget = e.target;
this.hidePopUp();
this.tooltipStore.hide();
},
openForumSite(e: Event, url: string | undefined) {
@@ -396,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 {
@@ -420,9 +416,8 @@ $rowCol: #424242;
font-size: 1.5em;
padding: 1em;
margin: 1em 0;
background: #333;
background: #1a1a1a;
}
table {
+2
View File
@@ -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;
@@ -1,24 +1,24 @@
<template>
<div class="popup-content">
<span>{{ store.popUpData.content }}</span>
<div class="tooltip-content">
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
.tooltip-content {
display: flex;
justify-content: center;
align-items: center;
@@ -1,25 +1,25 @@
<template>
<div class="popup-content">
<div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="" />
<span>{{ store.popUpData.content }}</span>
<span>{{ tooltipStore.content }}</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.popup-content {
.tooltip-content {
gap: 0.5em;
padding: 0.5em;
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="spawns.length != 0">
<span v-for="(spawn, i) in spawns">
<template v-if="i > 0"> | </template>
<b>{{ spawn.spawnName }}</b> ({{ spawn.spawnLength }}m)
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { ScenerySpawn } from '../../typings/common';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
},
computed: {
spawns() {
if (this.tooltipStore.content == '') return [];
const parsedSpawns = JSON.parse(this.tooltipStore.content) as ScenerySpawn[];
return parsedSpawns ?? [];
}
}
});
</script>
<style scoped>
.tooltip-content {
width: 300px;
padding: 0.25em 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #1b1b1b;
box-shadow: 0 0 5px 2px #aaa;
}
</style>
@@ -1,55 +1,58 @@
<template>
<div class="popup" v-show="store.popUpData.key" ref="preview">
<component v-if="store.popUpData.key" :is="store.popUpData.key" />
<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 DonatorPopUp from './DonatorPopUp.vue';
import TrainCommentsPopUp from './TrainCommentsPopUp.vue';
import VehiclePreviewPopUp from './VehiclePreviewPopUp.vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp },
components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
data() {
return {
store: useMainStore()
tooltipStore: useTooltipStore()
};
},
watch: {
'store.mousePos': {
'tooltipStore.mousePos': {
deep: true,
handler(val: typeof this.store.mousePos) {
// [x, y]
handler(val: [number, number]) {
this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0px',
let translateX = '0',
translateY = '30px';
if (clientWidth < 500) {
previewEl.style.left = '50%';
translateX = '-50%';
} else if (val.x <= boxWidth / 2) {
} else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0';
translateX = '0px';
} else if (val.x >= clientWidth - boxWidth / 2) {
} else if (val[0] >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%';
translateX = '-100%';
} else {
previewEl.style.left = `${val.x}px`;
previewEl.style.left = `${val[0]}px`;
translateX = '-50%';
}
previewEl.style.top = `${val.y}px`;
previewEl.style.top = `${val[1]}px`;
const isOutside =
val.y + previewEl.getBoundingClientRect().height + 30 >=
val[1] + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)';
@@ -62,7 +65,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.popup {
.tooltip {
position: absolute;
z-index: 250;
max-width: 400px;
+44
View File
@@ -0,0 +1,44 @@
<template>
<div class="tooltip-content" v-if="trains.length != 0">
<span v-for="(train, i) in trains">
<template v-if="i > 0"> | </template>
<b>{{ train.trainNo }}</b> {{ train.driverName }}
</span>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
import { 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>
@@ -1,5 +1,5 @@
<template>
<div class="popup-content">
<div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }}
</div>
@@ -7,29 +7,29 @@
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img
v-if="store.popUpData.key"
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/${store.popUpData.content}--300px.jpg`"
:src="`https://static.spythere.eu/images/${tooltipStore.content}--300px.jpg`"
/>
<div class="vehicle-name" v-if="imageState != 'error'">
{{ store.popUpData.content.replace(/_/g, ' ') }}
{{ tooltipStore.content.replace(/_/g, ' ') }}
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
store: useMainStore(),
tooltipStore: useTooltipStore(),
imageState: 'loading'
};
},
@@ -53,9 +53,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
.popup-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
.tooltip-content {
width: 300px;
min-height: 200px;
background-color: #333;
+5 -5
View File
@@ -45,8 +45,8 @@
<div class="train-driver">
<b
v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp"
:data-popup-content="$t('donations.driver-message')"
data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')"
>
{{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" />
@@ -74,8 +74,8 @@
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<span
v-if="getSceneriesWithComments(train.timetableData).length > 0"
data-popup-key="TrainCommentsPopUp"
:data-popup-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
data-tooltip-type="BaseTooltip"
:data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData
)})`"
>
@@ -163,12 +163,12 @@
import { defineComponent } from 'vue';
import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
+6 -6
View File
@@ -1,7 +1,7 @@
<template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0">
<div class="modal-background" @click="closeModal"></div>
<div class="modal-content" ref="content" tabindex="0">
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" />
</div>
@@ -13,7 +13,7 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue';
import Train from '../../scripts/interfaces/Train';
import { Train } from '../../typings/common';
export default defineComponent({
components: { TrainInfo, TrainSchedule },
@@ -71,7 +71,7 @@ export default defineComponent({
text-align: left;
}
.modal_background {
.modal-background {
position: absolute;
top: 0;
left: 0;
@@ -83,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;
+1 -1
View File
@@ -4,7 +4,7 @@
<button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-filter2.svg" alt="Open filters icon" />
{{ $t('options.filters') }} [F]
[F] {{ $t('options.filters') }}
<span class="active-indicator" v-if="currentOptionsActive"></span>
</button>
+1 -1
View File
@@ -81,11 +81,11 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train';
import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common';
export interface TrainScheduleStop {
nameHtml: string;
+1 -2
View File
@@ -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({
+24
View File
@@ -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
}
+35 -8
View File
@@ -27,11 +27,11 @@
"SKR": "Train with exceeded gauge"
},
"update": {
"title": "Stacjownik app update!",
"version": "Version {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "context tooltips when hovering over project sponsors and timetable comment warnings\nvehicle image preview when hovering over its thumbnail in the active timetable card view and timetable journal\nlink to the driver's timetable history in the active timetable card view\nlink to the driver's active timetable card view in the timetable journal (available for online trains only)\nnew update card with version changelog"
"title": "Stacjownik update!",
"confirm": "ROGER THAT!",
"no-data": "No data about the latest app update has been found",
"info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
},
"app": {
"sceneries": "SCENERIES",
@@ -81,7 +81,20 @@
"ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK",
"mechaniczne+SCS": "levers + SCS"
"mechaniczne+SCS": "levers + SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
},
"status": {
"online": "UNTIL ",
@@ -169,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",
@@ -222,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",
@@ -279,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...",
@@ -367,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",
+34 -9
View File
@@ -28,10 +28,10 @@
},
"update": {
"title": "Aktualizacja Stacjownika!",
"version": "Wersja {0}",
"confirm-button": "UPDATE NOW",
"later-button": "LATER",
"content": "dymki kontekstowe po najechaniu kursorem na m.in. sponsorów projektu i uwagi eksploatacyjne\npodgląd pojazdu po najechaniu kursorem na jego miniaturkę w karcie aktywnego rozkładu jazdy oraz dzienniku RJ\nodnośnik do historii RJ maszynisty w widoku karty aktywnego rozkładu jazdy\nodnośnik do karty aktywnego rozkładu jazdy maszynisty w dzienniku (dostępny tylko dla pociągów online)\nnowa karta ze zmianami w aktualizacji"
"confirm": "PRZYJĄŁEM!",
"no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
"info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
},
"app": {
"sceneries": "SCENERIE",
@@ -77,7 +77,20 @@
"ręczne+SCS": "ręczne z SCS",
"mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne z SPK",
"mechaniczne+SCS": "mechaniczne z SCS"
"mechaniczne+SCS": "mechaniczne z SCS",
"abbrevs": {
"SPK": "SPK",
"SCS": "SCS",
"SCS-SPK": "S/S",
"SPE": "SPE",
"ręczne": "R",
"ręczne+SPK": "R",
"ręczne+SCS": "R",
"mechaniczne": "M",
"mechaniczne+SPK": "M",
"mechaniczne+SCS": "M"
}
},
"status": {
"online": "DO ",
@@ -163,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",
@@ -209,7 +223,6 @@
"semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA",
"historical": "HISTORYCZNA",
"free": "WOLNA",
"occupied": "ZAJĘTA",
@@ -219,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)",
@@ -274,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...",
@@ -352,7 +377,7 @@
"timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE",
"stock-info": "SZCZEGÓŁY",
"stock-length": "Długość",
"stock-mass": "Masa",
"stock-max-speed": "Prędkość maks.",
+1 -2
View File
@@ -1,6 +1,5 @@
import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings';
import Train from '../scripts/interfaces/Train';
import { TrainStop } from '../store/typings';
import { Train, TrainStop } from '../typings/common';
function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1;
-16
View File
@@ -1,16 +0,0 @@
import { defineComponent } from 'vue';
import { useApiStore } from '../store/apiStore';
export default defineComponent({
data() {
return {
apiStore: useApiStore()
};
},
methods: {
isDonator(name: string) {
return this.apiStore.donatorsData.includes(name);
}
}
});
-27
View File
@@ -1,27 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
data: () => ({
observer: null as IntersectionObserver | null,
observerTarget: null as Element | null
}),
methods: {
mountObserver(actionFunction: () => void, target: Element) {
this.observer = new IntersectionObserver(
(entries) => {
if (entries[0].intersectionRatio > 0.5) actionFunction();
},
{ threshold: 0.2 }
);
this.observer.observe(target);
},
unmountObserver() {
if (!this.observerTarget) return;
this.observer?.unobserve(this.observerTarget);
}
}
});
+4 -2
View File
@@ -1,10 +1,12 @@
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()
};
},
@@ -17,7 +19,7 @@ export default defineComponent({
closeModal() {
this.store.chosenModalTrainId = undefined;
this.store.popUpData.key = null;
this.tooltipStore.hide();
setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus();
-27
View File
@@ -1,27 +0,0 @@
import { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore';
import { PopUpType, popupKeys } from '../store/typings';
const isPopUp = (v: any): v is PopUpType => popupKeys.includes(v);
export default defineComponent({
data() {
return {
store: useMainStore()
};
},
methods: {
showPopUp(e: MouseEvent, componentKey: string, value?: string) {
if (!isPopUp(componentKey)) return;
this.store.popUpData['key'] = componentKey;
this.store.popUpData['content'] = value ?? '';
},
hidePopUp() {
this.store.popUpData['key'] = null;
this.store.popUpData['content'] = '';
}
}
});
-24
View File
@@ -1,24 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
methods: {
getControlTypeAbbrev(controlType: string) {
switch (controlType) {
case 'mechaniczne':
return 'M';
case 'SCS-SPK':
return 'S/S';
case 'ręczne':
return 'R';
case 'mechaniczne+SPK':
return 'M';
case 'ręczne+SPK':
return 'R';
case 'mechaniczne+SCS':
return 'M';
default:
return controlType;
}
}
}
});
+3 -4
View File
@@ -1,6 +1,5 @@
import { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train';
import { TrainStop } from '../store/typings';
import { Train, TrainStop } from '../typings/common';
export default defineComponent({
data: () => ({
@@ -51,8 +50,8 @@ export default defineComponent({
return diffMins < 1
? this.$t('trains.last-seen-now')
: diffMins < 2
? this.$t('trains.last-seen-min')
: this.$t('trains.last-seen-ago', { minutes: diffMins });
? this.$t('trains.last-seen-min')
: this.$t('trains.last-seen-ago', { minutes: diffMins });
},
displayTrainPosition(train: Train) {
-34
View File
@@ -1,34 +0,0 @@
import { Availability, ActiveScenery } from '../../store/typings';
import { StationRoutes } from './StationRoutes';
export default interface Station {
name: string;
generalInfo?: {
name: string;
url: string;
abbr: string;
hash?: string;
reqLevel: number;
// supportersOnly: boolean;
lines: string;
project: string;
projectUrl?: string;
signalType: string;
controlType: string;
SUP: boolean;
ASDEK: boolean;
authors?: string[];
availability: Availability;
routes: StationRoutes;
checkpoints: string[];
};
onlineInfo?: ActiveScenery;
}
-12
View File
@@ -1,12 +0,0 @@
import { StationRoutesInfo } from '../../store/typings';
export interface StationRoutes {
single: StationRoutesInfo[];
double: StationRoutesInfo[];
singleElectrifiedNames: string[];
singleOtherNames: string[];
doubleElectrifiedNames: string[];
doubleOtherNames: string[];
sblNames: string[];
}
-38
View File
@@ -1,38 +0,0 @@
import { TrainStop } from '../../store/typings';
export default interface Train {
trainId: string;
mass: number;
length: number;
speed: number;
signal: string;
distance: number;
connectedTrack: string;
driverId: number;
trainNo: number;
driverName: string;
driverLevel: number;
currentStationName: string;
currentStationHash: string;
locoType: string;
online: boolean;
lastSeen: number;
region: string;
stockList: string[];
isTimeout: boolean;
isSupporter: boolean;
timetableData?: {
timetableId: number;
category: string;
route: string;
followingStops: TrainStop[];
TWR: boolean;
SKR: boolean;
routeDistance: number;
sceneries: string[];
sceneryNames: string[];
};
}
+6 -4
View File
@@ -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 -14
View File
@@ -54,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);
+52 -17
View File
@@ -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,11 +29,8 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined,
modalLastClickedTarget: null,
mousePos: { x: 0, y: 0 },
popUpData: { key: null, content: '' }
}) as StoreState,
modalLastClickedTarget: null
}) as MainStoreState,
getters: {
trainList(): Train[] {
@@ -96,6 +96,7 @@ export const useMainStore = defineStore('store', {
});
},
// computed active sceneries
activeSceneryList(state): ActiveScenery[] {
const apiStore = useApiStore();
@@ -156,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,
@@ -231,6 +232,7 @@ export const useMainStore = defineStore('store', {
return allActiveSceneries;
},
// computed station data
stationList(): Station[] {
const apiStore = useApiStore();
@@ -248,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;
@@ -259,7 +268,9 @@ export const useMainStore = defineStore('store', {
double: [],
doubleElectrifiedNames: [],
doubleOtherNames: [],
sblNames: []
sblNames: [],
minRouteSpeed: 0,
maxRouteSpeed: 0
} as StationRoutes
);
@@ -274,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
)
}))
];
}
}
});
+3 -21
View File
@@ -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));
}
+56
View File
@@ -0,0 +1,56 @@
import { defineStore } from 'pinia';
const isTooltip = (v: any): v is TooltipType => tooltipKeys.includes(v);
export const tooltipKeys = [
'DonatorTooltip',
'BaseTooltip',
'VehiclePreviewTooltip',
'SpawnsTooltip',
'UsersTooltip'
] as const;
export type TooltipType = (typeof tooltipKeys)[number];
export const useTooltipStore = defineStore('tooltipStore', {
state: () => ({
mousePos: [0, 0],
type: null as TooltipType | null,
content: ''
}),
actions: {
show(_e: MouseEvent, type: string, value?: string) {
if (!isTooltip(type)) return;
this.type = type;
this.content = value ?? '';
},
hide() {
this.type = null;
this.content = '';
},
handle(e: MouseEvent) {
const targetEl = e
.composedPath()
.find((p) => p instanceof HTMLElement && p.getAttribute('data-tooltip-type'));
if (!targetEl || !(targetEl instanceof HTMLElement)) {
if (this.type != null) this.hide();
return;
}
const tooltipType = targetEl.getAttribute('data-tooltip-type');
const tooltipContent = targetEl.getAttribute('data-tooltip-content');
if (tooltipType && tooltipContent) this.show(e, tooltipType, tooltipContent);
else if (this.type != null) this.hide();
this.mousePos[0] = e.pageX;
this.mousePos[1] = e.pageY;
}
}
});
+4 -134
View File
@@ -1,20 +1,10 @@
import { API } from '../typings/api';
import { Status } from '../typings/common';
import { Availability, StationRoutesInfo, Status } from '../typings/common';
export const popupKeys = ['DonatorPopUp', 'TrainCommentsPopUp', 'VehiclePreviewPopUp'] as const;
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type PopUpType = (typeof popupKeys)[number];
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface StoreState {
region: { id: string; value: string };
export interface MainStoreState {
region: { id: string; value: string; name: string };
isOffline: boolean;
isNewUpdate: boolean;
appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string;
@@ -22,19 +12,6 @@ export interface StoreState {
driverStatsStatus: Status.Data;
chosenModalTrainId?: string;
modalLastClickedTarget: EventTarget | null;
mousePos: { x: number; y: number };
popUpData: { key: PopUpType | null; content: string };
}
export interface StationRoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeLength: number;
routeSpeed: number;
routeTracks: number;
hidden?: boolean;
}
export interface StationJSONData {
@@ -62,110 +39,3 @@ export interface StationJSONData {
availability: Availability;
}
export interface ActiveScenery {
name: string;
hash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawns: { spawnName: string; spawnLength: number; isElectrified: boolean }[];
dispatcherName: string;
dispatcherRate: number;
dispatcherId: number;
dispatcherExp: number;
dispatcherIsSupporter: boolean;
dispatcherStatus: Status.ActiveDispatcher | number;
dispatcherTimestamp: number | null;
isOnline: boolean;
stationTrains?: StationTrain[];
scheduledTrains?: ScheduledTrain[];
scheduledTrainCount: {
all: number;
confirmed: number;
unconfirmed: number;
};
}
export interface StationTrain {
driverName: string;
driverId: number;
trainNo: number;
trainId: string;
stopStatus: string;
}
export interface ScheduledTrain {
checkpointName: string;
trainId: string;
trainNo: number;
driverName: string;
driverId: number;
currentStationName: string;
currentStationHash: string;
category: string;
stopInfo: TrainStop;
terminatesAt: string;
beginsAt: string;
prevStationName: string;
nextStationName: string;
arrivingLine: string | null;
departureLine: string | null;
prevDepartureLine: string | null;
nextArrivalLine: string | null;
signal: string;
connectedTrack: string;
stopLabel: string;
stopStatus: StopStatus;
stopStatusID: number;
region: string;
}
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export interface TrainStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
pointId: number;
comments?: string;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: boolean;
stopped: boolean;
stopTime: number | null;
}
+20 -5
View File
@@ -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 };
});
}
+13
View File
@@ -101,3 +101,16 @@
background-color: #be3728;
}
}
.spawn-badge {
color: white;
.length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > .name {
background-color: #007599;
}
}
+21
View File
@@ -213,6 +213,7 @@ a.a-button {
&.btn--action {
background-color: #424242;
border-radius: 0.25em;
font-weight: bold;
&:hover {
background-color: #555;
@@ -298,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
View File
@@ -128,8 +128,8 @@ export namespace API {
export type Response = Data[];
export interface Data {
id: string;
trainNo: number;
mass: number;
length: number;
speed: number;
+213
View File
@@ -1,3 +1,15 @@
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault';
export type ScenerySpawnType = 'passenger' | 'freight' | 'loco' | 'all';
export enum StopStatus {
ARRIVING = 'arriving',
DEPARTED = 'departed',
DEPARTED_AWAY = 'departed-away',
ONLINE = 'online',
STOPPED = 'stopped',
TERMINATED = 'terminated'
}
export namespace Status {
export enum ActiveDispatcher {
FREE = -3,
@@ -20,3 +32,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
View File
@@ -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;
+1 -1
View File
@@ -20,11 +20,11 @@ import { computed, ComputedRef, defineComponent, provide, reactive, ref, watch }
import TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train';
import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager';
import TrainStats from '../components/TrainsView/TrainStats.vue';
import { Train } from '../typings/common';
export default defineComponent({
components: {
+37 -24
View File
@@ -12,38 +12,28 @@ 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',
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',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
@@ -52,6 +42,20 @@ export default defineConfig({
statuses: [0, 200, 404]
}
}
},
{
urlPattern: new RegExp('^https://static.spythere.eu/images/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'spythere-images-cache',
expiration: {
maxEntries: 100,
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
},
@@ -60,5 +64,14 @@ export default defineConfig({
suppressWarnings: true
}
})
]
],
build: {
rollupOptions: {
output: {
entryFileNames: 'app-[name].js',
assetFileNames: 'app-[name].css',
chunkFileNames: 'chunk-[name].js'
}
}
}
});
+1964 -2600
View File
File diff suppressed because it is too large Load Diff