chore: added dark mode and language settings to the navbar

This commit is contained in:
2025-09-27 21:51:59 +02:00
parent f54eada94d
commit 73c397a1bc
9 changed files with 322 additions and 102 deletions
+3 -7
View File
@@ -10,6 +10,7 @@
<div class="app-body"> <div class="app-body">
<AppNavbar /> <AppNavbar />
<main> <main>
<RouterView /> <RouterView />
</main> </main>
@@ -92,18 +93,13 @@ export default defineComponent({
StorageManager.setStringValue(STORAGE_VERSION_KEY, this.appVersion); StorageManager.setStringValue(STORAGE_VERSION_KEY, this.appVersion);
}, },
changeLang(lang: string) {
this.$i18n.locale = lang;
this.store.currentAppLocale = lang;
StorageManager.setStringValue('lang', lang);
},
loadLang() { loadLang() {
const storageLang = StorageManager.getStringValue('lang'); const storageLang = StorageManager.getStringValue('lang');
if (storageLang) { if (storageLang) {
this.changeLang(storageLang); this.store.changeLang(storageLang);
return; return;
} }
@@ -112,7 +108,7 @@ export default defineComponent({
const naviLanguage = window.navigator.language.toString(); const naviLanguage = window.navigator.language.toString();
if (!naviLanguage.startsWith('pl')) { if (!naviLanguage.startsWith('pl')) {
this.changeLang('en'); this.store.changeLang('en');
} }
} }
} }
+20 -7
View File
@@ -3,26 +3,39 @@
<div class="navbar-brand"> <div class="navbar-brand">
<img src="/favicon.ico" alt="generator logo" width="30" /> <img src="/favicon.ico" alt="generator logo" width="30" />
<b> <b>
GeneraTOR <sup>v{{ version }}</sup> Genera<span class="text--accent">TOR</span> <sup class="text--grayed">v{{ version }}</sup>
</b> </b>
</div> </div>
<div class="navbar-actions"> <div class="navbar-actions">
<button class="g-button action icon"> <button class="g-button action icon" @click="switchDarkMode">
<LucideGlobe :size="20" /> <LucideMoon :size="20" v-if="store.orderDarkMode" />
<span>POL</span> <LucideSun :size="20" v-else />
</button> </button>
<button class="g-button action icon"> <button class="g-button action icon" @click="switchLang">
<LucideMoon :size="20" /> <LucideGlobe :size="20" />
<span>{{ store.currentAppLocale == 'pl' ? 'POL' : 'ENG' }}</span>
</button> </button>
</div> </div>
</nav> </nav>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { LucideGlobe, LucideMoon } from 'lucide-vue-next'; import { LucideGlobe, LucideMoon, LucideSun } from 'lucide-vue-next';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { useStore } from '../../store/store';
const store = useStore();
function switchDarkMode() {
store.orderDarkMode = !store.orderDarkMode;
window.localStorage.setItem('dark-mode', `${store.orderDarkMode}`);
}
function switchLang() {
store.changeLang(store.currentAppLocale == 'pl' ? 'en' : 'pl');
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+83
View File
@@ -11,8 +11,91 @@ import { useStore } from '../../store/store';
import OrderHeader from './OrderHeader.vue'; import OrderHeader from './OrderHeader.vue';
import OrderMainContent from './OrderMainContent.vue'; import OrderMainContent from './OrderMainContent.vue';
import OrderFooter from './OrderFooter.vue'; import OrderFooter from './OrderFooter.vue';
import { computed, onMounted, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const store = useStore(); const store = useStore();
onMounted(() => {
generateMessage();
});
watch(
store.orderData,
() => {
generateMessage();
},
{ deep: true }
);
watch(
computed(() => store.currentAppLocale),
() => {
generateMessage();
}
);
function generateMessage() {
let messageHtml = `<b>${t('order.title')}</b><br />`;
messageHtml += '-------------<br />';
const headerData = store.orderData['header'];
messageHtml += `${t('order.header.A')}: ${headerData['A']}<br />`;
messageHtml += `${t('order.header.B')}: ${headerData['B']}<br />`;
messageHtml += `${t('order.header.C')}: ${headerData['C']}<br />`;
messageHtml += `${t('order.header.D')}: ${headerData['D']}<br />`;
const instructions = store.orderData['instructions'];
Object.entries(instructions).forEach(([i, value]) => {
if (value.active) {
if (value.inputFields) {
const localeKey = `order.${value.key}`;
const messageValues = Object.values(value.inputFields).map((fieldKey: string) => {
if (fieldKey.startsWith('select')) return t(`order.${value.key}.${fieldKey}`);
return fieldKey || '---';
});
messageHtml += '-------------<br />';
messageHtml += `<b>[${value.name}]</b> ${t(
localeKey + '.message-html',
messageValues
)}<br />`;
if (value.key == '2310' && value.listFields) {
messageHtml += '<br />';
value.listFields.forEach((listItem, i) => {
if (!listItem.active) return;
const listItemValues = Object.values(listItem.values).map((itemFieldKey) => {
return itemFieldKey || '---';
});
messageHtml += t(`${localeKey}.message-html-list`, [i + 1, ...listItemValues]);
messageHtml += '<br />';
});
}
} else {
messageHtml += `<b>[${i}]</b> ${t('order.' + i + '.message-html')}<br />`;
}
}
});
const footerData = store.orderData['footer'];
messageHtml += '-------------<br />';
messageHtml += `${t('order.footer.V')}: ${footerData['V'] || '---'} | `;
messageHtml += `${t('order.footer.W')}: ${footerData['W'] || '---'}<br />`;
messageHtml += `${t('order.footer.Y')}: ${footerData['Y'] || '---'} | `;
messageHtml += `${t('order.footer.Z')}: ${footerData['Z'] || '---'}<br />`;
store.orderMessage = messageHtml;
}
</script> </script>
<style lang="scss"> <style lang="scss">
+3 -1
View File
@@ -2,7 +2,9 @@
<table class="order-table"> <table class="order-table">
<tbody> <tbody>
<tr> <tr>
<td style="padding: 0.25em"><b>Rozkaz pisemny</b></td> <td style="padding: 0.25em">
<b>{{ t('order.title') }}</b>
</td>
</tr> </tr>
<tr> <tr>
-74
View File
@@ -145,24 +145,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStore } from '../../store/store'; import { useStore } from '../../store/store';
import { onMounted, watch } from 'vue';
import OrderRowCheckbox from './OrderRowCheckbox.vue'; import OrderRowCheckbox from './OrderRowCheckbox.vue';
const { t } = useI18n(); const { t } = useI18n();
const store = useStore(); const store = useStore();
onMounted(() => {
generateMessage();
});
watch(
store.orderData,
() => {
generateMessage();
},
{ deep: true }
);
function calculateInputWidthByFieldName(fieldName: string) { function calculateInputWidthByFieldName(fieldName: string) {
if (fieldName.startsWith('track')) return '90px'; if (fieldName.startsWith('track')) return '90px';
else if (fieldName.startsWith('signalbox')) return '130px'; else if (fieldName.startsWith('signalbox')) return '130px';
@@ -173,67 +160,6 @@ function calculateInputWidthByFieldName(fieldName: string) {
return '100px'; return '100px';
} }
function generateMessage() {
let messageHtml = `<b>${t('order.title')}</b><br />`;
messageHtml += '-------------<br />';
const headerData = store.orderData['header'];
messageHtml += `${t('order.header.A')}: ${headerData['A']}<br />`;
messageHtml += `${t('order.header.B')}: ${headerData['B']}<br />`;
messageHtml += `${t('order.header.C')}: ${headerData['C']}<br />`;
messageHtml += `${t('order.header.D')}: ${headerData['D']}<br />`;
const instructions = store.orderData['instructions'];
Object.entries(instructions).forEach(([i, value]) => {
if (value.active) {
if (value.inputFields) {
const localeKey = `order.${value.key}`;
const messageValues = Object.values(value.inputFields).map((fieldKey: string) => {
if (fieldKey.startsWith('select')) return t(`order.${value.key}.${fieldKey}`);
return fieldKey || '---';
});
messageHtml += '-------------<br />';
messageHtml += `<b>[${value.name}]</b> ${t(
localeKey + '.message-html',
messageValues
)}<br />`;
if (value.key == '2310' && value.listFields) {
messageHtml += '<br />';
value.listFields.forEach((listItem, i) => {
if (!listItem.active) return;
const listItemValues = Object.values(listItem.values).map((itemFieldKey) => {
return itemFieldKey || '---';
});
messageHtml += t(`${localeKey}.message-html-list`, [i + 1, ...listItemValues]);
messageHtml += '<br />';
});
}
} else {
messageHtml += `<b>[${i}]</b> ${t('order.' + i + '.message-html')}<br />`;
}
}
});
const footerData = store.orderData['footer'];
messageHtml += '-------------<br />';
messageHtml += `${t('order.footer.V')}: ${footerData['V'] || '---'} | `;
messageHtml += `${t('order.footer.W')}: ${footerData['W'] || '---'}<br />`;
messageHtml += `${t('order.footer.Y')}: ${footerData['Y'] || '---'} | `;
messageHtml += `${t('order.footer.Z')}: ${footerData['Z'] || '---'}<br />`;
store.orderMessage = messageHtml;
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+1 -12
View File
@@ -27,17 +27,6 @@
</div> </div>
<div class="message_checkboxes"> <div class="message_checkboxes">
<label for="dark-mode" class="g-checkbox">
<input
type="checkbox"
name="dark-mode"
id="dark-mode"
v-model="store.orderDarkMode"
@change="onCheckboxChange"
/>
<span>{{ $t('order-options.dark-mode') }}</span>
</label>
<label for="copy-increment" class="g-checkbox"> <label for="copy-increment" class="g-checkbox">
<input <input
type="checkbox" type="checkbox"
@@ -269,7 +258,7 @@ export default defineComponent({
} }
.message_body { .message_body {
height: 250px; height: 350px;
overflow: auto; overflow: auto;
background-color: colors.$bgColLighter; background-color: colors.$bgColLighter;
+196
View File
@@ -68,5 +68,201 @@
"title": "Active timetables and trains on the scenery", "title": "Active timetables and trains on the scenery",
"subtitle": "Click on the user below to fill the current order with their information", "subtitle": "Click on the user below to fill the current order with their information",
"no-trains": "No trains to display" "no-trains": "No trains to display"
},
"order": {
"title": "Train order",
"header": {
"A": "A Nr pociągu {'|'} składu manewrowego",
"B": "B Data",
"C": "C Lokalizacja pociągu {'|'} składu manewrowego",
"D": "D Lokalizacja nadawcy"
},
"22": {
"text": "Dotyczy jazdy torem lewym",
"message-html": "<b>Dotyczy jazdy torem lewym</b>"
},
"99": {
"text": "Odwołanie rozkazu pisemnego",
"x1": "x.1 Identyfikator rozkazu pisemnego",
"message-html": "<b>Odwołanie rozkazu pisemnego</b> {0}"
},
"2110": {
"text": "{bold1} z toru nr {track1} z {signalbox1} {br} na tor nr {track2} w kierunku {signalbox2} {br} Pominięcie sygnałów stój {signal1} i {signal2} i {signal3}",
"bold1": "Zezwalam na wyjazd",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"track2": "x.3 tor",
"signalbox2": "x.4 posterunek",
"signal1": "x.5 sygnalizator",
"signal2": "x.6 sygnalizator",
"signal3": "x.7 sygnalizator",
"message-html": "<b>Zezwalam na wyjazd</b> z toru nr {0} z {1} na tor nr {2} w kierunku {3}. <br /> Pominięcie sygnałów stój {4} i {5} i {6}"
},
"2115": {
"text": "{bold1} z toru nr {track1} do {signalbox1} na tor nr {track2} {br} Pominięcie sygnałów {signal1} i {signal2} i {signal3}",
"bold1": "Zezwalam na wjazd",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"track2": "x.3 tor",
"signal1": "x.4 sygnalizator",
"signal2": "x.5 sygnalizator",
"signal3": "x.6 sygnalizator",
"message-html": "<b>Zezwalam na wjazd</b> z toru nr {0} do {1} na tor nr {2} <br /> Pominięcie sygnałów {3} i {4} i {5}"
},
"2120": {
"text": "Od {signalbox1} do {signalbox2} po torze {track1} {br} {highlight1} {br} {underline1}{highlight2}.",
"highlight1": "wskazania semaforów SBL są nieważne.",
"underline1": "Zachować ostrożność od semafora ze wskaźnikiem",
"highlight2": " W18",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"track1": "x.3 nr toru",
"message-html": "Od {0} do {1} po torze {2} <u><b>wskazania semaforów SBL są nieważne.</b> Zachować ostrożność od semafora ze wskaźnikiem <b>W18.</b></u>"
},
"2125": {
"text": "Zezwalam przejechać za {select1} w kierunku {signalbox1} torem {track1} do km {km1} do godz. {hour1}.",
"select1-a": "wskaźnik W5",
"select1-b": "ostatni rozjazd",
"signalbox1": "x.1 posterunek",
"track1": "x.2 tor",
"km1": "x.3 km",
"hour1": "x.4 godzina",
"message-html": "Zezwalam przejechać za {0} w kierunku {1} torem {2} do km {3} do godz. {4}."
},
"2135": {
"text": "{bold1} po torze nr {track1} w kierunku {signalbox1}.",
"bold1": "Zezwalam na kontynuacje jazdy",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"message-html": "<b>Zezwalam na kontynuacje jazdy</b> po torze {0} w kierunku {1}"
},
"2140": {
"text": "{bold1} na posterunku/szlaku {signalbox1}{'|'}{signalbox2} w km {km1} celem {other1}",
"bold1": "Zatrzymanie pociągu",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"km1": "x.3 km",
"other1": "x.96 inne",
"message-html": "<b>Zatrzymanie pociągu</b> na posterunku/szlaku {0} {'|'} {1} w km {2} celem {3}"
},
"2145": {
"text": "Na {signalbox1} na sygnalizatorze {signal1} {bold1}",
"bold1": "sygnał zezwalający jest nieważny, zatrzymać pociąg przed tym sygnalizatorem.",
"signalbox1": "x.1 posterunek",
"signal1": "x.2 sygnalizator",
"message-html": "Na {0} na sygnalizatorze {1} <b>sygnał zezwalający jest nieważny, zatrzymać pociąg przed tym sygnalizatorem.</b>"
},
"2150": {
"text": "{bold1} przejazdowych na posterunku/szlaku {br} {signalbox1} / {signalbox2} odnoszących się do przejazdu w km {km1}{br}{bold2}",
"bold1": "Wskazania tarcz ostrzegawczych",
"bold2": "są nieważne. Jazda z prędkością rozkładową.",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"km1": "x.3 km",
"message-html": "<b>Wskazania tarcz ostrzegawczych</b> przejazdowych na posterunku/szlaku {1} / {2} odnoszących się do przejazdu w km {3}, <b>są nieważne. Jazda z prędkością rozkładową.</b>"
},
"2155": {
"text": "{bold1} na posterunku/szlaku {signalbox1}{'|'}{signalbox2} odnoszące się do sygnalizatora {signal1}.",
"bold1": "Uszkodzone urządzenia SHP",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"signal1": "x.3 sygnalizator",
"message-html": "<b>Uszkodzone urządzenia SHP</b> na posterunku/szlaku {0}{'|'}{1} odnoszące się do sygnalizatora {3}"
},
"2160": {
"text": "{bold1} na odcinku od {signalbox1} do {signalbox2}, jazda przez {signalbox3} linią {line1} z prędkością {vmax1}.",
"bold1": "Zmiana trasy",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"signalbox3": "x.3 posterunek",
"line1": "x.4 linia",
"vmax1": "x.5 km/h",
"message-html": "<b>Zmiana trasy</b> na odcinku od {0} do {1}, jazda przez {2} linią {3} z prędkością {4}."
},
"2165": {
"text": "{bold1} na odcinku od {km1} do {km2}.",
"bold1": "Jazda z opuszczonymi pantografami",
"km1": "x.1 km",
"km2": "x.2 km",
"message-html": "<b>Jazda z opuszczonymi pantografami</b> na odcinku od {0} do {1}."
},
"2170": {
"text": "{bold1} od {signalbox1} do {signalbox2} linii nr {line1}{br}{bold2}",
"bold1": "Na odcinku",
"bold2": "jazda pociągu z łącznością analogową.",
"signalbox1": "x.1 posterunek",
"signalbox2": "x.2 posterunek",
"line1": "x.3 linia",
"message-html": "<b>Na odcinku</b> na odcinku od {0} do {1} linii nr {2} <b>jazda pociągu z łącznością analogową.</b>"
},
"2180": {
"text": "{bold1} nr {track1} w kierunku {signalbox1} do km {km1} zjazd do {signalbox2} do godz. {hour1}",
"bold1": "Polecam jazdę po torze zamkniętym",
"track1": "x.1 tor",
"signalbox1": "x.2 posterunek",
"km1": "x.3 km",
"signalbox2": "x.4 posterunek",
"hour1": "x.5 godzina",
"message-html": "<b>Polecam jazdę po torze zamkniętym</b> nr {0} w kierunku {1} do km {2} zjazd do {3} do godz. {4}"
},
"2181": {
"text": "{bold1}",
"bold1": "Tor zamknięty wolny od taboru",
"message-html": "<b>Tor zamknięty wolny od taboru</b> "
},
"2182": {
"text": "{bold1} {train1} w km {km1}",
"bold1": "Na torze pracuje pociąg",
"train1": "x.1 numer pociągu",
"km1": "x.2 km",
"message-html": "<b>Na torze pracuje pociąg</b> {0} w km {1}"
},
"2183": {
"text": "{bold1} {train1} do km {km1}",
"bold1": "Na tor zostanie wyprawiony pociąg",
"train1": "x.1 numer pociągu",
"km1": "x.2 km",
"message-html": "<b>Na tor zostanie wyprawiony pociąg</b> {0} do km {1}"
},
"2185": {
"text": "{bold1} na tor nr {track1} w km {km1} na szlaku {'|'} {signalbox1} {'|'} {signalbox2}",
"bold1": "Zezwalam na wstawienie PSD",
"track1": "x.1 tor",
"km1": "x.2 km",
"signalbox1": "x.3 posterunek",
"signalbox2": "x.4 posterunek",
"message-html": "<b>Zezwalam na wstawienie PSD</b> na tor nr {0} w km {1} na szlaku {'|'} {2} {'|'} {3}"
},
"2310": {
"text": "{bold1}{br}{text-list}",
"bold1": "Nie przekraczać prędkości i zachować ostrożność:",
"text-list": "{bold} {signalbox1}/{signalbox2} tor nr {track1} {v} {vmax1} od {km1} do {km2} {other1}",
"bold": "{0}. Na posterunku/szlaku",
"signalbox1": "x.{0} posterunek",
"signalbox2": "x.{0} posterunek",
"track1": "x.{0} tor",
"vmax1": "x.{0} km/h",
"km1": "x.{0} km",
"km2": "x.{0} km",
"other1": "x.{1} przyczyna",
"message-html": "<b>Nie przekraczać prędkości i zachować ostrożność:</b>",
"message-html-list": "<b>{0}. Na posterunku/szlaku</b> {1}/{2} tor nr {3} v{4} od {5}km do {6}km - przyczyna: {7}"
},
"2311": {
"text": "{bold1}",
"bold1": "Podawać sygnał „Baczność”",
"message-html": "<b>Podawać sygnał „Baczność”</b>"
},
"2320": {
"text": "{other2320}",
"other2320": "x.96 inne",
"message-html": "Inne: {0}"
},
"footer": {
"V": "V Identyfikator maszynisty",
"W": "W Identyfikator nadawcy",
"Y": "Y Godzina",
"Z": "Z Identyfikator rozkazu pisemnego"
}
} }
} }
+11
View File
@@ -6,6 +6,9 @@ import {
currentFormattedMinutes currentFormattedMinutes
} from '../utils/dateUtils'; } from '../utils/dateUtils';
import StorageManager from '../managers/storageManager';
import i18n from '../i18n';
export const useStore = defineStore('store', { export const useStore = defineStore('store', {
state: () => { state: () => {
return { return {
@@ -528,5 +531,13 @@ export const useStore = defineStore('store', {
] ]
} as IOrderS } as IOrderS
}; };
},
actions: {
changeLang(lang: string) {
i18n.global.locale.value = lang as typeof i18n.global.locale.value;
this.currentAppLocale = lang;
StorageManager.setStringValue('lang', lang);
}
} }
}); });
+4
View File
@@ -129,6 +129,10 @@ button.g-button {
&--warn { &--warn {
color: colors.$warnCol; color: colors.$warnCol;
} }
&--grayed {
color: #ccc;
}
} }
// Select style // Select style