Compare commits

..

16 Commits

Author SHA1 Message Date
Spythere d8d8a00fd9 chore(app): added the Creator badge 2026-05-02 15:40:05 +02:00
Spythere 6765c075a5 bump(version): v1.35.0 2026-04-29 13:43:07 +02:00
Spythere eb0821046c chore(DriverView): improved propositions locale texts 2026-04-28 14:34:04 +02:00
Spythere 676cb206c8 fix(TrainInfo): element layout and padding 2026-04-28 14:33:34 +02:00
Spythere 42bef618b4 fix(DriverView): action buttons layout 2026-04-28 14:32:48 +02:00
Spythere 0ea96700b3 fix(UpdateCard): adjusted deep style paddings 2026-04-28 14:06:15 +02:00
Spythere 642efec141 fix(stats): increased dropdown offset for small devices 2026-04-27 14:47:00 +02:00
Spythere 08ee303886 chore(sceneries): added preserving scroll state in the stations table after leaving the view 2026-04-27 13:57:32 +02:00
Spythere c4decd1003 chore(scenery): make return button redirect to Sceneries tab 2026-04-27 13:56:29 +02:00
Spythere 21725d4019 chore(driver): updated cargo warnings in propositions 2026-04-27 13:51:58 +02:00
Spythere 9b917a7b3b chore: added cargo warnings to driver propositions 2026-04-26 22:18:28 +02:00
Spythere db720d11f5 refactor(driver): moved driver propositions card to separate component 2026-04-22 02:12:44 +02:00
Spythere 8f430f1f8d fix(driver): es2022 features 2026-04-22 02:01:30 +02:00
Spythere cda1516424 chore(locale): added custom messages rule for English ordinal numbers 2026-04-22 01:57:31 +02:00
Spythere 104a094fd8 chore(scenery): removed default bold text from top list action buttons 2026-04-22 01:42:11 +02:00
Spythere 19a6929e6f chore(scenery): added info box about no available best scores in scenery top list 2026-04-22 01:41:28 +02:00
24 changed files with 599 additions and 329 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "stacjownik", "name": "stacjownik",
"version": "1.34.0", "version": "1.35.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+19 -3
View File
@@ -1,7 +1,9 @@
<template> <template>
<Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)"> <Card :is-open="isUpdateCardOpen" @toggle-card="toggleCard(false)">
<div class="content" tabindex="0" ref="content"> <div class="content" tabindex="0" ref="content">
<h1 class="content-title"><i class="fa-solid fa-wand-sparkles"></i> {{ $t('update.title') }}</h1> <h1 class="content-title">
<i class="fa-solid fa-wand-sparkles"></i> {{ $t('update.title') }}
</h1>
<div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div> <div class="features-body" v-if="htmlChangelog != ''" v-html="htmlChangelog"></div>
<div class="no-features" v-else>{{ $t('update.no-data') }}</div> <div class="no-features" v-else>{{ $t('update.no-data') }}</div>
@@ -87,13 +89,27 @@ export default defineComponent({
::v-deep(h2) { ::v-deep(h2) {
padding: 0.5em 0; padding: 0.5em 0;
border-bottom: 1px solid #aaa;
&::after {
content: '';
display: block;
height: 2px;
width: 100%;
background-color: #aaa;
margin-top: 0.25em;
}
}
::v-deep(h3) {
padding-bottom: 0.25em;
} }
::v-deep(ul) { ::v-deep(ul) {
list-style: disc; list-style: disc;
padding: 0.5em 1.5em;
line-height: 1.5em; line-height: 1.5em;
padding: 0 1.5em;
padding-bottom: 0.5em;
} }
.content { .content {
@@ -0,0 +1,325 @@
<template>
<div class="driver-propositions">
<h3>{{ t('trains.number-propositions-header') }}</h3>
<div class="categories-select">
<button
v-for="(category, i) in availableCategories"
class="btn btn--option btn--action"
@click="selectCategory(i)"
:class="{ checked: i == chosenCategoryIndex }"
>
{{ category }}
</button>
</div>
<div v-if="numberPropositions.length > 0" class="propositions-numbers">
<div v-if="chosenCategory">
<b>{{ chosenCategory }} </b> -
{{ t(`categories.${chosenCategory.slice(0, 2)}`) }}
({{ t(`categories.${chosenCategory.slice(2)}`) }})
</div>
<div v-if="chosenCategoryRules">
<span v-if="chosenCategoryRules[0]"
>{{ t('trains.number-propositions-third-number') }}
<b class="text--primary">{{ chosenCategoryRules[0] }}</b> &bull;
</span>
<span
>{{
t('trains.number-propositions-last-nums', {
count: chosenCategoryRules[1].length
})
}}
<b class="text--primary">{{ chosenCategoryRules[1] }}</b> -
<b class="text--primary">{{ chosenCategoryRules[2] }}</b></span
>
</div>
<div style="margin-top: 0.5em">
<b>{{ t('trains.number-propositions-title') }}&nbsp;</b>
<i>{{ numberPropositions.join(', ') }}</i>
</div>
</div>
<div class="no-propositions" v-else>{{ t('trains.number-propositions-empty') }}</div>
<div class="cargo-warnings" v-if="getCargoWarnings.size > 0">
<hr />
<h3>{{ t('cargo-warnings.title') }}</h3>
<div class="warnings-container">
<div
v-for="warning in getCargoWarnings"
class="train-badge"
:class="`${warning.split('-')[0]}`"
>
{{ t('cargo-warnings.' + warning) }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, PropType, watch, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { Train } from '../../typings/common';
import rulesJSON from '../../data/trainNumberRules.json';
import { useApiStore } from '../../store/apiStore';
const { t } = useI18n();
const apiStore = useApiStore();
const props = defineProps({
chosenTrain: {
type: Object as PropType<Train>,
required: true
}
});
const emits = defineEmits(['selectCategory']);
const chosenCategoryIndex = ref(0);
const numberPropositions = ref<string[]>([]);
const chosenCategoryRules = ref<any[]>([]);
watch(
computed(() => props.chosenTrain.trainNo),
() => {
chosenCategoryIndex.value = 0;
generateNumberPropositions();
}
);
onMounted(() => {
generateNumberPropositions();
});
function generateNumberPropositions() {
const categoryCode = chosenCategory.value?.slice(0, 2);
const trainNoStr = props.chosenTrain.trainNo.toString();
// Get category rules
const rules = categoryCode
? ((rulesJSON.categoriesRules as any)[categoryCode] as any[])
: undefined;
if (!categoryCode || !rules) {
numberPropositions.value.length = 0;
chosenCategoryRules.value.length = 0;
return;
}
const [thirdNumber, minRange, maxRange] = rules;
const propositionsArr: string[] = [];
for (let i = 0; i < 5; i++) {
let generatedNumStr = '';
generatedNumStr += trainNoStr[0] ?? Math.floor(Math.random() * 10);
generatedNumStr += trainNoStr[1] ?? Math.floor(Math.random() * 10);
// Third number
generatedNumStr += thirdNumber ?? '';
// Remaining numbers
const rangeNums = minRange?.length ?? 3;
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
).toString();
const leadingZeros = new Array(Math.abs(randRange.toString().length - rangeNums))
.fill('0')
.join('');
generatedNumStr += `${leadingZeros}${randRange}`;
const isNumberTaken =
apiStore.activeData?.trains?.some((t) => t.trainNo.toString() == generatedNumStr) ?? false;
if (!isNumberTaken) {
propositionsArr.push(generatedNumStr);
} else {
i--;
}
if (Number(randRange) > Number(maxRange)) break;
}
numberPropositions.value = propositionsArr;
chosenCategoryRules.value = rules;
}
const chosenCategory = computed(() => {
return availableCategories.value[chosenCategoryIndex.value];
});
const getCargoWarnings = computed(() => {
const stockList = props.chosenTrain.stockList;
let warnings: Set<string> = new Set();
stockList.forEach((stockVehicle) => {
const [vehicleName, vehicleCargo] = stockVehicle.split(':');
if (vehicleName.startsWith('WB117')) warnings.add(vehicleCargo ? 'twr-un1965' : 'tn-un1965');
else if (vehicleName.startsWith('445Rb'))
warnings.add(vehicleCargo ? 'tn-un1202' : 'tn-un1202-empty');
else if (vehicleName.startsWith('EDK80')) warnings.add('pn-edk80');
if (vehicleCargo) {
if (vehicleCargo.startsWith('wt_20')) warnings.add('pn-innofreight');
else if (/^(tank|vehicles_01|truck)/.test(vehicleCargo)) warnings.add('pn-military');
}
});
return warnings;
});
const availableCategories = computed(() => {
const stockList = props.chosenTrain.stockList;
const headVehicle = stockList[0]?.split('-')[0] ?? '';
let availableCategories: string[] = [];
let categoryTraction = 'E';
let vehicleTypesSet = new Set<string>();
let wagonsNamesSet = new Set<string>();
let cargoNamesSet = new Set<string>();
for (const stockName of stockList) {
const [vehicleName, ...cargoList] = stockName.split(':');
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
if (!vehicleData) continue;
vehicleTypesSet.add(vehicleData.type);
if (vehicleData.type.startsWith('wagon-')) wagonsNamesSet.add(vehicleData.name.split('_')[0]);
if (cargoList !== undefined) cargoList.forEach((c) => cargoNamesSet.add(c.split('_')[0]));
}
let vehicleTypesArr = [...vehicleTypesSet];
let wagonsNamesArr = [...wagonsNamesSet];
// Traction
if (vehicleTypesArr[0] == 'loco-electric') categoryTraction = 'E';
else if (vehicleTypesArr[0] == 'loco-diesel') categoryTraction = 'S';
else if (vehicleTypesArr[0] == 'unit-electric') categoryTraction = 'J';
else categoryTraction = 'M';
// EMU / DMU - M*, R*, P*
if (vehicleTypesArr.length == 1 && (categoryTraction == 'J' || categoryTraction == 'M')) {
availableCategories.push('MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Only locos (up to 3) - LT, LP, LS
else if (stockList.length <= 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
if (/^(EU|ET|201E|4E|SU|ST|M62|CTLR4C)/.test(headVehicle)) availableCategories.push('LT');
if (/^(EU|EP|SU|SP)/.test(headVehicle)) availableCategories.push('LP');
if (/^(SM)/.test(headVehicle)) availableCategories.push('LS');
}
// Only locos (more than 3) - TH
else if (stockList.length > 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
availableCategories.push('TH');
}
// Loco(s) + passenger only wagons - M*, R*, E*, P*
else if (vehicleTypesArr.every((v) => v.startsWith('loco-') || v == 'wagon-passenger')) {
availableCategories.push('EI', 'EC', 'EN', 'MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Loco(s) + cargo only / mixed wagons - T*, Z*
else {
if (wagonsNamesArr.every((v) => /^(627Z|412Z)/.test(v)))
availableCategories.push('TC', 'TD', 'TS');
else if (stockList.slice(1).every((v) => /PKPE/.test(v))) {
availableCategories.push('ZU', 'ZN');
} else if (wagonsNamesArr.length < 3 || cargoNamesSet.size < 3) {
availableCategories.push('TM', 'TG', 'TS', 'TK');
} else {
availableCategories.push('TN', 'TR', 'TS', 'TK');
}
}
return availableCategories.map((c) => `${c}${categoryTraction}`);
});
function selectCategory(i: number) {
chosenCategoryIndex.value = i;
generateNumberPropositions();
}
</script>
<style lang="scss" scoped>
@use '../../styles/responsive';
@use '../../styles/badge';
.driver-propositions {
margin-bottom: 1em;
padding: 0.5em;
background-color: #111;
}
.categories-select {
display: inline-flex;
flex-wrap: wrap;
gap: 0.5em;
margin-top: 0.5em;
position: relative;
&::after {
content: '';
position: absolute;
bottom: calc(-0.5em);
left: 0;
width: 100%;
height: 2px;
background-color: #aaa;
}
}
.propositions-numbers {
margin-top: 1em;
}
.no-propositions {
margin-top: 1em;
color: #ccc;
}
.cargo-warnings {
margin-top: 0.5em;
h3 {
margin: 0.5em 0;
}
}
.warnings-container {
display: flex;
flex-wrap: wrap;
gap: 0.5em;
}
@include responsive.smallScreen {
.driver-propositions {
text-align: center;
}
.categories-select {
justify-content: center;
}
.warnings-container {
justify-content: center;
}
}
</style>
+7 -244
View File
@@ -4,63 +4,18 @@
<!-- Train action buttons --> <!-- Train action buttons -->
<div class="train-stock-actions"> <div class="train-stock-actions">
<button class="btn btn--action" style="margin: 1em 0" @click="copyStockToClipboard()"> <button class="btn btn--action" @click="copyStockToClipboard()">
<i class="fa-regular fa-copy"></i> {{ i18n.t('trains.stock-copy') }} <i class="fa-regular fa-copy"></i> {{ i18n.t('trains.stock-copy') }}
</button> </button>
<button class="btn btn--action" style="margin: 1em 0" @click="toggleNumberPropositions()"> <button class="btn btn--action" @click="toggleNumberPropositions()">
<i class="fa-regular fa-lightbulb"></i> {{ i18n.t('trains.number-propositions') }} <i class="fa-regular fa-lightbulb"></i> {{ i18n.t('trains.number-propositions') }}
</button> </button>
</div> </div>
<!-- Proposed numbers container --> <!-- Proposed numbers container -->
<transition name="view-anim" class="propositions-container"> <transition name="view-anim">
<div v-if="arePropositionsVisible"> <DriverPropositions :chosenTrain="chosenTrain" v-if="arePropositionsVisible" />
<h3 style="margin-bottom: 0.5em">{{ i18n.t('trains.number-propositions-header') }}</h3>
<div class="categories-select">
<button
v-for="(category, i) in availableCategories"
class="btn btn--option btn--action"
@click="selectCategory(i)"
:class="{ checked: i == chosenCategoryIndex }"
>
{{ category }}
</button>
</div>
<div v-if="numberPropositions.length > 0" class="propositions-numbers">
<div v-if="chosenCategory">
<b>{{ chosenCategory }} </b> -
{{ i18n.t(`categories.${chosenCategory.slice(0, 2)}`) }}
({{ i18n.t(`categories.${chosenCategory.slice(2)}`) }})
</div>
<div v-if="chosenCategoryRules">
<span v-if="chosenCategoryRules[0]"
>{{ i18n.t('trains.number-propositions-third-number') }}
<b class="text--primary">{{ chosenCategoryRules[0] }}</b> &bull;
</span>
<span
>{{
i18n.t('trains.number-propositions-last-nums', {
count: chosenCategoryRules[1].length
})
}}
<b class="text--primary">{{ chosenCategoryRules[1] }}</b> -
<b class="text--primary">{{ chosenCategoryRules[2] }}</b></span
>
</div>
<div style="margin-top: 0.5em">
<b>{{ i18n.t('trains.number-propositions-title') }}&nbsp;</b>
<i>{{ numberPropositions.join(', ') }}</i>
</div>
</div>
<div class="no-propositions" v-else>{{ i18n.t('trains.number-propositions-empty') }}</div>
</div>
</transition> </transition>
<StockList :trainStockList="chosenTrain.stockList" :key="chosenTrain.id" :showPreviews="true" /> <StockList :trainStockList="chosenTrain.stockList" :key="chosenTrain.id" :showPreviews="true" />
@@ -72,25 +27,15 @@
import { PropType, ref } from 'vue'; import { PropType, ref } from 'vue';
import { Train } from '../../typings/common'; import { Train } from '../../typings/common';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useApiStore } from '../../store/apiStore';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import TrainSchedule from '../TrainsView/TrainSchedule.vue'; import TrainSchedule from '../TrainsView/TrainSchedule.vue';
import TrainInfo from '../TrainsView/TrainInfo.vue'; import TrainInfo from '../TrainsView/TrainInfo.vue';
import DriverPropositions from './DriverPropositions.vue';
import rulesJSON from '../../data/trainNumberRules.json';
import { computed } from 'vue';
import { watch } from 'vue';
const apiStore = useApiStore();
const i18n = useI18n(); const i18n = useI18n();
const arePropositionsVisible = ref(false); const arePropositionsVisible = ref(false);
const chosenCategoryIndex = ref(0);
const numberPropositions = ref<string[]>([]);
const chosenCategoryRules = ref<any[]>([]);
const props = defineProps({ const props = defineProps({
chosenTrain: { chosenTrain: {
@@ -119,153 +64,7 @@ function copyStockToClipboard() {
function toggleNumberPropositions() { function toggleNumberPropositions() {
arePropositionsVisible.value = !arePropositionsVisible.value; arePropositionsVisible.value = !arePropositionsVisible.value;
if (arePropositionsVisible.value) generateNumberPropositions();
} }
function selectCategory(i: number) {
chosenCategoryIndex.value = i;
generateNumberPropositions();
}
function generateNumberPropositions() {
const categoryCode = chosenCategory.value?.slice(0, 2);
const trainNoStr = props.chosenTrain.trainNo.toString();
// Get category rules
const rules = categoryCode
? ((rulesJSON.categoriesRules as any)[categoryCode] as any[])
: undefined;
if (!categoryCode || !rules) {
numberPropositions.value.length = 0;
chosenCategoryRules.value.length = 0;
return;
}
const [thirdNumber, minRange, maxRange] = rules;
const propositionsArr: string[] = [];
for (let i = 0; i < 5; i++) {
let generatedNumStr = '';
generatedNumStr += trainNoStr.at(0) ?? Math.floor(Math.random() * 10);
generatedNumStr += trainNoStr.at(1) ?? Math.floor(Math.random() * 10);
// Third number
generatedNumStr += thirdNumber ?? '';
// Remaining numbers
const rangeNums = minRange?.length ?? 3;
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
).toString();
const leadingZeros = new Array(Math.abs(randRange.toString().length - rangeNums))
.fill('0')
.join('');
generatedNumStr += `${leadingZeros}${randRange}`;
const isNumberTaken =
apiStore.activeData?.trains?.some((t) => t.trainNo.toString() == generatedNumStr) ?? false;
if (!isNumberTaken) {
propositionsArr.push(generatedNumStr);
} else {
i--;
}
if (Number(randRange) > Number(maxRange)) break;
}
numberPropositions.value = propositionsArr;
chosenCategoryRules.value = rules;
}
const chosenCategory = computed(() => {
return availableCategories.value.at(chosenCategoryIndex.value);
});
const availableCategories = computed(() => {
const stockList = props.chosenTrain.stockList;
const headVehicle = stockList.at(0)?.split('-')[0] ?? '';
let availableCategories: string[] = [];
let categoryTraction = 'E';
let vehicleTypesSet = new Set<string>();
let wagonsNamesSet = new Set<string>();
let cargoNamesSet = new Set<string>();
for (const stockName of stockList) {
const [vehicleName, ...cargoList] = stockName.split(':');
const vehicleData = apiStore.vehiclesData?.vehicles.find((v) => v.name == vehicleName);
if (!vehicleData) continue;
vehicleTypesSet.add(vehicleData.type);
if (vehicleData.type.startsWith('wagon-')) wagonsNamesSet.add(vehicleData.name.split('_')[0]);
if (cargoList !== undefined) cargoList.forEach((c) => cargoNamesSet.add(c.split('_')[0]));
}
let vehicleTypesArr = [...vehicleTypesSet];
let wagonsNamesArr = [...wagonsNamesSet];
// Traction
if (vehicleTypesArr[0] == 'loco-electric') categoryTraction = 'E';
else if (vehicleTypesArr[0] == 'loco-diesel') categoryTraction = 'S';
else if (vehicleTypesArr[0] == 'unit-electric') categoryTraction = 'J';
else categoryTraction = 'M';
// EMU / DMU - M*, R*, P*
if (vehicleTypesArr.length == 1 && (categoryTraction == 'J' || categoryTraction == 'M')) {
availableCategories.push('MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Only locos (up to 3) - LT, LP, LS
else if (stockList.length <= 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
if (/^(EU|ET|201E|4E|SU|ST|M62|CTLR4C)/.test(headVehicle)) availableCategories.push('LT');
if (/^(EU|EP|SU|SP)/.test(headVehicle)) availableCategories.push('LP');
if (/^(SM)/.test(headVehicle)) availableCategories.push('LS');
}
// Only locos (more than 3) - TH
else if (stockList.length > 3 && vehicleTypesArr.every((v) => v.startsWith('loco-'))) {
availableCategories.push('TH');
}
// Loco(s) + passenger only wagons - M*, R*, E*, P*
else if (vehicleTypesArr.every((v) => v.startsWith('loco-') || v == 'wagon-passenger')) {
availableCategories.push('EI', 'EC', 'EN', 'MO', 'MP', 'MM', 'RO', 'RP', 'RA', 'RM', 'PW');
}
// Loco(s) + cargo only / mixed wagons - T*, Z*
else {
if (wagonsNamesArr.every((v) => /^(627Z|412Z)/.test(v)))
availableCategories.push('TC', 'TD', 'TS');
else if (stockList.slice(1).every((v) => /PKPE/.test(v))) {
availableCategories.push('ZU', 'ZN');
} else if (wagonsNamesArr.length < 3 || cargoNamesSet.size < 3) {
availableCategories.push('TM', 'TG', 'TS', 'TK');
} else {
availableCategories.push('TN', 'TR', 'TS', 'TK');
}
}
return availableCategories.map((c) => `${c}${categoryTraction}`);
});
watch(
computed(() => `${props.chosenTrain.trainNo}`),
() => {
chosenCategoryIndex.value = 0;
generateNumberPropositions();
}
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -279,49 +78,13 @@ watch(
.train-stock-actions { .train-stock-actions {
display: flex; display: flex;
gap: 0.5em;
}
.propositions-container {
margin-bottom: 1em;
padding: 0.5em;
background-color: #111;
}
.categories-select {
display: inline-flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
margin: 1em 0;
position: relative;
&::after {
content: '';
position: absolute;
bottom: calc(-0.5em);
left: 0;
width: 100%;
height: 2px;
background-color: #aaa;
}
}
.propositions-numbers {
margin-top: 1em;
}
.no-propositions {
margin-top: 1em;
color: #ccc;
} }
@include responsive.smallScreen { @include responsive.smallScreen {
.propositions-container { .train-stock-actions {
text-align: center;
}
.categories-select {
justify-content: center; justify-content: center;
} }
} }
@@ -18,7 +18,20 @@
</b> </b>
<span <span
v-if="apiStore.donatorsData.includes(entry.dispatcherName)" v-if="isCreator(entry.dispatcherName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<router-link
class="text--creator"
:to="`/journal/dispatchers?search-dispatcher=${entry.dispatcherName}`"
>
{{ entry.dispatcherName }}
</router-link>
</span>
<span
v-else-if="apiStore.donatorsData.includes(entry.dispatcherName)"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')" :data-tooltip-content="$t('donations.dispatcher-message')"
> >
@@ -122,6 +135,7 @@ import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import StationStatusBadge from '../../Global/StationStatusBadge.vue'; import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import FlagIcon from '../../Global/FlagIcon.vue'; import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -134,7 +148,7 @@ export default defineComponent({
emits: ['toggleShowExtraInfo'], emits: ['toggleShowExtraInfo'],
data() { data() {
return { regions, apiStore: useApiStore() }; return { regions, apiStore: useApiStore(), isCreator };
}, },
methods: { methods: {
@@ -64,6 +64,7 @@ function navigateToProfile() {
<style lang="scss" scoped> <style lang="scss" scoped>
@use '../../styles/dropdown'; @use '../../styles/dropdown';
@use '../../styles/dropdown-filters'; @use '../../styles/dropdown-filters';
@use '../../styles/responsive';
.dropdown_wrapper { .dropdown_wrapper {
left: auto; left: auto;
@@ -71,4 +72,10 @@ function navigateToProfile() {
max-width: 700px; max-width: 700px;
top: 3.5em; top: 3.5em;
} }
@include responsive.smallScreen {
.dropdown_wrapper {
top: 6.25em;
}
}
</style> </style>
@@ -59,7 +59,17 @@
</strong> </strong>
<router-link <router-link
v-if="apiStore.donatorsData.includes(timetable.driverName)" v-if="isCreator(timetable.driverName)"
class="text--creator"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
:to="`/journal/timetables?search-driver=${timetable.driverName}`"
>
<strong>{{ timetable.driverName }}</strong>
</router-link>
<router-link
v-else-if="apiStore.donatorsData.includes(timetable.driverName)"
class="text--donator" class="text--donator"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')" :data-tooltip-content="$t('donations.driver-message')"
@@ -115,6 +125,7 @@ import styleMixin from '../../../mixins/styleMixin';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import trainCategoryMixin from '../../../mixins/trainCategoryMixin'; import trainCategoryMixin from '../../../mixins/trainCategoryMixin';
import FlagIcon from '../../Global/FlagIcon.vue'; import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({ export default defineComponent({
components: { FlagIcon }, components: { FlagIcon },
@@ -122,7 +133,8 @@ export default defineComponent({
data() { data() {
return { return {
apiStore: useApiStore() apiStore: useApiStore(),
isCreator
}; };
}, },
@@ -5,7 +5,10 @@
<ProfilePlayerAvatar :playerTD2Info="playerTD2Info" /> <ProfilePlayerAvatar :playerTD2Info="playerTD2Info" />
<div> <div>
<h2 class="player-name-header" :class="{ 'text--donator': isPlayerDonator }"> <h2
class="player-name-header"
:class="{ 'text--donator': isPlayerDonator, 'text--creator': isPlayerCreator }"
>
<a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank"> <a :href="`https://td2.info.pl/profile/?u=${route.query.playerId}`" target="_blank">
<img <img
v-if="isPlayerDonator" v-if="isPlayerDonator"
@@ -232,6 +235,7 @@ import { useApiStore } from '../../store/apiStore';
import StationStatusBadge from '../Global/StationStatusBadge.vue'; import StationStatusBadge from '../Global/StationStatusBadge.vue';
import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue'; import ProfilePlayerAvatar from './ProfilePlayerAvatar.vue';
import { getRegionNameById } from '../../utils/regionUtils'; import { getRegionNameById } from '../../utils/regionUtils';
import { isCreator } from '../../utils/userUtils';
const { t } = useI18n(); const { t } = useI18n();
@@ -257,6 +261,8 @@ const isPlayerDonator = computed(() =>
props.playerName ? apiStore.donatorsData.includes(props.playerName) : false props.playerName ? apiStore.donatorsData.includes(props.playerName) : false
); );
const isPlayerCreator = computed(() => (props.playerName ? isCreator(props.playerName) : false));
const activeDispatches = computed(() => { const activeDispatches = computed(() => {
if (!props.playerName) return []; if (!props.playerName) return [];
if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return []; if (!apiStore.activeData || !apiStore.activeData.activeSceneries) return [];
+1 -7
View File
@@ -24,12 +24,6 @@ import { useRoute, useRouter } from 'vue-router';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const prevPath = ref('/');
onMounted(() => {
prevPath.value = (route.meta['prevPath'] as string) ?? '/';
});
defineProps({ defineProps({
station: { station: {
type: Object as PropType<Station> type: Object as PropType<Station>
@@ -46,7 +40,7 @@ defineProps({
}); });
function onReturnButtonClick() { function onReturnButtonClick() {
router.push(prevPath.value); router.push('/');
} }
</script> </script>
@@ -9,9 +9,16 @@
</span> </span>
<router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`"> <router-link class="dispatcher-name" :to="`/profile?playerId=${onlineScenery.dispatcherId}`">
<span
class="text--creator"
v-if="isCreator(onlineScenery.dispatcherName)"
:title="$t('donations.creator-message')"
>
{{ onlineScenery.dispatcherName }}
</span>
<span <span
class="text--donator" class="text--donator"
v-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)" v-else-if="apiStore.donatorsData.includes(onlineScenery.dispatcherName)"
:title="$t('donations.dispatcher-message')" :title="$t('donations.dispatcher-message')"
> >
{{ onlineScenery.dispatcherName }} {{ onlineScenery.dispatcherName }}
@@ -51,6 +58,7 @@ import StationStatusBadge from '../../Global/StationStatusBadge.vue';
import { ActiveScenery } from '../../../typings/common'; import { ActiveScenery } from '../../../typings/common';
import { useApiStore } from '../../../store/apiStore'; import { useApiStore } from '../../../store/apiStore';
import FlagIcon from '../../Global/FlagIcon.vue'; import FlagIcon from '../../Global/FlagIcon.vue';
import { isCreator } from '../../../utils/userUtils';
export default defineComponent({ export default defineComponent({
mixins: [styleMixin, dateMixin, routerMixin], mixins: [styleMixin, dateMixin, routerMixin],
@@ -58,7 +66,8 @@ export default defineComponent({
data() { data() {
return { return {
apiStore: useApiStore() apiStore: useApiStore(),
isCreator
}; };
}, },
+15 -6
View File
@@ -28,14 +28,15 @@
<Loading v-if="listState == Status.Data.Loading" /> <Loading v-if="listState == Status.Data.Loading" />
<div v-else-if="listState == Status.Data.Error">Ups, coś poszło nie tak...</div> <div v-else-if="listState == Status.Data.Error">Ups, coś poszło nie tak...</div>
<ul v-else> <ul v-else-if="bestScoreList.length > 0">
<li v-for="(value, i) in bestScoreList"> <li v-for="(value, i) in bestScoreList">
<div> <div>
{{ t('scenery.top-list.place', i + 1) }} - {{ t('ordinal', { count: i + 1 }) }} {{ t('scenery.top-list.place') }} -
<router-link :to="`/profile?playerId=${value.dispatcherId}`">{{ <router-link :to="`/profile?playerId=${value.dispatcherId}`">{{
value.dispatcherName value.dispatcherName
}}</router-link> }}</router-link>
</div> </div>
<div> <div>
<b class="text--primary" v-if="currentListMode == 'dutyCount'">{{ <b class="text--primary" v-if="currentListMode == 'dutyCount'">{{
t('scenery.top-list.duty-count', value.value) t('scenery.top-list.duty-count', value.value)
@@ -52,6 +53,11 @@
</div> </div>
</li> </li>
</ul> </ul>
<div v-else class="no-data">
<span v-if="currentListScope == 'name'">{{ t('scenery.top-list.no-data-general') }}</span>
<span v-else>{{ t('scenery.top-list.no-data-current-hash') }}</span>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -167,10 +173,6 @@ async function fetchTopDispatchersList() {
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
font-weight: bold; font-weight: bold;
button {
font-weight: bold;
}
} }
.rating-list-wrapper { .rating-list-wrapper {
@@ -201,4 +203,11 @@ async function fetchTopDispatchersList() {
font-weight: bold; font-weight: bold;
} }
} }
.no-data {
padding: 1em 0.5em;
font-size: 1.1em;
background-color: #333;
color: #ccc;
}
</style> </style>
+25 -5
View File
@@ -1,5 +1,5 @@
<template> <template>
<section class="station_table"> <section class="station_table" @scroll="onScroll" ref="tableRef">
<Loading <Loading
v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0" v-if="apiStore.dataStatuses.connection == Status.Loading && filteredStationList.length == 0"
/> />
@@ -131,7 +131,16 @@
<td class="station-dispatcher-name"> <td class="station-dispatcher-name">
<span v-if="station.onlineInfo?.dispatcherName"> <span v-if="station.onlineInfo?.dispatcherName">
<b <b
v-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)" v-if="isCreator(station.onlineInfo.dispatcherName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<img src="/images/icon-creator.png" alt="creator icon" />
<span class="text--creator">&nbsp;{{ station.onlineInfo.dispatcherName }}</span>
</b>
<b
v-else-if="apiStore.donatorsData.includes(station.onlineInfo.dispatcherName)"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.dispatcher-message')" :data-tooltip-content="$t('donations.dispatcher-message')"
> >
@@ -353,6 +362,7 @@ import { ActiveSorter, HeadIdsType, headIconsIds, headIds } from './typings';
import { filterStations, sortStations } from './utils'; import { filterStations, sortStations } from './utils';
import { getLanguageNameById } from '../../utils/languageUtils'; import { getLanguageNameById } from '../../utils/languageUtils';
import FlagIcon from '../Global/FlagIcon.vue'; import FlagIcon from '../Global/FlagIcon.vue';
import { isCreator } from '../../utils/userUtils';
export default defineComponent({ export default defineComponent({
emits: ['toggleDonationCard'], emits: ['toggleDonationCard'],
@@ -363,7 +373,9 @@ export default defineComponent({
data: () => ({ data: () => ({
headIconsIds, headIconsIds,
headIds, headIds,
getChangedFilters scrollTop: 0,
getChangedFilters,
isCreator
}), }),
setup() { setup() {
@@ -391,6 +403,10 @@ export default defineComponent({
}; };
}, },
activated() {
(this.$refs['tableRef'] as HTMLElement).scrollTop = this.scrollTop;
},
methods: { methods: {
getSceneryRoute(station: Station) { getSceneryRoute(station: Station) {
this.$router.push({ this.$router.push({
@@ -431,6 +447,10 @@ export default defineComponent({
})); }));
return JSON.stringify(usersTrains); return JSON.stringify(usersTrains);
},
onScroll(e: Event) {
this.scrollTop = (e.target as HTMLElement).scrollTop;
} }
} }
}); });
@@ -613,8 +633,8 @@ tbody tr {
.station-dispatcher-name { .station-dispatcher-name {
img { img {
max-width: 1.35em; max-height: 1.3em;
vertical-align: text-bottom; vertical-align: text-top;
} }
} }
+36
View File
@@ -0,0 +1,36 @@
<template>
<div class="tooltip-content">
<img src="/images/icon-creator.png" alt="creator icon" />
<b class="text--creator">&nbsp;{{ tooltipStore.content }}</b>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore';
export default defineComponent({
data() {
return {
tooltipStore: useTooltipStore()
};
}
});
</script>
<style lang="scss" scoped>
.tooltip-content {
padding: 0.5em;
border-radius: 0.25em;
width: 100%;
background-color: #333;
box-shadow: 0 0 10px 2px #aaa;
}
img {
vertical-align: text-bottom;
height: 1.25em;
}
</style>
+3 -8
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="tooltip-content"> <div class="tooltip-content">
<img src="/images/icon-diamond.svg" alt="donator diamond icon" /> <img src="/images/icon-diamond.svg" alt="donator diamond icon" />
<span>{{ tooltipStore.content }}</span> <b class="text--donator">&nbsp;{{ tooltipStore.content }}</b>
</div> </div>
</template> </template>
@@ -20,11 +20,6 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
.tooltip-content { .tooltip-content {
display: flex;
align-items: center;
gap: 0.5em;
flex-wrap: wrap;
padding: 0.5em; padding: 0.5em;
border-radius: 0.25em; border-radius: 0.25em;
@@ -35,7 +30,7 @@ export default defineComponent({
} }
img { img {
vertical-align: middle; vertical-align: text-bottom;
height: 1em; height: 1.25em;
} }
</style> </style>
+4 -2
View File
@@ -8,12 +8,13 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useTooltipStore } from '../../store/tooltipStore'; import { useTooltipStore } from '../../store/tooltipStore';
import DonatorTooltip from './DonatorTooltip.vue'; import DonatorTooltip from './DonatorTooltip.vue';
import CreatorTooltip from './CreatorTooltip.vue';
import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue'; import VehiclePreviewTooltip from './VehiclePreviewTooltip.vue';
import BaseTooltip from './BaseTooltip.vue'; import BaseTooltip from './BaseTooltip.vue';
import SpawnsTooltip from './SpawnsTooltip.vue'; import SpawnsTooltip from './SpawnsTooltip.vue';
import UsersTooltip from './UsersTooltip.vue'; import UsersTooltip from './UsersTooltip.vue';
import HtmlTooltip from './HtmlTooltip.vue'; import HtmlTooltip from './HtmlTooltip.vue';
import TrainInfoTooltip from "./TrainInfoTooltip.vue"; import TrainInfoTooltip from './TrainInfoTooltip.vue';
const BOX_PADDING_PX = 20; const BOX_PADDING_PX = 20;
@@ -25,7 +26,8 @@ export default defineComponent({
SpawnsTooltip, SpawnsTooltip,
UsersTooltip, UsersTooltip,
HtmlTooltip, HtmlTooltip,
TrainInfoTooltip TrainInfoTooltip,
CreatorTooltip
}, },
data() { data() {
+15 -8
View File
@@ -56,12 +56,21 @@
</b> </b>
<b <b
v-if="apiStore.donatorsData.includes(train.driverName)" v-if="isCreator(train.driverName)"
data-tooltip-type="CreatorTooltip"
:data-tooltip-content="$t('donations.creator-message')"
>
<img src="/images/icon-creator.png" alt="creator icon" />
<span class="text--creator">&nbsp;{{ train.driverName }}</span>
</b>
<b
v-else-if="apiStore.donatorsData.includes(train.driverName)"
data-tooltip-type="DonatorTooltip" data-tooltip-type="DonatorTooltip"
:data-tooltip-content="$t('donations.driver-message')" :data-tooltip-content="$t('donations.driver-message')"
> >
<span class="text--donator">{{ train.driverName }}&nbsp;</span>
<img src="/images/icon-diamond.svg" alt="donator diamond icon" /> <img src="/images/icon-diamond.svg" alt="donator diamond icon" />
<span class="text--donator">&nbsp;{{ train.driverName }}</span>
</b> </b>
<span v-else>{{ train.driverName }}</span> <span v-else>{{ train.driverName }}</span>
@@ -204,6 +213,7 @@ import trainCategoryMixin from '../../mixins/trainCategoryMixin';
import ProgressBar from '../Global/ProgressBar.vue'; import ProgressBar from '../Global/ProgressBar.vue';
import StockList from '../Global/StockList.vue'; import StockList from '../Global/StockList.vue';
import FlagIcon from '../Global/FlagIcon.vue'; import FlagIcon from '../Global/FlagIcon.vue';
import { isCreator } from '../../utils/userUtils';
export default defineComponent({ export default defineComponent({
mixins: [trainInfoMixin, styleMixin, trainCategoryMixin], mixins: [trainInfoMixin, styleMixin, trainCategoryMixin],
@@ -222,7 +232,8 @@ export default defineComponent({
data() { data() {
return { return {
store: useMainStore(), store: useMainStore(),
apiStore: useApiStore() apiStore: useApiStore(),
isCreator
}; };
}, },
@@ -278,8 +289,6 @@ export default defineComponent({
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-size: 1em; font-size: 1em;
gap: 0.25em;
background-color: #1a1a1a; background-color: #1a1a1a;
gap: 0.5em; gap: 0.5em;
} }
@@ -358,8 +367,6 @@ export default defineComponent({
.status-badges { .status-badges {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
margin-left: 0.25em;
gap: 0.25em; gap: 0.25em;
img { img {
@@ -372,7 +379,7 @@ export default defineComponent({
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5em; gap: 0.5em;
padding: 0.5em 0; padding: 0.25em 0;
} }
.progress-distance { .progress-distance {
+33 -22
View File
@@ -3,25 +3,6 @@ import plLang from './locales/pl.json';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
function customRule(choice: number, choicesLength: number) {
if (choice === 0) {
return 0;
}
const teen = choice > 10 && choice < 20;
const endsWithOne = choice % 10 === 1;
if (!teen && endsWithOne) {
return 1;
}
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
return 2;
}
return choicesLength < 4 ? 2 : 3;
}
const i18n = createI18n({ const i18n = createI18n({
locale: 'pl', locale: 'pl',
legacy: false, legacy: false,
@@ -29,12 +10,42 @@ const i18n = createI18n({
fallbackLocale: 'pl', fallbackLocale: 'pl',
pluralizationRules: { pluralizationRules: {
pl: customRule en: {
ordinal: (ctx: { named: (arg0: string) => number }) => {
const number = ctx.named('count');
const suffixes: Record<number, string> = {
1: 'st',
2: 'nd',
3: 'rd'
};
const suffix = suffixes[number % 10] || 'th';
return `${number}${suffix}`;
}
}
}, },
messages: { messages: {
en: enLang, en: {
pl: plLang ...enLang,
ordinal: (ctx: { named: (arg0: string) => number }) => {
const number = ctx.named('count');
const suffixes: Record<number, string> = {
1: 'st',
2: 'nd',
3: 'rd'
};
const suffix = suffixes[number % 10] || 'th';
return `${number}${suffix}`;
}
},
pl: {
...plLang,
ordinal: '{count}.'
}
}, },
enableLegacy: false enableLegacy: false
}); });
+21 -7
View File
@@ -42,7 +42,8 @@
"action-paypal": "DONATE WITH PAYPAL", "action-paypal": "DONATE WITH PAYPAL",
"action-buycoffee": "BUY ME A COFFEE!", "action-buycoffee": "BUY ME A COFFEE!",
"dispatcher-message": "Dispatcher supporting the Stacjownik project!", "dispatcher-message": "Dispatcher supporting the Stacjownik project!",
"driver-message": "Driver supporting the Stacjownik project!" "driver-message": "Driver supporting the Stacjownik project!",
"creator-message": "Creator of the Stacjownik project"
}, },
"warnings": { "warnings": {
"TWR": "Train with high risk cargo", "TWR": "Train with high risk cargo",
@@ -438,15 +439,25 @@
"driver-not-found-others": "Player {driver} is online as:", "driver-not-found-others": "Player {driver} is online as:",
"driver-not-found-return": "RETURN TO THE MAIN SITE", "driver-not-found-return": "RETURN TO THE MAIN SITE",
"stock-copy": "COPY THE STOCK", "stock-copy": "COPY THE STOCK",
"number-propositions": "PROPOSE NUMBER", "number-propositions": "NUMBER & WARNINGS SUGGESTIONS",
"stock-clipboard-success": "Successfully copied the railway stock in a text form to your clipboard!", "stock-clipboard-success": "Successfully copied the railway stock in a text form to your clipboard!",
"stock-clipboard-failure": "Oops! Something happened and the railway stock couldn't be copied to your clipboard! :/", "stock-clipboard-failure": "Oops! Something happened and the railway stock couldn't be copied to your clipboard! :/",
"number-propositions-header": "Generate number examples for selected category:", "number-propositions-header": "Generate number examples for a train category:",
"number-propositions-third-number": "Third digit:", "number-propositions-third-number": "Third digit:",
"number-propositions-last-nums": "{count} last digits from the range of:", "number-propositions-last-nums": "{count} last digits from the range of:",
"number-propositions-title": "Propositions:", "number-propositions-title": "Propositions:",
"number-propositions-empty": "No propositions available for the chosen category! :/" "number-propositions-empty": "No propositions available for the chosen category! :/"
}, },
"cargo-warnings": {
"title": "Additional cargo warnings:",
"pn-innofreight": "PN: Innofreight C45: exceeded gauge",
"twr-un1965": "TWR: UN1965 (LPG)",
"tn-un1965": "TN: unclean tanks after UN1965",
"tn-un1202": "TN: UN1202 (diesel fuel)",
"tn-un1202-empty": "TN: unclean tanks after UN1202",
"pn-military": "PN: military transport",
"pn-edk80": "PN: EDK80 railway crane"
},
"train-stats": { "train-stats": {
"stats-button": "STATISTICS", "stats-button": "STATISTICS",
"title": "ONLINE TRAINS STATS", "title": "ONLINE TRAINS STATS",
@@ -559,7 +570,7 @@
"no-users": "NO ACTIVE PLAYERS", "no-users": "NO ACTIVE PLAYERS",
"no-spawns": "NO OPEN SPAWNS", "no-spawns": "NO OPEN SPAWNS",
"no-scenery": "Oops! This scenery doesn't exist!", "no-scenery": "Oops! This scenery doesn't exist!",
"return-btn": "BACK TO THE MAIN SITE", "return-btn": "BACK TO SCENERIES",
"history-btn": "View the dispatcher history", "history-btn": "View the dispatcher history",
"info-btn": "Return to the scenery view", "info-btn": "Return to the scenery view",
"authors-title": "Scenery author | Scenery authors", "authors-title": "Scenery author | Scenery authors",
@@ -587,7 +598,7 @@
"dispatcher-rate": "Rate:", "dispatcher-rate": "Rate:",
"dispatcher-status-changes": "Status changes:", "dispatcher-status-changes": "Status changes:",
"req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required", "req-level": "all dispatcher levels | dispatcher level {lvl} required | dispatcher level {lvl} required",
"history-list-empty": "No recorded scenery history!", "history-list-empty": "No saved scenery history!",
"forum-topic": "Scenery's forum topic", "forum-topic": "Scenery's forum topic",
"gnr-link": "Train orders generator", "gnr-link": "Train orders generator",
"pragotron-link": "Timetable pallet board", "pragotron-link": "Timetable pallet board",
@@ -603,10 +614,13 @@
"scope-name": "GENERAL", "scope-name": "GENERAL",
"scope-hash": "CURRENT HASH", "scope-hash": "CURRENT HASH",
"place": "{n}. place", "place": "place",
"dispatcher-rating": "Rating: {n}", "dispatcher-rating": "Rating: {n}",
"duty-count": "No duties | 1 duty | Duties: {n}", "duty-count": "No duties | 1 duty | Duties: {n}",
"duration": "Duration:" "duration": "Duration:",
"no-data-general": "No best scores for this scenery on the PL1 server!",
"no-data-current-hash": "No best scores for the current scenery hash on the PL1 server!"
} }
}, },
"availability": { "availability": {
+20 -6
View File
@@ -42,7 +42,8 @@
"action-paypal": "PRZELEJ PAYPALEM", "action-paypal": "PRZELEJ PAYPALEM",
"action-buycoffee": "POSTAW KAWĘ!", "action-buycoffee": "POSTAW KAWĘ!",
"dispatcher-message": "Dyżurny wspierający projekt Stacjownika!", "dispatcher-message": "Dyżurny wspierający projekt Stacjownika!",
"driver-message": "Maszynista wspierający projekt Stacjownika!" "driver-message": "Maszynista wspierający projekt Stacjownika!",
"creator-message": "Twórca projektu Stacjownik"
}, },
"warnings": { "warnings": {
"TWR": "Pociąg z towarami niebezpiecznymi wysokiego ryzyka", "TWR": "Pociąg z towarami niebezpiecznymi wysokiego ryzyka",
@@ -425,15 +426,25 @@
"driver-not-found-others": "Gracz {driver} jest online jako:", "driver-not-found-others": "Gracz {driver} jest online jako:",
"driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ", "driver-not-found-return": "WRÓĆ NA STRONĘ GŁÓWNĄ",
"stock-copy": "SKOPIUJ SKŁAD", "stock-copy": "SKOPIUJ SKŁAD",
"number-propositions": "ZAPROPONUJ NUMER", "number-propositions": "PROPOZYCJE NUMERÓW I UWAG",
"stock-clipboard-success": "Pomyślnie skopiowano skład w postaci tekstowej do schowka!", "stock-clipboard-success": "Pomyślnie skopiowano skład w postaci tekstowej do schowka!",
"stock-clipboard-failure": "Ups! Nie udało się skopiować składu do schowka! :/", "stock-clipboard-failure": "Ups! Nie udało się skopiować składu do schowka! :/",
"number-propositions-header": "Wygeneruj propozycje numerów dla kategorii pociągu:", "number-propositions-header": "Wygeneruj propozycje numerów dla pociągu kategorii:",
"number-propositions-third-number": "Trzecia cyfra:", "number-propositions-third-number": "Trzecia cyfra:",
"number-propositions-last-nums": "{count} ostatnie cyfry z przedziału:", "number-propositions-last-nums": "{count} ostatnie cyfry z przedziału:",
"number-propositions-title": "Propozycje:", "number-propositions-title": "Propozycje:",
"number-propositions-empty": "Brak propozycji dla wybranej kategorii! :/" "number-propositions-empty": "Brak propozycji dla wybranej kategorii! :/"
}, },
"cargo-warnings": {
"title": "Dodatkowe uwagi przewozowe:",
"pn-innofreight": "PN: Innofreight C45: przekroczona skrajnia",
"twr-un1965": "TWR: UN1965 (LPG)",
"tn-un1965": "TN: brudne cysterny po UN1965",
"tn-un1202": "TN: UN1202 (olej napędowy)",
"tn-un1202-empty": "TN: brudne cysterny po UN1202",
"pn-military": "PN: transport wojskowy",
"pn-edk80": "PN: żuraw kolejowy EDK80"
},
"train-stats": { "train-stats": {
"stats-button": "STATYSTYKI", "stats-button": "STATYSTYKI",
"title": "STATYSTYKI AKTYWNYCH POCIĄGÓW", "title": "STATYSTYKI AKTYWNYCH POCIĄGÓW",
@@ -545,7 +556,7 @@
"no-users": "BRAK AKTYWNYCH GRACZY", "no-users": "BRAK AKTYWNYCH GRACZY",
"no-spawns": "BRAK OTWARTYCH SPAWNÓW", "no-spawns": "BRAK OTWARTYCH SPAWNÓW",
"no-scenery": "Ups! Ta sceneria nie istnieje!", "no-scenery": "Ups! Ta sceneria nie istnieje!",
"return-btn": "POWRÓT DO STRONY GŁÓWNEJ", "return-btn": "POWRÓT DO SCENERII",
"history-btn": "Przejdź do widoku historii dyżurnych ruchu", "history-btn": "Przejdź do widoku historii dyżurnych ruchu",
"info-btn": "Wróć do widoku scenerii", "info-btn": "Wróć do widoku scenerii",
"authors-title": "Autor scenerii | Autorzy scenerii", "authors-title": "Autor scenerii | Autorzy scenerii",
@@ -589,10 +600,13 @@
"scope-name": "OGÓLNIE", "scope-name": "OGÓLNIE",
"scope-hash": "OBECNY HASH", "scope-hash": "OBECNY HASH",
"place": "{n}. miejsce", "place": "miejsce",
"dispatcher-rating": "Ocena: {n}", "dispatcher-rating": "Ocena: {n}",
"duty-count": "Brak dyżurów | 1 dyżur | Dyżury: {n}", "duty-count": "Brak dyżurów | 1 dyżur | Dyżury: {n}",
"duration": "Czas:" "duration": "Czas:",
"no-data-general": "Brak zapisanych rekordów scenerii na serwerze PL1!",
"no-data-current-hash": "Brak zapisanych rekordów scenerii z obecnym hashem na serwerze PL1!"
} }
}, },
"availability": { "availability": {
+2 -1
View File
@@ -9,7 +9,8 @@ export const tooltipKeys = [
'SpawnsTooltip', 'SpawnsTooltip',
'UsersTooltip', 'UsersTooltip',
'HtmlTooltip', 'HtmlTooltip',
'TrainInfoTooltip' 'TrainInfoTooltip',
'CreatorTooltip'
] as const; ] as const;
export type TooltipType = (typeof tooltipKeys)[number]; export type TooltipType = (typeof tooltipKeys)[number];
+1 -2
View File
@@ -85,7 +85,6 @@
padding: 0.1em 0.3em; padding: 0.1em 0.3em;
border-radius: 0.2em; border-radius: 0.2em;
font-weight: bold; font-weight: bold;
user-select: none;
&.twr { &.twr {
background-color: var(--clr-twr); background-color: var(--clr-twr);
@@ -151,4 +150,4 @@
&.active { &.active {
background-color: lightblue; background-color: lightblue;
} }
} }
+13
View File
@@ -217,6 +217,19 @@ ul {
text-shadow: #f050ff 0 0 10px; text-shadow: #f050ff 0 0 10px;
} }
&--creator {
color: var(--clr-primary);
color: transparent;
background: var(--clr-primary);
background: linear-gradient(90deg, gold 30%, #ffffff 70%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: gold 0 0 10px;
}
&--discord { &--discord {
color: var(--clr-donator); color: var(--clr-donator);
color: transparent; color: transparent;
+3
View File
@@ -0,0 +1,3 @@
export function isCreator(name: string) {
return /(spythere|kowbojyt)/.test(name.toLowerCase());
}