Update project documentation and improve frontend functionality
- Updated the backend documentation in CONTRIBUTING.md and README.md to reflect changes in application structure and API endpoints. - Enhanced the frontend components in MapView.vue for better handling of context menu actions. - Added new types and interfaces in TypeScript for improved type safety in the frontend. - Introduced new utility classes for managing characters and markers in the map. - Updated .gitignore to include .vscode directory for better development environment management.
This commit is contained in:
@@ -99,10 +99,10 @@
|
||||
class="fixed z-[1000] bg-base-100 shadow-lg rounded-lg border border-base-300 py-1 min-w-[180px]"
|
||||
:style="{ left: contextMenu.tile.x + 'px', top: contextMenu.tile.y + 'px' }"
|
||||
>
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="wipeTile(contextMenu.tile.data); contextMenu.tile.show = false">
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.tile.data && (wipeTile(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
||||
Wipe tile {{ contextMenu.tile.data?.coords?.x }}, {{ contextMenu.tile.data?.coords?.y }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="openCoordSet(contextMenu.tile.data); contextMenu.tile.show = false">
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.tile.data && (openCoordSet(contextMenu.tile.data), (contextMenu.tile.show = false))">
|
||||
Rewrite tile coords
|
||||
</button>
|
||||
</div>
|
||||
@@ -112,7 +112,7 @@
|
||||
class="fixed z-[1000] bg-base-100 shadow-lg rounded-lg border border-base-300 py-1 min-w-[180px]"
|
||||
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
||||
>
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="hideMarkerById(contextMenu.marker.data?.id); contextMenu.marker.show = false">
|
||||
<button type="button" class="btn btn-ghost btn-sm w-full justify-start" @click="contextMenu.marker.data?.id != null && (hideMarkerById(contextMenu.marker.data.id), (contextMenu.marker.show = false))">
|
||||
Hide marker {{ contextMenu.marker.data?.name }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -157,7 +157,6 @@ const props = withDefaults(
|
||||
)
|
||||
|
||||
const mapRef = ref<HTMLElement | null>(null)
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const api = useMapApi()
|
||||
|
||||
@@ -191,8 +190,8 @@ let markerLayer: L.LayerGroup | null = null
|
||||
let source: EventSource | null = null
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||
let mapid = 0
|
||||
let markers: InstanceType<typeof UniqueList> | null = null
|
||||
let characters: InstanceType<typeof UniqueList> | null = null
|
||||
let markers: UniqueList<InstanceType<typeof Marker>> | null = null
|
||||
let characters: UniqueList<InstanceType<typeof Character>> | null = null
|
||||
let markersHidden = false
|
||||
let autoMode = false
|
||||
|
||||
@@ -305,7 +304,7 @@ onMounted(async () => {
|
||||
maps.value = mapsList
|
||||
mapsLoaded.value = true
|
||||
|
||||
const config = await api.getConfig().catch(() => ({}))
|
||||
const config = (await api.getConfig().catch(() => ({}))) as { title?: string; auths?: string[] }
|
||||
if (config?.title) document.title = config.title
|
||||
if (config?.auths) auths.value = config.auths
|
||||
|
||||
@@ -322,7 +321,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const initialMapId =
|
||||
props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? mapsList[0].ID : 0
|
||||
props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0
|
||||
mapid = initialMapId
|
||||
|
||||
const tileBase = (useRuntimeConfig().app.baseURL as string) ?? '/'
|
||||
@@ -336,10 +335,10 @@ onMounted(async () => {
|
||||
updateWhenIdle: true,
|
||||
keepBuffer: 2,
|
||||
}) as any
|
||||
layer.map = initialMapId
|
||||
layer.invalidTile =
|
||||
layer!.map = initialMapId
|
||||
layer!.invalidTile =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
|
||||
layer.addTo(map)
|
||||
layer!.addTo(map)
|
||||
|
||||
overlayLayer = new SmartTileLayer(tileUrl, {
|
||||
minZoom: 1,
|
||||
@@ -351,10 +350,10 @@ onMounted(async () => {
|
||||
updateWhenIdle: true,
|
||||
keepBuffer: 2,
|
||||
}) as any
|
||||
overlayLayer.map = -1
|
||||
overlayLayer.invalidTile =
|
||||
overlayLayer!.map = -1
|
||||
overlayLayer!.invalidTile =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
||||
overlayLayer.addTo(map)
|
||||
overlayLayer!.addTo(map)
|
||||
|
||||
coordLayer = new GridCoordLayer({
|
||||
tileSize: TileSize,
|
||||
@@ -362,7 +361,7 @@ onMounted(async () => {
|
||||
maxZoom: HnHMaxZoom,
|
||||
opacity: 0,
|
||||
visible: false,
|
||||
})
|
||||
} as any)
|
||||
coordLayer.addTo(map)
|
||||
coordLayer.setZIndex(500)
|
||||
|
||||
@@ -398,9 +397,9 @@ onMounted(async () => {
|
||||
if (!Array.isArray(updates)) return
|
||||
for (const u of updates) {
|
||||
const key = `${u.M}:${u.X}:${u.Y}:${u.Z}`
|
||||
layer.cache[key] = u.T
|
||||
layer!.cache[key] = u.T
|
||||
if (overlayLayer) overlayLayer.cache[key] = u.T
|
||||
if (layer.map === u.M) layer.refresh(u.X, u.Y, u.Z)
|
||||
if (layer!.map === u.M) layer!.refresh(u.X, u.Y, u.Z)
|
||||
if (overlayLayer && overlayLayer.map === u.M) overlayLayer.refresh(u.X, u.Y, u.Z)
|
||||
}
|
||||
// After initial batch (or any batch), redraw so tiles re-request with filled cache
|
||||
@@ -436,8 +435,8 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
markers = new UniqueList()
|
||||
characters = new UniqueList()
|
||||
markers = new UniqueList<InstanceType<typeof Marker>>()
|
||||
characters = new UniqueList<InstanceType<typeof Character>>()
|
||||
|
||||
updateCharacters(charactersData as any[])
|
||||
|
||||
@@ -449,9 +448,12 @@ onMounted(async () => {
|
||||
selectedMapId.value = props.mapId
|
||||
map.setView(latLng, props.zoom)
|
||||
} else if (mapsList.length > 0) {
|
||||
changeMap(mapsList[0].ID)
|
||||
selectedMapId.value = mapsList[0].ID
|
||||
map.setView([0, 0], HnHDefaultZoom)
|
||||
const first = mapsList[0]
|
||||
if (first) {
|
||||
changeMap(first.ID)
|
||||
selectedMapId.value = first.ID
|
||||
map.setView([0, 0], HnHDefaultZoom)
|
||||
}
|
||||
}
|
||||
|
||||
// Recompute map size after layout (fixes grid/container height chain in Nuxt)
|
||||
@@ -518,13 +520,13 @@ onMounted(async () => {
|
||||
|
||||
watch(showGridCoordinates, (v) => {
|
||||
if (coordLayer) {
|
||||
coordLayer.options.visible = v
|
||||
;(coordLayer.options as { visible?: boolean }).visible = v
|
||||
coordLayer.setOpacity(v ? 1 : 0)
|
||||
if (v && map) {
|
||||
coordLayer.bringToFront?.()
|
||||
coordLayer.redraw?.()
|
||||
map.invalidateSize()
|
||||
} else {
|
||||
} else if (coordLayer) {
|
||||
coordLayer.redraw?.()
|
||||
}
|
||||
}
|
||||
@@ -549,7 +551,6 @@ onMounted(async () => {
|
||||
changeMap(character.map)
|
||||
const latlng = map!.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
||||
map!.setView(latlng, HnHMaxZoom)
|
||||
router.push(`/character/${value}`)
|
||||
autoMode = true
|
||||
} else {
|
||||
map!.setView([0, 0], HnHMinZoom)
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
export interface MeResponse {
|
||||
username: string
|
||||
auths: string[]
|
||||
tokens?: string[]
|
||||
prefix?: string
|
||||
}
|
||||
import type { ConfigResponse, MapInfo, MapInfoAdmin, MeResponse, SettingsResponse } from '~/types/api'
|
||||
|
||||
export interface MapInfoAdmin {
|
||||
ID: number
|
||||
Name: string
|
||||
Hidden: boolean
|
||||
Priority: boolean
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
prefix: string
|
||||
defaultHide: boolean
|
||||
title: string
|
||||
}
|
||||
export type { ConfigResponse, MapInfo, MapInfoAdmin, MeResponse, SettingsResponse }
|
||||
|
||||
// Singleton: shared by all useMapApi() callers so 401 triggers one global handler (e.g. app.vue)
|
||||
const onApiErrorCallbacks: (() => void)[] = []
|
||||
@@ -46,7 +30,7 @@ export function useMapApi() {
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
return request<{ title?: string; auths?: string[] }>('config')
|
||||
return request<ConfigResponse>('config')
|
||||
}
|
||||
|
||||
async function getCharacters() {
|
||||
@@ -58,7 +42,7 @@ export function useMapApi() {
|
||||
}
|
||||
|
||||
async function getMaps() {
|
||||
return request<Record<string, { ID: number; Name: string; size?: number }>>('maps')
|
||||
return request<Record<string, MapInfo>>('maps')
|
||||
}
|
||||
|
||||
// Auth
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
import { HnHMaxZoom } from '~/lib/LeafletCustomTypes'
|
||||
import * as L from 'leaflet'
|
||||
|
||||
export interface CharacterData {
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
id: number
|
||||
map: number
|
||||
}
|
||||
|
||||
export interface MapViewRef {
|
||||
map: L.Map
|
||||
mapid: number
|
||||
markerLayer?: L.LayerGroup
|
||||
}
|
||||
|
||||
export class Character {
|
||||
constructor(characterData) {
|
||||
name: string
|
||||
position: { x: number; y: number }
|
||||
type: string
|
||||
id: number
|
||||
map: number
|
||||
marker: L.Marker | null = null
|
||||
text: string
|
||||
value: number
|
||||
onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||
|
||||
constructor(characterData: CharacterData) {
|
||||
this.name = characterData.name
|
||||
this.position = characterData.position
|
||||
this.type = characterData.type
|
||||
this.id = characterData.id
|
||||
this.map = characterData.map
|
||||
this.marker = null
|
||||
this.text = this.name
|
||||
this.value = this.id
|
||||
this.onClick = null
|
||||
}
|
||||
|
||||
getId() {
|
||||
getId(): string {
|
||||
return `${this.name}`
|
||||
}
|
||||
|
||||
remove(mapview) {
|
||||
remove(mapview: MapViewRef): void {
|
||||
if (this.marker) {
|
||||
const layer = mapview.markerLayer ?? mapview.map
|
||||
layer.removeLayer(this.marker)
|
||||
@@ -26,7 +48,7 @@ export class Character {
|
||||
}
|
||||
}
|
||||
|
||||
add(mapview) {
|
||||
add(mapview: MapViewRef): void {
|
||||
if (this.map === mapview.mapid) {
|
||||
const position = mapview.map.unproject([this.position.x, this.position.y], HnHMaxZoom)
|
||||
this.marker = L.marker(position, { title: this.name })
|
||||
@@ -36,7 +58,7 @@ export class Character {
|
||||
}
|
||||
}
|
||||
|
||||
update(mapview, updated) {
|
||||
update(mapview: MapViewRef, updated: CharacterData): void {
|
||||
if (this.map !== updated.map) {
|
||||
this.remove(mapview)
|
||||
}
|
||||
@@ -51,11 +73,11 @@ export class Character {
|
||||
}
|
||||
}
|
||||
|
||||
setClickCallback(callback) {
|
||||
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
|
||||
this.onClick = callback
|
||||
}
|
||||
|
||||
callCallback(e) {
|
||||
callCallback(e: L.LeafletMouseEvent): void {
|
||||
if (this.onClick != null) this.onClick(e)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export const GridCoordLayer = L.GridLayer.extend({
|
||||
options: {
|
||||
visible: true,
|
||||
},
|
||||
createTile(coords) {
|
||||
createTile(coords: { x: number; y: number; z: number }) {
|
||||
if (!this.options.visible) {
|
||||
const element = document.createElement('div')
|
||||
element.style.width = TileSize + 'px'
|
||||
@@ -61,23 +61,23 @@ export const GridCoordLayer = L.GridLayer.extend({
|
||||
}
|
||||
return element
|
||||
},
|
||||
})
|
||||
}) as unknown as new (options?: L.GridLayerOptions) => L.GridLayer
|
||||
|
||||
export const ImageIcon = L.Icon.extend({
|
||||
options: {
|
||||
iconSize: [32, 32],
|
||||
iconAnchor: [16, 16],
|
||||
},
|
||||
})
|
||||
}) as unknown as new (options?: L.IconOptions) => L.Icon
|
||||
|
||||
const latNormalization = (90.0 * TileSize) / 2500000.0
|
||||
const lngNormalization = (180.0 * TileSize) / 2500000.0
|
||||
|
||||
const HnHProjection = {
|
||||
project(latlng) {
|
||||
project(latlng: LatLng) {
|
||||
return new Point(latlng.lat / latNormalization, latlng.lng / lngNormalization)
|
||||
},
|
||||
unproject(point) {
|
||||
unproject(point: Point) {
|
||||
return new LatLng(point.x * latNormalization, point.y * lngNormalization)
|
||||
},
|
||||
bounds: (() => new Bounds([-latNormalization, -lngNormalization], [latNormalization, lngNormalization]))(),
|
||||
@@ -85,4 +85,4 @@ const HnHProjection = {
|
||||
|
||||
export const HnHCRS = L.extend({}, L.CRS.Simple, {
|
||||
projection: HnHProjection,
|
||||
})
|
||||
}) as L.CRS
|
||||
@@ -1,38 +1,63 @@
|
||||
import { HnHMaxZoom, ImageIcon } from '~/lib/LeafletCustomTypes'
|
||||
import * as L from 'leaflet'
|
||||
|
||||
function detectType(name) {
|
||||
export interface MarkerData {
|
||||
id: number
|
||||
position: { x: number; y: number }
|
||||
name: string
|
||||
image: string
|
||||
hidden: boolean
|
||||
map: number
|
||||
}
|
||||
|
||||
export interface MapViewRef {
|
||||
map: L.Map
|
||||
mapid: number
|
||||
markerLayer: L.LayerGroup
|
||||
}
|
||||
|
||||
function detectType(name: string): string {
|
||||
if (name === 'gfx/invobjs/small/bush' || name === 'gfx/invobjs/small/bumling') return 'quest'
|
||||
if (name === 'custom') return 'custom'
|
||||
return name.substring('gfx/terobjs/mm/'.length)
|
||||
}
|
||||
|
||||
export class Marker {
|
||||
constructor(markerData) {
|
||||
id: number
|
||||
position: { x: number; y: number }
|
||||
name: string
|
||||
image: string
|
||||
type: string
|
||||
marker: L.Marker | null = null
|
||||
text: string
|
||||
value: number
|
||||
hidden: boolean
|
||||
map: number
|
||||
onClick: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||
onContext: ((e: L.LeafletMouseEvent) => void) | null = null
|
||||
|
||||
constructor(markerData: MarkerData) {
|
||||
this.id = markerData.id
|
||||
this.position = markerData.position
|
||||
this.name = markerData.name
|
||||
this.image = markerData.image
|
||||
this.type = detectType(this.image)
|
||||
this.marker = null
|
||||
this.text = this.name
|
||||
this.value = this.id
|
||||
this.hidden = markerData.hidden
|
||||
this.map = markerData.map
|
||||
this.onClick = null
|
||||
this.onContext = null
|
||||
}
|
||||
|
||||
remove(mapview) {
|
||||
remove(_mapview: MapViewRef): void {
|
||||
if (this.marker) {
|
||||
this.marker.remove()
|
||||
this.marker = null
|
||||
}
|
||||
}
|
||||
|
||||
add(mapview) {
|
||||
add(mapview: MapViewRef): void {
|
||||
if (!this.hidden) {
|
||||
let icon
|
||||
let icon: L.Icon
|
||||
if (this.image === 'gfx/terobjs/mm/custom') {
|
||||
icon = new ImageIcon({
|
||||
iconUrl: 'gfx/terobjs/mm/custom.png',
|
||||
@@ -53,7 +78,7 @@ export class Marker {
|
||||
}
|
||||
}
|
||||
|
||||
update(mapview, updated) {
|
||||
update(mapview: MapViewRef, updated: MarkerData): void {
|
||||
this.position = updated.position
|
||||
this.name = updated.name
|
||||
this.hidden = updated.hidden
|
||||
@@ -64,26 +89,26 @@ export class Marker {
|
||||
}
|
||||
}
|
||||
|
||||
jumpTo(map) {
|
||||
jumpTo(map: L.Map): void {
|
||||
if (this.marker) {
|
||||
const position = map.unproject([this.position.x, this.position.y], HnHMaxZoom)
|
||||
this.marker.setLatLng(position)
|
||||
}
|
||||
}
|
||||
|
||||
setClickCallback(callback) {
|
||||
setClickCallback(callback: (e: L.LeafletMouseEvent) => void): void {
|
||||
this.onClick = callback
|
||||
}
|
||||
|
||||
callClickCallback(e) {
|
||||
callClickCallback(e: L.LeafletMouseEvent): void {
|
||||
if (this.onClick != null) this.onClick(e)
|
||||
}
|
||||
|
||||
setContextMenu(callback) {
|
||||
setContextMenu(callback: (e: L.LeafletMouseEvent) => void): void {
|
||||
this.onContext = callback
|
||||
}
|
||||
|
||||
callContextCallback(e) {
|
||||
callContextCallback(e: L.LeafletMouseEvent): void {
|
||||
if (this.onContext != null) this.onContext(e)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import L, { Util, Browser } from 'leaflet'
|
||||
|
||||
interface SmartTileLayerCache {
|
||||
[key: string]: number | undefined
|
||||
}
|
||||
|
||||
export const SmartTileLayer = L.TileLayer.extend({
|
||||
cache: {},
|
||||
cache: {} as SmartTileLayerCache,
|
||||
invalidTile: '',
|
||||
map: 0,
|
||||
|
||||
getTileUrl(coords) {
|
||||
getTileUrl(coords: { x: number; y: number; z: number }) {
|
||||
if (!this._map) return this.invalidTile
|
||||
let zoom
|
||||
try {
|
||||
@@ -16,8 +20,8 @@ export const SmartTileLayer = L.TileLayer.extend({
|
||||
return this.getTrueTileUrl(coords, zoom)
|
||||
},
|
||||
|
||||
getTrueTileUrl(coords, zoom) {
|
||||
const data = {
|
||||
getTrueTileUrl(coords: { x: number; y: number }, zoom: number) {
|
||||
const data: Record<string, string | number | undefined> = {
|
||||
r: Browser.retina ? '@2x' : '',
|
||||
s: this._getSubdomain(coords),
|
||||
x: coords.x,
|
||||
@@ -37,7 +41,8 @@ export const SmartTileLayer = L.TileLayer.extend({
|
||||
data.cache = this.cache[cacheKey]
|
||||
|
||||
// Don't request tiles for invalid/unknown map (avoids 404 spam in console)
|
||||
if (data.map === undefined || data.map === null || data.map < 1) {
|
||||
const mapId = Number(data.map)
|
||||
if (data.map === undefined || data.map === null || mapId < 1) {
|
||||
return this.invalidTile
|
||||
}
|
||||
// Only use placeholder when server explicitly marks tile as invalid (-1)
|
||||
@@ -53,7 +58,7 @@ export const SmartTileLayer = L.TileLayer.extend({
|
||||
return Util.template(this._url, Util.extend(data, this.options))
|
||||
},
|
||||
|
||||
refresh(x, y, z) {
|
||||
refresh(x: number, y: number, z: number) {
|
||||
let zoom = z
|
||||
const maxZoom = this.options.maxZoom
|
||||
const zoomReverse = this.options.zoomReverse
|
||||
@@ -70,4 +75,10 @@ export const SmartTileLayer = L.TileLayer.extend({
|
||||
tile.el.src = this.getTrueTileUrl({ x, y }, z)
|
||||
}
|
||||
},
|
||||
})
|
||||
}) as unknown as new (urlTemplate: string, options?: L.TileLayerOptions) => L.TileLayer & {
|
||||
cache: SmartTileLayerCache
|
||||
invalidTile: string
|
||||
map: number
|
||||
getTrueTileUrl: (coords: { x: number; y: number }, zoom: number) => string
|
||||
refresh: (x: number, y: number, z: number) => void
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/**
|
||||
* Elements should have unique field "id"
|
||||
*/
|
||||
export class UniqueList {
|
||||
constructor() {
|
||||
this.elements = {}
|
||||
}
|
||||
|
||||
update(dataList, addCallback, removeCallback, updateCallback) {
|
||||
const elementsToAdd = dataList.filter((it) => this.elements[it.id] === undefined)
|
||||
const elementsToRemove = Object.keys(this.elements)
|
||||
.filter((it) => dataList.find((up) => String(up.id) === it) === undefined)
|
||||
.map((id) => this.elements[id])
|
||||
if (removeCallback) {
|
||||
elementsToRemove.forEach((it) => removeCallback(it))
|
||||
}
|
||||
if (updateCallback) {
|
||||
dataList.forEach((newElement) => {
|
||||
const oldElement = this.elements[newElement.id]
|
||||
if (oldElement) {
|
||||
updateCallback(oldElement, newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (addCallback) {
|
||||
elementsToAdd.forEach((it) => addCallback(it))
|
||||
}
|
||||
elementsToRemove.forEach((it) => delete this.elements[it.id])
|
||||
elementsToAdd.forEach((it) => (this.elements[it.id] = it))
|
||||
}
|
||||
|
||||
getElements() {
|
||||
return Object.values(this.elements)
|
||||
}
|
||||
|
||||
byId(id) {
|
||||
return this.elements[id]
|
||||
}
|
||||
}
|
||||
50
frontend-nuxt/lib/UniqueList.ts
Normal file
50
frontend-nuxt/lib/UniqueList.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Elements should have unique field "id"
|
||||
*/
|
||||
export interface Identifiable {
|
||||
id: number | string
|
||||
}
|
||||
|
||||
export class UniqueList<T extends Identifiable> {
|
||||
elements: Record<string, T> = {}
|
||||
|
||||
update(
|
||||
dataList: T[],
|
||||
addCallback?: (it: T) => void,
|
||||
removeCallback?: (it: T) => void,
|
||||
updateCallback?: (oldElement: T, newElement: T) => void
|
||||
): void {
|
||||
const elementsToAdd = dataList.filter((it) => this.elements[String(it.id)] === undefined)
|
||||
const elementsToRemove: T[] = []
|
||||
for (const id of Object.keys(this.elements)) {
|
||||
if (dataList.find((up) => String(up.id) === id) === undefined) {
|
||||
const el = this.elements[id]
|
||||
if (el) elementsToRemove.push(el)
|
||||
}
|
||||
}
|
||||
if (removeCallback) {
|
||||
elementsToRemove.forEach((it) => removeCallback(it))
|
||||
}
|
||||
if (updateCallback) {
|
||||
dataList.forEach((newElement) => {
|
||||
const oldElement = this.elements[String(newElement.id)]
|
||||
if (oldElement) {
|
||||
updateCallback(oldElement, newElement)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (addCallback) {
|
||||
elementsToAdd.forEach((it) => addCallback(it))
|
||||
}
|
||||
elementsToRemove.forEach((it) => delete this.elements[String(it.id)])
|
||||
elementsToAdd.forEach((it) => (this.elements[String(it.id)] = it))
|
||||
}
|
||||
|
||||
getElements(): T[] {
|
||||
return Object.values(this.elements)
|
||||
}
|
||||
|
||||
byId(id: number | string): T | undefined {
|
||||
return this.elements[String(id)]
|
||||
}
|
||||
}
|
||||
16
frontend-nuxt/package-lock.json
generated
16
frontend-nuxt/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"daisyui": "^3.9.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3"
|
||||
@@ -2579,6 +2580,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"daisyui": "^3.9.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.6.3"
|
||||
|
||||
30
frontend-nuxt/types/api.ts
Normal file
30
frontend-nuxt/types/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export interface MeResponse {
|
||||
username: string
|
||||
auths: string[]
|
||||
tokens?: string[]
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export interface MapInfoAdmin {
|
||||
ID: number
|
||||
Name: string
|
||||
Hidden: boolean
|
||||
Priority: boolean
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
prefix: string
|
||||
defaultHide: boolean
|
||||
title: string
|
||||
}
|
||||
|
||||
export interface ConfigResponse {
|
||||
title?: string
|
||||
auths?: string[]
|
||||
}
|
||||
|
||||
export interface MapInfo {
|
||||
ID: number
|
||||
Name: string
|
||||
size?: number
|
||||
}
|
||||
Reference in New Issue
Block a user