Enhance map functionality with bookmark features and UI improvements
- Introduced a new MapBookmarkNameModal for adding and editing bookmarks. - Updated MapView to manage selected markers for bookmarking and handle bookmark name submissions. - Enhanced MapContextMenu with an option to add markers to bookmarks. - Improved MapBookmarks component to support editing bookmark names and adding selected markers. - Refactored MapControls and MapControlsContent to integrate selected marker functionality for bookmarks. - Updated useMapBookmarks composable to include bookmark updating logic. - Removed unused grid coordinates toggle from the UI for a cleaner interface.
This commit is contained in:
@@ -34,14 +34,20 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="sseConnectionState === 'error' && mapReady"
|
v-if="sseConnectionState === 'error' && mapReady"
|
||||||
class="absolute bottom-4 left-1/2 z-[501] -translate-x-1/2 rounded-lg px-3 py-2 text-sm bg-warning/90 text-warning-content shadow"
|
class="absolute bottom-4 left-1/2 z-[501] -translate-x-1/2 rounded-lg px-3 py-2 text-sm bg-warning/90 text-warning-content shadow flex items-center gap-2"
|
||||||
role="status"
|
role="status"
|
||||||
title="Live updates will resume when reconnected"
|
|
||||||
>
|
>
|
||||||
<span class="inline-flex items-center gap-2">
|
<span class="inline-flex items-center gap-2">
|
||||||
<span class="inline-block size-2 animate-pulse rounded-full bg-current" aria-hidden="true" />
|
<span class="inline-block size-2 animate-pulse rounded-full bg-current" aria-hidden="true" />
|
||||||
Reconnecting…
|
Connection lost. Reconnecting…
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs text-warning-content hover:bg-warning-content/20"
|
||||||
|
@click="reloadPage"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-2 right-2 z-[501] flex flex-col gap-1">
|
<div class="absolute top-2 right-2 z-[501] flex flex-col gap-1">
|
||||||
<button
|
<button
|
||||||
@@ -78,8 +84,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<MapControls
|
<MapControls
|
||||||
:show-grid-coordinates="mapLogic.state.showGridCoordinates.value"
|
|
||||||
@update:show-grid-coordinates="(v) => (mapLogic.state.showGridCoordinates.value = v)"
|
|
||||||
:hide-markers="mapLogic.state.hideMarkers.value"
|
:hide-markers="mapLogic.state.hideMarkers.value"
|
||||||
@update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
|
@update:hide-markers="(v) => (mapLogic.state.hideMarkers.value = v)"
|
||||||
:selected-map-id="mapLogic.state.selectedMapId.value"
|
:selected-map-id="mapLogic.state.selectedMapId.value"
|
||||||
@@ -95,6 +99,7 @@
|
|||||||
:players="players"
|
:players="players"
|
||||||
:current-map-id="mapLogic.state.mapid.value"
|
:current-map-id="mapLogic.state.mapid.value"
|
||||||
:current-coords="mapLogic.state.displayCoords.value"
|
:current-coords="mapLogic.state.displayCoords.value"
|
||||||
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
@zoom-in="mapLogic.zoomIn(leafletMap)"
|
||||||
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
@zoom-out="mapLogic.zoomOutControl(leafletMap)"
|
||||||
@reset-view="mapLogic.resetView(leafletMap)"
|
@reset-view="mapLogic.resetView(leafletMap)"
|
||||||
@@ -102,9 +107,11 @@
|
|||||||
/>
|
/>
|
||||||
<MapContextMenu
|
<MapContextMenu
|
||||||
:context-menu="mapLogic.contextMenu"
|
:context-menu="mapLogic.contextMenu"
|
||||||
|
:is-admin="auths.includes('admin')"
|
||||||
@wipe-tile="onWipeTile"
|
@wipe-tile="onWipeTile"
|
||||||
@rewrite-coords="onRewriteCoords"
|
@rewrite-coords="onRewriteCoords"
|
||||||
@hide-marker="onHideMarker"
|
@hide-marker="onHideMarker"
|
||||||
|
@add-to-bookmarks="onAddMarkerToBookmarks"
|
||||||
/>
|
/>
|
||||||
<MapCoordSetModal
|
<MapCoordSetModal
|
||||||
:coord-set-from="mapLogic.coordSetFrom.value"
|
:coord-set-from="mapLogic.coordSetFrom.value"
|
||||||
@@ -113,6 +120,13 @@
|
|||||||
@close="mapLogic.closeCoordSetModal()"
|
@close="mapLogic.closeCoordSetModal()"
|
||||||
@submit="onSubmitCoordSet"
|
@submit="onSubmitCoordSet"
|
||||||
/>
|
/>
|
||||||
|
<MapBookmarkNameModal
|
||||||
|
:open="bookmarkNameModalOpen"
|
||||||
|
:default-name="bookmarkNameModalDefaultName"
|
||||||
|
:title="bookmarkNameModalTitle"
|
||||||
|
@close="bookmarkNameModalOpen = false"
|
||||||
|
@submit="onBookmarkNameModalSubmit"
|
||||||
|
/>
|
||||||
<MapShortcutsOverlay :open="showShortcutsOverlay" @close="showShortcutsOverlay = false" />
|
<MapShortcutsOverlay :open="showShortcutsOverlay" @close="showShortcutsOverlay = false" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -122,6 +136,7 @@ import MapControls from '~/components/map/MapControls.vue'
|
|||||||
import MapCoordsDisplay from '~/components/map/MapCoordsDisplay.vue'
|
import MapCoordsDisplay from '~/components/map/MapCoordsDisplay.vue'
|
||||||
import MapContextMenu from '~/components/map/MapContextMenu.vue'
|
import MapContextMenu from '~/components/map/MapContextMenu.vue'
|
||||||
import MapCoordSetModal from '~/components/map/MapCoordSetModal.vue'
|
import MapCoordSetModal from '~/components/map/MapCoordSetModal.vue'
|
||||||
|
import MapBookmarkNameModal from '~/components/map/MapBookmarkNameModal.vue'
|
||||||
import MapShortcutsOverlay from '~/components/map/MapShortcutsOverlay.vue'
|
import MapShortcutsOverlay from '~/components/map/MapShortcutsOverlay.vue'
|
||||||
import { HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
import { HnHDefaultZoom, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
import { initLeafletMap, type MapInitResult } from '~/composables/useMapInit'
|
import { initLeafletMap, type MapInitResult } from '~/composables/useMapInit'
|
||||||
@@ -146,10 +161,59 @@ const props = withDefaults(
|
|||||||
const mapContainerRef = ref<HTMLElement | null>(null)
|
const mapContainerRef = ref<HTMLElement | null>(null)
|
||||||
const mapRef = ref<HTMLElement | null>(null)
|
const mapRef = ref<HTMLElement | null>(null)
|
||||||
const api = useMapApi()
|
const api = useMapApi()
|
||||||
|
const toast = useToast()
|
||||||
/** Global state for navbar Live/Offline: true when at least one character is owned by current user's tokens. */
|
/** 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 mapLive = useState<boolean>('mapLive', () => false)
|
||||||
const mapLogic = useMapLogic()
|
const mapLogic = useMapLogic()
|
||||||
|
const mapBookmarks = useMapBookmarks()
|
||||||
const { resolvePath } = useAppPaths()
|
const { resolvePath } = useAppPaths()
|
||||||
|
|
||||||
|
/** Payload for bookmark name modal: add new bookmark or edit existing by id. */
|
||||||
|
type BookmarkModalPayload =
|
||||||
|
| { kind: 'add'; mapId: number; x: number; y: number; zoom?: number }
|
||||||
|
| { kind: 'edit'; editId: string }
|
||||||
|
|
||||||
|
const bookmarkNameModalOpen = ref(false)
|
||||||
|
const bookmarkNameModalDefaultName = ref('')
|
||||||
|
const bookmarkNameModalTitle = ref('Add bookmark')
|
||||||
|
const bookmarkNameModalPendingData = ref<BookmarkModalPayload | null>(null)
|
||||||
|
|
||||||
|
function openBookmarkModal(defaultName: string, title: string, data: BookmarkModalPayload) {
|
||||||
|
bookmarkNameModalDefaultName.value = defaultName
|
||||||
|
bookmarkNameModalTitle.value = title
|
||||||
|
bookmarkNameModalPendingData.value = data
|
||||||
|
bookmarkNameModalOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBookmarkNameModalSubmit(name: string) {
|
||||||
|
const data = bookmarkNameModalPendingData.value
|
||||||
|
bookmarkNameModalOpen.value = false
|
||||||
|
bookmarkNameModalPendingData.value = null
|
||||||
|
if (!data) return
|
||||||
|
if (data.kind === 'add') {
|
||||||
|
mapBookmarks.add({ name, mapId: data.mapId, x: data.x, y: data.y, zoom: data.zoom })
|
||||||
|
} else {
|
||||||
|
mapBookmarks.update(data.editId, { name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAddMarkerToBookmarks() {
|
||||||
|
mapLogic.closeContextMenus()
|
||||||
|
const id = mapLogic.contextMenu.marker.data?.id
|
||||||
|
if (id == null || !layersManager) return
|
||||||
|
const marker = layersManager.findMarkerById(id)
|
||||||
|
if (!marker) return
|
||||||
|
const x = Math.floor(marker.position.x / TileSize)
|
||||||
|
const y = Math.floor(marker.position.y / TileSize)
|
||||||
|
openBookmarkModal(marker.name, 'Add bookmark', {
|
||||||
|
kind: 'add',
|
||||||
|
mapId: marker.map,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
provide('openBookmarkModal', openBookmarkModal)
|
||||||
const mapNavigate = useMapNavigate()
|
const mapNavigate = useMapNavigate()
|
||||||
const fullscreen = useFullscreen(mapContainerRef)
|
const fullscreen = useFullscreen(mapContainerRef)
|
||||||
const showShortcutsOverlay = ref(false)
|
const showShortcutsOverlay = ref(false)
|
||||||
@@ -185,12 +249,36 @@ const maps = ref<MapInfo[]>([])
|
|||||||
const mapsLoaded = ref(false)
|
const mapsLoaded = ref(false)
|
||||||
const questGivers = ref<Array<{ id: number; name: string }>>([])
|
const questGivers = ref<Array<{ id: number; name: string }>>([])
|
||||||
const players = ref<Array<{ id: number; name: string }>>([])
|
const players = ref<Array<{ id: number; name: string }>>([])
|
||||||
const auths = ref<string[]>([])
|
/** Single source of truth: layout updates me, we derive auths for context menu. */
|
||||||
|
const me = useState<MeResponse | null>('me', () => null)
|
||||||
|
const auths = computed(() => me.value?.auths ?? [])
|
||||||
|
|
||||||
let leafletMap: L.Map | null = null
|
let leafletMap: L.Map | null = null
|
||||||
let mapInit: MapInitResult | null = null
|
let mapInit: MapInitResult | null = null
|
||||||
let updatesHandle: UseMapUpdatesReturn | null = null
|
let updatesHandle: UseMapUpdatesReturn | null = null
|
||||||
let layersManager: MapLayersManager | null = null
|
let layersManager: MapLayersManager | null = null
|
||||||
|
|
||||||
|
/** Selected marker as bookmark target: grid coords + name, or null. Passed to MapControls → MapBookmarks. */
|
||||||
|
const selectedMarkerForBookmark = ref<{ mapId: number; x: number; y: number; name: string } | null>(null)
|
||||||
|
|
||||||
|
function updateSelectedMarkerForBookmark() {
|
||||||
|
const id = mapLogic.state.selectedMarkerId.value
|
||||||
|
if (id == null || !layersManager) {
|
||||||
|
selectedMarkerForBookmark.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const marker = layersManager.findMarkerById(id)
|
||||||
|
if (!marker) {
|
||||||
|
selectedMarkerForBookmark.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
selectedMarkerForBookmark.value = {
|
||||||
|
mapId: marker.map,
|
||||||
|
x: Math.floor(marker.position.x / TileSize),
|
||||||
|
y: Math.floor(marker.position.y / TileSize),
|
||||||
|
name: marker.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
let intervalId: ReturnType<typeof setInterval> | null = null
|
let intervalId: ReturnType<typeof setInterval> | null = null
|
||||||
let autoMode = false
|
let autoMode = false
|
||||||
let mapContainer: HTMLElement | null = null
|
let mapContainer: HTMLElement | null = null
|
||||||
@@ -202,10 +290,16 @@ function toLatLng(x: number, y: number) {
|
|||||||
return leafletMap!.unproject([x, y], HnHMaxZoom)
|
return leafletMap!.unproject([x, y], HnHMaxZoom)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
async function onWipeTile(coords: { x: number; y: number } | undefined) {
|
||||||
if (!coords) return
|
if (!coords) return
|
||||||
mapLogic.closeContextMenus()
|
mapLogic.closeContextMenus()
|
||||||
api.adminWipeTile({ map: mapLogic.state.mapid.value, x: coords.x, y: coords.y })
|
try {
|
||||||
|
await api.adminWipeTile({ map: mapLogic.state.mapid.value, x: coords.x, y: coords.y })
|
||||||
|
toast.success('Tile wiped')
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : 'Failed to wipe tile'
|
||||||
|
toast.error(msg === 'Forbidden' ? 'No permission' : msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
||||||
@@ -214,16 +308,23 @@ function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
|||||||
mapLogic.openCoordSet(coords)
|
mapLogic.openCoordSet(coords)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onHideMarker(id: number | undefined) {
|
async function onHideMarker(id: number | undefined) {
|
||||||
if (id == null) return
|
if (id == null) return
|
||||||
mapLogic.closeContextMenus()
|
mapLogic.closeContextMenus()
|
||||||
api.adminHideMarker({ id })
|
try {
|
||||||
|
await api.adminHideMarker({ id })
|
||||||
const m = layersManager?.findMarkerById(id)
|
const m = layersManager?.findMarkerById(id)
|
||||||
if (m) m.remove({ map: leafletMap!, markerLayer: mapInit!.markerLayer, mapid: mapLogic.state.mapid.value })
|
if (m) m.remove({ map: leafletMap!, markerLayer: mapInit!.markerLayer, mapid: mapLogic.state.mapid.value })
|
||||||
|
toast.success('Marker hidden')
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : 'Failed to hide marker'
|
||||||
|
toast.error(msg === 'Forbidden' ? 'No permission' : msg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: number }) {
|
async function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: number }) {
|
||||||
api.adminSetCoords({
|
try {
|
||||||
|
await api.adminSetCoords({
|
||||||
map: mapLogic.state.mapid.value,
|
map: mapLogic.state.mapid.value,
|
||||||
fx: from.x,
|
fx: from.x,
|
||||||
fy: from.y,
|
fy: from.y,
|
||||||
@@ -231,6 +332,15 @@ function onSubmitCoordSet(from: { x: number; y: number }, to: { x: number; y: nu
|
|||||||
ty: to.y,
|
ty: to.y,
|
||||||
})
|
})
|
||||||
mapLogic.closeCoordSetModal()
|
mapLogic.closeCoordSetModal()
|
||||||
|
toast.success('Coordinates updated')
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : 'Failed to set coordinates'
|
||||||
|
toast.error(msg === 'Forbidden' ? 'No permission' : msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reloadPage() {
|
||||||
|
if (import.meta.client) window.location.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeydown(e: KeyboardEvent) {
|
function onKeydown(e: KeyboardEvent) {
|
||||||
@@ -253,13 +363,18 @@ function onKeydown(e: KeyboardEvent) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (e.key === 'G') {
|
if (e.key === 'f' || e.key === 'F') {
|
||||||
mapLogic.state.showGridCoordinates.value = !mapLogic.state.showGridCoordinates.value
|
mapNavigate.focusSearch()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (e.key === 'f' || e.key === 'F') {
|
if (e.key === '+' || e.key === '=') {
|
||||||
mapNavigate.focusSearch()
|
mapLogic.zoomIn(leafletMap)
|
||||||
|
e.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.key === '-') {
|
||||||
|
mapLogic.zoomOutControl(leafletMap)
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -274,12 +389,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const L = (await import('leaflet')).default
|
const L = (await import('leaflet')).default
|
||||||
|
|
||||||
// Load maps, characters, config and me in parallel so map can show sooner.
|
// Load maps, characters and config in parallel. User (me) is loaded by layout.
|
||||||
const [charactersData, mapsData, config, user] = await Promise.all([
|
const [charactersData, mapsData, config] = await Promise.all([
|
||||||
api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
|
api.getCharacters().then((d) => (Array.isArray(d) ? d : [])).catch(() => []),
|
||||||
api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
|
api.getMaps().then((d) => (d && typeof d === 'object' ? d : {})).catch(() => ({})),
|
||||||
api.getConfig().catch(() => ({})) as Promise<ConfigResponse>,
|
api.getConfig().catch(() => ({})) as Promise<ConfigResponse>,
|
||||||
api.me().catch(() => null) as Promise<MeResponse | null>,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const mapsList: MapInfo[] = []
|
const mapsList: MapInfo[] = []
|
||||||
@@ -302,7 +416,6 @@ onMounted(async () => {
|
|||||||
mapsLoaded.value = true
|
mapsLoaded.value = true
|
||||||
|
|
||||||
if (config?.title) document.title = config.title
|
if (config?.title) document.title = config.title
|
||||||
auths.value = user?.auths ?? config?.auths ?? []
|
|
||||||
|
|
||||||
const initialMapId =
|
const initialMapId =
|
||||||
props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0
|
props.mapId != null && props.mapId >= 1 ? props.mapId : mapsList.length > 0 ? (mapsList[0]?.ID ?? 0) : 0
|
||||||
@@ -316,8 +429,9 @@ onMounted(async () => {
|
|||||||
contextMenuHandler = (ev: MouseEvent) => {
|
contextMenuHandler = (ev: MouseEvent) => {
|
||||||
const target = ev.target as Node
|
const target = ev.target as Node
|
||||||
if (!mapContainer?.contains(target)) return
|
if (!mapContainer?.contains(target)) return
|
||||||
|
// Right-click on a marker: let the marker's context menu open instead of tile menu
|
||||||
|
if ((target as Element).closest?.('.leaflet-marker-icon')) return
|
||||||
const isAdmin = auths.value.includes('admin')
|
const isAdmin = auths.value.includes('admin')
|
||||||
if (import.meta.dev) console.log('[MapView contextmenu]', { isAdmin, auths: auths.value })
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
ev.preventDefault()
|
ev.preventDefault()
|
||||||
ev.stopPropagation()
|
ev.stopPropagation()
|
||||||
@@ -414,6 +528,7 @@ onMounted(async () => {
|
|||||||
if (!mounted) return
|
if (!mounted) return
|
||||||
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
layersManager!.updateMarkers(Array.isArray(body) ? body : [])
|
||||||
questGivers.value = layersManager!.getQuestGivers()
|
questGivers.value = layersManager!.getQuestGivers()
|
||||||
|
updateSelectedMarkerForBookmark()
|
||||||
})
|
})
|
||||||
|
|
||||||
const CHARACTER_POLL_MS = 4000
|
const CHARACTER_POLL_MS = 4000
|
||||||
@@ -453,85 +568,73 @@ onMounted(async () => {
|
|||||||
document.addEventListener('visibilitychange', visibilityChangeHandler)
|
document.addEventListener('visibilitychange', visibilityChangeHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(mapLogic.state.showGridCoordinates, (v) => {
|
|
||||||
if (!mapInit?.coordLayer || !leafletMap) return
|
|
||||||
const coordLayer = mapInit.coordLayer
|
|
||||||
const layerWithMap = coordLayer as L.GridLayer & { _map?: L.Map }
|
|
||||||
if (v) {
|
|
||||||
;(coordLayer.options as { visible?: boolean }).visible = true
|
|
||||||
if (!layerWithMap._map) {
|
|
||||||
coordLayer.addTo(leafletMap)
|
|
||||||
coordLayer.setZIndex(500)
|
|
||||||
coordLayer.setOpacity(1)
|
|
||||||
coordLayer.bringToFront?.()
|
|
||||||
}
|
|
||||||
leafletMap.invalidateSize()
|
|
||||||
nextTick(() => {
|
|
||||||
coordLayer.redraw?.()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
coordLayer.setOpacity(0)
|
|
||||||
coordLayer.remove()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => fullscreen.isFullscreen.value,
|
() => fullscreen.isFullscreen.value,
|
||||||
() => {
|
() => {
|
||||||
|
if (!mounted) return
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (leafletMap) leafletMap.invalidateSize()
|
if (mounted && leafletMap) leafletMap.invalidateSize()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(mapLogic.state.hideMarkers, (v) => {
|
watch(mapLogic.state.hideMarkers, (v) => {
|
||||||
layersManager?.refreshMarkersVisibility(v)
|
if (!mounted || !layersManager) return
|
||||||
|
layersManager.refreshMarkersVisibility(v)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mapLogic.state.trackingCharacterId, (value) => {
|
watch(mapLogic.state.trackingCharacterId, (value) => {
|
||||||
|
if (!mounted || !leafletMap || !layersManager) return
|
||||||
if (value === -1) return
|
if (value === -1) return
|
||||||
const character = layersManager?.findCharacterById(value)
|
const character = layersManager.findCharacterById(value)
|
||||||
if (character) {
|
if (character) {
|
||||||
layersManager!.changeMap(character.map)
|
layersManager.changeMap(character.map)
|
||||||
const latlng = leafletMap!.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
const latlng = leafletMap.unproject([character.position.x, character.position.y], HnHMaxZoom)
|
||||||
leafletMap!.setView(latlng, HnHMaxZoom)
|
leafletMap.setView(latlng, HnHMaxZoom)
|
||||||
autoMode = true
|
autoMode = true
|
||||||
} else {
|
} else {
|
||||||
leafletMap!.setView([0, 0], HnHMinZoom)
|
leafletMap.setView([0, 0], HnHMinZoom)
|
||||||
mapLogic.state.trackingCharacterId.value = -1
|
mapLogic.state.trackingCharacterId.value = -1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mapLogic.state.selectedMapId, (value) => {
|
watch(mapLogic.state.selectedMapId, (value) => {
|
||||||
|
if (!mounted || !leafletMap || !layersManager) return
|
||||||
if (value == null) return
|
if (value == null) return
|
||||||
layersManager?.changeMap(value)
|
layersManager.changeMap(value)
|
||||||
const zoom = leafletMap!.getZoom()
|
const zoom = leafletMap.getZoom()
|
||||||
leafletMap!.setView([0, 0], zoom)
|
leafletMap.setView([0, 0], zoom)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mapLogic.state.overlayMapId, (value) => {
|
watch(mapLogic.state.overlayMapId, (value) => {
|
||||||
layersManager?.refreshOverlayMarkers(value ?? -1)
|
if (!mounted || !layersManager) return
|
||||||
|
layersManager.refreshOverlayMarkers(value ?? -1)
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mapLogic.state.selectedMarkerId, (value) => {
|
watch(mapLogic.state.selectedMarkerId, (value) => {
|
||||||
|
if (!mounted) return
|
||||||
|
updateSelectedMarkerForBookmark()
|
||||||
|
if (!leafletMap || !layersManager) return
|
||||||
if (value == null) return
|
if (value == null) return
|
||||||
const marker = layersManager?.findMarkerById(value)
|
const marker = layersManager.findMarkerById(value)
|
||||||
if (marker?.leafletMarker) leafletMap!.setView(marker.leafletMarker.getLatLng(), leafletMap!.getZoom())
|
if (marker?.leafletMarker) leafletMap.setView(marker.leafletMarker.getLatLng(), leafletMap.getZoom())
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(mapLogic.state.selectedPlayerId, (value) => {
|
watch(mapLogic.state.selectedPlayerId, (value) => {
|
||||||
|
if (!mounted) return
|
||||||
if (value != null) mapLogic.state.trackingCharacterId.value = value
|
if (value != null) mapLogic.state.trackingCharacterId.value = value
|
||||||
})
|
})
|
||||||
|
|
||||||
mapNavigate.setGoTo((mapId: number, x: number, y: number, zoom?: number) => {
|
mapNavigate.setGoTo((mapId: number, x: number, y: number, zoom?: number) => {
|
||||||
|
if (!mounted || !leafletMap) return
|
||||||
if (mapLogic.state.mapid.value !== mapId && layersManager) {
|
if (mapLogic.state.mapid.value !== mapId && layersManager) {
|
||||||
layersManager.changeMap(mapId)
|
layersManager.changeMap(mapId)
|
||||||
mapLogic.state.selectedMapId.value = mapId
|
mapLogic.state.selectedMapId.value = mapId
|
||||||
}
|
}
|
||||||
const latLng = toLatLng(x * TileSize, y * TileSize)
|
const latLng = toLatLng(x * TileSize, y * TileSize)
|
||||||
leafletMap!.setView(latLng, zoom ?? leafletMap!.getZoom(), { animate: true })
|
leafletMap.setView(latLng, zoom ?? leafletMap.getZoom(), { animate: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
leafletMap.on('moveend', () => mapLogic.updateDisplayCoords(leafletMap))
|
leafletMap.on('moveend', () => mapLogic.updateDisplayCoords(leafletMap))
|
||||||
@@ -567,6 +670,7 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch([measurePointA, measurePointB], () => {
|
watch([measurePointA, measurePointB], () => {
|
||||||
|
if (!mounted || !leafletMap) return
|
||||||
const layer = measureLayer.value
|
const layer = measureLayer.value
|
||||||
if (!layer) return
|
if (!layer) return
|
||||||
layer.clearLayers()
|
layer.clearLayers()
|
||||||
@@ -611,23 +715,4 @@ onBeforeUnmount(() => {
|
|||||||
mix-blend-mode: normal;
|
mix-blend-mode: normal;
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
}
|
}
|
||||||
:deep(.map-tile) {
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
min-width: 100px;
|
|
||||||
min-height: 100px;
|
|
||||||
position: relative;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.2;
|
|
||||||
pointer-events: none;
|
|
||||||
text-shadow: 0 0 2px #000, 0 1px 2px #000;
|
|
||||||
}
|
|
||||||
:deep(.map-tile-text) {
|
|
||||||
position: absolute;
|
|
||||||
left: 2px;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
74
frontend-nuxt/components/map/MapBookmarkNameModal.vue
Normal file
74
frontend-nuxt/components/map/MapBookmarkNameModal.vue
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<dialog ref="modalRef" class="modal" @cancel="$emit('close')">
|
||||||
|
<div class="modal-box transition-all duration-200" @click.stop>
|
||||||
|
<h3 class="font-bold text-lg">{{ title }}</h3>
|
||||||
|
<div class="py-2">
|
||||||
|
<label class="label py-0"><span>Name</span></label>
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="localName"
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Bookmark name"
|
||||||
|
@keydown.enter.prevent="onSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<form method="dialog" @submit.prevent="onSubmit">
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
<button type="button" class="btn" @click="$emit('close')">Cancel</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-backdrop cursor-pointer" aria-label="Close" @click="$emit('close')" />
|
||||||
|
</dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
open: boolean
|
||||||
|
defaultName: string
|
||||||
|
title?: string
|
||||||
|
}>(),
|
||||||
|
{ title: 'Add bookmark' }
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
close: []
|
||||||
|
submit: [name: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const modalRef = ref<HTMLDialogElement | null>(null)
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
const localName = ref(props.defaultName)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
(open) => {
|
||||||
|
if (open) {
|
||||||
|
localName.value = props.defaultName || ''
|
||||||
|
nextTick(() => {
|
||||||
|
modalRef.value?.showModal()
|
||||||
|
inputRef.value?.focus()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
modalRef.value?.close()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.defaultName,
|
||||||
|
(name) => {
|
||||||
|
if (props.open) localName.value = name || ''
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const name = localName.value.trim() || props.defaultName || 'Bookmark'
|
||||||
|
emit('submit', name)
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,6 +22,15 @@
|
|||||||
>
|
>
|
||||||
<span class="truncate">{{ b.name }}</span>
|
<span class="truncate">{{ b.name }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="openBookmarkModal"
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-xs btn-square shrink-0 opacity-70 hover:opacity-100"
|
||||||
|
aria-label="Edit bookmark name"
|
||||||
|
@click="openBookmarkModal(b.name, 'Edit bookmark name', { kind: 'edit', editId: b.id })"
|
||||||
|
>
|
||||||
|
<icons-icon-pencil class="size-3.5" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs btn-square shrink-0 opacity-70 hover:opacity-100 hover:text-error"
|
class="btn btn-ghost btn-xs btn-square shrink-0 opacity-70 hover:opacity-100 hover:text-error"
|
||||||
@@ -33,6 +42,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="openBookmarkModal" class="flex flex-col gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary btn-sm w-full"
|
||||||
|
:class="touchFriendly ? 'min-h-11' : ''"
|
||||||
|
:disabled="!selectedMarkerForBookmark"
|
||||||
|
title="Add selected quest giver as bookmark"
|
||||||
|
@click="onAddSelectedMarker"
|
||||||
|
>
|
||||||
|
<icons-icon-plus class="size-4" />
|
||||||
|
Add selected marker
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline btn-sm w-full"
|
class="btn btn-outline btn-sm w-full"
|
||||||
@@ -44,6 +65,7 @@
|
|||||||
<icons-icon-plus class="size-4" />
|
<icons-icon-plus class="size-4" />
|
||||||
Add current location
|
Add current location
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -52,18 +74,27 @@ import type { MapInfo } from '~/types/api'
|
|||||||
import { useMapBookmarks } from '~/composables/useMapBookmarks'
|
import { useMapBookmarks } from '~/composables/useMapBookmarks'
|
||||||
import { useMapNavigate } from '~/composables/useMapNavigate'
|
import { useMapNavigate } from '~/composables/useMapNavigate'
|
||||||
|
|
||||||
|
export type SelectedMarkerForBookmark = { mapId: number; x: number; y: number; name: string } | null
|
||||||
|
|
||||||
|
type BookmarkModalPayload =
|
||||||
|
| { kind: 'add'; mapId: number; x: number; y: number; zoom?: number }
|
||||||
|
| { kind: 'edit'; editId: string }
|
||||||
|
type OpenBookmarkModalFn = (defaultName: string, title: string, data: BookmarkModalPayload) => void
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
maps: MapInfo[]
|
maps: MapInfo[]
|
||||||
currentMapId: number | null
|
currentMapId: number | null
|
||||||
currentCoords: { x: number; y: number; z: number } | null
|
currentCoords: { x: number; y: number; z: number } | null
|
||||||
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
||||||
touchFriendly?: boolean
|
touchFriendly?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{ touchFriendly: false }
|
{ selectedMarkerForBookmark: null, touchFriendly: false }
|
||||||
)
|
)
|
||||||
|
|
||||||
const { bookmarks, add, remove } = useMapBookmarks()
|
const { bookmarks, remove } = useMapBookmarks()
|
||||||
const { goToCoords } = useMapNavigate()
|
const { goToCoords } = useMapNavigate()
|
||||||
|
const openBookmarkModal = inject<OpenBookmarkModalFn>('openBookmarkModal')
|
||||||
|
|
||||||
const canAddCurrent = computed(
|
const canAddCurrent = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -80,17 +111,20 @@ function onRemove(id: string) {
|
|||||||
remove(id)
|
remove(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onAddSelectedMarker() {
|
||||||
|
const m = props.selectedMarkerForBookmark
|
||||||
|
if (!m || !openBookmarkModal) return
|
||||||
|
openBookmarkModal(m.name, 'Add bookmark', { kind: 'add', mapId: m.mapId, x: m.x, y: m.y })
|
||||||
|
}
|
||||||
|
|
||||||
function onAddCurrent() {
|
function onAddCurrent() {
|
||||||
if (!canAddCurrent.value) return
|
if (!canAddCurrent.value || !openBookmarkModal) return
|
||||||
const mapId = props.currentMapId!
|
const mapId = props.currentMapId!
|
||||||
const { x, y, z } = props.currentCoords!
|
const { x, y, z } = props.currentCoords!
|
||||||
const mapName = props.maps.find((m) => m.ID === mapId)?.Name ?? `Map ${mapId}`
|
openBookmarkModal(
|
||||||
add({
|
`${props.maps.find((map) => map.ID === mapId)?.Name ?? `Map ${mapId}`} ${x}, ${y}`,
|
||||||
name: `${mapName} ${x}, ${y}`,
|
'Add bookmark',
|
||||||
mapId,
|
{ kind: 'add', mapId, x, y, zoom: z }
|
||||||
x,
|
)
|
||||||
y,
|
|
||||||
zoom: z,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -28,6 +28,14 @@
|
|||||||
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
:style="{ left: contextMenu.marker.x + 'px', top: contextMenu.marker.y + 'px' }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
||||||
|
@click="onAddToBookmarks"
|
||||||
|
>
|
||||||
|
Add to bookmarks
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="isAdmin"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
class="btn btn-ghost btn-sm w-full justify-start hover:bg-base-200 transition-colors"
|
||||||
@click="onHideMarker(contextMenu.marker.data?.id)"
|
@click="onHideMarker(contextMenu.marker.data?.id)"
|
||||||
@@ -41,14 +49,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ContextMenuState } from '~/composables/useMapLogic'
|
import type { ContextMenuState } from '~/composables/useMapLogic'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
contextMenu: ContextMenuState
|
contextMenu: ContextMenuState
|
||||||
}>()
|
isAdmin?: boolean
|
||||||
|
}>(),
|
||||||
|
{ isAdmin: false }
|
||||||
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
wipeTile: [coords: { x: number; y: number } | undefined]
|
wipeTile: [coords: { x: number; y: number } | undefined]
|
||||||
rewriteCoords: [coords: { x: number; y: number } | undefined]
|
rewriteCoords: [coords: { x: number; y: number } | undefined]
|
||||||
hideMarker: [id: number | undefined]
|
hideMarker: [id: number | undefined]
|
||||||
|
addToBookmarks: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
function onWipeTile(coords: { x: number; y: number } | undefined) {
|
||||||
@@ -59,6 +72,10 @@ function onRewriteCoords(coords: { x: number; y: number } | undefined) {
|
|||||||
if (coords) emit('rewriteCoords', coords)
|
if (coords) emit('rewriteCoords', coords)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onAddToBookmarks() {
|
||||||
|
emit('addToBookmarks')
|
||||||
|
}
|
||||||
|
|
||||||
function onHideMarker(id: number | undefined) {
|
function onHideMarker(id: number | undefined) {
|
||||||
if (id != null) emit('hideMarker', id)
|
if (id != null) emit('hideMarker', id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
class="flex flex-col p-4 gap-4 flex-1 min-w-0 overflow-hidden"
|
class="flex flex-col p-4 gap-4 flex-1 min-w-0 overflow-hidden"
|
||||||
>
|
>
|
||||||
<MapControlsContent
|
<MapControlsContent
|
||||||
v-model:show-grid-coordinates="showGridCoordinates"
|
|
||||||
v-model:hide-markers="hideMarkers"
|
v-model:hide-markers="hideMarkers"
|
||||||
:selected-map-id-select="selectedMapIdSelect"
|
:selected-map-id-select="selectedMapIdSelect"
|
||||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||||
@@ -52,6 +51,7 @@
|
|||||||
:players="players"
|
:players="players"
|
||||||
:current-map-id="currentMapId ?? undefined"
|
:current-map-id="currentMapId ?? undefined"
|
||||||
:current-coords="currentCoords"
|
:current-coords="currentCoords"
|
||||||
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
@zoom-in="$emit('zoomIn')"
|
@zoom-in="$emit('zoomIn')"
|
||||||
@zoom-out="$emit('zoomOut')"
|
@zoom-out="$emit('zoomOut')"
|
||||||
@reset-view="$emit('resetView')"
|
@reset-view="$emit('resetView')"
|
||||||
@@ -128,7 +128,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto overscroll-contain flex-1 p-4 pb-8">
|
<div class="overflow-y-auto overscroll-contain flex-1 p-4 pb-8">
|
||||||
<MapControlsContent
|
<MapControlsContent
|
||||||
v-model:show-grid-coordinates="showGridCoordinates"
|
|
||||||
v-model:hide-markers="hideMarkers"
|
v-model:hide-markers="hideMarkers"
|
||||||
:selected-map-id-select="selectedMapIdSelect"
|
:selected-map-id-select="selectedMapIdSelect"
|
||||||
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
@update:selected-map-id-select="(v) => (selectedMapIdSelect = v)"
|
||||||
@@ -143,6 +142,7 @@
|
|||||||
:players="players"
|
:players="players"
|
||||||
:current-map-id="currentMapId ?? undefined"
|
:current-map-id="currentMapId ?? undefined"
|
||||||
:current-coords="currentCoords"
|
:current-coords="currentCoords"
|
||||||
|
:selected-marker-for-bookmark="selectedMarkerForBookmark"
|
||||||
:touch-friendly="true"
|
:touch-friendly="true"
|
||||||
@zoom-in="$emit('zoomIn')"
|
@zoom-in="$emit('zoomIn')"
|
||||||
@zoom-out="$emit('zoomOut')"
|
@zoom-out="$emit('zoomOut')"
|
||||||
@@ -167,6 +167,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo } from '~/types/api'
|
||||||
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
||||||
import MapControlsContent from '~/components/map/MapControlsContent.vue'
|
import MapControlsContent from '~/components/map/MapControlsContent.vue'
|
||||||
|
|
||||||
interface QuestGiver {
|
interface QuestGiver {
|
||||||
@@ -186,8 +187,16 @@ const props = withDefaults(
|
|||||||
players: Player[]
|
players: Player[]
|
||||||
currentMapId?: number | null
|
currentMapId?: number | null
|
||||||
currentCoords?: { x: number; y: number; z: number } | null
|
currentCoords?: { x: number; y: number; z: number } | null
|
||||||
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
||||||
}>(),
|
}>(),
|
||||||
{ maps: () => [], questGivers: () => [], players: () => [], currentMapId: null, currentCoords: null }
|
{
|
||||||
|
maps: () => [],
|
||||||
|
questGivers: () => [],
|
||||||
|
players: () => [],
|
||||||
|
currentMapId: null,
|
||||||
|
currentCoords: null,
|
||||||
|
selectedMarkerForBookmark: null,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
@@ -197,7 +206,6 @@ defineEmits<{
|
|||||||
jumpToMarker: [id: number]
|
jumpToMarker: [id: number]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showGridCoordinates = defineModel<boolean>('showGridCoordinates', { default: false })
|
|
||||||
const hideMarkers = defineModel<boolean>('hideMarkers', { default: false })
|
const hideMarkers = defineModel<boolean>('hideMarkers', { default: false })
|
||||||
const panelCollapsed = ref(false)
|
const panelCollapsed = ref(false)
|
||||||
const sheetOpen = ref(false)
|
const sheetOpen = ref(false)
|
||||||
@@ -233,6 +241,8 @@ const selectedPlayerIdSelect = computed({
|
|||||||
selectedPlayerId.value = v === '' ? null : Number(v)
|
selectedPlayerId.value = v === '' ? null : Number(v)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedMarkerForBookmark = toRef(props, 'selectedMarkerForBookmark')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -55,15 +55,6 @@
|
|||||||
<icons-icon-eye class="size-3.5 opacity-80" aria-hidden="true" />
|
<icons-icon-eye class="size-3.5 opacity-80" aria-hidden="true" />
|
||||||
Display
|
Display
|
||||||
</h3>
|
</h3>
|
||||||
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2 touch-manipulation" :class="touchFriendly ? 'min-h-11' : ''">
|
|
||||||
<input
|
|
||||||
v-model="showGridCoordinates"
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-sm"
|
|
||||||
data-testid="show-grid-coordinates"
|
|
||||||
/>
|
|
||||||
<span>Show grid coordinates</span>
|
|
||||||
</label>
|
|
||||||
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2 touch-manipulation" :class="touchFriendly ? 'min-h-11' : ''">
|
<label class="label cursor-pointer justify-start gap-2 py-0 hover:bg-base-200/50 rounded-lg px-2 -mx-2 touch-manipulation" :class="touchFriendly ? 'min-h-11' : ''">
|
||||||
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
|
<input v-model="hideMarkers" type="checkbox" class="checkbox checkbox-sm" />
|
||||||
<span>Hide markers</span>
|
<span>Hide markers</span>
|
||||||
@@ -125,6 +116,7 @@
|
|||||||
:maps="maps"
|
:maps="maps"
|
||||||
:current-map-id="currentMapId ?? null"
|
:current-map-id="currentMapId ?? null"
|
||||||
:current-coords="currentCoords ?? null"
|
:current-coords="currentCoords ?? null"
|
||||||
|
:selected-marker-for-bookmark="selectedMarkerForBookmark ?? null"
|
||||||
:touch-friendly="touchFriendly"
|
:touch-friendly="touchFriendly"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,6 +124,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo } from '~/types/api'
|
||||||
|
import type { SelectedMarkerForBookmark } from '~/components/map/MapBookmarks.vue'
|
||||||
import MapBookmarks from '~/components/map/MapBookmarks.vue'
|
import MapBookmarks from '~/components/map/MapBookmarks.vue'
|
||||||
|
|
||||||
interface QuestGiver {
|
interface QuestGiver {
|
||||||
@@ -156,8 +149,9 @@ const props = withDefaults(
|
|||||||
selectedPlayerIdSelect: string
|
selectedPlayerIdSelect: string
|
||||||
currentMapId?: number
|
currentMapId?: number
|
||||||
currentCoords?: { x: number; y: number; z: number } | null
|
currentCoords?: { x: number; y: number; z: number } | null
|
||||||
|
selectedMarkerForBookmark?: SelectedMarkerForBookmark
|
||||||
}>(),
|
}>(),
|
||||||
{ touchFriendly: false, currentMapId: 0, currentCoords: null }
|
{ touchFriendly: false, currentMapId: 0, currentCoords: null, selectedMarkerForBookmark: null }
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -165,7 +159,6 @@ const emit = defineEmits<{
|
|||||||
zoomOut: []
|
zoomOut: []
|
||||||
resetView: []
|
resetView: []
|
||||||
jumpToMarker: [id: number]
|
jumpToMarker: [id: number]
|
||||||
'update:showGridCoordinates': [v: boolean]
|
|
||||||
'update:hideMarkers': [v: boolean]
|
'update:hideMarkers': [v: boolean]
|
||||||
'update:selectedMapIdSelect': [v: string]
|
'update:selectedMapIdSelect': [v: string]
|
||||||
'update:overlayMapId': [v: number]
|
'update:overlayMapId': [v: number]
|
||||||
@@ -173,7 +166,6 @@ const emit = defineEmits<{
|
|||||||
'update:selectedPlayerIdSelect': [v: string]
|
'update:selectedPlayerIdSelect': [v: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showGridCoordinates = defineModel<boolean>('showGridCoordinates', { required: true })
|
|
||||||
const hideMarkers = defineModel<boolean>('hideMarkers', { required: true })
|
const hideMarkers = defineModel<boolean>('hideMarkers', { required: true })
|
||||||
|
|
||||||
const selectedMapIdSelect = computed({
|
const selectedMapIdSelect = computed({
|
||||||
|
|||||||
@@ -40,10 +40,6 @@
|
|||||||
<dt class="text-base-content/80"><kbd class="kbd kbd-sm">H</kbd></dt>
|
<dt class="text-base-content/80"><kbd class="kbd kbd-sm">H</kbd></dt>
|
||||||
<dd>Toggle markers visibility</dd>
|
<dd>Toggle markers visibility</dd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-between gap-4 py-1 border-b border-base-300/50">
|
|
||||||
<dt class="text-base-content/80"><kbd class="kbd kbd-sm">G</kbd></dt>
|
|
||||||
<dd>Toggle grid coordinates</dd>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-between gap-4 py-1 border-b border-base-300/50">
|
<div class="flex justify-between gap-4 py-1 border-b border-base-300/50">
|
||||||
<dt class="text-base-content/80"><kbd class="kbd kbd-sm">F</kbd></dt>
|
<dt class="text-base-content/80"><kbd class="kbd kbd-sm">F</kbd></dt>
|
||||||
<dd>Focus search</dd>
|
<dd>Focus search</dd>
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ describe('useMapLogic', () => {
|
|||||||
|
|
||||||
it('initializes with default state', () => {
|
it('initializes with default state', () => {
|
||||||
const { state } = useMapLogic()
|
const { state } = useMapLogic()
|
||||||
expect(state.showGridCoordinates.value).toBe(false)
|
|
||||||
expect(state.hideMarkers.value).toBe(false)
|
expect(state.hideMarkers.value).toBe(false)
|
||||||
expect(state.panelCollapsed.value).toBe(false)
|
expect(state.panelCollapsed.value).toBe(false)
|
||||||
expect(state.trackingCharacterId.value).toBe(-1)
|
expect(state.trackingCharacterId.value).toBe(-1)
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ export function useMapBookmarks() {
|
|||||||
return id
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update(id: string, patch: { name: string }) {
|
||||||
|
const list = loadBookmarks()
|
||||||
|
const idx = list.findIndex((b) => b.id === id)
|
||||||
|
if (idx < 0) return false
|
||||||
|
list[idx] = { ...list[idx], name: patch.name }
|
||||||
|
saveBookmarks(list)
|
||||||
|
refresh()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function remove(id: string) {
|
function remove(id: string) {
|
||||||
const list = loadBookmarks().filter((b) => b.id !== id)
|
const list = loadBookmarks().filter((b) => b.id !== id)
|
||||||
saveBookmarks(list)
|
saveBookmarks(list)
|
||||||
@@ -70,6 +80,7 @@ export function useMapBookmarks() {
|
|||||||
bookmarks: readonly(bookmarks),
|
bookmarks: readonly(bookmarks),
|
||||||
refresh,
|
refresh,
|
||||||
add,
|
add,
|
||||||
|
update,
|
||||||
remove,
|
remove,
|
||||||
clear,
|
clear,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type L from 'leaflet'
|
import type L from 'leaflet'
|
||||||
import { GridCoordLayer, HnHCRS, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
import { HnHCRS, HnHMaxZoom, HnHMinZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
import type { GridCoordLayerOptions } from '~/lib/LeafletCustomTypes'
|
|
||||||
import { SmartTileLayer } from '~/lib/SmartTileLayer'
|
import { SmartTileLayer } from '~/lib/SmartTileLayer'
|
||||||
import type { MapInfo } from '~/types/api'
|
import type { MapInfo } from '~/types/api'
|
||||||
|
|
||||||
@@ -35,7 +34,6 @@ export interface MapInitResult {
|
|||||||
map: L.Map
|
map: L.Map
|
||||||
layer: SmartTileLayerInstance
|
layer: SmartTileLayerInstance
|
||||||
overlayLayer: SmartTileLayerInstance
|
overlayLayer: SmartTileLayerInstance
|
||||||
coordLayer: L.GridLayer
|
|
||||||
markerLayer: L.LayerGroup
|
markerLayer: L.LayerGroup
|
||||||
backendBase: string
|
backendBase: string
|
||||||
}
|
}
|
||||||
@@ -95,19 +93,6 @@ export async function initLeafletMap(
|
|||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
||||||
overlayLayer.addTo(map)
|
overlayLayer.addTo(map)
|
||||||
|
|
||||||
// Not added to map here — added in MapView when user enables "Show grid coordinates"
|
|
||||||
// so createTile() always runs with visible: true and labels render (no empty tiles from init).
|
|
||||||
const coordLayer = new GridCoordLayer({
|
|
||||||
tileSize: TileSize,
|
|
||||||
minZoom: HnHMinZoom,
|
|
||||||
maxZoom: HnHMaxZoom,
|
|
||||||
opacity: 0,
|
|
||||||
visible: false,
|
|
||||||
pane: 'tilePane',
|
|
||||||
updateWhenIdle: true,
|
|
||||||
keepBuffer: 4,
|
|
||||||
} as GridCoordLayerOptions)
|
|
||||||
|
|
||||||
const markerLayer = L.layerGroup()
|
const markerLayer = L.layerGroup()
|
||||||
markerLayer.addTo(map)
|
markerLayer.addTo(map)
|
||||||
markerLayer.setZIndex(600)
|
markerLayer.setZIndex(600)
|
||||||
@@ -122,5 +107,5 @@ export async function initLeafletMap(
|
|||||||
}
|
}
|
||||||
preloadMarkerIcons(resolvePath)
|
preloadMarkerIcons(resolvePath)
|
||||||
|
|
||||||
return { map, layer, overlayLayer, coordLayer, markerLayer, backendBase }
|
return { map, layer, overlayLayer, markerLayer, backendBase }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,9 @@ export function createMapLayers(options: MapLayersOptions): MapLayersManager {
|
|||||||
if (marker.leafletMarker) map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
|
if (marker.leafletMarker) map.setView(marker.leafletMarker.getLatLng(), HnHMaxZoom)
|
||||||
})
|
})
|
||||||
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
marker.setContextMenu((mev: L.LeafletMouseEvent) => {
|
||||||
if (getAuths().includes('admin')) {
|
|
||||||
mev.originalEvent.preventDefault()
|
mev.originalEvent.preventDefault()
|
||||||
mev.originalEvent.stopPropagation()
|
mev.originalEvent.stopPropagation()
|
||||||
onMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
onMarkerContextMenu(mev.originalEvent.clientX, mev.originalEvent.clientY, marker.id, marker.name)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(marker: MapMarker) => marker.remove(ctx),
|
(marker: MapMarker) => marker.remove(ctx),
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type L from 'leaflet'
|
|||||||
import { HnHMinZoom, HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
import { HnHMinZoom, HnHMaxZoom, TileSize } from '~/lib/LeafletCustomTypes'
|
||||||
|
|
||||||
export interface MapLogicState {
|
export interface MapLogicState {
|
||||||
showGridCoordinates: Ref<boolean>
|
|
||||||
hideMarkers: Ref<boolean>
|
hideMarkers: Ref<boolean>
|
||||||
panelCollapsed: Ref<boolean>
|
panelCollapsed: Ref<boolean>
|
||||||
trackingCharacterId: Ref<number>
|
trackingCharacterId: Ref<number>
|
||||||
@@ -48,7 +47,6 @@ export interface CoordSetState {
|
|||||||
* Map instance is passed to functions that need it (set by MapView after init).
|
* Map instance is passed to functions that need it (set by MapView after init).
|
||||||
*/
|
*/
|
||||||
export function useMapLogic() {
|
export function useMapLogic() {
|
||||||
const showGridCoordinates = ref(false)
|
|
||||||
const hideMarkers = ref(false)
|
const hideMarkers = ref(false)
|
||||||
const panelCollapsed = ref(false)
|
const panelCollapsed = ref(false)
|
||||||
const trackingCharacterId = ref(-1)
|
const trackingCharacterId = ref(-1)
|
||||||
@@ -127,7 +125,6 @@ export function useMapLogic() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const state: MapLogicState = {
|
const state: MapLogicState = {
|
||||||
showGridCoordinates,
|
|
||||||
hideMarkers,
|
hideMarkers,
|
||||||
panelCollapsed,
|
panelCollapsed,
|
||||||
trackingCharacterId,
|
trackingCharacterId,
|
||||||
|
|||||||
@@ -36,26 +36,22 @@ export interface UseMapUpdatesReturn {
|
|||||||
cleanup: () => void
|
cleanup: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const RECONNECT_INITIAL_MS = 1000
|
||||||
|
const RECONNECT_MAX_MS = 30000
|
||||||
|
|
||||||
export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesReturn {
|
export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesReturn {
|
||||||
const { backendBase, layer, overlayLayer, map, getCurrentMapId, onMerge, connectionStateRef } = options
|
const { backendBase, layer, overlayLayer, map, getCurrentMapId, onMerge, connectionStateRef } = options
|
||||||
|
|
||||||
const updatesPath = `${backendBase}/updates`
|
const updatesPath = `${backendBase}/updates`
|
||||||
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
const updatesUrl = import.meta.client ? `${window.location.origin}${updatesPath}` : updatesPath
|
||||||
const source = new EventSource(updatesUrl)
|
|
||||||
|
|
||||||
if (connectionStateRef) {
|
|
||||||
connectionStateRef.value = 'connecting'
|
|
||||||
}
|
|
||||||
source.onopen = () => {
|
|
||||||
if (connectionStateRef) connectionStateRef.value = 'open'
|
|
||||||
}
|
|
||||||
source.onerror = () => {
|
|
||||||
if (connectionStateRef) connectionStateRef.value = 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
const BATCH_MS = 50
|
const BATCH_MS = 50
|
||||||
let batch: TileUpdate[] = []
|
let batch: TileUpdate[] = []
|
||||||
let batchScheduled = false
|
let batchScheduled = false
|
||||||
|
let source: EventSource | null = null
|
||||||
|
let reconnectTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let reconnectDelayMs = RECONNECT_INITIAL_MS
|
||||||
|
let destroyed = false
|
||||||
|
|
||||||
const VISIBLE_TILE_BUFFER = 1
|
const VISIBLE_TILE_BUFFER = 1
|
||||||
|
|
||||||
@@ -101,6 +97,29 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
|
|||||||
setTimeout(applyBatch, BATCH_MS)
|
setTimeout(applyBatch, BATCH_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (destroyed || !import.meta.client) return
|
||||||
|
source = new EventSource(updatesUrl)
|
||||||
|
if (connectionStateRef) connectionStateRef.value = 'connecting'
|
||||||
|
|
||||||
|
source.onopen = () => {
|
||||||
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
||||||
|
reconnectDelayMs = RECONNECT_INITIAL_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
source.onerror = () => {
|
||||||
|
if (destroyed || !source) return
|
||||||
|
if (connectionStateRef) connectionStateRef.value = 'error'
|
||||||
|
source.close()
|
||||||
|
source = null
|
||||||
|
if (destroyed) return
|
||||||
|
reconnectTimeoutId = setTimeout(() => {
|
||||||
|
reconnectTimeoutId = null
|
||||||
|
connect()
|
||||||
|
reconnectDelayMs = Math.min(reconnectDelayMs * 2, RECONNECT_MAX_MS)
|
||||||
|
}, reconnectDelayMs)
|
||||||
|
}
|
||||||
|
|
||||||
source.onmessage = (event: MessageEvent) => {
|
source.onmessage = (event: MessageEvent) => {
|
||||||
if (connectionStateRef) connectionStateRef.value = 'open'
|
if (connectionStateRef) connectionStateRef.value = 'open'
|
||||||
try {
|
try {
|
||||||
@@ -117,8 +136,6 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source.onerror = () => {}
|
|
||||||
|
|
||||||
source.addEventListener('merge', (e: MessageEvent) => {
|
source.addEventListener('merge', (e: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
const merge: MergeEvent = JSON.parse((e?.data as string) ?? '{}')
|
const merge: MergeEvent = JSON.parse((e?.data as string) ?? '{}')
|
||||||
@@ -134,9 +151,20 @@ export function startMapUpdates(options: UseMapUpdatesOptions): UseMapUpdatesRet
|
|||||||
// Ignore merge parse errors
|
// Ignore merge parse errors
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connect()
|
||||||
|
|
||||||
function cleanup() {
|
function cleanup() {
|
||||||
|
destroyed = true
|
||||||
|
if (reconnectTimeoutId != null) {
|
||||||
|
clearTimeout(reconnectTimeoutId)
|
||||||
|
reconnectTimeoutId = null
|
||||||
|
}
|
||||||
|
if (source) {
|
||||||
source.close()
|
source.close()
|
||||||
|
source = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { cleanup }
|
return { cleanup }
|
||||||
|
|||||||
@@ -5,51 +5,6 @@ export const HnHMaxZoom = 6
|
|||||||
export const HnHMinZoom = 1
|
export const HnHMinZoom = 1
|
||||||
export const HnHDefaultZoom = 6
|
export const HnHDefaultZoom = 6
|
||||||
|
|
||||||
export interface GridCoordLayerOptions extends L.GridLayerOptions {
|
|
||||||
visible?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grid layer that draws one coordinate label per Leaflet tile in the top-left corner.
|
|
||||||
* Uses only coords.z (do not use map.getZoom()) so labels stay in sync with tiles.
|
|
||||||
* SmartTileLayer has zoomReverse: true, so URL z = 6 - coords.z; backend maps z=6 → storageZ=0.
|
|
||||||
* Game tile for this Leaflet tile: scaleFactor = 2^(HnHMaxZoom - coords.z),
|
|
||||||
* topLeft = (coords.x * scaleFactor, coords.y * scaleFactor) — same (x,y) as backend tile.
|
|
||||||
*/
|
|
||||||
export const GridCoordLayer = L.GridLayer.extend({
|
|
||||||
options: {
|
|
||||||
visible: true,
|
|
||||||
},
|
|
||||||
createTile(coords: { x: number; y: number; z: number }) {
|
|
||||||
if (!this.options.visible) {
|
|
||||||
const element = document.createElement('div')
|
|
||||||
element.style.width = TileSize + 'px'
|
|
||||||
element.style.height = TileSize + 'px'
|
|
||||||
element.classList.add('map-tile')
|
|
||||||
return element
|
|
||||||
}
|
|
||||||
const element = document.createElement('div')
|
|
||||||
element.style.width = TileSize + 'px'
|
|
||||||
element.style.height = TileSize + 'px'
|
|
||||||
element.style.position = 'relative'
|
|
||||||
element.classList.add('map-tile')
|
|
||||||
|
|
||||||
const scaleFactor = Math.pow(2, HnHMaxZoom - coords.z)
|
|
||||||
const topLeft = { x: coords.x * scaleFactor, y: coords.y * scaleFactor }
|
|
||||||
|
|
||||||
// One label per Leaflet tile at top-left (2px, 2px); same (x,y) as backend tile for this coords.
|
|
||||||
const textElement = document.createElement('div')
|
|
||||||
textElement.classList.add('map-tile-text')
|
|
||||||
textElement.textContent = `(${topLeft.x}, ${topLeft.y})`
|
|
||||||
textElement.style.position = 'absolute'
|
|
||||||
textElement.style.left = '2px'
|
|
||||||
textElement.style.top = '2px'
|
|
||||||
textElement.style.fontSize = Math.max(8, 12 - Math.log2(scaleFactor) * 2) + 'px'
|
|
||||||
element.appendChild(textElement)
|
|
||||||
return element
|
|
||||||
},
|
|
||||||
}) as unknown as new (options?: GridCoordLayerOptions) => L.GridLayer
|
|
||||||
|
|
||||||
export interface ImageIconOptions extends L.IconOptions {
|
export interface ImageIconOptions extends L.IconOptions {
|
||||||
/** When the main icon image fails to load, use this URL (e.g. data URL or default marker). */
|
/** When the main icon image fails to load, use this URL (e.g. data URL or default marker). */
|
||||||
fallbackIconUrl?: string
|
fallbackIconUrl?: string
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ async function submit() {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await api.login(user.value, pass.value)
|
await api.login(user.value, pass.value)
|
||||||
await router.push(redirect.value || '/profile')
|
await router.push(redirect.value || '/')
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
error.value = e instanceof Error ? e.message : 'Login failed'
|
error.value = e instanceof Error ? e.message : 'Login failed'
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user