- Added a custom light theme in app.css to match the dark theme's palette. - Introduced AdminBreadcrumbs component for improved navigation in admin pages. - Implemented Skeleton component for loading states in various views. - Added ToastContainer for displaying notifications and alerts. - Enhanced MapView with loading indicators and improved marker handling. - Updated MapCoordsDisplay to allow copying of shareable links. - Refactored MapControls and MapContextMenu for better usability. - Improved user experience in profile and admin pages with loading states and search functionality.
203 lines
6.8 KiB
TypeScript
203 lines
6.8 KiB
TypeScript
import type L from 'leaflet'
|
|
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
|
import { createMarker, type MapMarker, type MarkerData, type MapViewRef } from '~/lib/Marker'
|
|
import { createCharacter, type MapCharacter, type CharacterData, type CharacterMapViewRef } from '~/lib/Character'
|
|
import {
|
|
createUniqueList,
|
|
uniqueListUpdate,
|
|
uniqueListGetElements,
|
|
uniqueListById,
|
|
type UniqueList,
|
|
} from '~/lib/UniqueList'
|
|
import type { SmartTileLayer } from '~/lib/SmartTileLayer'
|
|
import type { Marker as ApiMarker, Character as ApiCharacter } from '~/types/api'
|
|
|
|
type SmartTileLayerInstance = InstanceType<typeof SmartTileLayer>
|
|
|
|
export interface MapLayersOptions {
|
|
map: L.Map
|
|
markerLayer: L.LayerGroup
|
|
layer: SmartTileLayerInstance
|
|
overlayLayer: SmartTileLayerInstance
|
|
getCurrentMapId: () => number
|
|
setCurrentMapId: (id: number) => void
|
|
setSelectedMapId: (id: number) => void
|
|
getAuths: () => string[]
|
|
getTrackingCharacterId: () => number
|
|
setTrackingCharacterId: (id: number) => void
|
|
onMarkerContextMenu: (clientX: number, clientY: number, id: number, name: string) => void
|
|
/** Resolves relative marker icon path to absolute URL. If omitted, relative paths are used. */
|
|
resolveIconUrl?: (path: string) => string
|
|
/** Fallback icon URL when a marker image fails to load. */
|
|
fallbackIconUrl?: string
|
|
}
|
|
|
|
export interface MapLayersManager {
|
|
markers: UniqueList<MapMarker>
|
|
characters: UniqueList<MapCharacter>
|
|
changeMap: (id: number) => void
|
|
updateMarkers: (markersData: ApiMarker[]) => void
|
|
updateCharacters: (charactersData: ApiCharacter[]) => void
|
|
getQuestGivers: () => Array<{ id: number; name: string }>
|
|
getPlayers: () => Array<{ id: number; name: string }>
|
|
refreshMarkersVisibility: (hidden: boolean) => void
|
|
refreshOverlayMarkers: (overlayMapIdValue: number) => void
|
|
findMarkerById: (id: number) => MapMarker | undefined
|
|
findCharacterById: (id: number) => MapCharacter | undefined
|
|
}
|
|
|
|
export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|
const {
|
|
map,
|
|
markerLayer,
|
|
layer,
|
|
overlayLayer,
|
|
getCurrentMapId,
|
|
setCurrentMapId,
|
|
setSelectedMapId,
|
|
getAuths,
|
|
getTrackingCharacterId,
|
|
setTrackingCharacterId,
|
|
onMarkerContextMenu,
|
|
resolveIconUrl,
|
|
fallbackIconUrl,
|
|
} = options
|
|
|
|
const markers = createUniqueList<MapMarker>()
|
|
const characters = createUniqueList<MapCharacter>()
|
|
|
|
function markerCtx(): MapViewRef {
|
|
return { map, markerLayer, mapid: getCurrentMapId() }
|
|
}
|
|
|
|
function characterCtx(): CharacterMapViewRef {
|
|
return { map, mapid: getCurrentMapId() }
|
|
}
|
|
|
|
function changeMap(id: number) {
|
|
if (id === getCurrentMapId()) return
|
|
setCurrentMapId(id)
|
|
setSelectedMapId(id)
|
|
layer.map = id
|
|
layer.redraw()
|
|
overlayLayer.map = -1
|
|
overlayLayer.redraw()
|
|
|
|
const ctx = markerCtx()
|
|
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
|
|
uniqueListGetElements(markers)
|
|
.filter((it) => it.map === id)
|
|
.forEach((it) => it.add(ctx))
|
|
|
|
const cCtx = characterCtx()
|
|
uniqueListGetElements(characters).forEach((it) => {
|
|
it.remove(cCtx)
|
|
it.add(cCtx)
|
|
})
|
|
}
|
|
|
|
function updateMarkers(markersData: ApiMarker[]) {
|
|
const list = Array.isArray(markersData) ? markersData : []
|
|
const ctx = markerCtx()
|
|
const iconOptions =
|
|
resolveIconUrl != null
|
|
? { resolveIconUrl, fallbackIconUrl }
|
|
: undefined
|
|
uniqueListUpdate(
|
|
markers,
|
|
list.map((it) => createMarker(it as MarkerData, iconOptions)),
|
|
(marker: MapMarker) => {
|
|
if (marker.map === getCurrentMapId() || marker.map === overlayLayer.map) marker.add(ctx)
|
|
marker.setClickCallback(() => {
|
|
if (marker.leafletMarker) map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
|
|
})
|
|
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
|
if (getAuths().includes('admin')) {
|
|
mev.originalEvent.preventDefault()
|
|
mev.originalEvent.stopPropagation()
|
|
onMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
|
}
|
|
})
|
|
},
|
|
(marker: MapMarker) => marker.remove(ctx),
|
|
(marker: MapMarker, updated: MapMarker) => marker.update(ctx, updated)
|
|
)
|
|
}
|
|
|
|
function updateCharacters(charactersData: ApiCharacter[]) {
|
|
const list = Array.isArray(charactersData) ? charactersData : []
|
|
const ctx = characterCtx()
|
|
uniqueListUpdate(
|
|
characters,
|
|
list.map((it) => createCharacter(it as CharacterData)),
|
|
(character: MapCharacter) => {
|
|
character.add(ctx)
|
|
character.setClickCallback(() => setTrackingCharacterId(character.id))
|
|
},
|
|
(character: MapCharacter) => character.remove(ctx),
|
|
(character: MapCharacter, updated: MapCharacter) => {
|
|
if (getTrackingCharacterId() === updated.id) {
|
|
if (getCurrentMapId() !== updated.map) changeMap(updated.map)
|
|
const latlng = map.unproject([updated.position.x, updated.position.y], HnHMaxZoom)
|
|
map.setView(latlng, HnHMaxZoom)
|
|
}
|
|
character.update(ctx, updated)
|
|
}
|
|
)
|
|
}
|
|
|
|
function getQuestGivers(): Array<{ id: number; name: string }> {
|
|
return uniqueListGetElements(markers)
|
|
.filter((it) => it.type === 'quest')
|
|
.map((it) => ({ id: it.id, name: it.name }))
|
|
}
|
|
|
|
function getPlayers(): Array<{ id: number; name: string }> {
|
|
return uniqueListGetElements(characters).map((it) => ({ id: it.id, name: it.name }))
|
|
}
|
|
|
|
function refreshMarkersVisibility(hidden: boolean) {
|
|
const ctx = markerCtx()
|
|
if (hidden) {
|
|
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
|
|
} else {
|
|
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
|
|
uniqueListGetElements(markers)
|
|
.filter((it) => it.map === getCurrentMapId() || it.map === overlayLayer.map)
|
|
.forEach((it) => it.add(ctx))
|
|
}
|
|
}
|
|
|
|
function refreshOverlayMarkers(overlayMapIdValue: number) {
|
|
overlayLayer.map = overlayMapIdValue
|
|
overlayLayer.redraw()
|
|
const ctx = markerCtx()
|
|
uniqueListGetElements(markers).forEach((it) => it.remove(ctx))
|
|
uniqueListGetElements(markers)
|
|
.filter((it) => it.map === getCurrentMapId() || it.map === overlayMapIdValue)
|
|
.forEach((it) => it.add(ctx))
|
|
}
|
|
|
|
function findMarkerById(id: number): MapMarker | undefined {
|
|
return uniqueListById(markers, id)
|
|
}
|
|
|
|
function findCharacterById(id: number): MapCharacter | undefined {
|
|
return uniqueListById(characters, id)
|
|
}
|
|
|
|
return {
|
|
markers,
|
|
characters,
|
|
changeMap,
|
|
updateMarkers,
|
|
updateCharacters,
|
|
getQuestGivers,
|
|
getPlayers,
|
|
refreshMarkersVisibility,
|
|
refreshOverlayMarkers,
|
|
findMarkerById,
|
|
findCharacterById,
|
|
}
|
|
}
|