Skip to content
4 changes: 4 additions & 0 deletions map/src/assets/icons/ic_action_folder_smart.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 63 additions & 1 deletion map/src/context/AppContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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
Expand Down Expand Up @@ -95,6 +96,7 @@ async function loadListFiles(
setProcessingGroups,
setVisibleTracks,
setShareWithMeFiles,
setTracksGroups,
setUpdateFiles
) {
if (loginUser !== listFiles.loginUser) {
Expand All @@ -111,8 +113,9 @@ async function loadListFiles(
res.uniqueFiles.forEach((f) => {
res.totalUniqueZipSize += f.zipSize;
});
getFilesForUpdateDetails(res.uniqueFiles, setUpdateFiles);
getFilesForUpdateDetails(res.uniqueFiles, setUpdateFiles, setTracksGroups);
setListFiles(res);
loadSmartFolders(setTracksGroups);
const favFiles = await loadShareFiles(setShareWithMeFiles);
const ownFavorites = TracksManager.getFavoriteGroups(res);
const allFavorites = [...ownFavorites, ...favFiles];
Expand All @@ -127,6 +130,63 @@ async function loadListFiles(
}
}

export async function loadSmartFolders(setTracksGroups) {
const res = await getSmartFolders();
const smartFolderGroups = (res ?? []).map((smartFolder) => {
return {
name: smartFolder.name,
fullName: smartFolder.name,
type: SMART_TYPE,
subfolders: [],
groupFiles: [],
files: [],
realSize: smartFolder.userFilePaths?.length ?? 0,
lastModifiedMs: null,
lastModifiedDate: null,
userFilePaths: smartFolder.userFilePaths ?? [],
};
});

setTracksGroups((prev) => {
const withoutSmartFolders = prev.filter((g) => g.type !== SMART_TYPE);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplication of logic

return [...withoutSmartFolders, ...smartFolderGroups];
});
}

export function populateSmartFolderFiles(smartFolder, listFiles) {
const filesArray = [];
let maxMs = -Infinity;
let maxDate = null;

(smartFolder.userFilePaths ?? []).forEach((path) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not sort the files on the backend?

const file = listFiles?.find((f) => f.name === path);
if (file) {
filesArray.push({ ...file, smartFolder: true });
if (file.updatetimems > maxMs) {
maxMs = file.updatetimems;
maxDate = file.updatetime;
}
}
});

return {
...smartFolder,
groupFiles: filesArray,
files: filesArray,
realSize: filesArray.length,
lastModifiedMs: maxMs,
lastModifiedDate: maxDate,
};
}

async function getSmartFolders() {
const res = await apiGet(`${process.env.REACT_APP_USER_API_SITE}/mapapi/create-smart-folders`, {});
if (res.ok) {
return res.json();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take the date right away

}
return 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))
Expand Down Expand Up @@ -653,6 +713,7 @@ export const AppContextProvider = (props) => {
uniqueFiles: updatedUniqueFiles,
};
});
await loadSmartFolders(setTracksGroups);
}
}
};
Expand All @@ -676,6 +737,7 @@ export const AppContextProvider = (props) => {
setProcessingGroups,
setVisibleTracks,
setShareWithMeFiles,
setTracksGroups,
setUpdateFiles
).then(() => {
setGpxLoading(false);
Expand Down
4 changes: 3 additions & 1 deletion map/src/frame/GlobalFrame.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -367,7 +368,8 @@ const GlobalFrame = () => {
const trackGroups = createTrackGroups({ files, ctx });
ctx.setTracksGroups(trackGroups);
} else {
ctx.setTracksGroups([]);
const smartFolders = ctx.tracksGroups?.filter((g) => g.type === SMART_TYPE) || [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are empty groups here where smartFolders come from.

ctx.setTracksGroups(smartFolders);
}
}, [ctx.listFiles, ctx.selectedSort]);

Expand Down
9 changes: 8 additions & 1 deletion map/src/manager/track/DeleteTrackManager.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -79,6 +85,7 @@ async function deleteCloudFile(name, type, ctx) {
}
return { ...o };
});
await loadSmartFolders(ctx.setTracksGroups);
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions map/src/manager/track/SaveTrackManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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];
Expand Down Expand Up @@ -337,7 +338,7 @@ export async function refreshGlobalFiles({
const respGetFiles = await apiGet(`${process.env.REACT_APP_USER_API_SITE}/mapapi/list-files`, {});
const resJson = await respGetFiles.json();
if (resJson && resJson.uniqueFiles) {
getFilesForUpdateDetails(resJson.uniqueFiles, ctx.setUpdateFiles);
getFilesForUpdateDetails(resJson.uniqueFiles, ctx.setUpdateFiles, ctx.setTracksGroups);
ctx.setListFiles(resJson);
}
if (type === OBJECT_TYPE_FAVORITE) {
Expand Down Expand Up @@ -406,7 +407,8 @@ function updateTrackGroups(listFiles, ctx) {
const trackGroups = createTrackGroups({ files, ctx });
ctx.setTracksGroups(trackGroups);
} else {
ctx.setTracksGroups([]);
const smartFolders = ctx.tracksGroups?.filter((g) => g.type === SMART_TYPE) || [];
ctx.setTracksGroups(smartFolders);
}
}

Expand Down
39 changes: 25 additions & 14 deletions map/src/manager/track/TracksManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -586,7 +586,7 @@ export function createTrackGroups({ files, isSmartf = false, ctx }) {
subfolders: [],
groupFiles: [],
lastModifiedMs: null,
lastModifiedData: null,
lastModifiedDate: null,
};
currentGroups.push(existingGroup);
}
Expand All @@ -605,12 +605,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 = ctx.tracksGroups?.filter((g) => g.type === SMART_TYPE) || [];
trackGroups.push(...smartFolders);

addFilesAndCalculateLastModified(trackGroups);

const sorted = doSort({
Expand Down Expand Up @@ -645,30 +648,38 @@ 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.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) {
Expand Down
7 changes: 6 additions & 1 deletion map/src/menu/actions/SortActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -254,6 +255,10 @@ const SortActions = forwardRef(

const groups = () => {
if (trackGroup) {
if (trackGroup.name === DEFAULT_GROUP_NAME || trackGroup.fullName === DEFAULT_GROUP_NAME) {
const smartFolders = ctx.tracksGroups?.filter((g) => g.type === SMART_TYPE) || [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplication of logic

return [...(trackGroup.subfolders || []), ...smartFolders];
}
return trackGroup.subfolders;
} else if (favoriteGroup) {
if (smartf?.type === SHARE_TYPE) {
Expand Down
1 change: 1 addition & 0 deletions map/src/menu/share/shareConstants.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
55 changes: 45 additions & 10 deletions map/src/menu/tracks/CloudTrackGroup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
Expand All @@ -19,31 +22,63 @@ export default function CloudTrackGroup({ index, group }) {
const [processDownload, setProcessDownload] = useState(false);
const anchorEl = useRef(null);

const [populatedGroup, setPopulatedGroup] = useState(group);

useEffect(() => {
if (ctx.openedPopper && ctx.openedPopper !== anchorEl) {
setOpenActions(false);
}
}, [ctx.openedPopper]);

useEffect(() => {
if (group.type === SMART_TYPE && openActions && group.files?.length === 0) {
const populated = populateSmartFolderFiles(group, ctx.listFiles?.uniqueFiles);
setPopulatedGroup(populated);
} else {
setPopulatedGroup(group);
}
}, [openActions, group, ctx.listFiles?.uniqueFiles]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list of files for filters should only be fetched when a specific folder is opened. The cache should be stored after the first read, and cleared only when either listFiles changes or the context is updated.


const getFolderIcon = () => {
if (group.type === SMART_TYPE) {
return <SmartIcon />;
}
return <FolderIcon />;
};

const handleClick = (e) => {
if (e.target !== 'path') {
let groupToOpen = group;

if (group.type === SMART_TYPE) {
groupToOpen = populateSmartFolderFiles(group, ctx.listFiles?.uniqueFiles);
}

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 (
<>
<MenuItem
className={styles.group}
key={'group' + group.name + index}
id={'se-menu-cloud-' + group.name}
onClick={(e) => {
if (e.target !== 'path') {
ctx.setOpenGroups((prevState) => [...prevState, group]);
}
}}
onClick={handleClick}
>
<ListItemIcon className={styles.icon}>
<FolderIcon />
</ListItemIcon>
<ListItemIcon className={styles.icon}>{getFolderIcon()}</ListItemIcon>
<ListItemText>
<MenuItemWithLines name={group.name} maxLines={2} />
<Typography variant="body2" className={styles.groupInfo} noWrap>
{`${fmt.monthShortDay(group.lastModifiedData)}, ${t('shared_string_gpx_files').toLowerCase()} ${group.realSize}`}
{getInfoText()}
</Typography>
</ListItemText>
<ThreeDotsButton
Expand All @@ -62,7 +97,7 @@ export default function CloudTrackGroup({ index, group }) {
anchorEl={anchorEl}
actions={
<GroupActions
group={group}
group={populatedGroup}
setOpenActions={setOpenActions}
setProcessDownload={setProcessDownload}
/>
Expand Down
Loading