Files
yourpart3/frontend/src/components/falukant/StatusBar.vue
Torsten Schulz (local) 028da5a9f0 feat(MessagesDialog, StatusBar, i18n): enhance character death notifications and UI updates
- Added support for displaying character death notifications with detailed information including age, region, and relationships in both German and English.
- Updated MessagesDialog to format additional parameters such as region, spouses, children, and lovers based on the locale.
- Enhanced StatusBar to show the character's name dynamically, improving user experience.
- Modified i18n files to include new notification messages for character deaths, ensuring accurate translations for both languages.
2026-03-30 10:25:54 +02:00

447 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="statusbar">
<div class="status-item messages" @click="openMessages" :title="$t('falukant.messages.tooltip')">
<span class="badge" v-if="unreadCount > 0">{{ unreadCount }}</span>
<img src="/images/icons/falukant/messages24.png" class="menu-icon" />
</div>
<div v-if="characterName" class="status-identity" :title="characterName">
<span class="status-identity__label">{{ $t('falukant.overview.metadata.name') }}</span>
<strong class="status-identity__name">{{ characterName }}</strong>
</div>
<template v-for="item in statusItems" :key="item.key">
<div class="status-item" v-if="item.value !== null && item.image == null">
<span class="status-icon-wrapper">
<template v-if="item.iconImage">
<img :src="'/images/icons/' + item.iconImage" class="inline-icon" width="16" height="16" :title="String(item.value)" />
</template>
<template v-else>
<span class="status-icon-symbol" :title="String(item.value)">{{ item.icon }}</span>
</template>
</span>
<span class="status-label" :title="$t(`falukant.statusbar.${item.key}`)">{{ item.value }}</span>
</div>
<div class="status-item" v-else-if="item.image !== null">
<span class="status-icon-wrapper">
<span class="status-icon-symbol" :title="$t(`falukant.statusbar.${item.key}`)">{{ item.icon }}</span>
</span>
<img :src="'/images/icons/falukant/relationship-' + item.image + '.png'" class="relationship-icon" :title="$t(`falukant.statusbar.${item.key}`)" />
</div>
</template>
<div
v-if="statusItems.length > 0 && menu.falukant && menu.falukant.children"
class="quick-access"
>
<template v-for="(menuItem, key) in menu.falukant.children" :key="menuItem.id" >
<img
:src="'/images/icons/falukant/shortmap/' + key + '.png'"
class="menu-icon"
@click="openPage(menuItem)"
:title="$t(`navigation.m-falukant.${key}`)"
/>
</template>
</div>
<div v-if="debtorsPrison.active" class="statusbar-warning" :class="{ 'is-prison': debtorsPrison.inDebtorsPrison }">
<strong>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.bank.debtorsPrison.titlePrison')
: $t('falukant.bank.debtorsPrison.titleWarning') }}
</strong>
<span>
{{ debtorsPrison.inDebtorsPrison
? $t('falukant.debtorsPrison.globalLocked')
: $t('falukant.debtorsPrison.globalWarning') }}
</span>
</div>
<MessagesDialog ref="msgs" />
</div>
</template>
<script>
import { mapState, mapGetters } from "vuex";
import apiClient from "@/utils/axios.js";
import { EventBus } from '@/utils/eventBus.js';
import MessagesDialog from './MessagesDialog.vue';
export default {
name: "StatusBar",
components: { MessagesDialog },
data() {
return {
statusItems: [
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: 0 },
{ key: "relationship", icon: "💑", image: null },
{ key: "wealth", icon: "💰", value: 0 },
{ key: "health", icon: "❤️", value: "Good" },
{ key: "events", icon: "📰", value: null, image: null },
{ key: "children", icon: "👶", value: null },
],
unreadCount: 0,
debtorsPrison: {
active: false,
inDebtorsPrison: false
},
characterName: '',
pendingStatusRefresh: null,
};
},
computed: {
...mapState(["socket", "daemonSocket", "user"]),
...mapGetters(['menu']),
},
watch: {
// Wenn sich das Menü ändert, lade die Bilder neu
'menu.falukant': {
handler() {
this.preloadQuickAccessImages();
},
deep: true
},
socket(newVal, oldVal) {
if (oldVal) this.teardownSocketListeners();
if (newVal) this.setupSocketListeners();
},
daemonSocket(newVal, oldVal) {
if (oldVal && this._daemonHandler) {
oldVal.removeEventListener('message', this._daemonHandler);
this._daemonHandler = null;
}
if (newVal) this.setupDaemonListeners();
}
},
async mounted() {
await this.fetchStatus();
// Bilder für Schnellzugriff vorladen und cachen
this.preloadQuickAccessImages();
// Socket.IO (Backend notifyUser) Hauptkanal für Falukant-Events
this.setupSocketListeners();
this.setupDaemonListeners();
EventBus.on('open-falukant-messages', this.openMessages);
},
beforeUnmount() {
this.teardownSocketListeners();
this.teardownDaemonListeners();
if (this.pendingStatusRefresh) {
clearTimeout(this.pendingStatusRefresh);
this.pendingStatusRefresh = null;
}
EventBus.off('open-falukant-messages', this.openMessages);
},
methods: {
preloadQuickAccessImages() {
// Lade alle Schnellzugriffs-Bilder vor, damit sie gecacht werden
if (this.menu.falukant && this.menu.falukant.children) {
Object.keys(this.menu.falukant.children).forEach(key => {
const img = new Image();
img.src = `/images/icons/falukant/shortmap/${key}.png`;
});
}
// Lade auch andere häufig verwendete Bilder vor
const commonImages = [
'/images/icons/falukant/messages24.png',
'/images/icons/falukant/age.png',
// Relationship-Bilder vorladen
'/images/icons/falukant/relationship-engaged.png',
'/images/icons/falukant/relationship-wooing.png',
'/images/icons/falukant/relationship-married.png',
'/images/icons/falukant/relationship-widow.png',
'/images/icons/falukant/relationship-.png'
];
commonImages.forEach(src => {
const img = new Image();
img.src = src;
});
},
async fetchStatus() {
try {
const response = await apiClient.get("/api/falukant/info");
const { money, character, events } = response.data;
const { age, health } = character;
const firstName = character?.definedFirstName?.name || '';
const lastName = character?.definedLastName?.name || '';
this.characterName = [firstName, lastName].filter(Boolean).join(' ');
const relationship = response.data.character.relationshipsAsCharacter1[0]?.relationshipType?.tr
|| response.data.character.relationshipsAsCharacter2[0]?.relationshipType?.tr
|| null;
// Children counts and unread notifications are now part of /info
const childCount = Number(response.data.childrenCount) || 0;
const unbaptisedCount = Number(response.data.unbaptisedChildrenCount) || 0;
this.unreadCount = Number(response.data.unreadNotifications) || 0;
this.debtorsPrison = response.data.debtorsPrison || {
active: false,
inDebtorsPrison: false
};
const childrenDisplay = `${childCount}${unbaptisedCount > 0 ? `(${unbaptisedCount})` : ''}`;
let healthStatus = '';
if (health > 90) {
healthStatus = this.$t("falukant.health.amazing");
} else if (health > 75) {
healthStatus = this.$t("falukant.health.good");
} else if (health > 50) {
healthStatus = this.$t("falukant.health.normal");
} else if (health > 25) {
healthStatus = this.$t("falukant.health.bad");
} else {
healthStatus = this.$t("falukant.health.very_bad");
}
this.statusItems = [
{ key: "age", icon: null, iconImage: 'falukant/age.png', value: age },
{ key: "relationship", icon: "💑", image: relationship },
{ key: "wealth", icon: "💰", value: Intl.NumberFormat(navigator.language, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(money) },
{ key: "health", icon: "❤️", value: healthStatus },
{ key: "events", icon: "📰", value: events || null, image: null },
{ key: "children", icon: "👶", value: childrenDisplay },
];
} catch (error) {
// Error fetching status
}
},
setupSocketListeners() {
this.teardownSocketListeners();
if (!this.socket) return;
this._statusSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateStatus', ...data });
this._familySocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateFamily', ...data });
this._churchSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateChurch', ...data });
this._debtSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateDebt', ...data });
this._childrenSocketHandler = (data) => this.handleEvent({ event: 'children_update', ...data });
this._productionCertificateSocketHandler = (data) => this.handleEvent({ event: 'falukantUpdateProductionCertificate', ...data });
this._stockSocketHandler = (data) => this.handleEvent({ event: 'stock_change', ...data });
this._familyChangedSocketHandler = (data) => this.handleEvent({ event: 'familychanged', ...data });
this.socket.on('falukantUpdateStatus', this._statusSocketHandler);
this.socket.on('falukantUpdateFamily', this._familySocketHandler);
this.socket.on('falukantUpdateChurch', this._churchSocketHandler);
this.socket.on('falukantUpdateDebt', this._debtSocketHandler);
this.socket.on('children_update', this._childrenSocketHandler);
this.socket.on('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
this.socket.on('stock_change', this._stockSocketHandler);
this.socket.on('familychanged', this._familyChangedSocketHandler);
},
teardownSocketListeners() {
if (this.socket) {
if (this._statusSocketHandler) this.socket.off('falukantUpdateStatus', this._statusSocketHandler);
if (this._familySocketHandler) this.socket.off('falukantUpdateFamily', this._familySocketHandler);
if (this._churchSocketHandler) this.socket.off('falukantUpdateChurch', this._churchSocketHandler);
if (this._debtSocketHandler) this.socket.off('falukantUpdateDebt', this._debtSocketHandler);
if (this._childrenSocketHandler) this.socket.off('children_update', this._childrenSocketHandler);
if (this._productionCertificateSocketHandler) this.socket.off('falukantUpdateProductionCertificate', this._productionCertificateSocketHandler);
if (this._stockSocketHandler) this.socket.off('stock_change', this._stockSocketHandler);
if (this._familyChangedSocketHandler) this.socket.off('familychanged', this._familyChangedSocketHandler);
}
},
setupDaemonListeners() {
this.teardownDaemonListeners();
if (!this.daemonSocket) return;
this._daemonHandler = (event) => {
try {
const data = JSON.parse(event.data);
if (['falukantUpdateStatus', 'falukantUpdateFamily', 'falukantUpdateChurch', 'falukantUpdateDebt', 'children_update', 'falukantUpdateProductionCertificate', 'stock_change', 'familychanged'].includes(data.event)) {
this.handleEvent(data);
}
} catch (_) {}
};
this.daemonSocket.addEventListener('message', this._daemonHandler);
},
matchesCurrentUser(eventData) {
if (eventData?.user_id == null) {
return true;
}
const currentIds = [this.user?.id, this.user?.hashedId]
.filter(Boolean)
.map((value) => String(value));
return currentIds.includes(String(eventData.user_id));
},
teardownDaemonListeners() {
const sock = this.daemonSocket;
if (sock && this._daemonHandler) {
sock.removeEventListener('message', this._daemonHandler);
this._daemonHandler = null;
}
},
queueStatusRefresh() {
if (this.pendingStatusRefresh) {
clearTimeout(this.pendingStatusRefresh);
}
this.pendingStatusRefresh = setTimeout(async () => {
this.pendingStatusRefresh = null;
await this.fetchStatus();
}, 120);
},
handleEvent(eventData) {
if (!this.matchesCurrentUser(eventData)) {
return;
}
switch (eventData.event) {
case 'falukantUpdateStatus':
case 'falukantUpdateFamily':
case 'falukantUpdateChurch':
case 'falukantUpdateDebt':
case 'children_update':
case 'falukantUpdateProductionCertificate':
case 'stock_change':
case 'familychanged':
this.queueStatusRefresh();
break;
}
},
openPage(url, hasSubmenu = false) {
if (hasSubmenu) {
return;
}
if (url) {
this.$router.push(url);
}
},
openMessages() {
this.$refs.msgs.open();
// After opening, refresh unread count after a short delay (server marks them shown)
setTimeout(() => this.fetchStatus(), 500);
},
},
};
</script>
<style scoped>
.statusbar {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
background:
linear-gradient(180deg, rgba(255, 251, 246, 0.96) 0%, rgba(247, 238, 224, 0.98) 100%);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
box-sizing: border-box;
width: 100%;
max-width: 100%;
gap: 1.2em;
padding: 0.55rem 0.9rem;
margin: 0 0 1.5em 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-soft);
}
.statusbar-warning {
flex: 1 1 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: var(--radius-md);
border: 1px solid rgba(180, 120, 40, 0.35);
background: rgba(255, 244, 223, 0.92);
color: #8a5411;
font-size: 0.92rem;
}
.statusbar-warning.is-prison {
border-color: rgba(146, 57, 40, 0.42);
background: rgba(255, 232, 225, 0.94);
color: #8b2f23;
}
.status-item {
text-align: center;
cursor: pointer;
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 0 10px;
border-radius: 999px;
background: rgba(255,255,255,0.68);
border: 1px solid rgba(93, 64, 55, 0.08);
}
.status-identity {
display: inline-flex;
align-items: baseline;
gap: 0.5rem;
min-height: 34px;
padding: 0.2rem 0.95rem;
border-radius: 999px;
background: linear-gradient(180deg, rgba(255,255,255,0.94) 0%, rgba(248, 241, 231, 0.98) 100%);
border: 1px solid rgba(126, 71, 27, 0.16);
box-shadow: inset 0 1px 0 rgba(255,255,255,0.65);
max-width: min(100%, 26rem);
}
.status-identity__label {
font-size: 0.72rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #9a7a5f;
white-space: nowrap;
}
.status-identity__name {
font-size: 0.98rem;
color: #5f3617;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quick-access {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 0.2rem;
}
.status-icon-wrapper {
display: inline-flex;
align-items: center;
margin-right: 4px;
}
.status-icon-symbol {
font-size: 14px;
}
.status-label {
font-size: 14px;
}
.menu-icon {
width: 30px;
height: 30px;
display: block;
flex: 0 0 auto;
cursor: pointer;
padding: 4px 2px 0 0;
}
.relationship-icon {
max-width: 24px;
max-height: 24px;
}
.messages { position: relative; }
.badge {
position: absolute;
top: -6px;
right: -2px;
background: #e53935;
color: #fff;
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
line-height: 18px;
min-width: 18px;
text-align: center;
}
.inline-icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-right: 4px;
}
</style>