Skip to content

Commit

Permalink
most of the implementation
Browse files Browse the repository at this point in the history
remaining bugs:

- modification time not being reliable
- map name should not come from file name but from style.json
- handle FileSystem.documentDirectory being null?
  • Loading branch information
achou11 committed Oct 17, 2024
1 parent fe74e7e commit b96a8ff
Show file tree
Hide file tree
Showing 11 changed files with 786 additions and 10 deletions.
63 changes: 63 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@
"Screens.Settings.AppSettings.languageDesc": {
"message": "Display language for app"
},
"Screens.Settings.AppSettings.mapManagement": {
"message": "Map Management"
},
"Screens.Settings.AppSettings.mapManagementDesc": {
"message": "Backgrounds, Map Data"
},
"Screens.Settings.AppSettings.title": {
"message": "App Settings"
},
Expand Down Expand Up @@ -1264,6 +1270,63 @@
"screens.Settings.CreateOrJoinProject.whatIsAProject": {
"message": "What is a Project"
},
"screens.Settings.MapManagement.BackgroundMaps.ChooseMapFile.acceptedFileTypes": {
"message": "Accepted file types are .smp"
},
"screens.Settings.MapManagement.BackgroundMaps.ChooseMapFile.chooseFile": {
"message": "Choose File"
},
"screens.Settings.MapManagement.BackgroundMaps.about": {
"message": "About Custom Map"
},
"screens.Settings.MapManagement.BackgroundMaps.cannotBeUndone": {
"message": "This cannot be undone."
},
"screens.Settings.MapManagement.BackgroundMaps.close": {
"message": "Close"
},
"screens.Settings.MapManagement.BackgroundMaps.customMapAddedTitle": {
"message": "Custom Map Added"
},
"screens.Settings.MapManagement.BackgroundMaps.deleteCustomMapDescription": {
"message": "This will delete the map and its offline areas. No collected observation data will be deleted."
},
"screens.Settings.MapManagement.BackgroundMaps.deleteCustomMapTitle": {
"message": "Delete CUstom Map?"
},
"screens.Settings.MapManagement.BackgroundMaps.deleteMapButtonText": {
"message": "Delete Map"
},
"screens.Settings.MapManagement.BackgroundMaps.description1": {
"message": "Adding a custom map will enable you to see a map when you are offline."
},
"screens.Settings.MapManagement.BackgroundMaps.description2": {
"message": "Your custom map is not shared with other devices in your project."
},
"screens.Settings.MapManagement.BackgroundMaps.offlineMapAddedDescription": {
"message": "You will see this map when you are offline, but you will not see a map outside the area defined in your custom map."
},
"screens.Settings.MapManagement.BackgroundMaps.screenTitle": {
"message": "Background Maps"
},
"screens.Settings.MapManagement.MapsList.CustomMapDetails.dateAdded": {
"message": "Date Added"
},
"screens.Settings.MapManagement.MapsList.CustomMapDetails.mapNameColumn": {
"message": "Map Name"
},
"screens.Settings.MapManagement.MapsList.CustomMapDetails.removeMap": {
"message": "Remove Map"
},
"screens.Settings.MapManagement.MapsList.CustomMapDetails.sizeInMegabytes": {
"message": "{value} MB"
},
"screens.Settings.MapManagement.backgroundMaps": {
"message": "Background Maps"
},
"screens.Settings.MapManagement.screenTitle": {
"message": "Map Management"
},
"screens.Settings.YourTeam.InviteDeclined": {
"message": "Invitation Declined"
},
Expand Down
22 changes: 20 additions & 2 deletions src/frontend/Navigation/Stack/AppScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ import {
Audio,
navigationOptions as audioNavigationOptions,
} from '../../screens/Audio/index.tsx';
import {
createNavigationOptions as createMapManagementNavigationOptions,
MapManagementScreen,
} from '../../screens/Settings/MapManagement';
import {
createNavigationOptions as createBackgroundMapsNavigationOptions,
BackgroundMapsScreen,
} from '../../screens/Settings/MapManagement/BackgroundMaps.tsx';

export const TAB_BAR_HEIGHT = 70;

Expand Down Expand Up @@ -314,13 +322,11 @@ export const createDefaultScreenGroup = ({
component={TrackEdit}
options={{headerTitle: intl(TrackEdit.navTitle)}}
/>

<RootStack.Screen
name="Config"
component={Config}
options={{headerTitle: intl(Config.navTitle)}}
/>

<RootStack.Screen
name="HowToLeaveProject"
component={HowToLeaveProject}
Expand All @@ -333,6 +339,18 @@ export const createDefaultScreenGroup = ({
component={Audio}
/>
)}

<RootStack.Screen
name="MapManagement"
component={MapManagementScreen}
options={createMapManagementNavigationOptions({intl})}
/>
<RootStack.Screen
name="BackgroundMaps"
component={BackgroundMapsScreen}
options={createBackgroundMapsNavigationOptions({intl})}
/>

{process.env.EXPO_PUBLIC_FEATURE_TEST_DATA_UI && (
<RootStack.Screen
name="CreateTestData"
Expand Down
145 changes: 145 additions & 0 deletions src/frontend/hooks/customMaps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system';

// TODO: What to do when `FileSystem.documentDirectory` is null?
export const CUSTOM_STYLED_MAPS_DIRECTORY =
FileSystem.documentDirectory + 'styled-maps/';

export const CUSTOM_MAPS_QUERY_KEY = 'custom-maps';

export function useSelectOfflineMapFile() {
return useMutation({
mutationFn: () => {
return selectFile({
extensionFilters: ['smp'],
});
},
});
}

export function useImportCustomMapFile() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: async (opts: {uri: string}) => {
if (!FileSystem.documentDirectory) {
throw new Error('Document directory is unknown');
}

const fileName = opts.uri.split('/').at(-1);

const directoryFileInfo = await FileSystem.getInfoAsync(
CUSTOM_STYLED_MAPS_DIRECTORY,
);

if (!directoryFileInfo.exists) {
await FileSystem.makeDirectoryAsync(CUSTOM_STYLED_MAPS_DIRECTORY);
}

return FileSystem.moveAsync({
from: opts.uri,
to: CUSTOM_STYLED_MAPS_DIRECTORY + fileName,
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [CUSTOM_MAPS_QUERY_KEY],
});
},
});
}

export function useRemoveCustomMapFile() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (opts: {uri: string}) => {
return FileSystem.deleteAsync(opts.uri, {
idempotent: true,
});
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [CUSTOM_MAPS_QUERY_KEY],
});
},
});
}

/**
* Returns `null` if no viable map is found. Throws an error if a detected map is invalid.
*/
export function useGetCustomMapDetails() {
return useQuery({
queryKey: [CUSTOM_MAPS_QUERY_KEY, 'active'],
queryFn: async () => {
const files = await FileSystem.readDirectoryAsync(
CUSTOM_STYLED_MAPS_DIRECTORY,
);

const activeUri = files[0];

if (!activeUri) {
return null;
}

const mapFileInfo = await FileSystem.getInfoAsync(
CUSTOM_STYLED_MAPS_DIRECTORY + activeUri,
);

if (!mapFileInfo.exists || mapFileInfo.isDirectory) {
return null;
}

const mapName = mapFileInfo.uri.split('/').at(-1);

if (!mapName) {
throw new Error('Unable to derive map name');
}

return {
size: mapFileInfo.size,
uri: mapFileInfo.uri,
// TODO: Cannot seem to rely on this being accurate. May need to keep an adjacent timestamp file
modificationTime: mapFileInfo.modificationTime,
name: mapName,
};
},
});
}

async function selectFile(opts: {
copyToCache?: boolean;
mimeFilters?: Array<string>;
extensionFilters?: Array<string>;
}) {
const documentResult = await DocumentPicker.getDocumentAsync({
type: opts.mimeFilters,
copyToCacheDirectory: opts.copyToCache,
multiple: false,
});

if (documentResult.canceled) return null;

const asset = documentResult.assets[0];

if (!asset) {
throw new Error();
}

const hasValidExtension = opts.extensionFilters
? opts.extensionFilters.some(extension =>
asset.uri.endsWith(`.${extension}`),
)
: true;

if (!hasValidExtension) {
FileSystem.deleteAsync(asset.uri).catch(err => {
console.log(err);
});
throw new Error('Invalid extension');
}

return asset;
}
3 changes: 3 additions & 0 deletions src/frontend/lib/bytesToMegabytes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function bytesToMegabytes(bytes: number) {
return bytes / 2 ** 20;
}
19 changes: 19 additions & 0 deletions src/frontend/screens/Settings/AppSettings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ const m = defineMessages({
id: 'Screens.Settings.AppSettings.coordinateSystemDesc',
defaultMessage: 'UTM,Lat/Lon,DMS',
},
mapManagement: {
id: 'Screens.Settings.AppSettings.mapManagement',
defaultMessage: 'Map Management',
},
mapManagementDesc: {
id: 'Screens.Settings.AppSettings.mapManagementDesc',
defaultMessage: 'Backgrounds, Map Data',
},
});

export const AppSettings: NativeNavigationComponent<'AppSettings'> = ({
Expand Down Expand Up @@ -57,6 +65,17 @@ export const AppSettings: NativeNavigationComponent<'AppSettings'> = ({
secondary={<FormattedMessage {...m.coordinateSystemDesc} />}
/>
</ListItem>
<ListItem
onPress={() => {
navigation.navigate('MapManagement');
}}
testID="mapManagementButton">
<ListItemIcon iconName="map" />
<ListItemText
primary={<FormattedMessage {...m.mapManagement} />}
secondary={<FormattedMessage {...m.mapManagementDesc} />}
/>
</ListItem>
</List>
</ScrollView>
);
Expand Down
Loading

0 comments on commit b96a8ff

Please sign in to comment.