chore: improved stock actions layout; added stock bookmarking

This commit is contained in:
2025-03-08 02:14:43 +01:00
parent 90e257127c
commit 7596b7ec2c
7 changed files with 283 additions and 113 deletions
+22 -2
View File
@@ -18,10 +18,30 @@ export default defineComponent({
return { store: useStore() };
},
async created() {
this.store.handleRouting();
created() {
this.loadStockDataFromStorage();
this.store.setupAPIData();
},
methods: {
loadStockDataFromStorage() {
const savedData = localStorage.getItem('savedStockData');
if (!savedData) {
localStorage.setItem('savedStockData', JSON.stringify({}));
return;
}
try {
this.store.storageStockData = JSON.parse(savedData);
} catch (error) {
console.error(
'Wystąpił błąd podczas przetwarzania danych o składach z localStorage!',
error
);
}
},
},
});
</script>
+60 -13
View File
@@ -2,15 +2,21 @@
<section class="stock-list-tab">
<div class="tab_content">
<div class="stock_actions">
<button class="btn btn--image" @click="clickFileInput">
<button
class="btn btn--image"
@click="clickFileInput"
:data-button-tooltip="$t('stocklist.action-upload-file')"
>
<input type="file" @change="uploadStockFromFile" ref="conFile" accept=".con,.txt" />
<ArrowUpTrayIcon />
{{ $t('stocklist.action-upload-file') }}
<FolderPlusIcon />
</button>
<button class="btn btn--image" @click="uploadStockFromClipboard">
<ArrowUpTrayIcon />
{{ $t('stocklist.action-upload-clipboard') }}
<button
class="btn btn--image"
@click="uploadStockFromClipboard"
:data-button-tooltip="$t('stocklist.action-upload-clipboard')"
>
<ClipboardDocumentCheckIcon />
</button>
<button
@@ -18,9 +24,9 @@
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="downloadStock"
:data-button-tooltip="$t('stocklist.action-download')"
>
<ArrowDownTrayIcon />
{{ $t('stocklist.action-download') }}
</button>
<button
@@ -28,9 +34,19 @@
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="copyToClipboard"
:data-button-tooltip="$t('stocklist.action-copy')"
>
<ClipboardDocumentIcon />
{{ $t('stocklist.action-copy') }}
<DocumentDuplicateIcon />
</button>
<button
class="btn btn--image"
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="saveStockDataToStorage"
:data-button-tooltip="$t('stocklist.action-bookmark')"
>
<BookmarkIcon />
</button>
<button
@@ -38,9 +54,9 @@
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="resetStock"
:data-button-tooltip="$t('stocklist.action-reset')"
>
<ArrowUturnLeftIcon />
{{ $t('stocklist.action-reset') }}
</button>
<button
@@ -48,9 +64,9 @@
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="shuffleCars"
:data-button-tooltip="$t('stocklist.action-shuffle')"
>
<ArrowPathIcon />
{{ $t('stocklist.action-shuffle') }}
</button>
</div>
@@ -244,10 +260,14 @@ import {
ArrowPathIcon,
ArrowUpTrayIcon,
ArrowUturnLeftIcon,
BookmarkIcon,
ChevronDownIcon,
ChevronUpIcon,
ClipboardDocumentIcon,
FolderPlusIcon,
ClipboardDocumentCheckIcon,
TrashIcon,
DocumentDuplicateIcon,
} from '@heroicons/vue/20/solid';
export default defineComponent({
@@ -255,13 +275,17 @@ export default defineComponent({
components: {
StockThumbnails,
Checkbox,
FolderPlusIcon,
ArrowDownTrayIcon,
ArrowPathIcon,
ArrowUpTrayIcon,
ArrowUturnLeftIcon,
BookmarkIcon,
ChevronDownIcon,
ChevronUpIcon,
ClipboardDocumentIcon,
ClipboardDocumentCheckIcon,
DocumentDuplicateIcon,
TrashIcon,
},
@@ -289,7 +313,6 @@ export default defineComponent({
return this.store.realCompositionList.find((rc) => rc.stockString == currentStockString);
},
stockIsEmpty() {
return this.store.stockList.length == 0;
},
@@ -495,6 +518,30 @@ export default defineComponent({
inputEl.value = '';
},
saveStockDataToStorage() {
if (this.store.stockList.length == 0) return;
const defaultName = `${this.store.stockList[0].vehicleRef.type} ${(this.store.totalWeight / 1000).toFixed(1)}t; ${this.store.totalLength}m; vmax ${this.store.maxStockSpeed}`;
const entryName = prompt(this.$t('stocklist.prompt-bookmark'), defaultName);
if (!entryName) return;
if (entryName in this.store.storageStockData) {
const overwriteData = confirm(this.$t('stocklist.prompt-bookmark-overwrite'));
if (!overwriteData) return;
}
this.store.storageStockData[entryName] = this.store.stockString;
try {
localStorage.setItem('savedStockData', JSON.stringify(this.store.storageStockData));
this.store.chosenStorageStockName = entryName;
} catch (error) {
console.error('Wystąpił błąd podczas zapisywania składu do localStorage!', error);
}
},
async uploadStockFromClipboard() {
try {
const content = await navigator.clipboard.readText();
@@ -588,7 +635,7 @@ export default defineComponent({
display: grid;
gap: 0.5em;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
button {
width: 100%;
+151 -75
View File
@@ -6,91 +6,106 @@
</div>
<div class="tab_content">
<div class="tab_actions">
<button
class="btn btn--image"
@click="saveStockDataToStorage"
:disabled="store.stockList.length == 0"
:data-disabled="store.stockList.length == 0"
>
<FolderArrowDownIcon />
ZAPISZ OBECNY SKŁAD
</button>
<button
class="btn btn--image"
@click="removeStockIndexFromStorage"
:disabled="currentStockIndex == -1"
:data-disabled="currentStockIndex == -1"
>
<TrashIcon />
<span>USUŃ ZAPISANY SKŁAD</span>
</button>
</div>
<div class="storage-list-wrapper">
<transition-group name="storage-list-anim" tag="ul" class="storage-list">
<li
v-for="(stockString, stockName) in store.storageStockData"
:key="stockName"
:data-current="store.chosenStorageStockName == stockName"
>
<div class="storage-item-top">
<button class="btn btn--text btn-name" @click="chooseStorageStock(stockName)">
{{ stockName }}
</button>
<ul class="storage-stock-list">
<li v-for="(stockList, i) in storageStockData" :key="i">
</li>
</ul>
<div class="storage-item-top-actions">
<button class="btn btn--text" @click="toggleStorageEntryExpand(stockName)">
<ChevronDownIcon
v-if="!expandedEntries.includes(stockName)"
style="width: 25px"
/>
<ChevronUpIcon v-else style="width: 25px" />
</button>
<button class="btn btn--text" @click="removeStockIndexFromStorage(stockName)">
<TrashIcon style="width: 25px" />
</button>
</div>
</div>
<div class="storage-item-expandable" v-if="expandedEntries.includes(stockName)">
{{
stockString
.split(';')
.map((s) => s.split(/:|,/)[0])
.join(' + ')
}}
</div>
</li>
</transition-group>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { onActivated, Reactive, reactive, ref } from 'vue';
import { IStock } from '../../types/common.types';
import { FolderArrowDownIcon, TrashIcon } from '@heroicons/vue/20/solid';
<script lang="ts">
import { defineComponent } from 'vue';
import {
ChevronDownIcon,
ChevronUpIcon,
FolderArrowDownIcon,
TrashIcon,
} from '@heroicons/vue/20/solid';
import { useStore } from '../../store';
import stockMixin from '../../mixins/stockMixin';
const store = useStore();
export default defineComponent({
components: {
ChevronDownIcon,
ChevronUpIcon,
FolderArrowDownIcon,
TrashIcon,
},
let storageStockData: Reactive<IStock[][]> = reactive([]);
const currentStockIndex = ref(-1);
mixins: [stockMixin],
onActivated(() => {
// loadStockDataFromStorage();
data: () => ({
store: useStore(),
expandedEntries: [] as string[],
}),
methods: {
removeStockIndexFromStorage(stockName: string) {
delete this.store.storageStockData[stockName];
this.store.chosenStorageStockName = '';
try {
localStorage.setItem('savedStockData', JSON.stringify(this.store.storageStockData));
} catch (error) {
console.error('Wystąpił błąd podczas usuwania składu z localStorage!', error);
}
},
chooseStorageStock(stockName: string) {
try {
this.loadStockFromString(this.store.storageStockData[stockName]);
this.store.chosenStorageStockName = stockName;
} catch (error) {
console.log(error);
}
},
toggleStorageEntryExpand(stockName: string) {
const stockIndex = this.expandedEntries.indexOf(stockName);
if (stockIndex == -1) this.expandedEntries.push(stockName);
else this.expandedEntries.splice(stockIndex, 1);
},
},
});
function loadStockDataFromStorage() {
const savedData = localStorage.getItem('savedStockData');
if (!savedData) {
localStorage.setItem('savedStockData', JSON.stringify([]));
return;
}
try {
storageStockData = JSON.parse(savedData);
} catch (error) {
console.error('Wystąpił błąd podczas przetwarzania danych o składach z localStorage!', error);
}
}
function saveStockDataToStorage() {
if (store.stockList.length == 0) return;
storageStockData.push(store.stockList);
try {
localStorage.setItem('savedStockData', JSON.stringify(storageStockData));
currentStockIndex.value = storageStockData.length;
} catch (error) {
console.error('Wystąpił błąd podczas zapisywania składu do localStorage!', error);
storageStockData.pop();
}
}
function removeStockIndexFromStorage() {
if (currentStockIndex.value == -1) return;
storageStockData.splice(currentStockIndex.value, 1);
try {
localStorage.setItem('savedStockData', JSON.stringify(storageStockData));
currentStockIndex.value = currentStockIndex.value - 1;
} catch (error) {
console.error('Wystąpił błąd podczas usuwania składu z localStorage!', error);
}
}
</script>
<style lang="scss" scoped>
@@ -99,4 +114,65 @@ function removeStockIndexFromStorage() {
.tab_actions {
grid-template-columns: repeat(2, 1fr);
}
.storage-list-wrapper {
position: relative;
}
ul.storage-list {
display: flex;
flex-direction: column;
gap: 0.5em;
margin-top: 0.5em;
}
ul.storage-list > li {
padding: 0.5em;
background-color: global.$secondaryColor;
&[data-current='true'] {
background-color: #3b3b3b;
}
}
.storage-item-top {
display: flex;
align-items: center;
}
.storage-item-top button.btn-name {
font-size: 1.2em;
width: 100%;
text-align: left;
}
.storage-item-top-actions {
display: flex;
gap: 0.5em;
}
.storage-item-expandable {
margin-top: 0.5em;
}
.storage-list-anim {
&-move,
&-enter-active,
&-leave-active {
transition: all 120ms ease-in-out;
}
&-enter-from {
opacity: 0;
transform: translateY(-25px);
}
&-leave-to {
opacity: 0;
}
&-leave-active {
position: absolute;
}
}
</style>
+3
View File
@@ -52,6 +52,8 @@
"alert-copied": "The rolling stock has been copied to your clipboard!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Name a file and download it to the Presets folder (Documents/TTSK/TrainDriver2):",
"prompt-bookmark": "Enter the name of this composition:",
"prompt-bookmark-overwrite": "A composition with this name is already bookmarked! Do you want to overwrite it?",
"vehicle-no": "VEHICLE NO.",
"no-vehicle-chosen": "NO VEHICLE CHOSEN",
"action-move-up": "MOVE UP",
@@ -63,6 +65,7 @@
"action-copy": "COPY TO CLIPBOARD",
"action-reset": "RESET",
"action-shuffle": "SHUFFLE",
"action-bookmark": "BOOKMARK",
"mass": "Mass",
"mass-accepted": "accepted",
"length": "Length",
+5 -2
View File
@@ -53,17 +53,20 @@
"alert-copied": "Skład został skopiowany do twojego schowka!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):",
"prompt-bookmark": "Nazwij skład do zapisania:",
"prompt-bookmark-overwrite": "Skład o tej nazwie jest już zapisany. Czy chcesz go podmienić?",
"vehicle-no": "POJAZD NR",
"no-vehicle-chosen": "NIE WYBRANO POJAZDU",
"action-move-up": "PRZENIEŚ WYŻEJ",
"action-move-down": "PRZENIEŚ NIŻEJ",
"action-remove": "USUŃ",
"action-upload-file": "WCZYTAJ (PLIK)",
"action-upload-clipboard": "WCZYTAJ (SCHOWEK)",
"action-upload-file": "WCZYTAJ Z PLIKU",
"action-upload-clipboard": "WCZYTAJ ZE SCHOWKA",
"action-download": "POBIERZ DO PLIKU",
"action-copy": "SKOPIUJ DO SCHOWKA",
"action-reset": "ZRESETUJ",
"action-shuffle": "PRZETASUJ",
"action-bookmark": "ZAPISZ",
"mass": "Masa",
"mass-accepted": "dopuszczalna",
"length": "Długość",
+3 -6
View File
@@ -61,6 +61,9 @@ export const useStore = defineStore({
lastFocusedElement: null as HTMLElement | null,
storageStockData: {} as Record<string, string>,
chosenStorageStockName: '',
compatibleSimulatorVersion: '2024.3.1',
}),
@@ -137,11 +140,5 @@ export const useStore = defineStore({
async setupAPIData() {
await this.fetchVehiclesAPI();
},
handleRouting() {
if (window.location.search.includes('trainId=')) {
const trainId = window.location.search;
}
},
},
});
+39 -15
View File
@@ -122,21 +122,43 @@ button {
}
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translateX(10px);
content: attr(data-tooltip);
color: white;
background: black;
padding: 0.5em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: pointer;
&:hover::after,
&:focus-visible::after {
position: absolute;
transform: translateX(10px);
content: attr(data-tooltip);
color: white;
background: black;
padding: 0.5em;
max-width: 300px;
z-index: 100;
}
}
[data-button-tooltip] {
position: relative;
&:hover::after,
&:focus-visible::after {
position: absolute;
width: 100%;
max-width: 300px;
top: 100%;
transform: translateY(5px);
border-radius: inherit;
content: attr(data-button-tooltip);
color: white;
background: #111;
padding: 0.5em;
z-index: 100;
}
}
.btn,
@@ -190,7 +212,8 @@ button {
align-items: center;
gap: 0.5em;
img, svg {
img,
svg {
width: 20px;
height: 20px;
}
@@ -200,7 +223,8 @@ button {
font-weight: bold;
transition: all 250ms;
background: none;
padding: 0;
padding: 0.25em;
border-radius: 0;
&:focus-visible {
outline: 1px solid white;