Compare commits

...

34 Commits

Author SHA1 Message Date
Spythere 1f457d6389 Merge pull request #88 from Spythere/development
hotfix: minor adjustments for new simulator version (2024.1.1)
2024-05-13 15:05:28 +02:00
Spythere eb5b94c9f6 chore: vehicle images hotfixes 2024-05-13 15:02:15 +02:00
Spythere 328e8c0573 chore: fixed stock fallback thumbnnail 2024-05-13 14:54:21 +02:00
Spythere 9f58ae5428 Merge pull request #87 from Spythere/development
hotfix: modal positioning
2024-05-12 15:23:30 +02:00
Spythere ebd0eeb8c4 hotfix: modal positioning 2024-05-12 15:22:03 +02:00
Spythere fa656c2f26 Merge pull request #86 from Spythere/development
v1.24.0
2024-05-12 15:14:22 +02:00
Spythere 0cc3a12d1d fix: modal responsiveness 2024-05-12 14:55:35 +02:00
Spythere 392a6437f8 feature: current users tooltip 2024-05-09 17:19:22 +02:00
Spythere 122532f0ed chore: general fixes 2024-05-09 16:40:53 +02:00
Spythere 366ff91f60 hotfix: update modal 2024-05-08 20:12:07 +02:00
Spythere a0496736dd chore: modals update 2024-05-08 20:04:41 +02:00
Spythere f974120e87 fix: lock files 2024-05-08 18:42:33 +02:00
Spythere abd8b8178b chore: vue deep selector 2024-05-08 16:42:04 +02:00
Spythere f1fcde8459 feat: update modal 2024-05-08 16:41:14 +02:00
Spythere b3289d6aab chore: region dropdown fixes 2024-05-08 15:16:20 +02:00
Spythere 6481a4a3b0 chore: design improvements 2024-05-08 15:10:40 +02:00
Spythere 05dc268526 fix: spawns detection 2024-05-06 18:18:15 +02:00
Spythere 669acc98d2 chore: station stats translation 2024-05-06 18:16:30 +02:00
Spythere 3371b661c2 fix: ufactor calc 2024-05-06 17:53:07 +02:00
Spythere 871b2c0221 feature: open spawns tooltip 2024-05-06 17:36:23 +02:00
Spythere d366a877a4 refactor: popups -> tooltips 2024-05-06 16:37:56 +02:00
Spythere 405aab96bd feature: stations stats 2024-05-05 13:34:43 +02:00
Spythere f29c160000 fix: lock files 2024-05-04 14:47:30 +02:00
Spythere a2de0e2030 refactor: types & performance 2024-05-04 14:43:34 +02:00
Spythere 7dd1c06f3f chore: accessibility of filters 2024-05-03 19:29:10 +02:00
Spythere ff041b9aaf bump(version): 1.24.0 2024-05-03 19:02:49 +02:00
Spythere 4782dba444 feat(app): added min route speed & max route speed station filters 2024-05-03 19:02:16 +02:00
Spythere d6b8d032d6 fix(app): improved data fetching scheduler 2024-05-03 19:02:13 +02:00
Spythere c16616330c chore(packages): update & cleanup 2024-05-03 18:01:54 +02:00
Spythere 57cec8bfe7 chore: pwa adjustments 2024-05-03 17:49:54 +02:00
Spythere 6bea340e19 chore(pwa): changed sceneries cache to cachefirst 2024-05-01 19:37:51 +02:00
Spythere c181cf7e64 fix(workflows): release color 2024-04-27 01:11:38 +02:00
Spythere 8e4ae64cd3 chore(workflows): added release discord webhook notification 2024-04-15 15:13:22 +02:00
Spythere 5750490f01 refactor: journals 2024-04-08 23:21:50 +02:00
72 changed files with 6472 additions and 13527 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", "name": "stacjownik",
"version": "1.23.1", "version": "1.24.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -14,11 +14,9 @@
"dependencies": { "dependencies": {
"core-js": "^3.32.2", "core-js": "^3.32.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"firebase": "^10.4.0",
"howler": "^2.2.4",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"sass": "^1.67.0", "sass": "^1.67.0",
"socket.io-client": "^4.7.4", "showdown": "^2.1.0",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-i18n": "^9.4.1", "vue-i18n": "^9.4.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
@@ -26,7 +24,8 @@
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3", "@rushstack/eslint-patch": "^1.3.3",
"@types/node": "^20.6.2", "@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", "@vitejs/plugin-vue": "^4.3.4",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
@@ -37,7 +36,7 @@
"prettier": "^3.0.3", "prettier": "^3.0.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-pwa": "^0.16.5", "vite-plugin-pwa": "^0.20.0",
"vue-tsc": "^1.8.11" "vue-tsc": "^1.8.11"
}, },
"browserslist": [ "browserslist": [
+59 -66
View File
@@ -1,6 +1,11 @@
<template> <template>
<div class="app_container" v-cloak> <div class="app_container">
<PopUp /> <UpdateModal
:update-modal-open="isUpdateModalOpen"
@toggle-modal="() => (isUpdateModalOpen = false)"
/>
<Tooltip />
<transition name="modal-anim"> <transition name="modal-anim">
<keep-alive> <keep-alive>
@@ -22,7 +27,10 @@
&copy; &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> <a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | {{ 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 /> <br />
<a href="https://discord.gg/x2mpNN3svk"> <a href="https://discord.gg/x2mpNN3svk">
<img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b> <img src="/images/icon-discord.png" alt="" />&nbsp;<b>{{ $t('footer.discord') }}</b>
@@ -36,19 +44,21 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import axios from 'axios'; 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 { 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 Clock from './components/App/Clock.vue';
import StatusIndicator from './components/App/StatusIndicator.vue'; import StatusIndicator from './components/App/StatusIndicator.vue';
import AppHeader from './components/App/AppHeader.vue'; import AppHeader from './components/App/AppHeader.vue';
import TrainModal from './components/TrainsView/TrainModal.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 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'; const STORAGE_VERSION_KEY = 'app_version';
@@ -58,19 +68,22 @@ export default defineComponent({
StatusIndicator, StatusIndicator,
AppHeader, AppHeader,
TrainModal, TrainModal,
PopUp UpdateModal,
Tooltip
}, },
mixins: [popupMixin],
data: () => ({ data: () => ({
VERSION: version, VERSION: version,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(), apiStore: useApiStore(),
tooltipStore: useTooltipStore(),
isUpdateModalOpen: false,
currentLang: 'pl', currentLang: 'pl',
releaseURL: '', isOnProductionHost: location.hostname == 'stacjownik-td2.web.app',
isOnProductionHost: location.hostname == 'stacjownik-td2.web.app'
nextUpdateTime: 0
}), }),
created() { created() {
@@ -78,41 +91,52 @@ export default defineComponent({
}, },
async mounted() { async mounted() {
window.addEventListener('focus', () => { window.addEventListener('mousemove', (e: MouseEvent) => this.tooltipStore.handle(e));
if (Date.now() - this.apiStore.lastFetchData.getTime() < 15000) return;
this.apiStore.fetchActiveData();
});
window.addEventListener('mousemove', (e: MouseEvent) => this.handlePopUpEvents(e));
}, },
methods: { methods: {
init() { init() {
this.loadLang(); this.loadLang();
this.setReleaseURL();
this.setupOfflineHandling(); this.setupOfflineHandling();
this.checkAppVersion(); this.checkAppVersion();
this.apiStore.setupAPIData(); this.apiStore.setupAPIData();
window.requestAnimationFrame(this.update);
if (!this.isOnProductionHost) document.title = 'Stacjownik Dev'; if (!this.isOnProductionHost) document.title = 'Stacjownik Dev';
}, },
checkAppVersion() { update(t: number) {
if (import.meta.env.DEV) { if (t >= this.nextUpdateTime) {
this.store.isNewUpdate = true; this.apiStore.fetchActiveData();
this.nextUpdateTime = t + 20000;
return;
} }
window.requestAnimationFrame(this.update);
},
async checkAppVersion() {
const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY); const storageVersion = StorageManager.getStringValue(STORAGE_VERSION_KEY);
if (storageVersion === undefined || storageVersion != version) { try {
this.store.isNewUpdate = true; 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() { setupOfflineHandling() {
@@ -137,27 +161,6 @@ export default defineComponent({
this.apiStore.connectToAPI(); 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) { changeLang(lang: string) {
this.$i18n.locale = lang; this.$i18n.locale = lang;
this.currentLang = lang; this.currentLang = lang;
@@ -165,21 +168,6 @@ export default defineComponent({
StorageManager.setStringValue('lang', lang); 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() { loadLang() {
const storageLang = StorageManager.getStringValue('lang'); const storageLang = StorageManager.getStringValue('lang');
@@ -255,10 +243,15 @@ export default defineComponent({
} }
// FOOTER // FOOTER
footer.app_footer { .app_footer {
max-width: 100%; max-width: 100%;
padding: 0.5em; padding: 0.5em;
button {
display: inline-block;
padding: 0.1em;
}
img { img {
width: 1.1em; width: 1.1em;
vertical-align: text-bottom; vertical-align: text-bottom;
-11
View File
@@ -29,11 +29,6 @@
<img src="/images/icon-dispatcher.svg" alt="icon dispatcher" /> <img src="/images/icon-dispatcher.svg" alt="icon dispatcher" />
<span class="text--primary">{{ onlineDispatchersCount }}</span> <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--grayed"> / </span>
<span class="text--primary">{{ onlineTrainsCount }}</span> <span class="text--primary">{{ onlineTrainsCount }}</span>
<img src="/images/icon-train.svg" alt="icon train" /> <img src="/images/icon-train.svg" alt="icon train" />
@@ -103,12 +98,6 @@ export default defineComponent({
return this.store.activeSceneryList.filter( return this.store.activeSceneryList.filter(
(scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1 (scenery) => scenery.region == this.store.region.id && scenery.dispatcherId != -1
).length; ).length;
},
factorU() {
return this.onlineDispatchersCount == 0
? '-'
: (this.onlineTrainsCount / this.onlineDispatchersCount).toFixed(2);
} }
}, },
components: { StatusIndicator, Clock, RegionDropdown } components: { StatusIndicator, Clock, RegionDropdown }
+70 -82
View File
@@ -1,36 +1,20 @@
<template> <template>
<AnimatedModal :is-open="mainStore.isNewUpdate" @toggle-modal="toggleModal"> <AnimatedModal :is-open="updateModalOpen" @toggle-modal="toggleModal(false)">
<div class="modal_content"> <div class="modal-content">
<div> <h1 style="margin-bottom: 0.5em">🚀 {{ $t('update.title') }}</h1>
<h1 style="margin-bottom: 0.5em">{{ $t('update.title') }}</h1>
<h2 class="text--primary">{{ $t('update.version', [version]) }}</h2>
<hr class="separator" />
</div>
<div class="features-list"> <div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<h2>Nowości i zmiany:</h2> <div class="no-features" v-else>{{ $t('update.no-data') }}</div>
<ul>
<li v-for="content in localeChangesArray" :key="content">{{ content }}</li>
</ul>
</div>
<div class="modal_actions"> <button class="btn btn--action" ref="confirm-btn" @click="toggleModal(false)">
<button class="btn--action">Przyjąłem!</button> {{ $t('update.confirm') }}
</button>
<p>Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony!</p> <p class="bottom-info">
{{ $t('update.info-1') }}
<!-- <div class="actions-checkboxes"> <br />
<label> <span v-html="$t('update.info-2')"></span>
<input type="checkbox" /> </p>
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
<label>
<input type="checkbox" />
<span>nie pokazuj dla przyszłych aktualizacji</span>
</label>
</div> -->
</div>
</div> </div>
</AnimatedModal> </AnimatedModal>
</template> </template>
@@ -39,11 +23,24 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { Converter } from 'showdown';
import AnimatedModal from '../Global/AnimatedModal.vue'; import AnimatedModal from '../Global/AnimatedModal.vue';
const converter = new Converter();
export default defineComponent({ export default defineComponent({
components: { AnimatedModal }, components: { AnimatedModal },
props: {
updateModalOpen: {
type: Boolean,
required: true
}
},
emits: ['toggleModal'],
data() { data() {
return { return {
mainStore: useMainStore(), 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: { computed: {
localeChangesArray() { htmlChangelog() {
return this.$t('update.content').split('\n'); 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.modal_content { ::v-deep(h1) {
font-size: 1.2em;
text-align: center; 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; display: grid;
grid-template-rows: auto 1fr auto; grid-template-rows: auto 1fr auto;
gap: 0.5em; gap: 0.5em;
} padding: 1em;
min-height: 700px;
hr.separator {
margin: 0.5em 0;
padding: 0;
height: 3px;
background-color: #fff;
}
.features-list {
margin-top: 0.5em;
overflow: auto; overflow: auto;
text-align: justify;
ul {
text-align: left;
list-style: '\21D2 ';
padding: 1em;
}
li {
margin: 0.5em 0;
}
} }
.modal_actions { .no-features {
display: flex; text-align: center;
flex-wrap: wrap;
gap: 0.5em;
button {
font-weight: bold;
padding: 0.35em;
}
p {
font-size: 0.9em;
}
} }
.actions-checkboxes { button {
display: flex; margin: 0 auto;
flex-wrap: wrap; padding: 0.5em 0.75em;
justify-content: center; font-size: 1.1em;
}
gap: 1em; p.bottom-info {
text-align: center;
color: #ccc;
}
label { a {
font-size: 0.9em; text-decoration: underline;
}
label > input {
margin-right: 0.5em;
}
} }
</style> </style>
+16 -29
View File
@@ -1,8 +1,8 @@
<template> <template>
<transition name="modal-anim" tag="div" class="modal"> <transition name="modal-anim" tag="div">
<div class="body" v-if="isOpen"> <div class="modal" v-if="isOpen">
<div class="background" @click="toggleModal(false)"></div> <div class="modal-background" @click="toggleModal(false)"></div>
<div class="wrapper" ref="wrapper" tabindex="0"> <div class="modal-wrapper" ref="wrapper" tabindex="0">
<slot></slot> <slot></slot>
</div> </div>
<div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div> <div class="tab-exit" ref="exit" tabindex="0" @focus="toggleModal(false)"></div>
@@ -30,8 +30,7 @@ export default defineComponent({
watch: { watch: {
isOpen(v) { isOpen(v) {
this.$nextTick(() => { this.$nextTick(() => {
if (v) (this.$refs['wrapper'] as HTMLElement).focus(); if (v == false) (this.store.modalLastClickedTarget as HTMLElement)?.focus();
else (this.store.modalLastClickedTarget as HTMLElement)?.focus();
}); });
} }
}, },
@@ -47,17 +46,17 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/responsive.scss'; @import '../../styles/responsive.scss';
.body { .modal {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 200;
width: 100vw; width: 100%;
height: 100vh; height: 100vh;
z-index: 200;
} }
.background { .modal-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -69,33 +68,21 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.wrapper { .modal-wrapper {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 210;
background-color: #1a1a1a; overflow: auto;
box-shadow: 0 0 15px 10px #333333;
width: 95%;
max-width: 800px;
max-height: 95vh; max-height: 95vh;
& > :slotted(div) { & > :slotted(div) {
max-height: 95vh; background-color: #1a1a1a;
} box-shadow: 0 0 15px 10px #0e0e0e;
}
@include smallScreen { width: 95vw;
.wrapper { max-width: 850px;
top: 0;
transform: translate(-50%, 1em);
max-height: 90vh;
& > :slotted(div) {
max-height: 90vh;
}
} }
} }
</style> </style>
@@ -66,6 +66,7 @@
class="modal-action a-button btn--image coffee" class="modal-action a-button btn--image coffee"
href="https://buycoffee.to/spythere" href="https://buycoffee.to/spythere"
target="_blank" target="_blank"
ref="action"
> >
<img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" /> <img src="/images/icon-coffee.png" width="20" alt="buycoffee.to donation" />
{{ $t('donations.action-buycoffee') }} {{ $t('donations.action-buycoffee') }}
@@ -103,9 +104,13 @@ export default defineComponent({
emits: ['toggleModal'], emits: ['toggleModal'],
watch: { watch: {
isModalOpen(b: boolean) { isModalOpen(val: boolean) {
this.running = b; this.running = val;
this.lastUpdate = Date.now(); 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, immediate: true,
handler(regionQuery: string) { handler(regionQuery: string) {
if (regionQuery) { if (regionQuery) {
this.store.region.id = this.store.region =
regionsJSON.find( regionsJSON.find(
(reg) => (reg) =>
reg.id == regionQuery.toLocaleLowerCase() || reg.id == regionQuery.toLocaleLowerCase() ||
reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase() reg.value.toLocaleLowerCase() == regionQuery.toLocaleLowerCase()
)?.id || 'eu'; ) ?? regionsJSON[0];
} }
} }
} }
@@ -139,15 +139,10 @@ button.selected-region {
color: paleturquoise; color: paleturquoise;
font-weight: bold; font-weight: bold;
padding: 0.1em 0.5em;
&:focus { &:focus {
background-color: #262626; background-color: #262626;
} }
span {
margin-right: 10px;
}
} }
.content { .content {
@@ -197,6 +192,8 @@ li.option {
} }
label { label {
width: 100%;
padding: 0.5em 0;
position: relative; position: relative;
display: inline-block; display: inline-block;
@@ -207,10 +204,6 @@ li.option {
background-color: #333333f2; background-color: #333333f2;
} }
padding: 0.5em 0;
width: 100%;
cursor: pointer; cursor: pointer;
} }
} }
+12 -16
View File
@@ -27,8 +27,8 @@
<span> <span>
<img <img
:data-mouseover="stockName" :data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp" data-tooltip-type="VehiclePreviewTooltip"
:data-popup-content="stockName.split(':')[0]" :data-tooltip-content="stockName.split(':')[0]"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${ :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}${
/^EN/.test(stockName) ? 'rb' : '' /^EN/.test(stockName) ? 'rb' : ''
}.png`" }.png`"
@@ -41,8 +41,8 @@
<!-- /// Manualne dodawanie miniaturek członów dla kibelków /// --> <!-- /// Manualne dodawanie miniaturek członów dla kibelków /// -->
<img <img
:data-mouseover="stockName" :data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp" data-tooltip-type="VehiclePreviewTooltip"
:data-popup-content="stockName.split(':')[0]" :data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error=" @error="
@@ -53,8 +53,8 @@
<img <img
:data-mouseover="stockName" :data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp" data-tooltip-type="VehiclePreviewTooltip"
:data-popup-content="stockName.split(':')[0]" :data-tooltip-content="stockName.split(':')[0]"
v-if="/^EN71/.test(stockName)" v-if="/^EN71/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}s.png`"
@error=" @error="
@@ -65,8 +65,8 @@
<img <img
:data-mouseover="stockName" :data-mouseover="stockName"
data-popup-key="VehiclePreviewPopUp" data-tooltip-type="VehiclePreviewTooltip"
:data-popup-content="stockName.split(':')[0]" :data-tooltip-content="stockName.split(':')[0]"
v-if="/^(EN|2EN)/.test(stockName)" v-if="/^(EN|2EN)/.test(stockName)"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`" :src="`https://rj.td2.info.pl/dist/img/thumbnails/${stockName.split(':')[0]}ra.png`"
@error=" @error="
@@ -116,14 +116,10 @@ export default defineComponent({
const isLoco = /.-\d{3}/.test(stockName); const isLoco = /.-\d{3}/.test(stockName);
if (isLoco) { if (isLoco) {
fallbackName += 'loco-'; if (/^\d?EN\d{2}/.test(stockName)) fallbackName = 'loco-ezt';
fallbackName += /^\d?EN\d{2}/.test(stockName) else if (/^SN\d{2}/.test(stockName)) fallbackName = 'loco-szt';
? 'ezt' else if (/^\d{0,}?E/.test(stockName)) fallbackName = 'loco-e';
: /^SN\d{2}/.test(stockName) else fallbackName = 'loco-s';
? 'szt'
: /^\d?E/.test(stockName)
? 'e'
: 's';
} else { } else {
const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName); const isCarPassenger = /(\d{3}a|(Bau|Gor)\d{2}|304C)_/.test(stockName);
@@ -43,7 +43,7 @@
:to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`" :to="`/journal/dispatchers?search-dispatcher=${historyItem.dispatcherName}`"
> >
<b <b
v-if="isDonator(historyItem.dispatcherName)" v-if="apiStore.donatorsData.includes(historyItem.dispatcherName)"
class="text--donator" class="text--donator"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
> >
@@ -128,13 +128,13 @@ import { Status } from '../../../typings/common';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import donatorMixin from '../../../mixins/donatorMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
components: { Loading, AddDataButton }, components: { Loading, AddDataButton },
mixins: [dateMixin, styleMixin, donatorMixin], mixins: [dateMixin, styleMixin],
props: { props: {
dispatcherHistory: { dispatcherHistory: {
@@ -159,6 +159,7 @@ export default defineComponent({
return { return {
Status, Status,
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore(),
regions regions
}; };
}, },
@@ -9,7 +9,7 @@
ref="button" ref="button"
> >
<img src="/images/icon-filter2.svg" alt="Open filters" /> <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> <span class="active-indicator" v-if="currentOptionsActive"></span>
</button> </button>
@@ -17,7 +17,34 @@
</div> </div>
<div v-else> <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 <AddDataButton
:list="timetableHistory" :list="timetableHistory"
@@ -37,17 +64,29 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType, ref } from 'vue';
import Loading from '../../Global/Loading.vue'; import Loading from '../../Global/Loading.vue';
import AddDataButton from '../../Global/AddDataButton.vue'; import AddDataButton from '../../Global/AddDataButton.vue';
import TimetableHistoryList from './TimetableHistoryList.vue';
import { useMainStore } from '../../../store/mainStore'; import { useMainStore } from '../../../store/mainStore';
import { Status } from '../../../typings/common'; import { Status } from '../../../typings/common';
import { API } from '../../../typings/api'; 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({ export default defineComponent({
components: { Loading, AddDataButton, TimetableHistoryList }, components: {
Loading,
AddDataButton,
TimetableDetails,
TimetableGeneral,
TimetableStatus,
TimetableStops
},
props: { props: {
timetableHistory: { timetableHistory: {
@@ -73,6 +112,15 @@ export default defineComponent({
Status, Status,
store: useMainStore() store: useMainStore()
}; };
},
computed: {
computedTimetableHistory() {
return this.timetableHistory.map((timetable) => ({
timetable,
showExtraInfo: ref(false)
}));
}
} }
}); });
</script> </script>
@@ -80,4 +128,15 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection.scss';
@import '../../../styles/animations.scss'; @import '../../../styles/animations.scss';
@include smallScreen {
.journal_item-info {
text-align: center;
}
.item-route {
display: flex;
justify-content: center;
}
}
</style> </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>
<strong <strong
v-if="isDonator(timetable.driverName)" v-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator" class="text--donator"
:title="$t('donations.driver-message')" :title="$t('donations.driver-message')"
> >
@@ -57,18 +57,18 @@
!timetable.terminated !timetable.terminated
? $t('journal.timetable-active') ? $t('journal.timetable-active')
: timetable.fulfilled : timetable.fulfilled
? $t('journal.timetable-fulfilled') ? $t('journal.timetable-fulfilled')
: `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}` : `${$t('journal.timetable-abandoned')} ${localeTime(timetable.endDate, $i18n.locale)}`
}} }}
</b> </b>
<button <button
v-if="timetable.terminated == false" v-if="timetable.terminated == false"
class="btn--image btn--action btn-timetable" class="btn--action btn-timetable"
@click.stop="showTimetable(timetable, $event.currentTarget)" @click.stop="showTimetable(timetable, $event.currentTarget)"
> >
<img src="/images/icon-train.svg" alt="" /> <img src="/images/icon-train.svg" alt="train icon" />
{{ $t('journal.timetable-online-button') }} <b>{{ $t('journal.timetable-online-button') }}</b>
</button> </button>
</span> </span>
</div> </div>
@@ -81,10 +81,16 @@ import { API } from '../../../typings/api';
import dateMixin from '../../../mixins/dateMixin'; import dateMixin from '../../../mixins/dateMixin';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import donatorMixin from '../../../mixins/donatorMixin'; import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
mixins: [dateMixin, modalTrainMixin, styleMixin, donatorMixin], mixins: [dateMixin, modalTrainMixin, styleMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: { props: {
timetable: { timetable: {
@@ -104,8 +110,8 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/responsive.scss'; @import '../../../styles/responsive';
@import '../../../styles/badge.scss'; @import '../../../styles/badge';
.item-general { .item-general {
display: flex; display: flex;
@@ -117,8 +123,22 @@ export default defineComponent({
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.info-date { .general-train {
margin-right: 0.5em; 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 { .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 { .btn-timetable {
display: inline-block; display: flex;
padding: 0.1em 0.4em; padding: 0.2em 0.5em;
margin-left: 0.5em;
img { img {
vertical-align: top; height: 1.25em;
} }
} }
@@ -21,7 +21,7 @@
<!-- Status --> <!-- Status -->
<TimetableStatus :timetable="timetable" /> <TimetableStatus :timetable="timetable" />
<button class="btn--option btn--show"> <button class="btn--action btn--show">
{{ $t('journal.stock-info') }} {{ $t('journal.stock-info') }}
<img <img
:src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`" :src="`/images/icon-arrow-${showExtraInfo.value ? 'asc' : 'desc'}.svg`"
@@ -66,9 +66,9 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/variables.scss'; @import '../../../styles/variables';
@import '../../../styles/responsive.scss'; @import '../../../styles/responsive';
@import '../../../styles/JournalSection.scss'; @import '../../../styles/JournalSection';
.btn--show { .btn--show {
display: flex; display: flex;
@@ -72,18 +72,15 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import listObserverMixin from '../../mixins/listObserverMixin';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'SceneryDispatchersHistory', name: 'SceneryDispatchersHistory',
mixins: [dateMixin, styleMixin, listObserverMixin], mixins: [dateMixin, styleMixin],
components: { Loading }, components: { Loading },
props: { props: {
station: { station: {
+1 -2
View File
@@ -14,8 +14,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import Station from '../../scripts/interfaces/Station'; import { ActiveScenery, Station } from '../../typings/common';
import { ActiveScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
props: { props: {
+1 -2
View File
@@ -89,8 +89,7 @@ import SceneryInfoIcons from './SceneryInfo/SceneryInfoIcons.vue';
import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue'; import SceneryInfoUserList from './SceneryInfo/SceneryInfoUserList.vue';
import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue'; import SceneryInfoSpawnList from './SceneryInfo/SceneryInfoSpawnList.vue';
import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue'; import SceneryInfoRoutes from './SceneryInfo/SceneryInfoRoutes.vue';
import Station from '../../scripts/interfaces/Station'; import { ActiveScenery, Station } from '../../typings/common';
import { ActiveScenery } from '../../store/typings';
export default defineComponent({ export default defineComponent({
components: { components: {
@@ -14,7 +14,7 @@
> >
<span <span
class="text--donator" class="text--donator"
v-if="isDonator(onlineScenery.dispatcherName)" v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
> >
{{ onlineScenery.dispatcherName }} {{ onlineScenery.dispatcherName }}
@@ -49,11 +49,18 @@ import dateMixin from '../../../mixins/dateMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import StationStatusBadge from '../../Global/StationStatusBadge.vue'; import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { ActiveScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
import donatorMixin from '../../../mixins/donatorMixin'; import { useApiStore } from '../../../store/apiStore';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin, donatorMixin], mixins: [styleMixin, dateMixin, routerMixin],
data() {
return {
apiStore: useApiStore()
};
},
props: { props: {
onlineScenery: { onlineScenery: {
type: Object as PropType<ActiveScenery>, type: Object as PropType<ActiveScenery>,
@@ -24,8 +24,8 @@
:title=" :title="
$t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`) $t('sceneries.info.control-type') + $t(`controls.${station?.generalInfo.controlType}`)
" "
v-html="getControlTypeAbbrev(station?.generalInfo.controlType)"
> >
{{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</span> </span>
<img <img
@@ -88,12 +88,11 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import stationInfoMixin from '../../../mixins/stationInfoMixin';
import styleMixin from '../../../mixins/styleMixin'; import styleMixin from '../../../mixins/styleMixin';
import Station from '../../../scripts/interfaces/Station'; import { Station } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [stationInfoMixin, styleMixin], mixins: [styleMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -104,6 +103,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../styles/icons.scss'; @import '../../../styles/icons.scss';
.info-icons { .info-icons {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -111,6 +111,7 @@ export default defineComponent({
margin: 1em; margin: 1em;
} }
.icon-info { .icon-info {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -52,7 +52,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import Station from '../../../scripts/interfaces/Station'; import { Station } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -8,7 +8,7 @@
<transition-group name="spawns-anim" tag="ul"> <transition-group name="spawns-anim" tag="ul">
<li <li
class="badge spawn badge-none" class="badge badge-none"
v-if="!onlineScenery || onlineScenery.spawns.length == 0" v-if="!onlineScenery || onlineScenery.spawns.length == 0"
key="no-spawns" key="no-spawns"
> >
@@ -16,13 +16,13 @@
</li> </li>
<li <li
class="badge spawn" class="badge spawn-badge"
v-for="(spawn, i) in sortedSpawns" v-for="(spawn, i) in sortedSpawns"
:key="spawn.spawnName + onlineScenery?.dispatcherName + i" :key="spawn.spawnName + onlineScenery?.dispatcherName + i"
:data-electrified="spawn.isElectrified" :data-electrified="spawn.isElectrified"
> >
<span class="spawn_name">{{ spawn.spawnName }}</span> <span class="name">{{ spawn.spawnName }}</span>
<span class="spawn_length">{{ spawn.spawnLength }}m</span> <span class="length">{{ spawn.spawnLength }}m</span>
</li> </li>
</transition-group> </transition-group>
</section> </section>
@@ -30,7 +30,7 @@
<script lang="ts"> <script lang="ts">
import { PropType, defineComponent } from 'vue'; import { PropType, defineComponent } from 'vue';
import { ActiveScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -59,19 +59,6 @@ ul {
position: relative; position: relative;
} }
.spawn {
color: white;
&_length {
background-color: #404040;
color: #cfcfcf;
}
&[data-electrified='true'] > &_name {
background-color: #007599;
}
}
.spawns-anim { .spawns-anim {
&-move, &-move,
&-enter-active, &-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 { PropType, defineComponent } from 'vue';
import modalTrainMixin from '../../../mixins/modalTrainMixin'; import modalTrainMixin from '../../../mixins/modalTrainMixin';
import routerMixin from '../../../mixins/routerMixin'; import routerMixin from '../../../mixins/routerMixin';
import { ActiveScenery } from '../../../store/typings'; import { ActiveScenery } from '../../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [routerMixin, modalTrainMixin], mixins: [routerMixin, modalTrainMixin],
@@ -14,14 +14,6 @@
</span> </span>
<span class="header_links" v-if="station"> <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')"> <a :href="tabliceZbiorczeHref" target="_blank" :title="$t('scenery.tablice-link')">
<img src="/images/icon-tablice.ico" alt="icon-tablice" /> <img src="/images/icon-tablice.ico" alt="icon-tablice" />
</a> </a>
@@ -186,12 +178,11 @@ import { useRoute } from 'vue-router';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import routerMixin from '../../mixins/routerMixin'; import routerMixin from '../../mixins/routerMixin';
import Station from '../../scripts/interfaces/Station';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import ScheduledTrainStatus from './ScheduledTrainStatus.vue'; import ScheduledTrainStatus from './ScheduledTrainStatus.vue';
import { ActiveScenery } from '../../store/typings';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { ActiveScenery, Station } from '../../typings/common';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetable', name: 'SceneryTimetable',
@@ -414,13 +405,6 @@ export default defineComponent({
width: 100%; width: 100%;
} }
.g-tooltip > .content {
z-index: 100;
color: white;
left: 110%;
}
img { img {
width: 1.1em; width: 1.1em;
} }
@@ -71,17 +71,14 @@
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Station from '../../scripts/interfaces/Station';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import listObserverMixin from '../../mixins/listObserverMixin';
import { ActiveScenery } from '../../store/typings';
import { API } from '../../typings/api'; import { API } from '../../typings/api';
import { Status } from '../../typings/common'; import { ActiveScenery, Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
name: 'SceneryTimetablesHistory', name: 'SceneryTimetablesHistory',
mixins: [dateMixin, listObserverMixin], mixins: [dateMixin],
props: { props: {
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -11,7 +11,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ScheduledTrain, StopStatus } from '../../store/typings'; import { ScheduledTrain, StopStatus } from '../../typings/common';
interface ScheduledTrainComp extends ScheduledTrain { interface ScheduledTrainComp extends ScheduledTrain {
stopStatusIndicator: string; stopStatusIndicator: string;
@@ -3,7 +3,7 @@
<div class="card_controls"> <div class="card_controls">
<button class="btn--filled btn--image" @click="toggleCard"> <button class="btn--filled btn--image" @click="toggleCard">
<img class="button_icon" src="/images/icon-filter2.svg" alt="filter icon" /> <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> <span class="active-indicator" v-if="!filterStore.areFiltersAtDefault"></span>
</button> </button>
@@ -28,8 +28,8 @@
</div> </div>
<transition name="card-anim"> <transition name="card-anim">
<div class="card" v-if="isVisible" tabindex="0" ref="cardEl"> <div class="card" v-if="isVisible" tabindex="0" ref="cardRef" @keydown.r="resetFilters">
<div class="card_content"> <div class="card_content" @scroll="onScroll" ref="cardContentRef">
<div class="card_title flex">{{ $t('filters.title') }}</div> <div class="card_title flex">{{ $t('filters.title') }}</div>
<p class="card_info" v-html="$t('filters.desc')"></p> <p class="card_info" v-html="$t('filters.desc')"></p>
@@ -69,8 +69,8 @@
minimumHours == 0 minimumHours == 0
? $t('filters.now') ? $t('filters.now')
: minimumHours < 8 : minimumHours < 8
? minimumHours + $t('filters.hour') ? minimumHours + $t('filters.hour')
: $t('filters.no-limit') : $t('filters.no-limit')
}}</span> }}</span>
<button class="btn--action" @click="addHour">+</button> <button class="btn--action" @click="addHour">+</button>
</span> </span>
@@ -108,6 +108,7 @@
:id="slider.id" :id="slider.id"
:min="slider.minRange" :min="slider.minRange"
:max="slider.maxRange" :max="slider.maxRange"
:step="slider.step"
v-model="slider.value" v-model="slider.value"
@change="handleInput" @change="handleInput"
/> />
@@ -136,7 +137,7 @@
:disabled="filterStore.areFiltersAtDefault" :disabled="filterStore.areFiltersAtDefault"
:data-disabled="filterStore.areFiltersAtDefault" :data-disabled="filterStore.areFiltersAtDefault"
> >
{{ $t('filters.reset') }} [R] {{ $t('filters.reset') }}
</button> </button>
<button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button> <button class="btn--action" @click="closeCard">{{ $t('filters.close') }}</button>
</div> </div>
@@ -170,7 +171,10 @@ export default defineComponent({
currentRegion: { id: '', value: '' }, currentRegion: { id: '', value: '' },
delayInputTimer: -1, delayInputTimer: -1,
chosenSearchScenery: '' chosenSearchScenery: '',
scrollTop: 0,
lastFocusedEl: null as HTMLElement | null
}), }),
setup() { setup() {
@@ -236,7 +240,10 @@ export default defineComponent({
isVisible(value: boolean) { isVisible(value: boolean) {
this.$nextTick(() => { 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; this.isVisible = !this.isVisible;
}, },
onScroll(e: Event) {
this.scrollTop = (e.target as HTMLElement).scrollTop;
},
handleInput(e: Event) { handleInput(e: Event) {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
@@ -431,23 +442,16 @@ h3.section-header {
} }
.card_actions { .card_actions {
width: 100%;
padding: 0.5em; padding: 0.5em;
.filter-option {
max-width: 50%;
margin: 0 auto;
}
.action-buttons { .action-buttons {
display: flex; display: flex;
gap: 0.5em; gap: 0.5em;
width: 100%;
margin-top: 0.5em; margin-top: 0.5em;
button { button {
width: 50%; width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 0.5em; 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> <template>
<section class="station_table"> <section class="station_table">
<transition name="status-anim" mode="out-in"> <Loading
<div class="table_wrapper" :key="apiStore.dataStatuses.connection"> v-if="apiStore.dataStatuses.connection == Status.Loading && displayedStations.length == 0"
<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>
<img <div class="table_wrapper" v-else-if="displayedStations.length > 0">
class="sort-icon" <table>
v-if="sorterActive.headerName == headerName" <thead>
:src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`" <tr>
alt="sort icon" <th
/> v-for="headerName in headIds"
</span> :key="headerName"
</th> @click="changeSorter(headerName)"
class="header-text"
<th :class="headerName"
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"
> >
<td class="station-name" :class="station.generalInfo?.availability"> <span class="header_wrapper">
<b v-if="station.generalInfo?.project" style="color: salmon">{{ <div v-html="$t(`sceneries.headers.${headerName}`)"></div>
station.generalInfo.project
}}</b>
{{ station.name }}
</td>
<td class="station-level"> <img
<span v-if="station.generalInfo"> class="sort-icon"
<span v-if="sorterActive.headerName == headerName"
v-if=" :src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
station.generalInfo.reqLevel > -1 && alt="sort icon"
station.generalInfo.availability != 'nonPublic' &&
station.generalInfo.availability != 'unavailable'
"
:style="calculateExpStyle(station.generalInfo.reqLevel)"
>
{{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
</span>
<span v-else-if="station.generalInfo.availability == 'abandoned'">
<img
src="/images/icon-abandoned.svg"
alt="non-public"
:title="$t('sceneries.info.abandoned')"
/>
</span>
<span v-else-if="station.generalInfo.availability == 'nonPublic'">
<img
src="/images/icon-lock.svg"
alt="non-public"
:title="$t('sceneries.info.non-public')"
/>
</span>
<span v-else>
<img
src="/images/icon-unavailable.svg"
alt="unavailable"
:title="$t('sceneries.info.unavailable')"
/>
</span>
</span>
<span v-else> ? </span>
</td>
<td class="station-status">
<StationStatusBadge
:isOnline="station.onlineInfo ? true : false"
:dispatcherStatus="station.onlineInfo?.dispatcherStatus"
/> />
</td> </span>
</th>
<td class="station-dispatcher-name"> <th
<span v-if="station.onlineInfo?.dispatcherName"> v-for="headerName in headIconsIds"
<b :key="headerName"
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" @click="changeSorter(headerName)"
@click.stop="openDonationModal" class="header-image"
data-popup-key="DonatorPopUp" :class="headerName"
:data-popup-content="$t('donations.dispatcher-message')" >
> <span class="header_wrapper">
<img src="/images/icon-diamond.svg" alt="" /> <img
{{ station.onlineInfo.dispatcherName }} :src="`/images/icon-${headerName}.svg`"
</b> :alt="headerName"
:title="$t(`sceneries.headers.${headerName}`)"
/>
<div v-else> <img
{{ station.onlineInfo.dispatcherName }} class="sort-icon"
</div> v-if="sorterActive.headerName == headerName"
</span> :src="`/images/icon-arrow-${sorterActive.dir == 1 ? 'asc' : 'desc'}.svg`"
</td> 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 <span
v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1" v-if="
:style=" station.generalInfo.reqLevel > -1 &&
calculateExpStyle( station.generalInfo.availability != 'nonPublic' &&
station.onlineInfo.dispatcherExp, station.generalInfo.availability != 'unavailable'
station.onlineInfo.dispatcherIsSupporter
)
" "
:style="calculateExpStyle(station.generalInfo.reqLevel)"
> >
{{ {{ station.generalInfo.reqLevel >= 2 ? station.generalInfo.reqLevel : 'L' }}
station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp
}}
</span> </span>
</td>
<td class="station-tracks"> <span v-else-if="station.generalInfo.availability == 'abandoned'">
<div v-if="station.generalInfo"> <img
<span src="/images/icon-abandoned.svg"
v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0" alt="non-public"
class="track catenary" :title="$t('sceneries.info.abandoned')"
:title="`${$t('sceneries.info.single-track-routes-catenary')}${ />
station.generalInfo.routes.singleElectrifiedNames.length </span>
}`"
>
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span>
<span <span v-else-if="station.generalInfo.availability == 'nonPublic'">
v-if="station.generalInfo.routes.singleOtherNames.length != 0" <img
class="track no-catenary" src="/images/icon-lock.svg"
:title="`${$t('sceneries.info.single-track-routes-other')}${ alt="non-public"
station.generalInfo.routes.singleOtherNames.length :title="$t('sceneries.info.non-public')"
}`" />
> </span>
{{ station.generalInfo.routes.singleOtherNames.length }}
</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> </div>
</td> </span>
</td>
<td class="station-tracks"> <td class="station-dispatcher-exp">
<div v-if="station.generalInfo"> <span
<span v-if="station.onlineInfo && station.onlineInfo?.dispatcherExp != -1"
v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0" :style="
class="track catenary" calculateExpStyle(
:title="`${$t('sceneries.info.double-track-routes-catenary')}${ station.onlineInfo.dispatcherExp,
station.generalInfo.routes.doubleElectrifiedNames.length station.onlineInfo.dispatcherIsSupporter
}`" )
> "
{{ station.generalInfo.routes.doubleElectrifiedNames.length }} >
</span> {{ station.onlineInfo.dispatcherExp < 2 ? 'L' : station.onlineInfo.dispatcherExp }}
</span>
</td>
<span <td class="station-tracks">
v-if="station.generalInfo.routes.doubleOtherNames.length != 0" <div v-if="station.generalInfo">
class="track no-catenary"
:title="`${$t('sceneries.info.double-track-routes-other')}${
station.generalInfo.routes.doubleOtherNames.length
}`"
>
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<td class="station-info">
<span <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleElectrifiedNames.length != 0"
class="scenery-icon icon-info" class="track catenary"
:class="station.generalInfo?.controlType.replace('+', '-')" :title="`${$t('sceneries.info.single-track-routes-catenary')}${
:title=" station.generalInfo.routes.singleElectrifiedNames.length
$t('sceneries.info.control-type') + }`"
$t(`controls.${station.generalInfo?.controlType}`)
"
v-html="getControlTypeAbbrev(station.generalInfo.controlType)"
> >
{{ station.generalInfo.routes.singleElectrifiedNames.length }}
</span> </span>
<img <span
v-if="station.generalInfo?.signalType" v-if="station.generalInfo.routes.singleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
:src="`/images/icon-${station.generalInfo.signalType}.svg`" :title="`${$t('sceneries.info.single-track-routes-other')}${
:alt="station.generalInfo.signalType" station.generalInfo.routes.singleOtherNames.length
:title=" }`"
$t('sceneries.info.signals-type') + >
$t(`signals.${station.generalInfo.signalType}`) {{ station.generalInfo.routes.singleOtherNames.length }}
" </span>
/> </div>
</td>
<img <td class="station-tracks">
v-if="station.generalInfo?.SUP" <div v-if="station.generalInfo">
class="icon-info" <span
src="/images/icon-SUP.svg" v-if="station.generalInfo.routes.doubleElectrifiedNames.length != 0"
alt="SUP (RASP-UZK)" class="track catenary"
:title="$t('sceneries.info.SUP')" :title="`${$t('sceneries.info.double-track-routes-catenary')}${
/> station.generalInfo.routes.doubleElectrifiedNames.length
}`"
>
{{ station.generalInfo.routes.doubleElectrifiedNames.length }}
</span>
<img <span
v-if="station.generalInfo?.ASDEK" v-if="station.generalInfo.routes.doubleOtherNames.length != 0"
class="icon-info" class="track no-catenary"
src="/images/icon-ASDEK.svg" :title="`${$t('sceneries.info.double-track-routes-other')}${
alt="dSAT ASDEK" station.generalInfo.routes.doubleOtherNames.length
:title="$t('sceneries.info.ASDEK')" }`"
/> >
{{ station.generalInfo.routes.doubleOtherNames.length }}
</span>
</div>
</td>
<img <td class="station-info">
v-if="!station.generalInfo" <span
class="icon-info" v-if="station.generalInfo?.signalType"
src="/images/icon-unknown.svg" class="scenery-icon icon-info"
alt="icon-unknown" :class="station.generalInfo?.controlType.replace('+', '-')"
:title="$t('sceneries.info.unknown')" :title="
/> $t('sceneries.info.control-type') +
</td> $t(`controls.${station.generalInfo?.controlType}`)
"
<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 }"
> >
{{ station.onlineInfo?.scheduledTrainCount.all ?? '-' }} {{ $t(`controls.abbrevs.${station.generalInfo.controlType}`) }}
</td> </span>
<td <img
class="station-schedules unconfirmed" v-if="station.generalInfo?.signalType"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" :src="`/images/icon-${station.generalInfo.signalType}.svg`"
> :alt="station.generalInfo.signalType"
{{ station.onlineInfo?.scheduledTrainCount.unconfirmed ?? '-' }} :title="
</td> $t('sceneries.info.signals-type') +
$t(`signals.${station.generalInfo.signalType}`)
"
/>
<td <img
class="station-schedules confirmed" v-if="station.generalInfo?.SUP"
style="width: 30px" class="icon-info"
:class="{ inactive: !station.onlineInfo }" src="/images/icon-SUP.svg"
> alt="SUP (RASP-UZK)"
{{ station.onlineInfo?.scheduledTrainCount.confirmed ?? '-' }} :title="$t('sceneries.info.SUP')"
</td> />
</tr>
</tbody>
</table>
<Loading <img
v-if="apiStore.dataStatuses.connection == Status.Loading && stations.length == 0" 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"> <img
{{ $t('sceneries.no-stations') }} v-if="!station.generalInfo"
</div> class="icon-info"
</div> src="/images/icon-unknown.svg"
</transition> 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> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import stationInfoMixin from '../../mixins/stationInfoMixin';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import Station from '../../scripts/interfaces/Station';
import { useStationFiltersStore } from '../../store/stationFiltersStore'; import { useStationFiltersStore } from '../../store/stationFiltersStore';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames'; import { HeadIdsTypes, headIconsIds, headIds } from '../../scripts/data/stationHeaderNames';
import StationStatusBadge from '../Global/StationStatusBadge.vue'; import StationStatusBadge from '../Global/StationStatusBadge.vue';
import { Status } from '../../typings/common'; import { Station, Status } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import popupMixin from '../../mixins/popupMixin'; import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({ export default defineComponent({
props: {
stations: {
type: Array as PropType<Station[]>,
required: true
}
},
emits: ['toggleDonationModal'], emits: ['toggleDonationModal'],
components: { Loading, StationStatusBadge }, components: { Loading, StationStatusBadge },
mixins: [styleMixin, dateMixin, stationInfoMixin, popupMixin], mixins: [styleMixin, dateMixin],
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
@@ -335,12 +335,17 @@ export default defineComponent({
computed: { computed: {
sorterActive() { sorterActive() {
return this.stationFiltersStore.sorterActive; return this.stationFiltersStore.sorterActive;
},
displayedStations() {
return this.stationFiltersStore.filteredStationList;
} }
}, },
setup() { setup() {
const mainStore = useMainStore(); const mainStore = useMainStore();
const apiStore = useApiStore(); const apiStore = useApiStore();
const tooltipStore = useTooltipStore();
const stationFiltersStore = useStationFiltersStore(); const stationFiltersStore = useStationFiltersStore();
@@ -348,16 +353,19 @@ export default defineComponent({
Status: Status.Data, Status: Status.Data,
stationFiltersStore, stationFiltersStore,
mainStore, mainStore,
apiStore apiStore,
tooltipStore
}; };
}, },
methods: { methods: {
setScenery(name: string) { setScenery(name: string) {
const station = this.stations.find((station) => station.name === name); const station = this.displayedStations.find((station) => station.name === name);
if (!station) return; if (!station) return;
this.lastSelectedStationName = station.name; this.lastSelectedStationName = station.name;
this.tooltipStore.hide();
this.$router.push({ this.$router.push({
name: 'SceneryView', name: 'SceneryView',
@@ -371,7 +379,7 @@ export default defineComponent({
openDonationModal(e: Event) { openDonationModal(e: Event) {
this.$emit('toggleDonationModal', true); this.$emit('toggleDonationModal', true);
this.mainStore.modalLastClickedTarget = e.target; this.mainStore.modalLastClickedTarget = e.target;
this.hidePopUp(); this.tooltipStore.hide();
}, },
openForumSite(e: Event, url: string | undefined) { openForumSite(e: Event, url: string | undefined) {
@@ -396,23 +404,11 @@ export default defineComponent({
$rowCol: #424242; $rowCol: #424242;
.change-anim { .station_table {
&-enter-active, height: 80vh;
&-leave-active { min-height: 550px;
transition: opacity 100ms ease-in;
}
&-enter,
&-leave-to {
opacity: 0;
}
}
.table_wrapper {
overflow: auto; overflow: auto;
font-weight: 500; font-weight: 500;
height: 90vh;
min-height: 550px;
} }
.no-stations { .no-stations {
@@ -420,9 +416,8 @@ $rowCol: #424242;
font-size: 1.5em; font-size: 1.5em;
padding: 1em; padding: 1em;
margin: 1em 0;
background: #333; background: #1a1a1a;
} }
table { table {
+2
View File
@@ -36,6 +36,8 @@ export interface Filter {
minOneWay: number; minOneWay: number;
minTwoWayCatenary: number; minTwoWayCatenary: number;
minTwoWay: number; minTwoWay: number;
minVmax: number;
maxVmax: number;
'no-1track': boolean; 'no-1track': boolean;
'no-2track': boolean; 'no-2track': boolean;
'include-selected': boolean; 'include-selected': boolean;
@@ -1,24 +1,24 @@
<template> <template>
<div class="popup-content"> <div class="tooltip-content">
<span>{{ store.popUpData.content }}</span> <span>{{ tooltipStore.content }}</span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useMainStore() tooltipStore: useTooltipStore()
}; };
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.popup-content { .tooltip-content {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
@@ -1,25 +1,25 @@
<template> <template>
<div class="popup-content"> <div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="" /> <img src="/images/icon-diamond.svg" alt="" />
<span>{{ store.popUpData.content }}</span> <span>{{ tooltipStore.content }}</span>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useMainStore() tooltipStore: useTooltipStore()
}; };
} }
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.popup-content { .tooltip-content {
gap: 0.5em; gap: 0.5em;
padding: 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> <template>
<div class="popup" v-show="store.popUpData.key" ref="preview"> <div class="tooltip" v-show="tooltipStore.type" ref="preview">
<component v-if="store.popUpData.key" :is="store.popUpData.key" /> <component v-if="tooltipStore.type" :is="tooltipStore.type" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import DonatorPopUp from './DonatorPopUp.vue'; import { useTooltipStore } from '../../store/tooltipStore';
import TrainCommentsPopUp from './TrainCommentsPopUp.vue'; import DonatorTooltip from './DonatorTooltip.vue';
import VehiclePreviewPopUp from './VehiclePreviewPopUp.vue'; import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import { useMainStore } from '../../store/mainStore'; import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue';
export default defineComponent({ export default defineComponent({
components: { DonatorPopUp, TrainCommentsPopUp, VehiclePreviewPopUp }, components: { DonatorTooltip, VehiclePreviewTooltip, BaseTooltip, SpawnsTooltip, UsersTooltip },
data() { data() {
return { return {
store: useMainStore() tooltipStore: useTooltipStore()
}; };
}, },
watch: { watch: {
'store.mousePos': { 'tooltipStore.mousePos': {
deep: true, deep: true,
handler(val: typeof this.store.mousePos) { // [x, y]
handler(val: [number, number]) {
this.$nextTick(() => { this.$nextTick(() => {
const previewEl = this.$refs['preview'] as HTMLElement; const previewEl = this.$refs['preview'] as HTMLElement;
const clientWidth = document.body.clientWidth; const clientWidth = document.body.clientWidth;
const boxWidth = previewEl.getBoundingClientRect().width; const boxWidth = previewEl.getBoundingClientRect().width;
let translateX = '0px', let translateX = '0',
translateY = '30px'; translateY = '30px';
if (clientWidth < 500) { if (clientWidth < 500) {
previewEl.style.left = '50%'; previewEl.style.left = '50%';
translateX = '-50%'; translateX = '-50%';
} else if (val.x <= boxWidth / 2) { } else if (val[0] <= boxWidth / 2) {
previewEl.style.left = '0'; previewEl.style.left = '0';
translateX = '0px'; translateX = '0px';
} else if (val.x >= clientWidth - boxWidth / 2) { } else if (val[0] >= clientWidth - boxWidth / 2) {
previewEl.style.left = '100%'; previewEl.style.left = '100%';
translateX = '-100%'; translateX = '-100%';
} else { } else {
previewEl.style.left = `${val.x}px`; previewEl.style.left = `${val[0]}px`;
translateX = '-50%'; translateX = '-50%';
} }
previewEl.style.top = `${val.y}px`; previewEl.style.top = `${val[1]}px`;
const isOutside = const isOutside =
val.y + previewEl.getBoundingClientRect().height + 30 >= val[1] + previewEl.getBoundingClientRect().height + 30 >=
window.innerHeight + window.scrollY; window.innerHeight + window.scrollY;
if (isOutside) translateY = 'calc(-100% - 30px)'; if (isOutside) translateY = 'calc(-100% - 30px)';
@@ -62,7 +65,7 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.popup { .tooltip {
position: absolute; position: absolute;
z-index: 250; z-index: 250;
max-width: 400px; 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> <template>
<div class="popup-content"> <div class="tooltip-content">
<div v-if="imageState == 'loading'" class="loading-info"> <div v-if="imageState == 'loading'" class="loading-info">
{{ $t('vehicle-preview.loading') }} {{ $t('vehicle-preview.loading') }}
</div> </div>
@@ -7,29 +7,31 @@
<div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div> <div v-if="imageState == 'error'">{{ $t('vehicle-preview.error') }}</div>
<img <img
v-if="store.popUpData.key" v-if="tooltipStore.type"
@load="onImageLoad" @load="onImageLoad"
@error="onImageError" @error="onImageError"
width="300" width="300"
height="176" height="176"
class="rounded-md w-full h-auto" 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'"> <div v-if="imageState == 'error'" class="error-placeholder"></div>
{{ store.popUpData.content.replace(/_/g, ' ') }}
<div class="vehicle-name">
{{ tooltipStore.content.replace(/_/g, ' ') }}
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useMainStore } from '../../store/mainStore'; import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useMainStore(), tooltipStore: useTooltipStore(),
imageState: 'loading' imageState: 'loading'
}; };
}, },
@@ -38,6 +40,12 @@ export default defineComponent({
this.imageState = 'loading'; this.imageState = 'loading';
}, },
watch: {
'tooltipStore.type'(prev, val) {
if (prev != val) this.imageState = 'loading';
}
},
methods: { methods: {
onImageLoad() { onImageLoad() {
this.imageState = 'loaded'; this.imageState = 'loaded';
@@ -53,9 +61,7 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.popup-content { .tooltip-content {
// min-w-[300px] min-h-[200px] p-2 bg-slate-800 rounded-md
width: 300px; width: 300px;
min-height: 200px; min-height: 200px;
background-color: #333; background-color: #333;
@@ -82,4 +88,8 @@ img {
color: #ccc; color: #ccc;
text-wrap: wrap; text-wrap: wrap;
} }
.error-placeholder {
height: 176px;
}
</style> </style>
+5 -5
View File
@@ -45,8 +45,8 @@
<div class="train-driver"> <div class="train-driver">
<b <b
v-if="apiStore.donatorsData.includes(train.driverName)" v-if="apiStore.donatorsData.includes(train.driverName)"
data-popup-key="DonatorPopUp" data-tooltip-type="DonatorTooltip"
:data-popup-content="$t('donations.driver-message')" :data-tooltip-content="$t('donations.driver-message')"
> >
{{ train.driverName }} {{ train.driverName }}
<img src="/images/icon-diamond.svg" alt="donator diamond icon" /> <img src="/images/icon-diamond.svg" alt="donator diamond icon" />
@@ -74,8 +74,8 @@
<strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong> <strong>{{ train.timetableData.route.replace('|', ' - ') }}</strong>
<span <span
v-if="getSceneriesWithComments(train.timetableData).length > 0" v-if="getSceneriesWithComments(train.timetableData).length > 0"
data-popup-key="TrainCommentsPopUp" data-tooltip-type="BaseTooltip"
:data-popup-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments( :data-tooltip-content="`${$t('trains.timetable-comments')} (${getSceneriesWithComments(
train.timetableData train.timetableData
)})`" )})`"
> >
@@ -163,12 +163,12 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import styleMixin from '../../mixins/styleMixin'; import styleMixin from '../../mixins/styleMixin';
import trainInfoMixin from '../../mixins/trainInfoMixin'; import trainInfoMixin from '../../mixins/trainInfoMixin';
import Train from '../../scripts/interfaces/Train';
import ProgressBar from '../Global/ProgressBar.vue'; import ProgressBar from '../Global/ProgressBar.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin, modalTrainMixin], mixins: [trainInfoMixin, styleMixin, modalTrainMixin],
+6 -6
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal"> <div class="train-modal" v-if="chosenTrain" @keydown.esc="closeModal">
<div class="modal_background" @click="closeModal"></div> <div class="modal-background" @click="closeModal"></div>
<div class="modal_content" ref="content" tabindex="0"> <div class="modal-content" ref="content" tabindex="0">
<TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" /> <TrainInfo :train="chosenTrain" :extended="true" ref="trainInfo" />
<TrainSchedule :train="chosenTrain" tabindex="0" /> <TrainSchedule :train="chosenTrain" tabindex="0" />
</div> </div>
@@ -13,7 +13,7 @@ import { defineComponent } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import TrainSchedule from './TrainSchedule.vue'; import TrainSchedule from './TrainSchedule.vue';
import Train from '../../scripts/interfaces/Train'; import { Train } from '../../typings/common';
export default defineComponent({ export default defineComponent({
components: { TrainInfo, TrainSchedule }, components: { TrainInfo, TrainSchedule },
@@ -71,7 +71,7 @@ export default defineComponent({
text-align: left; text-align: left;
} }
.modal_background { .modal-background {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@@ -83,14 +83,14 @@ export default defineComponent({
background-color: rgba(0, 0, 0, 0.55); background-color: rgba(0, 0, 0, 0.55);
} }
.modal_content { .modal-content {
position: relative; position: relative;
overflow-y: scroll; overflow-y: scroll;
margin-top: 1em; margin-top: 1em;
width: 95vw; width: 95vw;
max-height: 96vh; max-height: 95vh;
background-color: #1a1a1a; background-color: #1a1a1a;
box-shadow: 0 0 15px 10px #0e0e0e; 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"> <button class="filter-button btn--filled btn--image" @click="toggleShowOptions" ref="button">
<img src="/images/icon-filter2.svg" alt="Open filters icon" /> <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> <span class="active-indicator" v-if="currentOptionsActive"></span>
</button> </button>
+1 -1
View File
@@ -81,11 +81,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import dateMixin from '../../mixins/dateMixin'; import dateMixin from '../../mixins/dateMixin';
import Train from '../../scripts/interfaces/Train';
import StopLabel from './StopLabel.vue'; import StopLabel from './StopLabel.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
import { Train } from '../../typings/common';
export interface TrainScheduleStop { export interface TrainScheduleStop {
nameHtml: string; nameHtml: string;
+1 -2
View File
@@ -30,11 +30,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, inject, PropType, Ref } from 'vue'; import { defineComponent, inject, PropType, Ref } from 'vue';
import modalTrainMixin from '../../mixins/modalTrainMixin'; import modalTrainMixin from '../../mixins/modalTrainMixin';
import Train from '../../scripts/interfaces/Train';
import { useMainStore } from '../../store/mainStore'; import { useMainStore } from '../../store/mainStore';
import Loading from '../Global/Loading.vue'; import Loading from '../Global/Loading.vue';
import TrainInfo from './TrainInfo.vue'; import TrainInfo from './TrainInfo.vue';
import { Status } from '../../typings/common'; import { Status, Train } from '../../typings/common';
import { useApiStore } from '../../store/apiStore'; import { useApiStore } from '../../store/apiStore';
export default defineComponent({ export default defineComponent({
+24
View File
@@ -265,6 +265,7 @@
"name": "minLevel", "name": "minLevel",
"minRange": 0, "minRange": 0,
"maxRange": 20, "maxRange": 20,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -273,14 +274,34 @@
"name": "maxLevel", "name": "maxLevel",
"minRange": 0, "minRange": 0,
"maxRange": 20, "maxRange": 20,
"step": 1,
"value": 20, "value": 20,
"defaultValue": 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", "id": "routes-1t-cat",
"name": "minOneWayCatenary", "name": "minOneWayCatenary",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -289,6 +310,7 @@
"name": "minOneWay", "name": "minOneWay",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -297,6 +319,7 @@
"name": "minTwoWayCatenary", "name": "minTwoWayCatenary",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
}, },
@@ -305,6 +328,7 @@
"name": "minTwoWay", "name": "minTwoWay",
"minRange": 0, "minRange": 0,
"maxRange": 5, "maxRange": 5,
"step": 1,
"value": 0, "value": 0,
"defaultValue": 0 "defaultValue": 0
} }
+35 -8
View File
@@ -27,11 +27,11 @@
"SKR": "Train with exceeded gauge" "SKR": "Train with exceeded gauge"
}, },
"update": { "update": {
"title": "Stacjownik app update!", "title": "Stacjownik update!",
"version": "Version {0}", "confirm": "ROGER THAT!",
"confirm-button": "UPDATE NOW", "no-data": "No data about the latest app update has been found",
"later-button": "LATER", "info-1": "This changelog will be available to see once again after clicking the version number in the footer",
"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" "info-2": "The full app changelog available on <a href='https://github.com/Spythere/stacjownik' target='_blank'>the project's GitHub</a>"
}, },
"app": { "app": {
"sceneries": "SCENERIES", "sceneries": "SCENERIES",
@@ -81,7 +81,20 @@
"ręczne+SCS": "manual + SCS", "ręczne+SCS": "manual + SCS",
"mechaniczne": "levers (mechanical)", "mechaniczne": "levers (mechanical)",
"mechaniczne+SPK": "levers + SPK", "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": { "status": {
"online": "UNTIL ", "online": "UNTIL ",
@@ -169,7 +182,8 @@
"addons": "ADDITIONAL PROGRAMS", "addons": "ADDITIONAL PROGRAMS",
"blockades": "BLOCK SIGNALLING", "blockades": "BLOCK SIGNALLING",
"status": "ONLINE STATUS", "status": "ONLINE STATUS",
"timetables": "ACTIVE TIMETABLES" "timetables": "ACTIVE TIMETABLES",
"spawns": "OPEN SPAWNS"
}, },
"all-available": "ALL AVAILABLE", "all-available": "ALL AVAILABLE",
@@ -222,11 +236,14 @@
"sliders": { "sliders": {
"min-lvl": "MIN. REQUIRED DISPATCHER LEVEL", "min-lvl": "MIN. REQUIRED DISPATCHER LEVEL",
"max-lvl": "MAX. 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-cat": "MIN. CATENARY SINGLE TRACK ROUTES",
"routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES", "routes-1t-other": "MIN. OTHER SINGLE TRACK ROUTES",
"routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES", "routes-2t-cat": "MIN. CATENARY DOUBLE TRACK ROUTES",
"routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES" "routes-2t-other": "MIN. OTHER DOUBLE TRACK ROUTES"
}, },
"authors-search": "SEARCH BY AUTHOR NAME (other filters apply):", "authors-search": "SEARCH BY AUTHOR NAME (other filters apply):",
"authors-placeholder": "Enter the author nickname...", "authors-placeholder": "Enter the author nickname...",
"authors-button-title": "Search", "authors-button-title": "Search",
@@ -279,6 +296,16 @@
"no-stations": "No stations to show here!", "no-stations": "No stations to show here!",
"scenery-search": "Search for scenery..." "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": { "trains": {
"no-trains": "No trains to show here!", "no-trains": "No trains to show here!",
"loading": "Loading train data...", "loading": "Loading train data...",
@@ -367,7 +394,7 @@
"minutes": "{value} min | {value} mins", "minutes": "{value} min | {value} mins",
"seconds": "{value} s", "seconds": "{value} s",
"stock-info": "EXTRA INFO", "stock-info": "DETAILS",
"stock-length": "Length", "stock-length": "Length",
"stock-mass": "Mass", "stock-mass": "Mass",
"stock-max-speed": "Max. speed", "stock-max-speed": "Max. speed",
+34 -9
View File
@@ -28,10 +28,10 @@
}, },
"update": { "update": {
"title": "Aktualizacja Stacjownika!", "title": "Aktualizacja Stacjownika!",
"version": "Wersja {0}", "confirm": "PRZYJĄŁEM!",
"confirm-button": "UPDATE NOW", "no-data": "Nie znaleziono informacji o ostatnich zmianach w aplikacji",
"later-button": "LATER", "info-1": "Ten changelog będzie zawsze dostępny po kliknięciu numeru wersji w stopce strony",
"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" "info-2": "Pełny changelog dostępny na <a href='https://github.com/Spythere/stacjownik' target='_blank'>GitHubie projektu</a>"
}, },
"app": { "app": {
"sceneries": "SCENERIE", "sceneries": "SCENERIE",
@@ -77,7 +77,20 @@
"ręczne+SCS": "ręczne z SCS", "ręczne+SCS": "ręczne z SCS",
"mechaniczne": "mechaniczne", "mechaniczne": "mechaniczne",
"mechaniczne+SPK": "mechaniczne z SPK", "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": { "status": {
"online": "DO ", "online": "DO ",
@@ -163,10 +176,11 @@
"access": "DOSTĘPNOŚĆ OGÓLNA", "access": "DOSTĘPNOŚĆ OGÓLNA",
"control": "TYP STEROWANIA", "control": "TYP STEROWANIA",
"signals": "TYP SYGNALIZACJI", "signals": "TYP SYGNALIZACJI",
"addons": "DODATKOWE PROGRAMY", "addons": "SZCZEGÓŁY",
"blockades": "BLOKADY LINIOWE", "blockades": "BLOKADY LINIOWE",
"status": "STATUS ONLINE", "status": "STATUS ONLINE",
"timetables": "AKTYWNE ROZKŁADY JAZDY" "timetables": "AKTYWNE ROZKŁADY JAZDY",
"spawns": "OTWARTE SPAWNY"
}, },
"all-available": "WSZYSTKIE DOSTĘPNE", "all-available": "WSZYSTKIE DOSTĘPNE",
@@ -209,7 +223,6 @@
"semaphores": "KSZTAŁTOWA", "semaphores": "KSZTAŁTOWA",
"mixed": "MIESZANA", "mixed": "MIESZANA",
"historical": "HISTORYCZNA", "historical": "HISTORYCZNA",
"free": "WOLNA", "free": "WOLNA",
"occupied": "ZAJĘTA", "occupied": "ZAJĘTA",
@@ -219,6 +232,8 @@
"sliders": { "sliders": {
"min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO", "min-lvl": "MIN. WYMAGANY POZIOM DYŻURNEGO",
"max-lvl": "MAKS. 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-cat": "SZLAKI JEDNOTOROWE ZELEKTR. (MINIMUM)",
"routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)", "routes-1t-other": "SZLAKI JEDNOTOROWE NIEZELEKTR. (MINIMUM)",
"routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)", "routes-2t-cat": "SZLAKI DWUTOROWE ZELEKTR. (MINIMUM)",
@@ -274,6 +289,16 @@
"no-stations": "Brak stacji do wyświetlenia!", "no-stations": "Brak stacji do wyświetlenia!",
"scenery-search": "Wyszukaj scenerię..." "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": { "trains": {
"no-trains": "Brak pociągów do wyświetlenia!", "no-trains": "Brak pociągów do wyświetlenia!",
"loading": "Pobieranie danych o pociągach...", "loading": "Pobieranie danych o pociągach...",
@@ -352,7 +377,7 @@
"timetable-abandoned": "PORZUCONY", "timetable-abandoned": "PORZUCONY",
"timetable-online-button": "RJ ONLINE", "timetable-online-button": "RJ ONLINE",
"stock-info": "DODATKOWE INFORMACJE", "stock-info": "SZCZEGÓŁY",
"stock-length": "Długość", "stock-length": "Długość",
"stock-mass": "Masa", "stock-mass": "Masa",
"stock-max-speed": "Prędkość maks.", "stock-max-speed": "Prędkość maks.",
+1 -2
View File
@@ -1,6 +1,5 @@
import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings'; import { TrainFilter, TrainFilterId } from '../components/TrainsView/typings';
import Train from '../scripts/interfaces/Train'; import { Train, TrainStop } from '../typings/common';
import { TrainStop } from '../store/typings';
function confirmedPercentage(stops: TrainStop[] | undefined) { function confirmedPercentage(stops: TrainStop[] | undefined) {
if (!stops) return -1; 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 { defineComponent } from 'vue';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { useTooltipStore } from '../store/tooltipStore';
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useMainStore() store: useMainStore(),
tooltipStore: useTooltipStore()
}; };
}, },
@@ -17,7 +19,7 @@ export default defineComponent({
closeModal() { closeModal() {
this.store.chosenModalTrainId = undefined; this.store.chosenModalTrainId = undefined;
this.store.popUpData.key = null; this.tooltipStore.hide();
setTimeout(() => { setTimeout(() => {
(this.store.modalLastClickedTarget as any)?.focus(); (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 { defineComponent } from 'vue';
import Train from '../scripts/interfaces/Train'; import { Train, TrainStop } from '../typings/common';
import { TrainStop } from '../store/typings';
export default defineComponent({ export default defineComponent({
data: () => ({ data: () => ({
@@ -51,8 +50,8 @@ export default defineComponent({
return diffMins < 1 return diffMins < 1
? this.$t('trains.last-seen-now') ? this.$t('trains.last-seen-now')
: diffMins < 2 : diffMins < 2
? this.$t('trains.last-seen-min') ? this.$t('trains.last-seen-min')
: this.$t('trains.last-seen-ago', { minutes: diffMins }); : this.$t('trains.last-seen-ago', { minutes: diffMins });
}, },
displayTrainPosition(train: Train) { 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 { Filter } from '../../components/StationsView/typings';
import { Status } from '../../typings/common'; import { Status } from '../../typings/common';
import { HeadIdsTypes } from '../data/stationHeaderNames'; import { HeadIdsTypes } from '../data/stationHeaderNames';
import Station from '../interfaces/Station'; import { Station } from '../../typings/common';
const dispatcherStatusPriority = [ const dispatcherStatusPriority = [
Status.ActiveDispatcher.UNKNOWN, Status.ActiveDispatcher.UNKNOWN,
@@ -68,8 +68,8 @@ export const sortStations = (
case 'user': case 'user':
diff = diff =
(b.onlineInfo ? b.onlineInfo.currentUsers : -1) - (b.onlineInfo?.stationTrains ? b.onlineInfo.stationTrains.length : -1) -
(a.onlineInfo ? a.onlineInfo.currentUsers : -1); (a.onlineInfo?.stationTrains ? a.onlineInfo.stationTrains.length : -1);
break; break;
case 'like': case 'like':
@@ -189,9 +189,11 @@ export const filterStations = (station: Station, filters: Filter) => {
availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned'; availability == 'nonPublic' || availability == 'unavailable' || availability == 'abandoned';
if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false; if (reqLevel + (otherAvailability ? 1 : 0) < filters['minLevel']) return false;
if (reqLevel + (otherAvailability ? 1 : 0) > filters['maxLevel']) 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 ( if (
filters['no-1track'] && filters['no-1track'] &&
(routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0) (routes.singleElectrifiedNames.length != 0 || routes.singleOtherNames.length != 0)
+2 -14
View File
@@ -54,31 +54,19 @@ export const useApiStore = defineStore('apiStore', {
// Static data // Static data
this.fetchDonatorsData(); this.fetchDonatorsData();
this.fetchStationsGeneralInfo(); this.fetchStationsGeneralInfo();
// Active data schedueler
this.fetchActiveData();
this.setupActiveDataFetcher();
},
async setupActiveDataFetcher() {
if (this.activeDataScheduler) return;
this.activeDataScheduler = window.setInterval(() => {
this.fetchActiveData();
}, 25000);
}, },
async fetchActiveData() { async fetchActiveData() {
if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading; if (!this.activeData) this.dataStatuses.connection = Status.Data.Loading;
try { try {
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData'); const response = await this.client!.get<API.ActiveData.Response>('api/getActiveData');
this.activeData = response.data; this.activeData = response.data;
this.lastFetchData = new Date(); this.lastFetchData = new Date();
this.dataStatuses.connection = Status.Data.Loaded; this.dataStatuses.connection = Status.Data.Loaded;
console.log('Fetching active data at ' + new Date().toLocaleTimeString('pl-PL'));
} catch (error) { } catch (error) {
this.dataStatuses.connection = Status.Data.Error; this.dataStatuses.connection = Status.Data.Error;
console.error('Ups! Wystąpił błąd podczas pobierania danych online:', 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 { defineStore } from 'pinia';
import Train from '../scripts/interfaces/Train';
import { parseSpawns, getScheduledTrains, getStationTrains } from './utils'; import { parseSpawns, getScheduledTrains, getStationTrains } from './utils';
import { ActiveScenery, ScheduledTrain, StoreState } from './typings'; import {
ActiveScenery,
import { Status } from '../typings/common'; ScheduledTrain,
import Station from '../scripts/interfaces/Station'; Station,
StationRoutes,
Status,
Train
} from '../typings/common';
import { useApiStore } from './apiStore'; 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: () => state: () =>
({ ({
region: { id: 'eu', value: 'PL1' }, region: { id: 'eu', value: 'PL1', name: 'PL1' },
isOffline: false, isOffline: false,
isNewUpdate: false, appUpdate: null,
dispatcherStatsName: '', dispatcherStatsName: '',
dispatcherStatsStatus: Status.Data.Initialized, dispatcherStatsStatus: Status.Data.Initialized,
@@ -26,11 +29,8 @@ export const useMainStore = defineStore('store', {
chosenModalTrainId: undefined, chosenModalTrainId: undefined,
modalLastClickedTarget: null, modalLastClickedTarget: null
}) as MainStoreState,
mousePos: { x: 0, y: 0 },
popUpData: { key: null, content: '' }
}) as StoreState,
getters: { getters: {
trainList(): Train[] { trainList(): Train[] {
@@ -96,6 +96,7 @@ export const useMainStore = defineStore('store', {
}); });
}, },
// computed active sceneries
activeSceneryList(state): ActiveScenery[] { activeSceneryList(state): ActiveScenery[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
@@ -156,8 +157,8 @@ export const useMainStore = defineStore('store', {
scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT scenery.dispatcherStatus == Status.ActiveDispatcher.NO_LIMIT
? Date.now() + 25500000 ? Date.now() + 25500000
: scenery.dispatcherStatus > 5 : scenery.dispatcherStatus > 5
? scenery.dispatcherStatus ? scenery.dispatcherStatus
: null; : null;
list.push({ list.push({
name: scenery.stationName, name: scenery.stationName,
@@ -231,6 +232,7 @@ export const useMainStore = defineStore('store', {
return allActiveSceneries; return allActiveSceneries;
}, },
// computed station data
stationList(): Station[] { stationList(): Station[] {
const apiStore = useApiStore(); const apiStore = useApiStore();
@@ -248,6 +250,13 @@ export const useMainStore = defineStore('store', {
if (!route.isInternal) acc[routesKey].push(route.routeName); if (!route.isInternal) acc[routesKey].push(route.routeName);
if (route.isRouteSBL) acc['sblNames'].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); acc[tracksKey].push(route);
return acc; return acc;
@@ -259,7 +268,9 @@ export const useMainStore = defineStore('store', {
double: [], double: [],
doubleElectrifiedNames: [], doubleElectrifiedNames: [],
doubleOtherNames: [], doubleOtherNames: [],
sblNames: [] sblNames: [],
minRouteSpeed: 0,
maxRouteSpeed: 0
} as StationRoutes } 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, unsignedStatus: false,
withActiveTimetables: false, withActiveTimetables: false,
withoutActiveTimetables: false, withoutActiveTimetables: false,
maxVmax: 200,
minVmax: 0,
authors: '', authors: '',
onlineFromHours: 0 onlineFromHours: 0
}; };
@@ -75,26 +75,8 @@ export const useStationFiltersStore = defineStore('stationFiltersStore', {
filteredStationList: (state) => { filteredStationList: (state) => {
const store = useMainStore(); const store = useMainStore();
const savedStationNames = store.stationList.map((s) => s.name);
const onlineUnsavedStations = store.activeSceneryList return store.allStationInfo
.filter((os) => !savedStationNames.includes(os.name) && os.region == store.region.id)
.map((os) => ({
name: os.name,
generalInfo: undefined,
onlineInfo: os
}));
return [
...onlineUnsavedStations,
...store.stationList.map((station) => ({
...station,
// append to 'onlineInfo' object for filtering legacy reasons - to optimize later (hopefully)
onlineInfo: store.activeSceneryList.find(
(os) => os.name == station.name && os.region == store.region.id
)
}))
]
.filter((station) => filterStations(station, state.filters)) .filter((station) => filterStations(station, state.filters))
.sort((a, b) => sortStations(a, b, state.sorterActive)); .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 { 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 interface MainStoreState {
export type Availability = 'default' | 'unavailable' | 'nonPublic' | 'abandoned' | 'nonDefault'; region: { id: string; value: string; name: string };
export type PopUpType = (typeof popupKeys)[number];
export interface RegionCounters {
stationCount: number;
trainsCount: number;
timetablesCount: number;
}
export interface StoreState {
region: { id: string; value: string };
isOffline: boolean; isOffline: boolean;
isNewUpdate: boolean; appUpdate: { version: string; changelog: string; releaseURL: string } | null;
dispatcherStatsName: string; dispatcherStatsName: string;
dispatcherStatsData?: API.DispatcherStats.Response; dispatcherStatsData?: API.DispatcherStats.Response;
driverStatsName: string; driverStatsName: string;
@@ -22,19 +12,6 @@ export interface StoreState {
driverStatsStatus: Status.Data; driverStatsStatus: Status.Data;
chosenModalTrainId?: string; chosenModalTrainId?: string;
modalLastClickedTarget: EventTarget | null; 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 { export interface StationJSONData {
@@ -62,110 +39,3 @@ export interface StationJSONData {
availability: Availability; 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 {
import Train from '../scripts/interfaces/Train'; TrainStop,
import { ScheduledTrain, StationTrain, StopStatus, TrainStop } from './typings'; StopStatus,
Train,
ScheduledTrain,
Station,
StationTrain,
ScenerySpawn,
ScenerySpawnType
} from '../typings/common';
export function getLocoURL(locoType: string): string { export function getLocoURL(locoType: string): string {
return `https://rj.td2.info.pl/dist/img/thumbnails/${ return `https://rj.td2.info.pl/dist/img/thumbnails/${
@@ -31,7 +38,7 @@ export function getStatusTimestamp(stationStatus: any): number {
return -1; return -1;
} }
export function parseSpawns(spawnString: string | null) { export function parseSpawns(spawnString: string | null): ScenerySpawn[] {
if (!spawnString) return []; if (!spawnString) return [];
if (spawnString === 'NO_SPAWN') return []; if (spawnString === 'NO_SPAWN') return [];
@@ -41,7 +48,15 @@ export function parseSpawns(spawnString: string | null) {
const spawnLength = parseInt(spawnArray[2]); const spawnLength = parseInt(spawnArray[2]);
const isElectrified = spawnArray[3] == 'True'; 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; 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 { &.btn--action {
background-color: #424242; background-color: #424242;
border-radius: 0.25em; border-radius: 0.25em;
font-weight: bold;
&:hover { &:hover {
background-color: #555; 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 type Response = Data[];
export interface Data { export interface Data {
id: string;
trainNo: number; trainNo: number;
mass: number; mass: number;
length: number; length: number;
speed: 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 namespace Status {
export enum ActiveDispatcher { export enum ActiveDispatcher {
FREE = -3, FREE = -3,
@@ -20,3 +32,204 @@ export namespace Status {
Warning = 3 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> </button>
</div> </div>
<Donation :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" /> <DonationModal :isModalOpen="isDonationModalOpen" @toggleModal="toggleDonationModal" />
<StationTable :stations="computedStationList" @toggleDonationModal="toggleDonationModal" /> <StationTable @toggleDonationModal="toggleDonationModal" />
<StationStats />
</div> </div>
</section> </section>
</template> </template>
@@ -31,36 +32,29 @@ import StationTable from '../components/StationsView/StationTable.vue';
import StationFilterCard from '../components/StationsView/StationFilterCard.vue'; import StationFilterCard from '../components/StationsView/StationFilterCard.vue';
import { useStationFiltersStore } from '../store/stationFiltersStore'; import { useStationFiltersStore } from '../store/stationFiltersStore';
import { useMainStore } from '../store/mainStore'; 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({ export default defineComponent({
components: { components: {
StationTable, StationTable,
StationFilterCard, StationFilterCard,
Donation StationStats,
DonationModal
}, },
data: () => ({ data: () => ({
filterCardOpen: false, filterCardOpen: false,
modalHidden: true, isDonationModalOpen: false,
STORAGE_KEY: 'options_saved',
focusedStationName: '',
filterStore: useStationFiltersStore(),
store: useMainStore(),
isDonationModalOpen: false filterStore: useStationFiltersStore(),
store: useMainStore()
}), }),
mounted() { mounted() {
this.filterStore.setupFilters(); this.filterStore.setupFilters();
}, },
computed: {
computedStationList() {
return this.filterStore.filteredStationList;
}
},
methods: { methods: {
toggleDonationModal(value: boolean) { toggleDonationModal(value: boolean) {
this.isDonationModalOpen = value; this.isDonationModalOpen = value;
@@ -73,30 +67,6 @@ export default defineComponent({
@import '../styles/variables.scss'; @import '../styles/variables.scss';
@import '../styles/responsive.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 { .stations-view {
position: relative; position: relative;
display: flex; 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 TrainOptions from '../components/TrainsView/TrainOptions.vue';
import TrainTable from '../components/TrainsView/TrainTable.vue'; import TrainTable from '../components/TrainsView/TrainTable.vue';
import modalTrainMixin from '../mixins/modalTrainMixin'; import modalTrainMixin from '../mixins/modalTrainMixin';
import Train from '../scripts/interfaces/Train';
import { useMainStore } from '../store/mainStore'; import { useMainStore } from '../store/mainStore';
import { TrainFilter, trainFilters } from '../components/TrainsView/typings'; import { TrainFilter, trainFilters } from '../components/TrainsView/typings';
import { filteredTrainList } from '../managers/trainFilterManager'; import { filteredTrainList } from '../managers/trainFilterManager';
import TrainStats from '../components/TrainsView/TrainStats.vue'; import TrainStats from '../components/TrainsView/TrainStats.vue';
import { Train } from '../typings/common';
export default defineComponent({ export default defineComponent({
components: { components: {
+33 -28
View File
@@ -12,46 +12,42 @@ export default defineConfig({
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
includeAssets: ['/images/*.png', '/fonts/*.woff', '/fonts/*.woff2'], includeAssets: ['/images/*.{png,svg,jpg}', '/fonts/*.{woff,woff2}'],
workbox: { workbox: {
disableDevLogs: true, disableDevLogs: true,
globPatterns: ['**/*.{js,css,html,png,svg,jpg}'], globPatterns: ['**/*.{js,css,html,png,svg,jpg}'],
cleanupOutdatedCaches: true,
runtimeCaching: [ runtimeCaching: [
{ {
urlPattern: new RegExp('^https://stacjownik.spythere.eu/api/getSceneries', 'i'), 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', handler: 'CacheFirst',
options: { options: {
cacheName: 'images-cache', cacheName: 'spythere-sceneries-cache',
expiration: { cacheableResponse: {
maxEntries: 100, statuses: [0, 200]
maxAgeSeconds: 60 * 60 * 24 * 7 // <== 7 days }
}, }
},
{
urlPattern: new RegExp('^https://rj.td2.info.pl/dist/img/thumbnails/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'swdr-images-cache',
cacheableResponse: { cacheableResponse: {
statuses: [0, 200, 404] statuses: [0, 200, 404]
} }
} }
},
{
urlPattern: new RegExp('^https://static.spythere.eu/images/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'spythere-images-cache',
cacheableResponse: {
statuses: [0, 200]
}
}
} }
] ]
}, },
@@ -60,5 +56,14 @@ export default defineConfig({
suppressWarnings: true 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