From 10148c4a44676aa70b473333e42db37bd4e331e1 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 11 Jun 2026 15:29:53 +0300 Subject: [PATCH 1/6] Import files to the cloud --- map/src/frame/util/CloudGpxUploader.jsx | 67 ++------------- map/src/map/layers/CloudTrackLayer.js | 49 ++++++++++- map/src/map/layers/LocalClientTrackLayer.js | 58 ++----------- map/src/util/hooks/useCloudGpxImport.js | 90 +++++++++++++++++++++ 4 files changed, 151 insertions(+), 113 deletions(-) create mode 100644 map/src/util/hooks/useCloudGpxImport.js diff --git a/map/src/frame/util/CloudGpxUploader.jsx b/map/src/frame/util/CloudGpxUploader.jsx index 989aa441d6..4468a38a30 100644 --- a/map/src/frame/util/CloudGpxUploader.jsx +++ b/map/src/frame/util/CloudGpxUploader.jsx @@ -1,69 +1,16 @@ -import React, { useContext, useEffect } from 'react'; -import AppContext from '../../context/AppContext'; -import { useMutator } from '../../util/Utils'; +import React, { useContext } from 'react'; import { styled } from '@mui/material/styles'; -import { removeFileExtension, createTrackFreeName, saveTrackToCloud } from '../../manager/track/SaveTrackManager'; import { FREE_ACCOUNT } from '../../manager/LoginManager'; +import { DEFAULT_GROUP_NAME } from '../../manager/track/TracksManager'; import LoginContext from '../../context/LoginContext'; -import { GPX_FILE_EXT, KMZ_FILE_EXT } from '../../manager/track/TracksManager'; +import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; -export default function CloudGpxUploader({ children, folder = null, style = null }) { - const ctx = useContext(AppContext); +export default function CloudGpxUploader({ children, folder = DEFAULT_GROUP_NAME, style = null }) { const ltx = useContext(LoginContext); + const { importGpxFiles } = useCloudGpxImport({ folder }); - const [uploadedFiles, mutateUploadedFiles] = useMutator({}); - - function validName(name) { - return name !== '' && name.trim().length > 0; - } - - useEffect(() => { - for (const file in uploadedFiles) { - let open = uploadedFiles[file].selected; - let fileName = uploadedFiles[file].name; - if (validName(fileName)) { - fileName = removeFileExtension(fileName); - fileName = createTrackFreeName(fileName, ctx.tracksGroups, folder); - saveTrackToCloud({ - ctx, - ltx, - currentFolder: folder, - fileName, - type: 'GPX', - uploadedFile: uploadedFiles[file], - open, - }).then(); - mutateUploadedFiles((o) => delete o[file]); - break; // process 1 file per 1 render - } - } - }, [uploadedFiles]); - - const fileSelected = async (e) => { - const selected = e.target.files.length === 1; - ctx.setTrackLoading(Array.from(e.target.files).map((track) => removeFileExtension(track.name) + GPX_FILE_EXT)); - Array.from(e.target.files).forEach((file) => { - const reader = new FileReader(); - reader.addEventListener('load', async (e) => { - const data = e.target.result; - if (data) { - mutateUploadedFiles( - (o) => (o[file.name] = { file, selected, data: data, name: file.name, originalName: file.name }) - ); - } else { - ctx.setTrackErrorMsg({ - title: 'Import error', - msg: `Unable to import ${file.name}`, - }); - ctx.setTrackLoading([...ctx.trackLoading.filter((n) => n !== file.name)]); - } - }); - if (file.name.toLowerCase().endsWith(KMZ_FILE_EXT)) { - reader.readAsArrayBuffer(file); - } else { - reader.readAsText(file); - } - }); + const fileSelected = (e) => { + importGpxFiles(e.target.files); }; const HiddenInput = styled('input')({ display: 'none' }); diff --git a/map/src/map/layers/CloudTrackLayer.js b/map/src/map/layers/CloudTrackLayer.js index ee6038442c..70e027a01d 100644 --- a/map/src/map/layers/CloudTrackLayer.js +++ b/map/src/map/layers/CloudTrackLayer.js @@ -1,13 +1,16 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import AppContext, { isCloudTrack, OBJECT_TYPE_CLOUD_TRACK } from '../../context/AppContext'; import MapContext from '../../context/MapContext'; import { useMap } from 'react-leaflet'; import TrackLayerProvider, { redrawWptsOnLayer, WPT_SIMPLIFY_THRESHOLD } from '../util/TrackLayerProvider'; import TracksManager, { + DEFAULT_GROUP_NAME, fitBoundsOptions, getResolvedPointsGroups, getTracksArrBounds, } from '../../manager/track/TracksManager'; +import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; +import GpxMapDropOverlay from '../components/GpxMapDropOverlay'; import { encodeString, useMutator } from '../../util/Utils'; import { INFO_MENU_URL, MAIN_URL_WITH_SLASH, MENU_INFO_OPEN_SIZE, TRACKS_URL } from '../../manager/GlobalManager'; import { clusterMarkers } from '../util/Clusterizer'; @@ -164,6 +167,7 @@ const CloudTrackLayer = () => { const [selectedPointMarker, setSelectedPointMarker] = useState(null); const map = useMap(); + const { importGpxFiles } = useCloudGpxImport({ folder: DEFAULT_GROUP_NAME }); const navigate = useNavigate(); @@ -171,6 +175,47 @@ const CloudTrackLayer = () => { const [prevZoom, setPrevZoom] = useState(null); const [move, setMove] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); + const dragCounterRef = useRef(0); + + useEffect(() => { + const container = map.getContainer(); + const hasFiles = (e) => e.dataTransfer?.types?.includes('Files'); + + const onDragEnter = (e) => { + e.preventDefault(); + if (!hasFiles(e)) return; + dragCounterRef.current += 1; + setIsDragOver(true); + }; + const onDragOver = (e) => { + e.preventDefault(); + if (hasFiles(e)) e.dataTransfer.dropEffect = 'copy'; + }; + const onDragLeave = () => { + dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); + if (dragCounterRef.current === 0) setIsDragOver(false); + }; + const onDrop = (e) => { + e.preventDefault(); + const files = Array.from(e.dataTransfer?.files || []); + dragCounterRef.current = 0; + setIsDragOver(false); + importGpxFiles(files); + }; + + container.addEventListener('dragenter', onDragEnter); + container.addEventListener('dragover', onDragOver); + container.addEventListener('dragleave', onDragLeave); + container.addEventListener('drop', onDrop); + return () => { + container.removeEventListener('dragenter', onDragEnter); + container.removeEventListener('dragover', onDragOver); + container.removeEventListener('dragleave', onDragLeave); + container.removeEventListener('drop', onDrop); + }; + }, [map, importGpxFiles]); + useZoomMoveMapHandlers(map, setZoom, setMove); const getCloudWptLayers = useCallback(() => { @@ -363,6 +408,8 @@ const CloudTrackLayer = () => { ctx.setFitBoundsShareTracks(null); } }, [ctx.fitBoundsShareTracks]); + + return ; }; export default CloudTrackLayer; diff --git a/map/src/map/layers/LocalClientTrackLayer.js b/map/src/map/layers/LocalClientTrackLayer.js index e6decb5bef..6f5494d6f5 100644 --- a/map/src/map/layers/LocalClientTrackLayer.js +++ b/map/src/map/layers/LocalClientTrackLayer.js @@ -2,8 +2,6 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'rea import AppContext, { isLocalTrack, OBJECT_TYPE_LOCAL_TRACK } from '../../context/AppContext'; import MapContext from '../../context/MapContext'; import { useMap } from 'react-leaflet'; -import useLocalGpxImport from '../../util/hooks/useLocalGpxImport'; -import GpxMapDropOverlay from '../components/GpxMapDropOverlay'; import L from 'leaflet'; import TrackLayerProvider, { redrawWptsOnLayer, @@ -48,7 +46,6 @@ export default function LocalClientTrackLayer() { const ctx = useContext(AppContext); const mtx = useContext(MapContext); const map = useMap(); - const { importGpxFiles } = useLocalGpxImport(); const [registeredLayers, setRegisteredLayers] = useState({}); @@ -71,48 +68,8 @@ export default function LocalClientTrackLayer() { const [prevZoom, setPrevZoom] = useState(null); const [move, setMove] = useState(false); - const [isDragOver, setIsDragOver] = useState(false); - const dragCounterRef = useRef(0); const pendingZoomRef = useRef(false); - useEffect(() => { - const container = map.getContainer(); - const hasFiles = (e) => e.dataTransfer?.types?.includes('Files'); - - const onDragEnter = (e) => { - e.preventDefault(); - if (!hasFiles(e)) return; - dragCounterRef.current += 1; - setIsDragOver(true); - }; - const onDragOver = (e) => { - e.preventDefault(); - if (hasFiles(e)) e.dataTransfer.dropEffect = 'copy'; - }; - const onDragLeave = () => { - dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); - if (dragCounterRef.current === 0) setIsDragOver(false); - }; - const onDrop = (e) => { - e.preventDefault(); - const files = Array.from(e.dataTransfer?.files || []).filter((f) => f.name.toLowerCase().endsWith('.gpx')); - dragCounterRef.current = 0; - setIsDragOver(false); - if (files.length > 0) importGpxFiles(files); - }; - - container.addEventListener('dragenter', onDragEnter); - container.addEventListener('dragover', onDragOver); - container.addEventListener('dragleave', onDragLeave); - container.addEventListener('drop', onDrop); - return () => { - container.removeEventListener('dragenter', onDragEnter); - container.removeEventListener('dragover', onDragOver); - container.removeEventListener('dragleave', onDragLeave); - container.removeEventListener('drop', onDrop); - }; - }, [map, importGpxFiles]); - useZoomMoveMapHandlers(map, setZoom, setMove); const getLocalWptLayers = useCallback(() => { @@ -918,14 +875,11 @@ export default function LocalClientTrackLayer() { }, [JSON.stringify(lastSegmentGeoProfile)]); return ( - <> - - {openAddRoutingToTrackDialog && ( - - )} - + openAddRoutingToTrackDialog && ( + + ) ); } diff --git a/map/src/util/hooks/useCloudGpxImport.js b/map/src/util/hooks/useCloudGpxImport.js new file mode 100644 index 0000000000..66a22bf58a --- /dev/null +++ b/map/src/util/hooks/useCloudGpxImport.js @@ -0,0 +1,90 @@ +import { useCallback, useContext, useEffect } from 'react'; +import AppContext from '../../context/AppContext'; +import LoginContext from '../../context/LoginContext'; +import { FREE_ACCOUNT } from '../../manager/LoginManager'; +import { DEFAULT_GROUP_NAME, GPX_FILE_EXT, KMZ_FILE_EXT } from '../../manager/track/TracksManager'; +import { createTrackFreeName, removeFileExtension, saveTrackToCloud } from '../../manager/track/SaveTrackManager'; +import { useMutator } from '../Utils'; + +const CLOUD_TRACK_EXTENSIONS = ['.gpx', '.kmz', '.kml']; + +function isCloudTrackFile(file) { + const name = file?.name?.toLowerCase() ?? ''; + + return CLOUD_TRACK_EXTENSIONS.some((ext) => name.endsWith(ext)); +} + +function validName(name) { + return name !== '' && name.trim().length > 0; +} + +export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) { + const ctx = useContext(AppContext); + const ltx = useContext(LoginContext); + const [uploadedFiles, mutateUploadedFiles] = useMutator({}); + + useEffect(() => { + for (const file in uploadedFiles) { + let open = uploadedFiles[file].selected; + let fileName = uploadedFiles[file].name; + if (validName(fileName)) { + fileName = removeFileExtension(fileName); + fileName = createTrackFreeName(fileName, ctx.tracksGroups, folder); + saveTrackToCloud({ + ctx, + ltx, + currentFolder: folder, + fileName, + type: 'GPX', + uploadedFile: uploadedFiles[file], + open, + }).then(); + mutateUploadedFiles((o) => delete o[file]); + break; // process 1 file per 1 render + } + } + }, [uploadedFiles]); + + const importGpxFiles = useCallback( + (fileList) => { + if (!ltx.loginUser || ltx.accountInfo?.account === FREE_ACCOUNT) { + return; + } + + const files = Array.from(fileList || []).filter(isCloudTrackFile); + if (files.length === 0) { + return; + } + + const selected = files.length === 1; + ctx.setTrackLoading(files.map((track) => removeFileExtension(track.name) + GPX_FILE_EXT)); + + files.forEach((file) => { + const reader = new FileReader(); + reader.addEventListener('load', (e) => { + const data = e.target.result; + if (data) { + mutateUploadedFiles( + (o) => + (o[file.name] = { file, selected, data: data, name: file.name, originalName: file.name }) + ); + } else { + ctx.setTrackErrorMsg({ + title: 'Import error', + msg: `Unable to import ${file.name}`, + }); + ctx.setTrackLoading([...ctx.trackLoading.filter((n) => n !== file.name)]); + } + }); + if (file.name.toLowerCase().endsWith(KMZ_FILE_EXT)) { + reader.readAsArrayBuffer(file); + } else { + reader.readAsText(file); + } + }); + }, + [ctx, folder, ltx.accountInfo?.account, ltx.loginUser, mutateUploadedFiles] + ); + + return { importGpxFiles }; +} From 69cb7e4bacf707bcd00a708a955e6c9649fc3a68 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Thu, 11 Jun 2026 17:31:19 +0300 Subject: [PATCH 2/6] Fix overlay appearance --- map/src/context/AppContext.js | 3 ++ map/src/frame/GlobalFrame.js | 7 ++-- map/src/map/components/GpxMapDropOverlay.jsx | 40 ++++++++++++++++++- .../components/gpxMapDropOverlay.module.css | 20 +++++----- 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/map/src/context/AppContext.js b/map/src/context/AppContext.js index 7e42ba04bf..c74dfe112c 100644 --- a/map/src/context/AppContext.js +++ b/map/src/context/AppContext.js @@ -127,6 +127,7 @@ export const AppContextProvider = (props) => { const [globalConfirmation, setGlobalConfirmation] = useState(null); const [openMenu, setOpenMenu] = useState(null); + const [openMainMenu, setOpenMainMenu] = useState(false); const [openContextMenu, setOpenContextMenu] = useState(false); const [cloudSettings, setCloudSettings] = useState({ @@ -632,6 +633,8 @@ export const AppContextProvider = (props) => { setSelectedWptId, openMenu, setOpenMenu, + openMainMenu, + setOpenMainMenu, openContextMenu, setOpenContextMenu, prevPageUrl, diff --git a/map/src/frame/GlobalFrame.js b/map/src/frame/GlobalFrame.js index aff1b44464..cc4b1a3888 100644 --- a/map/src/frame/GlobalFrame.js +++ b/map/src/frame/GlobalFrame.js @@ -47,7 +47,6 @@ const GlobalFrame = () => { const [showInfoBlock, setShowInfoBlock] = useState(false); const [clearState, setClearState] = useState(false); - const [openMainMenu, setOpenMainMenu] = useState(false); const [openErrorDialog, setOpenErrorDialog] = useState(false); const [menuInfo, setMenuInfo] = useState(null); @@ -58,7 +57,7 @@ const GlobalFrame = () => { const [showInstallBanner, setShowInstallBanner] = useState(false); - const MAIN_MENU_SIZE = openMainMenu ? `${MAIN_MENU_OPEN_SIZE}px` : `${MAIN_MENU_MIN_SIZE}px`; + const MAIN_MENU_SIZE = ctx.openMainMenu ? `${MAIN_MENU_OPEN_SIZE}px` : `${MAIN_MENU_MIN_SIZE}px`; const MENU_INFO_SIZE = menuInfo || ltx.openLoginMenu || ctx.infoBlockWidth === `${MENU_INFO_OPEN_SIZE}px` ? `${MENU_INFO_OPEN_SIZE}px` @@ -441,8 +440,8 @@ const GlobalFrame = () => { 0 + ? MAIN_MENU_MIN_SIZE + infoBlockWidthPx + : ctx.openMainMenu + ? MAIN_MENU_OPEN_SIZE + : MAIN_MENU_MIN_SIZE; + return { + top: HEADER_SIZE + OVERLAY_MARGIN, + left: leftChromePx + OVERLAY_MARGIN, + right: OVERLAY_MARGIN, + bottom: bottomPx + OVERLAY_MARGIN, + }; +} + export default function GpxMapDropOverlay({ active }) { + const ctx = useContext(AppContext); const { t } = useTranslation(); + const insets = useMemo(() => getVisibleMapInsets(ctx), [ + ctx.infoBlockWidth, + ctx.globalGraph?.show, + ctx.globalGraph?.size, + ctx.openMainMenu, + ]); if (!active) { return null; } return ( -
+
{t('web:drop_gpx_to_import')}
); diff --git a/map/src/map/components/gpxMapDropOverlay.module.css b/map/src/map/components/gpxMapDropOverlay.module.css index f01655988d..917a58af0d 100644 --- a/map/src/map/components/gpxMapDropOverlay.module.css +++ b/map/src/map/components/gpxMapDropOverlay.module.css @@ -1,22 +1,22 @@ .overlay { position: absolute; - top: 61px; - right: 0; - bottom: 0; - left: 67px; - z-index: 1200; + z-index: 3001; display: flex; align-items: center; justify-content: center; pointer-events: none; - border: 2px dashed #1976d2; - background: rgba(25, 118, 210, 0.12); + border: 3px dashed var(--selected-color); + border-radius: 12px; + background: color-mix(in srgb, var(--selected-color) 30%, transparent); } .text { - padding: 10px 16px; - border-radius: 8px; - background: rgba(0, 0, 0, 0.65); + padding: 9px 12px; + border-radius: 40px; + background: var(--selected-color); color: #fff; font-size: 16px !important; + font-weight: 400; + line-height: 120%; + letter-spacing: 0.01em; } From abf848870ce0c0b7d9ded40829255d6a2b9a9122 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 15 Jun 2026 23:09:09 +0300 Subject: [PATCH 3/6] Add GpxMenuDropOverlay; Refactoring --- map/src/context/AppContext.js | 3 + map/src/frame/GlobalFrame.js | 2 + map/src/frame/util/CloudGpxUploader.jsx | 4 +- map/src/manager/track/TracksManager.js | 1 + .../map/components/GpxFileDragController.jsx | 101 +++++++++++ map/src/map/components/GpxMapDropGeometry.js | 159 ++++++++++++++++++ map/src/map/components/GpxMapDropOverlay.jsx | 28 +-- map/src/map/components/GpxMenuDropOverlay.jsx | 68 ++++++++ .../components/gpxMapDropOverlay.module.css | 4 +- map/src/map/layers/CloudTrackLayer.js | 49 +----- map/src/menu/trackfavmenu.module.css | 9 + map/src/menu/tracks/CloudTrackGroup.jsx | 6 +- map/src/menu/tracks/TrackGroupFolder.jsx | 5 + map/src/menu/tracks/TracksMenu.jsx | 4 +- map/src/util/hooks/useCloudGpxImport.js | 25 ++- 15 files changed, 383 insertions(+), 85 deletions(-) create mode 100644 map/src/map/components/GpxFileDragController.jsx create mode 100644 map/src/map/components/GpxMapDropGeometry.js create mode 100644 map/src/map/components/GpxMenuDropOverlay.jsx diff --git a/map/src/context/AppContext.js b/map/src/context/AppContext.js index c74dfe112c..5f96402fb6 100644 --- a/map/src/context/AppContext.js +++ b/map/src/context/AppContext.js @@ -128,6 +128,7 @@ export const AppContextProvider = (props) => { const [openMenu, setOpenMenu] = useState(null); const [openMainMenu, setOpenMainMenu] = useState(false); + const [gpxFileDrag, setGpxFileDrag] = useState({ active: false, hoverFolder: null, overMap: false }); const [openContextMenu, setOpenContextMenu] = useState(false); const [cloudSettings, setCloudSettings] = useState({ @@ -635,6 +636,8 @@ export const AppContextProvider = (props) => { setOpenMenu, openMainMenu, setOpenMainMenu, + gpxFileDrag, + setGpxFileDrag, openContextMenu, setOpenContextMenu, prevPageUrl, diff --git a/map/src/frame/GlobalFrame.js b/map/src/frame/GlobalFrame.js index cc4b1a3888..76e0bcee80 100644 --- a/map/src/frame/GlobalFrame.js +++ b/map/src/frame/GlobalFrame.js @@ -33,6 +33,7 @@ import PhotosModal from '../menu/search/explore/PhotosModal'; import InstallBanner from './components/InstallBanner'; import { hideAllTracks } from '../manager/track/DeleteTrackManager'; import GlobalGraph from '../graph/mapGraph/GlobalGraph'; +import GpxFileDragController from '../map/components/GpxFileDragController'; import LoginContext from '../context/LoginContext'; import { poiUrlParams } from '../manager/PoiManager'; import { createUrlParams } from '../util/Utils'; @@ -414,6 +415,7 @@ const GlobalFrame = () => { }} > + {ctx.globalGraph?.show && } diff --git a/map/src/frame/util/CloudGpxUploader.jsx b/map/src/frame/util/CloudGpxUploader.jsx index 4468a38a30..4143a2fe5a 100644 --- a/map/src/frame/util/CloudGpxUploader.jsx +++ b/map/src/frame/util/CloudGpxUploader.jsx @@ -7,10 +7,10 @@ import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; export default function CloudGpxUploader({ children, folder = DEFAULT_GROUP_NAME, style = null }) { const ltx = useContext(LoginContext); - const { importGpxFiles } = useCloudGpxImport({ folder }); + const { importGpxFiles } = useCloudGpxImport(); const fileSelected = (e) => { - importGpxFiles(e.target.files); + importGpxFiles(e.target.files, folder); }; const HiddenInput = styled('input')({ display: 'none' }); diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index c49b78f867..7028d53961 100644 --- a/map/src/manager/track/TracksManager.js +++ b/map/src/manager/track/TracksManager.js @@ -58,6 +58,7 @@ const AUTO_SRTM_MAX_POINTS = 10000; // don't overload Auto-SRTM API with huge OS const AUTO_SRTM_MIN_BAD_POINTS_PERCENT = 10; // limit by % of no-elevation points (type=osmand might have up to 10%) export const FIT_BOUNDS_MAX_ZOOM = 17; export const DEFAULT_GROUP_NAME = ''; +export const IMPORT_FOLDER_NAME = 'Import'; export function fitBoundsOptions(mtx) { return { diff --git a/map/src/map/components/GpxFileDragController.jsx b/map/src/map/components/GpxFileDragController.jsx new file mode 100644 index 0000000000..aab615ceeb --- /dev/null +++ b/map/src/map/components/GpxFileDragController.jsx @@ -0,0 +1,101 @@ +import { useContext, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; +import AppContext from '../../context/AppContext'; +import { IMPORT_FOLDER_NAME } from '../../manager/track/TracksManager'; +import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; +import GpxMapDropOverlay from './GpxMapDropOverlay'; +import GpxMenuDropOverlay from './GpxMenuDropOverlay'; +import { resolveGpxDropTarget } from './GpxMapDropGeometry'; + +const GPX_FILE_DRAG_IDLE = { active: false, hoverFolder: null, overMap: false }; + +function hasFiles(e) { + return e.dataTransfer?.types?.includes('Files'); +} + +export default function GpxFileDragController() { + const ctx = useContext(AppContext); + const { importGpxFiles } = useCloudGpxImport(); + const dragCounterRef = useRef(0); + const ctxRef = useRef(ctx); + ctxRef.current = ctx; + + useEffect(() => { + const resetDrag = () => { + dragCounterRef.current = 0; + ctxRef.current.setGpxFileDrag(GPX_FILE_DRAG_IDLE); + }; + + const onDragEnter = (e) => { + if (!hasFiles(e)) { + return; + } + e.preventDefault(); + dragCounterRef.current += 1; + }; + + const onDragOver = (e) => { + if (!hasFiles(e)) { + return; + } + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + + const currentCtx = ctxRef.current; + const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx); + currentCtx.setGpxFileDrag({ active: true, hoverFolder, overMap }); + }; + + const onDragLeave = (e) => { + if (!hasFiles(e)) { + return; + } + dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); + if (dragCounterRef.current === 0) { + resetDrag(); + } + }; + + const onDrop = (e) => { + if (!hasFiles(e)) { + return; + } + e.preventDefault(); + + const currentCtx = ctxRef.current; + const files = Array.from(e.dataTransfer?.files || []); + const { hoverFolder, overMap } = resolveGpxDropTarget(e.clientX, e.clientY, currentCtx); + if (hoverFolder !== null) { + importGpxFiles(files, hoverFolder); + } else if (overMap) { + importGpxFiles(files, IMPORT_FOLDER_NAME); + } + resetDrag(); + }; + + const onDragEnd = () => { + resetDrag(); + }; + + window.addEventListener('dragenter', onDragEnter); + window.addEventListener('dragover', onDragOver); + window.addEventListener('dragleave', onDragLeave); + window.addEventListener('drop', onDrop); + window.addEventListener('dragend', onDragEnd); + return () => { + window.removeEventListener('dragenter', onDragEnter); + window.removeEventListener('dragover', onDragOver); + window.removeEventListener('dragleave', onDragLeave); + window.removeEventListener('drop', onDrop); + window.removeEventListener('dragend', onDragEnd); + }; + }, [importGpxFiles]); + + return createPortal( + <> + + + , + document.body + ); +} diff --git a/map/src/map/components/GpxMapDropGeometry.js b/map/src/map/components/GpxMapDropGeometry.js new file mode 100644 index 0000000000..ff87990048 --- /dev/null +++ b/map/src/map/components/GpxMapDropGeometry.js @@ -0,0 +1,159 @@ +import { HEADER_SIZE, MAIN_MENU_MIN_SIZE, MAIN_MENU_OPEN_SIZE } from '../../manager/GlobalManager'; + +export const OVERLAY_MARGIN = 16; + +export function getVisibleMapInsets(ctx) { + const infoBlockWidthPx = Number.parseInt(String(ctx.infoBlockWidth), 10) || 0; + const bottomPx = ctx.globalGraph?.show ? ctx.globalGraph.size : 0; + + const leftChromePx = + infoBlockWidthPx > 0 + ? MAIN_MENU_MIN_SIZE + infoBlockWidthPx + : ctx.openMainMenu + ? MAIN_MENU_OPEN_SIZE + : MAIN_MENU_MIN_SIZE; + + return { + top: HEADER_SIZE + OVERLAY_MARGIN, + left: leftChromePx + OVERLAY_MARGIN, + right: OVERLAY_MARGIN, + bottom: bottomPx + OVERLAY_MARGIN, + }; +} + +export function isPointInVisibleMap(ctx, clientX, clientY) { + const insets = getVisibleMapInsets(ctx); + const width = window.innerWidth; + const height = window.innerHeight; + + return ( + clientX >= insets.left && + clientX <= width - insets.right && + clientY >= insets.top && + clientY <= height - insets.bottom + ); +} + +export function getMenuDropContainers() { + return [ + document.getElementById('se-tracks-folder'), + document.getElementById('se-track-menu'), + ].filter((el) => el?.hasAttribute('data-cloud-track-folder')); +} + +export function getMenuOverlayContainer(hoverFolder) { + if (hoverFolder === null) { + return null; + } + + const openFolder = document.getElementById('se-tracks-folder'); + if (openFolder?.hasAttribute('data-cloud-track-folder')) { + const openFolderPath = openFolder.getAttribute('data-cloud-track-folder') ?? ''; + if (hoverFolder === openFolderPath) { + return openFolder; + } + } + + if (hoverFolder === '') { + return document.getElementById('se-track-menu'); + } + + return null; +} + +export function getMenuDropOverlayTop(container) { + const folders = container.querySelectorAll('[id^="se-menu-cloud-"]'); + if (folders.length > 0) { + return folders[folders.length - 1].getBoundingClientRect().bottom; + } + + const appBar = container.querySelector('.MuiAppBar-root'); + if (appBar) { + return appBar.getBoundingClientRect().bottom; + } + + const listAnchors = container.querySelectorAll('#se-visible-tracks-menu, [id^="se-shared-folder-"]'); + if (listAnchors.length > 0) { + let bottom = 0; + listAnchors.forEach((el) => { + bottom = Math.max(bottom, el.getBoundingClientRect().bottom); + }); + return bottom; + } + + return container.getBoundingClientRect().top; +} + +export function getMenuDropOverlayRect(container, ctx) { + if (!container) { + return null; + } + + const containerRect = container.getBoundingClientRect(); + const bottomPx = (ctx.globalGraph?.show ? ctx.globalGraph.size : 0) + OVERLAY_MARGIN; + const topPx = getMenuDropOverlayTop(container) + OVERLAY_MARGIN; + + if (topPx >= window.innerHeight - bottomPx) { + return null; + } + + return { + top: topPx, + left: containerRect.left + OVERLAY_MARGIN, + right: window.innerWidth - containerRect.right + OVERLAY_MARGIN, + bottom: bottomPx, + }; +} + +function isPointInOverlayRect(clientX, clientY, rect) { + const width = window.innerWidth; + const height = window.innerHeight; + + return ( + clientX >= rect.left && + clientX <= width - rect.right && + clientY >= rect.top && + clientY <= height - rect.bottom + ); +} + +function getMenuDropFolderAtPoint(clientX, clientY, ctx) { + for (const container of getMenuDropContainers()) { + const rect = getMenuDropOverlayRect(container, ctx); + if (rect && isPointInOverlayRect(clientX, clientY, rect)) { + return container.getAttribute('data-cloud-track-folder') ?? ''; + } + } + + return null; +} + +function getCloudTrackFolderFromElement(el) { + if (!el) { + return null; + } + const folderEl = el.closest('[data-cloud-track-folder]'); + if (!folderEl) { + return null; + } + + return folderEl.getAttribute('data-cloud-track-folder') ?? ''; +} + +export function resolveGpxDropTarget(clientX, clientY, ctx) { + const folderFromDom = getCloudTrackFolderFromElement(document.elementFromPoint(clientX, clientY)); + if (folderFromDom !== null) { + return { hoverFolder: folderFromDom, overMap: false }; + } + + const folderFromRect = getMenuDropFolderAtPoint(clientX, clientY, ctx); + if (folderFromRect !== null) { + return { hoverFolder: folderFromRect, overMap: false }; + } + + if (isPointInVisibleMap(ctx, clientX, clientY)) { + return { hoverFolder: null, overMap: true }; + } + + return { hoverFolder: null, overMap: false }; +} diff --git a/map/src/map/components/GpxMapDropOverlay.jsx b/map/src/map/components/GpxMapDropOverlay.jsx index de2f5cc395..7d0aa4e3bb 100644 --- a/map/src/map/components/GpxMapDropOverlay.jsx +++ b/map/src/map/components/GpxMapDropOverlay.jsx @@ -2,32 +2,14 @@ import { Typography } from '@mui/material'; import { useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import AppContext from '../../context/AppContext'; -import { HEADER_SIZE, MAIN_MENU_MIN_SIZE, MAIN_MENU_OPEN_SIZE } from '../../manager/GlobalManager'; +import { getVisibleMapInsets } from './GpxMapDropGeometry'; import styles from './gpxMapDropOverlay.module.css'; -const OVERLAY_MARGIN = 16; - -function getVisibleMapInsets(ctx) { - const infoBlockWidthPx = Number.parseInt(String(ctx.infoBlockWidth), 10) || 0; - const bottomPx = ctx.globalGraph?.show ? ctx.globalGraph.size : 0; - - const leftChromePx = - infoBlockWidthPx > 0 - ? MAIN_MENU_MIN_SIZE + infoBlockWidthPx - : ctx.openMainMenu - ? MAIN_MENU_OPEN_SIZE - : MAIN_MENU_MIN_SIZE; - return { - top: HEADER_SIZE + OVERLAY_MARGIN, - left: leftChromePx + OVERLAY_MARGIN, - right: OVERLAY_MARGIN, - bottom: bottomPx + OVERLAY_MARGIN, - }; -} - -export default function GpxMapDropOverlay({ active }) { +export default function GpxMapDropOverlay() { const ctx = useContext(AppContext); const { t } = useTranslation(); + const active = + ctx.gpxFileDrag?.active && ctx.gpxFileDrag?.overMap && ctx.gpxFileDrag?.hoverFolder === null; const insets = useMemo(() => getVisibleMapInsets(ctx), [ ctx.infoBlockWidth, ctx.globalGraph?.show, @@ -41,7 +23,7 @@ export default function GpxMapDropOverlay({ active }) { return (
{ + if (!active) { + setOverlayStyle(null); + return; + } + + const update = () => { + const container = getMenuOverlayContainer(ctxRef.current.gpxFileDrag?.hoverFolder); + if (!container) { + setOverlayStyle(null); + return; + } + const rect = getMenuDropOverlayRect(container, ctxRef.current); + if (!rect) { + setOverlayStyle(null); + return; + } + setOverlayStyle({ + top: `${rect.top}px`, + left: `${rect.left}px`, + right: `${rect.right}px`, + bottom: `${rect.bottom}px`, + }); + }; + + update(); + window.addEventListener('resize', update); + window.addEventListener('scroll', update, true); + return () => { + window.removeEventListener('resize', update); + window.removeEventListener('scroll', update, true); + }; + }, [ + active, + hoverFolder, + ctx.infoBlockWidth, + ctx.globalGraph?.show, + ctx.globalGraph?.size, + ]); + + if (!active || !overlayStyle) { + return null; + } + + return ( +
+ {t('web:drop_gpx_to_import')} +
+ ); +} diff --git a/map/src/map/components/gpxMapDropOverlay.module.css b/map/src/map/components/gpxMapDropOverlay.module.css index 917a58af0d..30fab27e63 100644 --- a/map/src/map/components/gpxMapDropOverlay.module.css +++ b/map/src/map/components/gpxMapDropOverlay.module.css @@ -1,5 +1,5 @@ -.overlay { - position: absolute; +.dropOverlay { + position: fixed; z-index: 3001; display: flex; align-items: center; diff --git a/map/src/map/layers/CloudTrackLayer.js b/map/src/map/layers/CloudTrackLayer.js index 70e027a01d..ee6038442c 100644 --- a/map/src/map/layers/CloudTrackLayer.js +++ b/map/src/map/layers/CloudTrackLayer.js @@ -1,16 +1,13 @@ -import { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { useCallback, useContext, useEffect, useState } from 'react'; import AppContext, { isCloudTrack, OBJECT_TYPE_CLOUD_TRACK } from '../../context/AppContext'; import MapContext from '../../context/MapContext'; import { useMap } from 'react-leaflet'; import TrackLayerProvider, { redrawWptsOnLayer, WPT_SIMPLIFY_THRESHOLD } from '../util/TrackLayerProvider'; import TracksManager, { - DEFAULT_GROUP_NAME, fitBoundsOptions, getResolvedPointsGroups, getTracksArrBounds, } from '../../manager/track/TracksManager'; -import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; -import GpxMapDropOverlay from '../components/GpxMapDropOverlay'; import { encodeString, useMutator } from '../../util/Utils'; import { INFO_MENU_URL, MAIN_URL_WITH_SLASH, MENU_INFO_OPEN_SIZE, TRACKS_URL } from '../../manager/GlobalManager'; import { clusterMarkers } from '../util/Clusterizer'; @@ -167,7 +164,6 @@ const CloudTrackLayer = () => { const [selectedPointMarker, setSelectedPointMarker] = useState(null); const map = useMap(); - const { importGpxFiles } = useCloudGpxImport({ folder: DEFAULT_GROUP_NAME }); const navigate = useNavigate(); @@ -175,47 +171,6 @@ const CloudTrackLayer = () => { const [prevZoom, setPrevZoom] = useState(null); const [move, setMove] = useState(false); - const [isDragOver, setIsDragOver] = useState(false); - const dragCounterRef = useRef(0); - - useEffect(() => { - const container = map.getContainer(); - const hasFiles = (e) => e.dataTransfer?.types?.includes('Files'); - - const onDragEnter = (e) => { - e.preventDefault(); - if (!hasFiles(e)) return; - dragCounterRef.current += 1; - setIsDragOver(true); - }; - const onDragOver = (e) => { - e.preventDefault(); - if (hasFiles(e)) e.dataTransfer.dropEffect = 'copy'; - }; - const onDragLeave = () => { - dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); - if (dragCounterRef.current === 0) setIsDragOver(false); - }; - const onDrop = (e) => { - e.preventDefault(); - const files = Array.from(e.dataTransfer?.files || []); - dragCounterRef.current = 0; - setIsDragOver(false); - importGpxFiles(files); - }; - - container.addEventListener('dragenter', onDragEnter); - container.addEventListener('dragover', onDragOver); - container.addEventListener('dragleave', onDragLeave); - container.addEventListener('drop', onDrop); - return () => { - container.removeEventListener('dragenter', onDragEnter); - container.removeEventListener('dragover', onDragOver); - container.removeEventListener('dragleave', onDragLeave); - container.removeEventListener('drop', onDrop); - }; - }, [map, importGpxFiles]); - useZoomMoveMapHandlers(map, setZoom, setMove); const getCloudWptLayers = useCallback(() => { @@ -408,8 +363,6 @@ const CloudTrackLayer = () => { ctx.setFitBoundsShareTracks(null); } }, [ctx.fitBoundsShareTracks]); - - return ; }; export default CloudTrackLayer; diff --git a/map/src/menu/trackfavmenu.module.css b/map/src/menu/trackfavmenu.module.css index 857103a5cb..403eeb39d6 100644 --- a/map/src/menu/trackfavmenu.module.css +++ b/map/src/menu/trackfavmenu.module.css @@ -79,6 +79,15 @@ padding: 10px 16px !important; gap: 24px !important; } +.groupDropTarget { + background: color-mix(in srgb, var(--selected-color) 30%, transparent) !important; + outline: 3px dashed var(--selected-color) !important; + outline-offset: -3px !important; + border-radius: 12px !important; +} +.folderDropTarget { + position: relative !important; +} .action { padding: 10px 16px !important; gap: 20px !important; diff --git a/map/src/menu/tracks/CloudTrackGroup.jsx b/map/src/menu/tracks/CloudTrackGroup.jsx index 54a33e5d23..872f3001e8 100644 --- a/map/src/menu/tracks/CloudTrackGroup.jsx +++ b/map/src/menu/tracks/CloudTrackGroup.jsx @@ -65,12 +65,16 @@ export default function CloudTrackGroup({ index, group }) { return `${fmt.monthShortDay(group.lastModifiedDate)}, ${t('shared_string_gpx_files').toLowerCase()} ${group.realSize}`; }; + const isDropTarget = group.type !== SMART_TYPE; + const isDropHover = isDropTarget && ctx.gpxFileDrag?.hoverFolder === group.fullName; + return ( <> {getFolderIcon()} diff --git a/map/src/menu/tracks/TrackGroupFolder.jsx b/map/src/menu/tracks/TrackGroupFolder.jsx index 9fad7d9c3d..78c20280b7 100644 --- a/map/src/menu/tracks/TrackGroupFolder.jsx +++ b/map/src/menu/tracks/TrackGroupFolder.jsx @@ -20,6 +20,7 @@ import { DEFAULT_SORT_METHOD } from './TracksMenu'; import Loading from '../errors/Loading'; import { SMART_TYPE } from '../share/shareConstants'; import { populateSmartFolderFiles } from '../../manager/SmartFoldersManager'; +import styles from '../trackfavmenu.module.css'; export default function TrackGroupFolder({ folder = null, smartf = null }) { const ctx = useContext(AppContext); @@ -138,10 +139,14 @@ export default function TrackGroupFolder({ folder = null, smartf = null }) { return (group?.realSize === 0 && ctx.trackLoading?.length === 0) || (!groupItems && !trackItems); } + const isDropTarget = group && group.type !== SMART_TYPE && !smartf; + return ( <> ) : ( - + 0; } -export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) { +export default function useCloudGpxImport() { const ctx = useContext(AppContext); const ltx = useContext(LoginContext); const [uploadedFiles, mutateUploadedFiles] = useMutator({}); useEffect(() => { for (const file in uploadedFiles) { - let open = uploadedFiles[file].selected; - let fileName = uploadedFiles[file].name; + const uploadedFile = uploadedFiles[file]; + let open = uploadedFile.selected; + let fileName = uploadedFile.name; + const folder = uploadedFile.folder; if (validName(fileName)) { fileName = removeFileExtension(fileName); fileName = createTrackFreeName(fileName, ctx.tracksGroups, folder); @@ -36,7 +38,7 @@ export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) currentFolder: folder, fileName, type: 'GPX', - uploadedFile: uploadedFiles[file], + uploadedFile: uploadedFile, open, }).then(); mutateUploadedFiles((o) => delete o[file]); @@ -46,7 +48,7 @@ export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) }, [uploadedFiles]); const importGpxFiles = useCallback( - (fileList) => { + (fileList, folder) => { if (!ltx.loginUser || ltx.accountInfo?.account === FREE_ACCOUNT) { return; } @@ -66,7 +68,14 @@ export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) if (data) { mutateUploadedFiles( (o) => - (o[file.name] = { file, selected, data: data, name: file.name, originalName: file.name }) + (o[file.name] = { + file, + selected, + data: data, + name: file.name, + originalName: file.name, + folder, + }) ); } else { ctx.setTrackErrorMsg({ @@ -83,7 +92,7 @@ export default function useCloudGpxImport({ folder = DEFAULT_GROUP_NAME } = {}) } }); }, - [ctx, folder, ltx.accountInfo?.account, ltx.loginUser, mutateUploadedFiles] + [ctx, ltx.accountInfo?.account, ltx.loginUser, mutateUploadedFiles] ); return { importGpxFiles }; From 182d33f9d7fa126005f75c88863c26ec2f2aba53 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Mon, 15 Jun 2026 23:26:35 +0300 Subject: [PATCH 4/6] Show the imported file item --- map/src/manager/track/SaveTrackManager.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index e62f2f05c7..342f47ff3e 100644 --- a/map/src/manager/track/SaveTrackManager.js +++ b/map/src/manager/track/SaveTrackManager.js @@ -441,6 +441,9 @@ async function downloadAfterUpload(ctx, file, showOnMap) { }); newGpxFiles[file.name].analysis = TracksManager.prepareAnalysis(newGpxFiles[file.name].analysis); newGpxFiles[file.name].showOnMap = showOnMap; + if (showOnMap) { + newGpxFiles[file.name].zoomToTrack = true; + } ctx.setGpxFiles(newGpxFiles); ctx.setSelectedGpxFile({ ...newGpxFiles[file.name] }); ctx.setProcessingSaveTrack(false); From 2092b79033c63ee2db1be09ed6dbd1df69212065 Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Tue, 16 Jun 2026 14:09:19 +0300 Subject: [PATCH 5/6] Rename and move components --- map/src/frame/GlobalFrame.js | 4 ++-- .../TracksFileDragController.jsx} | 18 +++++++++--------- .../TracksMapDropGeometry.js} | 2 +- .../TracksMapDropOverlay.jsx} | 8 ++++---- .../TracksMenuDropOverlay.jsx} | 8 ++++---- .../tracksMapDropOverlay.module.css} | 0 6 files changed, 20 insertions(+), 20 deletions(-) rename map/src/{map/components/GpxFileDragController.jsx => frame/TracksFileDragController.jsx} (85%) rename map/src/{map/components/GpxMapDropGeometry.js => frame/TracksMapDropGeometry.js} (99%) rename map/src/{map/components/GpxMapDropOverlay.jsx => frame/TracksMapDropOverlay.jsx} (81%) rename map/src/{map/components/GpxMenuDropOverlay.jsx => frame/TracksMenuDropOverlay.jsx} (91%) rename map/src/{map/components/gpxMapDropOverlay.module.css => frame/tracksMapDropOverlay.module.css} (100%) diff --git a/map/src/frame/GlobalFrame.js b/map/src/frame/GlobalFrame.js index 76e0bcee80..8ef1ea65bf 100644 --- a/map/src/frame/GlobalFrame.js +++ b/map/src/frame/GlobalFrame.js @@ -33,7 +33,7 @@ import PhotosModal from '../menu/search/explore/PhotosModal'; import InstallBanner from './components/InstallBanner'; import { hideAllTracks } from '../manager/track/DeleteTrackManager'; import GlobalGraph from '../graph/mapGraph/GlobalGraph'; -import GpxFileDragController from '../map/components/GpxFileDragController'; +import TracksFileDragController from './TracksFileDragController'; import LoginContext from '../context/LoginContext'; import { poiUrlParams } from '../manager/PoiManager'; import { createUrlParams } from '../util/Utils'; @@ -415,7 +415,7 @@ const GlobalFrame = () => { }} > - + {ctx.globalGraph?.show && } diff --git a/map/src/map/components/GpxFileDragController.jsx b/map/src/frame/TracksFileDragController.jsx similarity index 85% rename from map/src/map/components/GpxFileDragController.jsx rename to map/src/frame/TracksFileDragController.jsx index aab615ceeb..3c6f08feb4 100644 --- a/map/src/map/components/GpxFileDragController.jsx +++ b/map/src/frame/TracksFileDragController.jsx @@ -1,11 +1,11 @@ import { useContext, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; -import AppContext from '../../context/AppContext'; -import { IMPORT_FOLDER_NAME } from '../../manager/track/TracksManager'; -import useCloudGpxImport from '../../util/hooks/useCloudGpxImport'; -import GpxMapDropOverlay from './GpxMapDropOverlay'; -import GpxMenuDropOverlay from './GpxMenuDropOverlay'; -import { resolveGpxDropTarget } from './GpxMapDropGeometry'; +import AppContext from '../context/AppContext'; +import { IMPORT_FOLDER_NAME } from '../manager/track/TracksManager'; +import useCloudGpxImport from '../util/hooks/useCloudGpxImport'; +import TracksMapDropOverlay from './TracksMapDropOverlay'; +import TracksMenuDropOverlay from './TracksMenuDropOverlay'; +import { resolveGpxDropTarget } from './TracksMapDropGeometry'; const GPX_FILE_DRAG_IDLE = { active: false, hoverFolder: null, overMap: false }; @@ -13,7 +13,7 @@ function hasFiles(e) { return e.dataTransfer?.types?.includes('Files'); } -export default function GpxFileDragController() { +export default function TracksFileDragController() { const ctx = useContext(AppContext); const { importGpxFiles } = useCloudGpxImport(); const dragCounterRef = useRef(0); @@ -93,8 +93,8 @@ export default function GpxFileDragController() { return createPortal( <> - - + + , document.body ); diff --git a/map/src/map/components/GpxMapDropGeometry.js b/map/src/frame/TracksMapDropGeometry.js similarity index 99% rename from map/src/map/components/GpxMapDropGeometry.js rename to map/src/frame/TracksMapDropGeometry.js index ff87990048..87f8585f5d 100644 --- a/map/src/map/components/GpxMapDropGeometry.js +++ b/map/src/frame/TracksMapDropGeometry.js @@ -1,4 +1,4 @@ -import { HEADER_SIZE, MAIN_MENU_MIN_SIZE, MAIN_MENU_OPEN_SIZE } from '../../manager/GlobalManager'; +import { HEADER_SIZE, MAIN_MENU_MIN_SIZE, MAIN_MENU_OPEN_SIZE } from '../manager/GlobalManager'; export const OVERLAY_MARGIN = 16; diff --git a/map/src/map/components/GpxMapDropOverlay.jsx b/map/src/frame/TracksMapDropOverlay.jsx similarity index 81% rename from map/src/map/components/GpxMapDropOverlay.jsx rename to map/src/frame/TracksMapDropOverlay.jsx index 7d0aa4e3bb..ca059e4329 100644 --- a/map/src/map/components/GpxMapDropOverlay.jsx +++ b/map/src/frame/TracksMapDropOverlay.jsx @@ -1,11 +1,11 @@ import { Typography } from '@mui/material'; import { useContext, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import AppContext from '../../context/AppContext'; -import { getVisibleMapInsets } from './GpxMapDropGeometry'; -import styles from './gpxMapDropOverlay.module.css'; +import AppContext from '../context/AppContext'; +import { getVisibleMapInsets } from './TracksMapDropGeometry'; +import styles from './tracksMapDropOverlay.module.css'; -export default function GpxMapDropOverlay() { +export default function TracksMapDropOverlay() { const ctx = useContext(AppContext); const { t } = useTranslation(); const active = diff --git a/map/src/map/components/GpxMenuDropOverlay.jsx b/map/src/frame/TracksMenuDropOverlay.jsx similarity index 91% rename from map/src/map/components/GpxMenuDropOverlay.jsx rename to map/src/frame/TracksMenuDropOverlay.jsx index 0876f16ac2..f397f3ae38 100644 --- a/map/src/map/components/GpxMenuDropOverlay.jsx +++ b/map/src/frame/TracksMenuDropOverlay.jsx @@ -1,11 +1,11 @@ import { Typography } from '@mui/material'; import { useContext, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import AppContext from '../../context/AppContext'; -import { getMenuDropOverlayRect, getMenuOverlayContainer } from './GpxMapDropGeometry'; -import styles from './gpxMapDropOverlay.module.css'; +import AppContext from '../context/AppContext'; +import { getMenuDropOverlayRect, getMenuOverlayContainer } from './TracksMapDropGeometry'; +import styles from './tracksMapDropOverlay.module.css'; -export default function GpxMenuDropOverlay() { +export default function TracksMenuDropOverlay() { const ctx = useContext(AppContext); const { t } = useTranslation(); const ctxRef = useRef(ctx); diff --git a/map/src/map/components/gpxMapDropOverlay.module.css b/map/src/frame/tracksMapDropOverlay.module.css similarity index 100% rename from map/src/map/components/gpxMapDropOverlay.module.css rename to map/src/frame/tracksMapDropOverlay.module.css From 507eeee27ac2574d5e2ca912ffe33bc3e15ee7ff Mon Sep 17 00:00:00 2001 From: Dima-1 Date: Tue, 16 Jun 2026 18:46:12 +0300 Subject: [PATCH 6/6] Add check pro account, extract DropOverlay component, remove useRef --- map/src/frame/TracksFileDragController.jsx | 12 +++--- map/src/frame/TracksMapDropOverlay.jsx | 19 +-------- map/src/frame/TracksMenuDropOverlay.jsx | 40 +++++-------------- map/src/frame/components/DropOverlay.jsx | 25 ++++++++++++ .../dropOverlay.module.css} | 0 map/src/util/hooks/useCloudGpxImport.js | 5 +-- 6 files changed, 47 insertions(+), 54 deletions(-) create mode 100644 map/src/frame/components/DropOverlay.jsx rename map/src/frame/{tracksMapDropOverlay.module.css => components/dropOverlay.module.css} (100%) diff --git a/map/src/frame/TracksFileDragController.jsx b/map/src/frame/TracksFileDragController.jsx index 3c6f08feb4..4a60aa8616 100644 --- a/map/src/frame/TracksFileDragController.jsx +++ b/map/src/frame/TracksFileDragController.jsx @@ -1,6 +1,7 @@ import { useContext, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import AppContext from '../context/AppContext'; +import LoginContext from '../context/LoginContext'; import { IMPORT_FOLDER_NAME } from '../manager/track/TracksManager'; import useCloudGpxImport from '../util/hooks/useCloudGpxImport'; import TracksMapDropOverlay from './TracksMapDropOverlay'; @@ -15,6 +16,7 @@ function hasFiles(e) { export default function TracksFileDragController() { const ctx = useContext(AppContext); + const ltx = useContext(LoginContext); const { importGpxFiles } = useCloudGpxImport(); const dragCounterRef = useRef(0); const ctxRef = useRef(ctx); @@ -27,7 +29,7 @@ export default function TracksFileDragController() { }; const onDragEnter = (e) => { - if (!hasFiles(e)) { + if (!hasFiles(e) || !ltx.isProAccount()) { return; } e.preventDefault(); @@ -35,7 +37,7 @@ export default function TracksFileDragController() { }; const onDragOver = (e) => { - if (!hasFiles(e)) { + if (!hasFiles(e) || !ltx.isProAccount()) { return; } e.preventDefault(); @@ -47,7 +49,7 @@ export default function TracksFileDragController() { }; const onDragLeave = (e) => { - if (!hasFiles(e)) { + if (!hasFiles(e) || !ltx.isProAccount()) { return; } dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); @@ -57,7 +59,7 @@ export default function TracksFileDragController() { }; const onDrop = (e) => { - if (!hasFiles(e)) { + if (!hasFiles(e) || !ltx.isProAccount()) { return; } e.preventDefault(); @@ -89,7 +91,7 @@ export default function TracksFileDragController() { window.removeEventListener('drop', onDrop); window.removeEventListener('dragend', onDragEnd); }; - }, [importGpxFiles]); + }, [importGpxFiles, ltx.loginUser, ltx.accountInfo?.account]); return createPortal( <> diff --git a/map/src/frame/TracksMapDropOverlay.jsx b/map/src/frame/TracksMapDropOverlay.jsx index ca059e4329..5edb79037d 100644 --- a/map/src/frame/TracksMapDropOverlay.jsx +++ b/map/src/frame/TracksMapDropOverlay.jsx @@ -1,13 +1,10 @@ -import { Typography } from '@mui/material'; import { useContext, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; import AppContext from '../context/AppContext'; +import DropOverlay from './components/DropOverlay'; import { getVisibleMapInsets } from './TracksMapDropGeometry'; -import styles from './tracksMapDropOverlay.module.css'; export default function TracksMapDropOverlay() { const ctx = useContext(AppContext); - const { t } = useTranslation(); const active = ctx.gpxFileDrag?.active && ctx.gpxFileDrag?.overMap && ctx.gpxFileDrag?.hoverFolder === null; const insets = useMemo(() => getVisibleMapInsets(ctx), [ @@ -21,17 +18,5 @@ export default function TracksMapDropOverlay() { return null; } - return ( -
- {t('web:drop_gpx_to_import')} -
- ); + return ; } diff --git a/map/src/frame/TracksMenuDropOverlay.jsx b/map/src/frame/TracksMenuDropOverlay.jsx index f397f3ae38..08886cfa2c 100644 --- a/map/src/frame/TracksMenuDropOverlay.jsx +++ b/map/src/frame/TracksMenuDropOverlay.jsx @@ -1,44 +1,29 @@ -import { Typography } from '@mui/material'; -import { useContext, useEffect, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import { useContext, useEffect, useState } from 'react'; import AppContext from '../context/AppContext'; +import DropOverlay from './components/DropOverlay'; import { getMenuDropOverlayRect, getMenuOverlayContainer } from './TracksMapDropGeometry'; -import styles from './tracksMapDropOverlay.module.css'; export default function TracksMenuDropOverlay() { const ctx = useContext(AppContext); - const { t } = useTranslation(); - const ctxRef = useRef(ctx); - const [overlayStyle, setOverlayStyle] = useState(null); - - ctxRef.current = ctx; + const [overlayInsets, setOverlayInsets] = useState(null); const hoverFolder = ctx.gpxFileDrag?.hoverFolder; const active = ctx.gpxFileDrag?.active && !ctx.gpxFileDrag?.overMap && hoverFolder !== null; useEffect(() => { if (!active) { - setOverlayStyle(null); + setOverlayInsets(null); return; } const update = () => { - const container = getMenuOverlayContainer(ctxRef.current.gpxFileDrag?.hoverFolder); + const container = getMenuOverlayContainer(ctx.gpxFileDrag?.hoverFolder); if (!container) { - setOverlayStyle(null); - return; - } - const rect = getMenuDropOverlayRect(container, ctxRef.current); - if (!rect) { - setOverlayStyle(null); + setOverlayInsets(null); return; } - setOverlayStyle({ - top: `${rect.top}px`, - left: `${rect.left}px`, - right: `${rect.right}px`, - bottom: `${rect.bottom}px`, - }); + const rect = getMenuDropOverlayRect(container, ctx); + setOverlayInsets(rect); }; update(); @@ -54,15 +39,12 @@ export default function TracksMenuDropOverlay() { ctx.infoBlockWidth, ctx.globalGraph?.show, ctx.globalGraph?.size, + ctx.openMainMenu, ]); - if (!active || !overlayStyle) { + if (!active) { return null; } - return ( -
- {t('web:drop_gpx_to_import')} -
- ); + return ; } diff --git a/map/src/frame/components/DropOverlay.jsx b/map/src/frame/components/DropOverlay.jsx new file mode 100644 index 0000000000..ee94ca7c00 --- /dev/null +++ b/map/src/frame/components/DropOverlay.jsx @@ -0,0 +1,25 @@ +import { Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import styles from './dropOverlay.module.css'; + +export default function DropOverlay({ insets }) { + const { t } = useTranslation(); + + if (!insets) { + return null; + } + + return ( +
+ {t('web:drop_gpx_to_import')} +
+ ); +} diff --git a/map/src/frame/tracksMapDropOverlay.module.css b/map/src/frame/components/dropOverlay.module.css similarity index 100% rename from map/src/frame/tracksMapDropOverlay.module.css rename to map/src/frame/components/dropOverlay.module.css diff --git a/map/src/util/hooks/useCloudGpxImport.js b/map/src/util/hooks/useCloudGpxImport.js index 917aa27609..1ff7c7e846 100644 --- a/map/src/util/hooks/useCloudGpxImport.js +++ b/map/src/util/hooks/useCloudGpxImport.js @@ -1,7 +1,6 @@ import { useCallback, useContext, useEffect } from 'react'; import AppContext from '../../context/AppContext'; import LoginContext from '../../context/LoginContext'; -import { FREE_ACCOUNT } from '../../manager/LoginManager'; import { GPX_FILE_EXT, KMZ_FILE_EXT } from '../../manager/track/TracksManager'; import { createTrackFreeName, removeFileExtension, saveTrackToCloud } from '../../manager/track/SaveTrackManager'; import { useMutator } from '../Utils'; @@ -49,7 +48,7 @@ export default function useCloudGpxImport() { const importGpxFiles = useCallback( (fileList, folder) => { - if (!ltx.loginUser || ltx.accountInfo?.account === FREE_ACCOUNT) { + if (!ltx.isProAccount()) { return; } @@ -92,7 +91,7 @@ export default function useCloudGpxImport() { } }); }, - [ctx, ltx.accountInfo?.account, ltx.loginUser, mutateUploadedFiles] + [ctx, ltx.loginUser, ltx.accountInfo?.account, mutateUploadedFiles] ); return { importGpxFiles };