diff --git a/map/src/assets/icons/ic_action_folder_smart.svg b/map/src/assets/icons/ic_action_folder_smart.svg
new file mode 100644
index 000000000..2cd56a55f
--- /dev/null
+++ b/map/src/assets/icons/ic_action_folder_smart.svg
@@ -0,0 +1,4 @@
+
diff --git a/map/src/context/AppContext.js b/map/src/context/AppContext.js
index bb684acb7..a62c5da87 100644
--- a/map/src/context/AppContext.js
+++ b/map/src/context/AppContext.js
@@ -3,6 +3,8 @@ import LoginContext from '../context/LoginContext';
import Utils, { seleniumUpdateActivity, useMutator } from '../util/Utils';
import TracksManager, {
getGpxFiles,
+ filterSmartFolders,
+ filterRegularFolders,
GPX_FILE_EXT,
preparedGpxFile,
TRACK_VISIBLE_FLAG,
@@ -32,6 +34,7 @@ import {
SEARCH_RESULTS_KEY,
TRACKS_KEY,
} from '../util/hooks/menu/useRecentDataSaver';
+import { SMART_TYPE } from '../menu/share/shareConstants';
export const OBJECT_TYPE_LOCAL_TRACK = 'local_track'; // track in localStorage
export const OBJECT_TYPE_CLOUD_TRACK = 'cloud_track'; // track in OsmAnd Cloud
@@ -95,6 +98,8 @@ async function loadListFiles(
setProcessingGroups,
setVisibleTracks,
setShareWithMeFiles,
+ setTracksGroups,
+ setSmartFoldersCache,
setUpdateFiles
) {
if (loginUser !== listFiles.loginUser) {
@@ -113,6 +118,7 @@ async function loadListFiles(
});
getFilesForUpdateDetails(res.uniqueFiles, setUpdateFiles);
setListFiles(res);
+ loadSmartFolders(setTracksGroups, setSmartFoldersCache);
const favFiles = await loadShareFiles(setShareWithMeFiles);
const ownFavorites = TracksManager.getFavoriteGroups(res);
const allFavorites = [...ownFavorites, ...favFiles];
@@ -127,6 +133,57 @@ async function loadListFiles(
}
}
+export async function loadSmartFolders(setTracksGroups, setSmartFoldersCache) {
+ const res = await getSmartFolders();
+ setSmartFoldersCache({});
+ const smartFolderGroups = (res ?? []).map((smartFolder) => {
+ return {
+ name: smartFolder.name,
+ fullName: smartFolder.name,
+ type: SMART_TYPE,
+ subfolders: [],
+ groupFiles: [],
+ files: [],
+ realSize: smartFolder.userFilePaths?.length ?? 0,
+ lastModifiedMs: smartFolder.creationTime,
+ lastModifiedDate: null,
+ userFilePaths: smartFolder.userFilePaths ?? [],
+ };
+ });
+
+ setTracksGroups((prev) => {
+ const regularFolders = filterRegularFolders(prev);
+ return [...regularFolders, ...smartFolderGroups];
+ });
+}
+
+export function populateSmartFolderFiles(smartFolder, listFiles, smartFoldersCache, setSmartFoldersCache) {
+ const filesArray = [];
+ const cached = smartFoldersCache?.[smartFolder.name];
+ if (cached) {
+ return cached;
+ }
+ (smartFolder.userFilePaths ?? []).forEach((path) => {
+ const file = listFiles?.find((f) => f.name === path);
+ if (file) {
+ filesArray.push({ ...file, smartFolder: true });
+ }
+ });
+ const populated = {
+ ...smartFolder,
+ groupFiles: filesArray,
+ files: filesArray,
+ realSize: filesArray.length,
+ };
+ setSmartFoldersCache((prev) => ({ ...prev, [smartFolder.name]: populated }));
+ return populated;
+}
+
+async function getSmartFolders() {
+ const response = await apiGet(`${process.env.REACT_APP_USER_API_SITE}/mapapi/create-smart-folders`, {});
+ return response?.data || null;
+}
+
export function getFilesForUpdateDetails(files, setUpdateFiles) {
const filesToUpdate = files
.filter((f) => f.details && f.details.update && f.type === GPX && f.name.toLowerCase().endsWith(GPX_FILE_EXT))
@@ -363,6 +420,8 @@ export const AppContextProvider = (props) => {
const [shareFilesCache, setShareFilesCache] = useState({});
const [shareWithMeFiles, setShareWithMeFiles] = useState(null);
const [fitBoundsShareTracks, setFitBoundsShareTracks] = useState(null);
+
+ const [smartFoldersCache, setSmartFoldersCache] = useState(null);
// selected track
const [selectedGpxFile, setSelectedGpxFile] = useState({});
const [unverifiedGpxFile, setUnverifiedGpxFile] = useState(null); // see Effect in LocalClientTrackLayer
@@ -653,6 +712,7 @@ export const AppContextProvider = (props) => {
uniqueFiles: updatedUniqueFiles,
};
});
+ await loadSmartFolders(setTracksGroups, setSmartFoldersCache);
}
}
};
@@ -676,6 +736,8 @@ export const AppContextProvider = (props) => {
setProcessingGroups,
setVisibleTracks,
setShareWithMeFiles,
+ setTracksGroups,
+ setSmartFoldersCache,
setUpdateFiles
).then(() => {
setGpxLoading(false);
@@ -876,6 +938,8 @@ export const AppContextProvider = (props) => {
setShareWithMeFiles,
fitBoundsShareTracks,
setFitBoundsShareTracks,
+ smartFoldersCache,
+ setSmartFoldersCache,
trackAnalyzer,
setTrackAnalyzer,
excludedSegments,
diff --git a/map/src/frame/GlobalFrame.js b/map/src/frame/GlobalFrame.js
index 939a38538..a01899752 100644
--- a/map/src/frame/GlobalFrame.js
+++ b/map/src/frame/GlobalFrame.js
@@ -26,7 +26,7 @@ import dialogStyles from '../dialogs/dialog.module.css';
import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions';
import isEmpty from 'lodash-es/isEmpty';
-import { createTrackGroups, getGpxFiles, TRACK_VISIBLE_FLAG } from '../manager/track/TracksManager';
+import { createTrackGroups, getGpxFiles, filterSmartFolders, TRACK_VISIBLE_FLAG } from '../manager/track/TracksManager';
import { addCloseTracksToRecently, VISIBLE_SHARE_MARKER } from '../menu/visibletracks/VisibleTracks';
import PhotosModal from '../menu/search/explore/PhotosModal';
import InstallBanner from './components/InstallBanner';
@@ -35,6 +35,7 @@ import GlobalGraph from '../graph/mapGraph/GlobalGraph';
import LoginContext from '../context/LoginContext';
import { poiUrlParams } from '../manager/PoiManager';
import { createUrlParams } from '../util/Utils';
+import { SMART_TYPE } from '../menu/share/shareConstants';
const ENCODED_COMMA = '%2C';
const ENCODED_COLON = '%3A';
@@ -367,7 +368,8 @@ const GlobalFrame = () => {
const trackGroups = createTrackGroups({ files, ctx });
ctx.setTracksGroups(trackGroups);
} else {
- ctx.setTracksGroups([]);
+ const smartFolders = filterSmartFolders(ctx.tracksGroups);
+ ctx.setTracksGroups(smartFolders);
}
}, [ctx.listFiles, ctx.selectedSort]);
diff --git a/map/src/manager/track/DeleteTrackManager.js b/map/src/manager/track/DeleteTrackManager.js
index bb516a014..036533c0e 100644
--- a/map/src/manager/track/DeleteTrackManager.js
+++ b/map/src/manager/track/DeleteTrackManager.js
@@ -1,4 +1,10 @@
-import { isCloudTrack, isLocalTrack, loadShareFiles, OBJECT_TYPE_FAVORITE } from '../../context/AppContext';
+import {
+ isCloudTrack,
+ isLocalTrack,
+ loadShareFiles,
+ OBJECT_TYPE_FAVORITE,
+ loadSmartFolders,
+} from '../../context/AppContext';
import { apiGet, apiPost } from '../../util/HttpApi';
import { findGroupByName, getAllVisibleFiles } from './TracksManager';
import { refreshGlobalFiles } from './SaveTrackManager';
@@ -79,6 +85,7 @@ async function deleteCloudFile(name, type, ctx) {
}
return { ...o };
});
+ await loadSmartFolders(ctx.setTracksGroups, ctx.setSmartFoldersCache);
}
}
}
diff --git a/map/src/manager/track/SaveTrackManager.js b/map/src/manager/track/SaveTrackManager.js
index 9ead3eb84..9af0296fd 100644
--- a/map/src/manager/track/SaveTrackManager.js
+++ b/map/src/manager/track/SaveTrackManager.js
@@ -6,6 +6,7 @@ import TracksManager, {
isTrackExists,
isEmptyTrack,
getGpxFiles,
+ filterSmartFolders,
DEFAULT_GROUP_NAME,
GPX_FILE_TYPE,
getGpxFileFromTrackData,
@@ -26,6 +27,7 @@ import {
import Utils from '../../util/Utils';
import { updateSortList } from '../../menu/actions/SortActions';
import { deleteLocalTrack, saveTrackToLocalStorage } from '../../context/LocalTrackStorage';
+import { SMART_TYPE } from '../../menu/share/shareConstants';
export function saveTrackToLocal({ ctx, track, selected = true, overwrite = false, cloudAutoSave = false } = {}) {
const newLocalTracks = [...ctx.localTracks];
@@ -406,7 +408,8 @@ function updateTrackGroups(listFiles, ctx) {
const trackGroups = createTrackGroups({ files, ctx });
ctx.setTracksGroups(trackGroups);
} else {
- ctx.setTracksGroups([]);
+ const smartFolders = filterSmartFolders(ctx.tracksGroups);
+ ctx.setTracksGroups(smartFolders);
}
}
diff --git a/map/src/manager/track/TracksManager.js b/map/src/manager/track/TracksManager.js
index 626d31aa5..d8c753da8 100644
--- a/map/src/manager/track/TracksManager.js
+++ b/map/src/manager/track/TracksManager.js
@@ -20,7 +20,7 @@ import anchorme from 'anchorme';
import { isVisibleTrack, updateVisibleCache } from '../../menu/visibletracks/VisibleTracks';
import { getFileStorage, GPX } from '../GlobalManager';
import { closeTrack } from './DeleteTrackManager';
-import { SHARE_TYPE } from '../../menu/share/shareConstants';
+import { SHARE_TYPE, SMART_TYPE } from '../../menu/share/shareConstants';
import { doSort } from '../../menu/actions/SortActions';
import { DEFAULT_SORT_METHOD } from '../../menu/tracks/TracksMenu';
import { TRACKS_KEY } from '../../util/hooks/menu/useRecentDataSaver';
@@ -56,6 +56,14 @@ export function fitBoundsOptions(ctx) {
};
}
+export function filterSmartFolders(tracksGroups) {
+ return tracksGroups?.filter((g) => g.type === SMART_TYPE) || [];
+}
+
+export function filterRegularFolders(tracksGroups) {
+ return tracksGroups?.filter((g) => g.type !== SMART_TYPE) || [];
+}
+
export function prepareLocalTrack(track) {
const prepareTrack = cloneDeep(track);
return {
@@ -586,7 +594,7 @@ export function createTrackGroups({ files, isSmartf = false, ctx }) {
subfolders: [],
groupFiles: [],
lastModifiedMs: null,
- lastModifiedData: null,
+ lastModifiedDate: null,
};
currentGroups.push(existingGroup);
}
@@ -605,12 +613,15 @@ export function createTrackGroups({ files, isSmartf = false, ctx }) {
files: tracks,
groupFiles: tracks,
lastModifiedMs: null,
- lastModifiedData: null,
+ lastModifiedDate: null,
};
defaultGroup.subfolders = trackGroups.filter((group) => group.name !== DEFAULT_GROUP_NAME);
trackGroups.push(defaultGroup);
}
+ const smartFolders = filterSmartFolders(ctx.tracksGroups);
+ trackGroups.push(...smartFolders);
+
addFilesAndCalculateLastModified(trackGroups);
const sorted = doSort({
@@ -645,30 +656,41 @@ function addFilesAndCalculateLastModified(groups) {
group.files.push(file);
}
});
- const directTracksCount = (group.groupFiles || []).filter((file) => !isPlaceholderFile(file)).length;
- const subfoldersTracksCount = group.subfolders.reduce((acc, subfolder) => acc + (subfolder.realSize ?? 0), 0);
- group.realSize = directTracksCount + subfoldersTracksCount;
+
+ if (group.type === SMART_TYPE && group.userFilePaths && group.groupFiles.length === 0) {
+ group.realSize = group.userFilePaths.length;
+ } else {
+ const directTracksCount = (group.groupFiles || []).filter((file) => !isPlaceholderFile(file)).length;
+ const subfoldersTracksCount = group.subfolders.reduce(
+ (acc, subfolder) => acc + (subfolder.realSize ?? 0),
+ 0
+ );
+ group.realSize = directTracksCount + subfoldersTracksCount;
+ }
calculateLastModified(group);
});
}
function calculateLastModified(group) {
+ if (group.type === SMART_TYPE) {
+ return;
+ }
if (!group.files || group.files.length === 0) {
group.lastModifiedMs = null;
- group.lastModifiedData = null;
+ group.lastModifiedDate = null;
return;
}
- let minMs = Infinity;
- let minData = null;
+ let maxMs = -Infinity;
+ let maxDate = null;
for (const file of group.files) {
- if (file.updatetimems < minMs) {
- minMs = file.updatetimems;
- minData = file.updatetime;
+ if (file.updatetimems > maxMs) {
+ maxMs = file.updatetimems;
+ maxDate = file.updatetime;
}
}
- group.lastModifiedMs = minMs;
- group.lastModifiedData = minData;
+ group.lastModifiedMs = maxMs;
+ group.lastModifiedDate = maxDate;
}
export function findGroupByName(groups, groupName) {
diff --git a/map/src/menu/actions/GroupActions.jsx b/map/src/menu/actions/GroupActions.jsx
index 2a2ee9cd2..3d417c11a 100644
--- a/map/src/menu/actions/GroupActions.jsx
+++ b/map/src/menu/actions/GroupActions.jsx
@@ -10,6 +10,8 @@ import DeleteFolderDialog from '../../dialogs/tracks/DeleteFolderDialog';
import { apiPost } from '../../util/HttpApi';
import AppContext from '../../context/AppContext';
import { useTranslation } from 'react-i18next';
+import { SMART_TYPE } from '../share/shareConstants';
+import { populateSmartFolderFiles } from '../../context/AppContext';
const GroupActions = forwardRef(({ group, setOpenActions, setProcessDownload }, ref) => {
const ctx = useContext(AppContext);
@@ -19,6 +21,19 @@ const GroupActions = forwardRef(({ group, setOpenActions, setProcessDownload },
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
const { t } = useTranslation();
+ const getGroupFiles = () => {
+ if (group.type === SMART_TYPE && group.files?.length === 0) {
+ const populated = populateSmartFolderFiles(
+ group,
+ ctx.listFiles?.uniqueFiles,
+ ctx.smartFoldersCache,
+ ctx.setSmartFoldersCache
+ );
+ return populated.files;
+ }
+ return group.files;
+ };
+
async function downloadFolderBackup() {
setProcessDownload(true);
const res = await apiPost(`${process.env.REACT_APP_USER_API_SITE}/mapapi/download-backup-folder`, [], {
@@ -53,28 +68,30 @@ const GroupActions = forwardRef(({ group, setOpenActions, setProcessDownload },
<>
+ {group.type !== SMART_TYPE && (
+
+ )}
-
-
-
-
-
+ {group.type !== SMART_TYPE && (
+ <>
+
+
+
+
+ >
+ )}
{newCollection.length > 0 && (
diff --git a/map/src/menu/actions/SortActions.jsx b/map/src/menu/actions/SortActions.jsx
index be1399431..8921d0247 100644
--- a/map/src/menu/actions/SortActions.jsx
+++ b/map/src/menu/actions/SortActions.jsx
@@ -1,5 +1,5 @@
import { Box, Divider, FormControl, FormControlLabel, Paper, Radio, RadioGroup } from '@mui/material';
-import { getAnalysisData, getGpxTime } from '../../manager/track/TracksManager';
+import { getAnalysisData, getGpxTime, filterSmartFolders } from '../../manager/track/TracksManager';
import React, { forwardRef, useContext, useEffect, useState } from 'react';
import { ReactComponent as AscendingIcon } from '../../assets/icons/ic_action_sort_by_name_ascending.svg';
import { ReactComponent as TimeIcon } from '../../assets/icons/ic_action_time.svg';
@@ -17,7 +17,8 @@ import FavoritesManager, { DEFAULT_FAV_GROUP_NAME } from '../../manager/Favorite
import i18n from '../../i18n';
import ActionItem from '../components/ActionItem';
import { getSelectedSort } from '../components/buttons/SortFilesButton';
-import { SHARE_TYPE } from '../share/shareConstants';
+import { SHARE_TYPE, SMART_TYPE } from '../share/shareConstants';
+import { DEFAULT_GROUP_NAME } from '../../manager/track/TracksManager';
const az = (a, b) => (a > b) - (a < b);
@@ -254,6 +255,10 @@ const SortActions = forwardRef(
const groups = () => {
if (trackGroup) {
+ if (trackGroup.name === DEFAULT_GROUP_NAME || trackGroup.fullName === DEFAULT_GROUP_NAME) {
+ const smartFolders = filterSmartFolders(ctx.tracksGroups);
+ return [...(trackGroup.subfolders || []), ...smartFolders];
+ }
return trackGroup.subfolders;
} else if (favoriteGroup) {
if (smartf?.type === SHARE_TYPE) {
diff --git a/map/src/menu/share/shareConstants.js b/map/src/menu/share/shareConstants.js
index 7d0313530..1ae255131 100644
--- a/map/src/menu/share/shareConstants.js
+++ b/map/src/menu/share/shareConstants.js
@@ -1,4 +1,5 @@
export const SHARE_TYPE = 'share';
+export const SMART_TYPE = 'smart';
export const SHARE_FILE_TYPE = 'shared_with_me';
export const REQUEST_ACCESS_TYPE = 'request';
export const APPROVED_ACCESS_TYPE = 'approved';
diff --git a/map/src/menu/tracks/CloudTrackGroup.jsx b/map/src/menu/tracks/CloudTrackGroup.jsx
index eb4a44016..e717a7251 100644
--- a/map/src/menu/tracks/CloudTrackGroup.jsx
+++ b/map/src/menu/tracks/CloudTrackGroup.jsx
@@ -2,6 +2,7 @@ import { ListItemIcon, ListItemText, MenuItem, Typography } from '@mui/material'
import React, { useContext, useEffect, useRef, useState } from 'react';
import AppContext from '../../context/AppContext';
import { ReactComponent as FolderIcon } from '../../assets/icons/ic_action_folder.svg';
+import { ReactComponent as SmartIcon } from '../../assets/icons/ic_action_folder_smart.svg';
import styles from '../trackfavmenu.module.css';
import GroupActions from '../actions/GroupActions';
import ActionsMenu from '../actions/ActionsMenu';
@@ -10,6 +11,8 @@ import { useTranslation } from 'react-i18next';
import DividerWithMargin from '../../frame/components/dividers/DividerWithMargin';
import ThreeDotsButton from '../../frame/components/btns/ThreeDotsButton';
import { fmt } from '../../util/dateFmt';
+import { SMART_TYPE } from '../share/shareConstants';
+import { populateSmartFolderFiles } from '../../context/AppContext';
export default function CloudTrackGroup({ index, group }) {
const ctx = useContext(AppContext);
@@ -25,25 +28,51 @@ export default function CloudTrackGroup({ index, group }) {
}
}, [ctx.openedPopper]);
+ const getFolderIcon = () => {
+ if (group.type === SMART_TYPE) {
+ return ;
+ }
+ return ;
+ };
+
+ const handleClick = (e) => {
+ if (e.target !== 'path') {
+ let groupToOpen = group;
+
+ if (group.type === SMART_TYPE) {
+ groupToOpen = populateSmartFolderFiles(
+ group,
+ ctx.listFiles?.uniqueFiles,
+ ctx.smartFoldersCache,
+ ctx.setSmartFoldersCache
+ );
+ }
+
+ ctx.setOpenGroups((prevState) => [...prevState, groupToOpen]);
+ }
+ };
+
+ const getInfoText = () => {
+ if (group.type === SMART_TYPE) {
+ const filesCount = group.userFilePaths?.length ?? 0;
+ return `${filesCount} ${t('shared_string_gpx_files').toLowerCase()}`;
+ }
+ return `${fmt.monthShortDay(group.lastModifiedDate)}, ${t('shared_string_gpx_files').toLowerCase()} ${group.realSize}`;
+ };
+
return (
<>
{!isEmpty(ctx.shareWithMeFiles?.tracks) && (
-
+
)}
{ctx.tracksGroups &&
- (sortGroups && sortGroups.length > 0 ? sortGroups : ctx.tracksGroups)
+ (sortGroups?.length > 0 ? sortGroups : ctx.tracksGroups)
.filter((g) => g.name !== DEFAULT_GROUP_NAME)
.map((group, index) => {
return ;