chore: refreshed project

This commit is contained in:
2024-08-02 02:02:31 +02:00
parent a10f12e568
commit 03e4330ab5
27 changed files with 2977 additions and 1104 deletions
+31 -46
View File
@@ -1,13 +1,9 @@
<template>
<div class="app_content">
<nav class="navbar">
<router-link to="/">
Pragotron TD2 <span class="text--accent">v{{ VERSION }}</span> <sup>by Spythere</sup>
</router-link>
<!-- <button v-else class="back-btn btn--text" @click="selectedStation = null">&lt; powrót</button> -->
</nav>
<Navbar :version="version" />
<main>
<button @click="testAudio">test audio</button>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" :key="$route.path"></component>
@@ -18,27 +14,37 @@
</template>
<script lang="ts">
import { defineComponent } from '@vue/runtime-core';
import PragotronVue from './views/PragotronView.vue';
import IStationData from './types/ISceneryData';
import { defineComponent } from 'vue';
import packageInfo from '../package.json';
import { useApiStore } from './stores/apiStore';
import Navbar from './components/Navbar.vue';
export default defineComponent({
components: {
PragotronVue,
},
components: { Navbar },
data: () => ({
onlineStations: [] as IStationData[],
dataLoaded: false,
VERSION: packageInfo.version,
version: packageInfo.version,
apiStore: useApiStore()
}),
async mounted() {
this.dataLoaded = true;
this.apiStore.fetchSceneriesData();
this.apiStore.fetchActiveData();
setInterval(() => {
this.apiStore.fetchActiveData();
}, 30000);
},
methods: {
testAudio() {
const audio = new Audio('../public/pragotron.mp3');
audio.play();
audio.loop = true;
}
}
});
</script>
@@ -48,45 +54,24 @@ export default defineComponent({
.app_content {
text-align: center;
display: flex;
flex-direction: column;
display: grid;
grid-template-rows: auto 1fr;
min-height: 100vh;
overflow-x: hidden;
}
nav {
flex: 0 1 40px;
font-size: 1.35em;
padding: 0.25em;
display: flex;
align-items: center;
background-color: $accentBg;
sup {
font-size: 0.8em;
color: $dimmedText;
}
button {
padding: 0;
}
}
main {
flex: 1 1 auto;
display: flex;
justify-content: center;
align-items: center;
padding: 1em;
overflow-x: hidden;
}
a {
text-decoration: none;
color: white;
a:hover {
color: gold;
}
}
</style>
+104
View File
@@ -0,0 +1,104 @@
<template>
<div class="dropdown" v-click-outside="() => (store.optionsOpen = false)">
<button class="btn--image" @click="store.optionsOpen = !store.optionsOpen">
<img src="/options.svg" alt="options" />
</button>
<transition name="dropdown-anim">
<div class="dropdown-body" v-if="store.optionsOpen">
<h3>Opcje</h3>
<hr />
<div style="margin: 0.5em 0">
<label>
<input type="checkbox" v-model="store.filters.nonPassenger" />
Relacje niepasażerskie
</label>
<label>
<input type="checkbox" v-model="store.filters.terminating" />
Relacje kończące bieg
</label>
</div>
<div v-if="isPragotronOpen">
<label for="checkpoint">
Posterunek:
<select id="checkpoint" v-model="store.selectedCheckpointName">
<option v-for="cp in store.selectedStation?.stationCheckpoints" :value="cp" :key="cp">
{{ cp }}
</option>
</select>
</label>
</div>
<div tabindex="0" @focus="() => (store.optionsOpen = false)"></div>
</div>
</transition>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../stores/mainStore';
export default defineComponent({
data: () => ({
store: useMainStore()
}),
computed: {
isPragotronOpen() {
return this.$route.path == '/board';
}
}
});
</script>
<style lang="scss" scoped>
img {
max-width: 2em;
}
h3 {
font-size: 1.2em;
margin: 0;
}
.dropdown-bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 100;
}
.dropdown-body {
position: absolute;
top: 100%;
right: 0;
padding: 0.25em;
transform: translateY(0.5em);
width: 500px;
max-width: calc(100% - 0.5em);
z-index: 105;
background-color: #000000e1;
}
.dropdown-anim {
&-enter-active,
&-leave-active {
transition: all 90ms ease-out;
}
&-enter-from,
&-leave-to {
transform: translateY(20px);
opacity: 0;
}
}
</style>
+62
View File
@@ -0,0 +1,62 @@
<template>
<nav class="navbar">
<div class="navbar-body">
<router-link class="brand" to="/">
Pragotron TD2 <span class="text--accent">v{{ version }}</span> <sup>by Spythere</sup>
</router-link>
<div class="options">
<Dropdown />
</div>
</div>
</nav>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useMainStore } from '../stores/mainStore';
import Dropdown from './Dropdown.vue';
export default defineComponent({
components: { Dropdown },
props: {
version: String
},
data() {
return {
store: useMainStore()
};
}
});
</script>
<style lang="scss" scoped>
@import '../styles.scss';
nav.navbar {
background-color: $accentBg;
padding: 0 0.5em;
sup {
font-size: 0.8em;
color: $dimmedText;
}
}
.navbar-body {
padding: 0.25em;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
margin: 0 auto;
max-width: 1400px;
}
.brand {
font-size: 1.25em;
}
</style>
+129 -1
View File
@@ -1 +1,129 @@
["", "Aleksandrów Kujawski","Arkadia Zdrój","Babimost","Bargowice","Bargowice Zachód","Horz Zdrój","Bełchów","Blaszki","Borki","Brakowice","Buczek","Buk","Bystra Woda","Cenorzyce Nowe","Chełmik Wołowski","Chlorkowice","Cis","Czerepy","Czermin","Dobrzyca DTA","Dobrzyca DTB","Dobrzyca DTC","Dobrzyniec","Dobrzyniec Mącice","Drzewko","Dziewoszyce","Falewo","Glinnik","Grabów Miasto","Grabów Wieś","Góra Włodowska","Głogowo","Głębce","Głęboszów","Imielin","Jordanowo","Karszynek","Kcynia","Kieły","Kolsko","Kowalewo","Krzemienice","Krzęcz","Kszęty","Kudowa Zdrój","Głowno","Domaniewice","Ozorków","Chociszew","Skrzynki","Wykno","Żywiec","Węgierska Górka","Łodygowice","Wilkowice Bystra","BB Leszczyny","Legno","Lewków","Ligota Grabowska","Ligota Trzeszcze","Lisiczki","Lisków","TEFAMA","Lisków Miasto","Lublinek","Lutol Suchy","Luzino","Lębork","Milówka","Modlinków","Motławy","Naterki","Okoń Główny","Orniki","Otwocko","Parów","Piaskowo","Pilichowice","Poreńsk","Radostowice","Radowice","Radzikowo","Rajcza","Razemsko","Rebrowo Dolne","Redlin Sudecki","Santok Zdrój","Sieniawka","Skawce","Sowi Bór","Sroka","Stare Lipowo","Przęsy","Starzynki","Stefanowo","Stryków","Strączki","Sulechów","Szadek","Sól","Tarkowo","Tartakowo","Testowo","Trawniczki","Tłoki","Wełtawa","Wielichowo Główne","Wielichowo Główne gt","Wielichowo Wieś","Wijewo","Wilczyca","Witaszyczki","Witonia","Wodnica","Wola","Wola Nowska","Wschodna","Zgierz","Zgierz Kontrewers","Zwardoń","Łask","Łaskarzew","Łebnino","Łęczyca","Żerniki","Żory"]
[
"",
"Aleksandrów Kujawski",
"Arkadia Zdrój",
"Babimost",
"Bargowice",
"Bargowice Zachód",
"Horz Zdrój",
"Bełchów",
"Blaszki",
"Borki",
"Brakowice",
"Buczek",
"Buk",
"Bystra Woda",
"Cenorzyce Nowe",
"Chełmik Wołowski",
"Chlorkowice",
"Cis",
"Czerepy",
"Czermin",
"Dobrzyca DTA",
"Dobrzyca DTB",
"Dobrzyca DTC",
"Dobrzyniec",
"Dobrzyniec Mącice",
"Drzewko",
"Dziewoszyce",
"Falewo",
"Glinnik",
"Grabów Miasto",
"Grabów Wieś",
"Góra Włodowska",
"Głogowo",
"Głębce",
"Głęboszów",
"Imielin",
"Jordanowo",
"Karszynek",
"Kcynia",
"Kieły",
"Kolsko",
"Kowalewo",
"Krzemienice",
"Krzęcz",
"Kszęty",
"Kudowa Zdrój",
"Głowno",
"Domaniewice",
"Ozorków",
"Chociszew",
"Skrzynki",
"Wykno",
"Żywiec",
"Węgierska Górka",
"Łodygowice",
"Wilkowice Bystra",
"BB Leszczyny",
"Legno",
"Lewków",
"Ligota Grabowska",
"Ligota Trzeszcze",
"Lisiczki",
"Lisków",
"TEFAMA",
"Lisków Miasto",
"Lublinek",
"Lutol Suchy",
"Luzino",
"Lębork",
"Milówka",
"Modlinków",
"Motławy",
"Naterki",
"Okoń Główny",
"Orniki",
"Otwocko",
"Parów",
"Piaskowo",
"Pilichowice",
"Poreńsk",
"Radostowice",
"Radowice",
"Radzikowo",
"Rajcza",
"Razemsko",
"Rebrowo Dolne",
"Redlin Sudecki",
"Santok Zdrój",
"Sieniawka",
"Skawce",
"Sowi Bór",
"Sroka",
"Stare Lipowo",
"Przęsy",
"Starzynki",
"Stefanowo",
"Stryków",
"Strączki",
"Sulechów",
"Szadek",
"Sól",
"Tarkowo",
"Tartakowo",
"Testowo",
"Trawniczki",
"Tłoki",
"Wełtawa",
"Wielichowo Główne",
"Wielichowo Główne gt",
"Wielichowo Wieś",
"Wijewo",
"Wilczyca",
"Witaszyczki",
"Witonia",
"Wodnica",
"Wola",
"Wola Nowska",
"Wschodna",
"Zgierz",
"Zgierz Kontrewers",
"Zwardoń",
"Łask",
"Łaskarzew",
"Łebnino",
"Łęczyca",
"Żerniki",
"Żory"
]
-2
View File
@@ -1,2 +0,0 @@
{
}
+7
View File
@@ -0,0 +1,7 @@
import axios from 'axios';
const http = axios.create({
baseURL: 'https://stacjownik.spythere.eu'
});
export default http;
+20 -2
View File
@@ -1,6 +1,24 @@
import { createApp } from 'vue';
import { createApp, Directive } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
createApp(App).use(router).mount('#app');
const pinia = createPinia();
const clickOutsideDirective: Directive = {
mounted(el, binding) {
el.clickOutsideEvent = (event: Event) => {
if (!(el == event.target || el.contains(event.target))) {
binding.value();
}
};
document.addEventListener('click', el.clickOutsideEvent);
}
};
createApp(App)
.use(router)
.use(pinia)
.directive('click-outside', clickOutsideDirective)
.mount('#app');
+4 -4
View File
@@ -6,18 +6,18 @@ import PragotronView from './views/PragotronView.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
component: HomeView,
component: HomeView
},
{
path: '/board',
component: PragotronView,
props: (route) => ({ stationName: route.query.name }),
},
props: (route) => ({ stationName: route.query.name, region: route.query.region })
}
];
const router = createRouter({
history: createWebHistory(),
routes,
routes
});
export default router;
+53
View File
@@ -0,0 +1,53 @@
import { defineStore } from 'pinia';
import { API } from '../typings/api';
import http from '../http';
export enum DataStatus {
LOADING = 0,
LOADED = 1,
ERROR = 2
}
export const useApiStore = defineStore('api', {
state() {
return {
activeData: undefined as API.ActiveData.Response | undefined,
stationData: undefined as API.Sceneries.Response | undefined,
dataStatuses: {
activeData: DataStatus.LOADING,
stationData: DataStatus.LOADING
}
};
},
actions: {
async fetchActiveData() {
try {
const response = (await http.get<API.ActiveData.Response | undefined>('api/getActiveData'))
.data;
this.dataStatuses.activeData = DataStatus.LOADED;
this.activeData = response;
} catch (error) {
this.dataStatuses.activeData = DataStatus.ERROR;
console.error('Wystąpił błąd podczas pobierania danych:', error);
}
},
async fetchSceneriesData() {
try {
const response = (await http.get<API.Sceneries.Response | undefined>('api/getSceneries'))
.data;
this.dataStatuses.stationData = DataStatus.LOADED;
this.stationData = response;
} catch (error) {
this.dataStatuses.stationData = DataStatus.ERROR;
console.error('Wystąpił błąd podczas pobierania danych:', error);
}
}
}
});
+52
View File
@@ -0,0 +1,52 @@
import { defineStore } from 'pinia';
import ISceneryData from '../types/ISceneryData';
import { useApiStore } from './apiStore';
export enum Region {
PL1 = 'eu',
PL2 = 'cae',
CZE = 'usw',
DE = 'us ',
ENG = 'ru'
}
export const regionNames = {
[Region.PL1]: 'PL1',
[Region.PL2]: 'PL2',
[Region.CZE]: 'CZE',
[Region.DE]: 'DE',
[Region.ENG]: 'ENG'
};
export const useMainStore = defineStore('main', {
state() {
return {
region: Region.PL1,
optionsOpen: false,
filters: {
nonPassenger: true,
terminating: true
},
selectedStationName: '',
selectedCheckpointName: ''
};
},
getters: {
selectedStation(state): ISceneryData | undefined {
const apiStore = useApiStore();
const station = apiStore.stationData?.find(({ name }) => name == state.selectedStationName);
if (!station) return undefined;
return {
stationName: station.name,
stationCheckpoints:
station.checkpoints && station.checkpoints.length > 0
? station.checkpoints.split(';')
: [station.name],
nameAbbreviation: ''
};
}
}
});
+67 -7
View File
@@ -1,16 +1,16 @@
@import url('https://fonts.googleapis.com/css2?family=Monda:wght@400;700&display=swap');
@import 'theme.scss';
body,
html {
background: $primaryBg;
min-height: 100vh;
color: $primaryText;
padding: 0;
margin: 0;
font-size: 16px;
}
*,
@@ -19,22 +19,82 @@ html {
font-family: 'Monda', sans-serif;
}
input,
select,
option,
label {
font-size: inherit;
font-family: inherit;
}
ul {
list-style: none;
padding: 0;
}
button {
background: none;
border: none;
outline: none;
cursor: pointer;
color: white;
font-size: 1em;
&.btn--text {
background: none;
border: none;
outline: none;
color: white;
background-color: #1b1b1b;
&:hover {
background-color: #252525;
}
&:focus-visible {
color: $accentText;
}
&.btn--image {
display: flex;
background: none;
img {
vertical-align: middle;
}
&:focus-visible {
outline: 1px solid $accentText;
}
}
}
// Input radio
.g-selector {
label {
background-color: #202020;
cursor: pointer;
display: flex;
justify-content: center;
&:hover {
background-color: #2b2b2b;
}
span {
width: 100%;
padding: 0.25em 0.5em;
}
input[type='radio'] {
opacity: 0;
width: 1px;
height: 1px;
position: absolute;
&:checked + span {
color: $accentText;
}
&:focus-visible + span {
outline: 1px solid white;
}
}
}
}
+3 -3
View File
@@ -8,9 +8,9 @@ $secondaryBg: #aaa;
$accentBg: #327ea5;
.text--accent {
color: $accentText;
color: $accentText;
}
.text--grayed {
color: $dimmedText;
}
color: $dimmedText;
}
+1 -1
View File
@@ -2,4 +2,4 @@ export default interface ISceneryData {
stationName: string;
nameAbbreviation: string;
stationCheckpoints: string[];
}
}
+17 -17
View File
@@ -1,28 +1,28 @@
interface ITableRowValues {
routeTo: string;
routeVia: string;
routeTo: string;
routeVia: string;
// routeTo, routeVia, date1, date2, date3, date4
currentRowIndexes: [number, number, number, number, number, number];
// routeTo, routeVia, date1, date2, date3, date4
currentRowIndexes: [number, number, number, number, number, number];
dateDigits: string[],
dateDigits: string[];
}
export interface ITableRow {
trainNumber: string;
timetableId: number;
trainNumber: string;
timetableId: number;
routeTo: string;
routeVia: string;
routeTo: string;
routeVia: string;
checkpointName: string;
checkpointName: string;
arrivalTimestamp: number;
departureTimestamp: number;
arrivalTimestamp: number;
departureTimestamp: number;
delayMinutes: number,
date?: Date,
dateDigits: string[],
delayMinutes: number;
date?: Date;
dateDigits: string[];
tableValues: ITableRowValues;
}
tableValues: ITableRowValues;
}
+136
View File
@@ -0,0 +1,136 @@
export namespace API {
export namespace ActiveData {
export interface Response {
activeSceneries: ActiveScenery[];
trains: ActiveTrain[];
}
export interface ActiveScenery {
dispatcherId: number;
dispatcherName: string;
dispatcherIsSupporter: boolean;
stationName: string;
stationHash: string;
region: string;
maxUsers: number;
currentUsers: number;
spawn: number;
lastSeen: number;
dispatcherExp: number;
nameFromHeader: string;
spawnString: string | null;
networkConnectionString: string;
isOnline: number;
dispatcherRate: number;
dispatcherStatus: number;
}
export interface ActiveTrain {
trainNo: number;
mass: number;
length: number;
speed: number;
stockString: string;
signal: string;
distance: number;
connectedTrack: string;
driverName: string;
driverId: number;
driverIsSupporter: boolean;
driverLevel?: number;
currentStationName: string;
currentStationHash?: string;
online: number;
lastSeen: number;
region: string;
isTimeout: boolean;
timetable?: Timetable;
}
export interface TimetableStop {
stopName: string;
stopNameRAW: string;
stopType: string;
stopDistance: number;
pointId: string;
mainStop: boolean;
arrivalLine: string | null;
arrivalTimestamp: number;
arrivalRealTimestamp: number;
arrivalDelay: number;
departureLine: string | null;
departureTimestamp: number;
departureRealTimestamp: number;
departureDelay: number;
comments?: any;
beginsHere: boolean;
terminatesHere: boolean;
confirmed: number;
stopped: number;
stopTime: number | null;
}
export interface Timetable {
timetableId: number;
category: string;
route: string;
stopList: TimetableStop[];
TWR: boolean;
SKR: boolean;
sceneries: string[];
}
}
export namespace Sceneries {
export type Response = Scenery[];
export interface Scenery {
createdAt: string;
updatedAt?: string;
id: number;
name: string;
SUP: boolean;
authors: string;
availability: string;
backupJSON: any;
checkpoints?: string;
controlType: string;
lines?: string;
project?: string;
reqLevel: number;
routes?: string;
routesInfo: RoutesInfo[];
signalType: string;
supportersOnly?: boolean;
url?: string;
projectUrl?: string;
hash?: string;
abbr: string;
hidden: boolean;
}
export interface RoutesInfo {
routeName: string;
isElectric: boolean;
isInternal: boolean;
isRouteSBL: boolean;
routeSpeed: number;
routeLength: number;
routeTracks: number;
}
}
}
View File
+97 -40
View File
@@ -1,75 +1,132 @@
<template>
<div class="home-view">
<div>
<h1 style="margin: 0">Wybierz region i scenerię, aby otworzyć widok pragotronu</h1>
<div class="region-selector g-selector">
<label v-for="region in regions" :key="region">
<input
type="radio"
name="region"
@change="changeRegion(region)"
:checked="mainStore.region == region"
/>
<span>{{ regionNames[region] }}</span>
</label>
</div>
</div>
<div class="scenery-selector">
<h1 style="margin: 0">Wybierz scenerię, aby otworzyć widok pragotronu</h1>
<p style="margin: 0.5em; color: #ccc">Widoczne jedynie scenerie aktywne na serwerze PL1</p>
<!-- <p style="margin: 0.5em; color: #ccc">Widoczne są jedynie scenerie aktywne na serwerze PL1</p> -->
<ul class="scenery-list" v-if="dataLoaded && onlineStations.length > 0">
<li v-for="(stationName, i) in onlineStations">
<span v-if="i > 0">&bull;</span>
<button class="btn--text" @click="handleClick(stationName)">
{{ stationName }}
</button>
</li>
</ul>
<transition name="list-anim" tag="div" mode="out-in">
<h3 v-if="apiStore.dataStatuses.activeData == DataStatus.LOADING">
Ładowanie listy aktywnych scenerii...
</h3>
<h3 v-else-if="onlineStations.length == 0">Brak aktywnych scenerii</h3>
<h3 v-else>Ładowanie listy aktywnych scenerii...</h3>
<h3 v-else-if="sceneriesOnline.length == 0">Brak aktywnych scenerii</h3>
<ul v-else class="scenery-list">
<li v-for="station in sceneriesOnline" :key="station.stationName">
<button @click="handleClick(station.stationName)">
{{ station.stationName }}
</button>
</li>
</ul>
</transition>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { IOnlineStationsResponse } from '../types/IOnlineStationsResponse';
import { DataStatus, useApiStore } from '../stores/apiStore';
import { Region, useMainStore, regionNames } from '../stores/mainStore';
export default defineComponent({
data() {
return {
onlineStations: [] as string[],
dataLoaded: false,
apiStore: useApiStore(),
mainStore: useMainStore(),
regions: [Region.PL1, Region.PL2, Region.CZE, Region.DE, Region.ENG],
DataStatus: DataStatus,
regionNames
};
},
async mounted() {
const stationsAPIResponse: IOnlineStationsResponse = await (
await fetch('https://api.td2.info.pl/?method=getStationsOnline')
).json();
this.dataLoaded = true;
this.onlineStations = stationsAPIResponse.message
.reduce((acc, station) => {
if (station.region != 'eu') return acc;
if (!station.isOnline) return acc;
acc.push(station.stationName);
return acc;
}, [] as string[])
.sort((s1, s2) => (s1 > s2 ? 1 : -1));
computed: {
sceneriesOnline() {
return (
this.apiStore.activeData?.activeSceneries
.filter((station) => {
return station.region == this.mainStore.region && station.isOnline;
}, [])
.sort((s1, s2) => s1.stationName.localeCompare(s2.stationName)) || []
);
}
},
methods: {
handleClick(stationName: string) {
this.$router.push(`/board?name=${stationName}`);
// this.selectedStation = station;
this.$router.push({
path: '/board',
query: {
name: stationName,
region: this.mainStore.region
}
});
},
},
changeRegion(region: Region) {
this.mainStore.region = region;
}
}
});
</script>
<style ;ang="scss" scoped>
<style lang="scss" scoped>
.home-view {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 3em;
}
.region-selector {
display: grid;
justify-content: center;
grid-template-columns: repeat(5, 1fr);
max-width: 500px;
gap: 0.5em;
margin: 1em auto;
}
.scenery-selector > div {
position: relative;
}
ul.scenery-list {
display: flex;
justify-content: center;
flex-wrap: wrap;
padding: 1em;
margin: 0 auto;
gap: 0.5em;
font-size: 1.3em;
max-width: 1000px;
}
</style>
// List animation
.list-anim-enter-active,
.list-anim-leave-active {
transition: all 90ms ease;
}
.list-anim-enter-from,
.list-anim-leave-to {
opacity: 0;
transform: translateY(20px);
}
</style>
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}