Enhance API and frontend components for character management and map visibility

- Updated API documentation to include `ownedByMe` field in character responses, indicating if a character was last updated by the current user's tokens.
- Modified MapView component to track and display the live status based on user-owned characters.
- Enhanced map data handling to exclude hidden maps for non-admin users and improved character icon representation on the map.
- Refactored character data structures to support new properties and ensure accurate rendering in the frontend.
This commit is contained in:
2026-03-01 17:21:15 +03:00
parent 6a6977ddff
commit 7bdaa6bfcc
9 changed files with 82 additions and 16 deletions

View File

@@ -73,7 +73,7 @@
<span class="text-base-content/80">{{ measurePointA == null ? 'Click first point on map' : 'Click second point' }}</span>
</template>
<template v-else>
<span class="font-mono">Distance: {{ measureDistance.toFixed(2) }} units</span>
<span class="font-mono">Distance: {{ measureDistance.toFixed(3) }} units</span>
<button type="button" class="btn btn-ghost btn-xs ml-2" @click="clearMeasure">Clear</button>
</template>
</div>
@@ -146,6 +146,8 @@ const props = withDefaults(
const mapContainerRef = ref<HTMLElement | null>(null)
const mapRef = ref<HTMLElement | null>(null)
const api = useMapApi()
/** Global state for navbar Live/Offline: true when at least one character is owned by current user's tokens. */
const mapLive = useState<boolean>('mapLive', () => false)
const mapLogic = useMapLogic()
const { resolvePath } = useAppPaths()
const mapNavigate = useMapNavigate()
@@ -275,13 +277,18 @@ onMounted(async () => {
])
const mapsList: MapInfo[] = []
const raw = mapsData as Record<string, { ID?: number; Name?: string; id?: number; name?: string; size?: number }>
const raw = mapsData as Record<
string,
{ ID?: number; Name?: string; id?: number; name?: string; size?: number; Hidden?: boolean; hidden?: boolean }
>
for (const id in raw) {
const m = raw[id]
if (!m || typeof m !== 'object') continue
const idVal = m.ID ?? m.id
const nameVal = m.Name ?? m.name
if (idVal == null || nameVal == null) continue
const hidden = !!(m.Hidden ?? m.hidden)
if (hidden) continue
mapsList.push({ ID: Number(idVal), Name: String(nameVal), size: m.size })
}
mapsList.sort((a, b) => (b.size ?? 0) - (a.size ?? 0))
@@ -354,8 +361,10 @@ onMounted(async () => {
},
})
layersManager.updateCharacters(Array.isArray(charactersData) ? charactersData : [])
const charsList = Array.isArray(charactersData) ? charactersData : []
layersManager.updateCharacters(charsList)
players.value = layersManager.getPlayers()
mapLive.value = charsList.some((c) => c.ownedByMe)
if (props.characterId !== undefined && props.characterId >= 0) {
mapLogic.state.trackingCharacterId.value = props.characterId
@@ -386,8 +395,10 @@ onMounted(async () => {
api
.getCharacters()
.then((body) => {
layersManager!.updateCharacters(Array.isArray(body) ? body : [])
const list = Array.isArray(body) ? body : []
layersManager!.updateCharacters(list)
players.value = layersManager!.getPlayers()
mapLive.value = list.some((c) => c.ownedByMe)
})
.catch(() => clearInterval(intervalId!))
}, 2000)
@@ -482,8 +493,8 @@ onMounted(async () => {
if (!measureMode.value) return
e.originalEvent.stopPropagation()
const point = leafletMap!.project(e.latlng, HnHMaxZoom)
const x = Math.floor(point.x / TileSize)
const y = Math.floor(point.y / TileSize)
const x = point.x / TileSize
const y = point.y / TileSize
if (measurePointA.value == null) {
measurePointA.value = { x, y }
} else if (measurePointB.value == null) {

View File

@@ -224,7 +224,8 @@ function getInitialDark(): boolean {
const title = ref('HnH Map')
const dark = ref(false)
const live = ref(false)
/** Live when at least one of current user's characters is on the map (set by MapView). */
const live = useState<boolean>('mapLive', () => false)
const me = useState<MeResponse | null>('me', () => null)
const gravatarErrorDesktop = ref(false)
const gravatarErrorDrawer = ref(false)
@@ -300,6 +301,4 @@ async function doLogout() {
await router.push('/login')
me.value = null
}
defineExpose({ setLive: (v: boolean) => { live.value = v } })
</script>

View File

@@ -1,6 +1,23 @@
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
import * as L from 'leaflet'
/** SVG data URL for character marker icon (teal pin, bottom-center anchor). */
const CHARACTER_ICON_URL =
'data:image/svg+xml,' +
encodeURIComponent(
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 32" width="24" height="32">' +
'<path fill="#0d9488" stroke="#0f766e" stroke-width="1" d="M12 2a6 6 0 0 1 6 6c0 4-6 10-6 10s-6-6-6-10a6 6 0 0 1 6-6z"/>' +
'<circle cx="12" cy="8" r="2.5" fill="white"/>' +
'</svg>'
)
const CHARACTER_ICON = new L.Icon({
iconUrl: CHARACTER_ICON_URL,
iconSize: [24, 32],
iconAnchor: [12, 32],
popupAnchor: [0, -32],
})
export interface CharacterData {
name: string
position: { x: number; y: number }
@@ -58,7 +75,7 @@ export function createCharacter(data: CharacterData): MapCharacter {
add(mapview: CharacterMapViewRef): void {
if (character.map === mapview.mapid) {
const position = mapview.map.unproject([character.position.x, character.position.y], HnHMaxZoom)
leafletMarker = L.marker(position, { title: character.name })
leafletMarker = L.marker(position, { icon: CHARACTER_ICON, title: character.name })
leafletMarker.on('click', (e: L.LeafletMouseEvent) => {
if (onClick) onClick(e)
})

View File

@@ -28,6 +28,8 @@ export interface MapInfo {
ID: number
Name: string
size?: number
/** Present when returned by API (e.g. for admins); client should exclude Hidden maps from selectors. */
Hidden?: boolean
}
export interface Character {
@@ -36,6 +38,8 @@ export interface Character {
map: number
position: { x: number; y: number }
type: string
/** True when this character was last updated by one of the current user's tokens. */
ownedByMe?: boolean
}
export interface Marker {