diff --git a/map/src/context/AppContext.js b/map/src/context/AppContext.js index 7e42ba04b..5f96402fb 100644 --- a/map/src/context/AppContext.js +++ b/map/src/context/AppContext.js @@ -127,6 +127,8 @@ export const AppContextProvider = (props) => { const [globalConfirmation, setGlobalConfirmation] = useState(null); 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({ @@ -632,6 +634,10 @@ export const AppContextProvider = (props) => { setSelectedWptId, openMenu, setOpenMenu, + openMainMenu, + setOpenMainMenu, + gpxFileDrag, + setGpxFileDrag, openContextMenu, setOpenContextMenu, prevPageUrl, diff --git a/map/src/frame/GlobalFrame.js b/map/src/frame/GlobalFrame.js index aff1b4446..8ef1ea65b 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 TracksFileDragController from './TracksFileDragController'; import LoginContext from '../context/LoginContext'; import { poiUrlParams } from '../manager/PoiManager'; import { createUrlParams } from '../util/Utils'; @@ -47,7 +48,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 +58,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` @@ -415,6 +415,7 @@ const GlobalFrame = () => { }} > + {ctx.globalGraph?.show && } @@ -441,8 +442,8 @@ const GlobalFrame = () => { { + const resetDrag = () => { + dragCounterRef.current = 0; + ctxRef.current.setGpxFileDrag(GPX_FILE_DRAG_IDLE); + }; + + const onDragEnter = (e) => { + if (!hasFiles(e) || !ltx.isProAccount()) { + return; + } + e.preventDefault(); + dragCounterRef.current += 1; + }; + + const onDragOver = (e) => { + if (!hasFiles(e) || !ltx.isProAccount()) { + 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) || !ltx.isProAccount()) { + return; + } + dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); + if (dragCounterRef.current === 0) { + resetDrag(); + } + }; + + const onDrop = (e) => { + if (!hasFiles(e) || !ltx.isProAccount()) { + 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, ltx.loginUser, ltx.accountInfo?.account]); + + return createPortal( + <> + + + , + document.body + ); +} diff --git a/map/src/frame/TracksMapDropGeometry.js b/map/src/frame/TracksMapDropGeometry.js new file mode 100644 index 000000000..87f8585f5 --- /dev/null +++ b/map/src/frame/TracksMapDropGeometry.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/frame/TracksMapDropOverlay.jsx b/map/src/frame/TracksMapDropOverlay.jsx new file mode 100644 index 000000000..5edb79037 --- /dev/null +++ b/map/src/frame/TracksMapDropOverlay.jsx @@ -0,0 +1,22 @@ +import { useContext, useMemo } from 'react'; +import AppContext from '../context/AppContext'; +import DropOverlay from './components/DropOverlay'; +import { getVisibleMapInsets } from './TracksMapDropGeometry'; + +export default function TracksMapDropOverlay() { + const ctx = useContext(AppContext); + const active = + ctx.gpxFileDrag?.active && ctx.gpxFileDrag?.overMap && ctx.gpxFileDrag?.hoverFolder === null; + const insets = useMemo(() => getVisibleMapInsets(ctx), [ + ctx.infoBlockWidth, + ctx.globalGraph?.show, + ctx.globalGraph?.size, + ctx.openMainMenu, + ]); + + if (!active) { + return null; + } + + return ; +} diff --git a/map/src/frame/TracksMenuDropOverlay.jsx b/map/src/frame/TracksMenuDropOverlay.jsx new file mode 100644 index 000000000..08886cfa2 --- /dev/null +++ b/map/src/frame/TracksMenuDropOverlay.jsx @@ -0,0 +1,50 @@ +import { useContext, useEffect, useState } from 'react'; +import AppContext from '../context/AppContext'; +import DropOverlay from './components/DropOverlay'; +import { getMenuDropOverlayRect, getMenuOverlayContainer } from './TracksMapDropGeometry'; + +export default function TracksMenuDropOverlay() { + const ctx = useContext(AppContext); + const [overlayInsets, setOverlayInsets] = useState(null); + + const hoverFolder = ctx.gpxFileDrag?.hoverFolder; + const active = ctx.gpxFileDrag?.active && !ctx.gpxFileDrag?.overMap && hoverFolder !== null; + + useEffect(() => { + if (!active) { + setOverlayInsets(null); + return; + } + + const update = () => { + const container = getMenuOverlayContainer(ctx.gpxFileDrag?.hoverFolder); + if (!container) { + setOverlayInsets(null); + return; + } + const rect = getMenuDropOverlayRect(container, ctx); + setOverlayInsets(rect); + }; + + 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, + ctx.openMainMenu, + ]); + + if (!active) { + return null; + } + + return ; +} diff --git a/map/src/frame/components/DropOverlay.jsx b/map/src/frame/components/DropOverlay.jsx new file mode 100644 index 000000000..ee94ca7c0 --- /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/components/dropOverlay.module.css b/map/src/frame/components/dropOverlay.module.css new file mode 100644 index 000000000..30fab27e6 --- /dev/null +++ b/map/src/frame/components/dropOverlay.module.css @@ -0,0 +1,22 @@ +.dropOverlay { + position: fixed; + z-index: 3001; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + border: 3px dashed var(--selected-color); + border-radius: 12px; + background: color-mix(in srgb, var(--selected-color) 30%, transparent); +} + +.text { + 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; +} diff --git a/map/src/frame/util/CloudGpxUploader.jsx b/map/src/frame/util/CloudGpxUploader.jsx index 989aa441d..4143a2fe5 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(); - 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, folder); }; const HiddenInput = styled('input')({ display: 'none' }); diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js index e62f2f05c..342f47ff3 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); diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js index c49b78f86..7028d5396 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/GpxMapDropOverlay.jsx b/map/src/map/components/GpxMapDropOverlay.jsx deleted file mode 100644 index e130481a2..000000000 --- a/map/src/map/components/GpxMapDropOverlay.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import styles from './gpxMapDropOverlay.module.css'; - -export default function GpxMapDropOverlay({ active }) { - const { t } = useTranslation(); - - 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 deleted file mode 100644 index f01655988..000000000 --- a/map/src/map/components/gpxMapDropOverlay.module.css +++ /dev/null @@ -1,22 +0,0 @@ -.overlay { - position: absolute; - top: 61px; - right: 0; - bottom: 0; - left: 67px; - z-index: 1200; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; - border: 2px dashed #1976d2; - background: rgba(25, 118, 210, 0.12); -} - -.text { - padding: 10px 16px; - border-radius: 8px; - background: rgba(0, 0, 0, 0.65); - color: #fff; - font-size: 16px !important; -} diff --git a/map/src/map/layers/LocalClientTrackLayer.js b/map/src/map/layers/LocalClientTrackLayer.js index e6decb5be..6f5494d6f 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/menu/trackfavmenu.module.css b/map/src/menu/trackfavmenu.module.css index 857103a5c..403eeb39d 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 54a33e5d2..872f3001e 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 9fad7d9c3..78c20280b 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 ( <> ) : ( - + name.endsWith(ext)); +} + +function validName(name) { + return name !== '' && name.trim().length > 0; +} + +export default function useCloudGpxImport() { + const ctx = useContext(AppContext); + const ltx = useContext(LoginContext); + const [uploadedFiles, mutateUploadedFiles] = useMutator({}); + + useEffect(() => { + for (const file in uploadedFiles) { + 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); + saveTrackToCloud({ + ctx, + ltx, + currentFolder: folder, + fileName, + type: 'GPX', + uploadedFile: uploadedFile, + open, + }).then(); + mutateUploadedFiles((o) => delete o[file]); + break; // process 1 file per 1 render + } + } + }, [uploadedFiles]); + + const importGpxFiles = useCallback( + (fileList, folder) => { + if (!ltx.isProAccount()) { + 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, + folder, + }) + ); + } 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, ltx.loginUser, ltx.accountInfo?.account, mutateUploadedFiles] + ); + + return { importGpxFiles }; +}