|
@@ -15,26 +15,50 @@ import { offlineMapManager } from './OfflineMapManager';
|
|
import { NAVIGATION_PAGES } from 'src/types';
|
|
import { NAVIGATION_PAGES } from 'src/types';
|
|
import { formatBytes } from './formatters';
|
|
import { formatBytes } from './formatters';
|
|
import { useSubscription } from './useSubscription';
|
|
import { useSubscription } from './useSubscription';
|
|
-import { Header, Input, Loading, PageWrapper } from 'src/components';
|
|
|
|
|
|
+import { Header, Input, Loading, PageWrapper, WarningModal } from 'src/components';
|
|
import { Colors } from 'src/theme';
|
|
import { Colors } from 'src/theme';
|
|
import { getFontSize } from 'src/utils';
|
|
import { getFontSize } from 'src/utils';
|
|
import * as Progress from 'react-native-progress';
|
|
import * as Progress from 'react-native-progress';
|
|
import { storage, StoreType } from 'src/storage';
|
|
import { storage, StoreType } from 'src/storage';
|
|
|
|
+import EditIcon from 'assets/icons/travels-screens/pen-to-square.svg';
|
|
|
|
+import { usePostGetLastMapUpdateDateQuery } from '@api/maps';
|
|
|
|
|
|
const OFFLINE_MAPS_KEY = 'offline_maps';
|
|
const OFFLINE_MAPS_KEY = 'offline_maps';
|
|
|
|
|
|
export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
|
|
+ const token = storage.get('token', StoreType.STRING) as string;
|
|
const contentWidth = Dimensions.get('window').width * 0.9;
|
|
const contentWidth = Dimensions.get('window').width * 0.9;
|
|
|
|
+ const { data: lastMapUpdate } = usePostGetLastMapUpdateDateQuery(token, true);
|
|
const [maps, setMaps] = useState([]);
|
|
const [maps, setMaps] = useState([]);
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [editingId, setEditingId] = useState(null);
|
|
const [editingName, setEditingName] = useState('');
|
|
const [editingName, setEditingName] = useState('');
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
const { isPremium } = useSubscription();
|
|
const { isPremium } = useSubscription();
|
|
|
|
+ const [lastUpdate, setLastUpdate] = useState<string>(new Date().toISOString().slice(0, 10));
|
|
|
|
+ const [modalState, setModalState] = useState({
|
|
|
|
+ visible: false,
|
|
|
|
+ type: 'confirm',
|
|
|
|
+ action: () => {},
|
|
|
|
+ message: '',
|
|
|
|
+ title: ''
|
|
|
|
+ });
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
offlineMapManager.init();
|
|
offlineMapManager.init();
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (lastMapUpdate) {
|
|
|
|
+ setLastUpdate(lastMapUpdate.last_update);
|
|
|
|
+ }
|
|
|
|
+ }, [lastMapUpdate]);
|
|
|
|
+
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ if (lastUpdate) {
|
|
|
|
+ loadMaps();
|
|
|
|
+ }
|
|
|
|
+ }, [lastUpdate]);
|
|
|
|
+
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
const intervalId = setInterval(() => {
|
|
const intervalId = setInterval(() => {
|
|
const mapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
const mapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
@@ -73,6 +97,14 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ const mapUpdateDate = new Date(map.updatedAt).toISOString().slice(0, 10);
|
|
|
|
+ if (mapUpdateDate < lastUpdate) {
|
|
|
|
+ return {
|
|
|
|
+ ...map,
|
|
|
|
+ status: 'invalid'
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+
|
|
return {
|
|
return {
|
|
...map,
|
|
...map,
|
|
status: isPremium && availablePackIds.includes(map.id) ? 'valid' : 'invalid'
|
|
status: isPremium && availablePackIds.includes(map.id) ? 'valid' : 'invalid'
|
|
@@ -87,7 +119,7 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
} finally {
|
|
} finally {
|
|
setLoading(false);
|
|
setLoading(false);
|
|
}
|
|
}
|
|
- }, [isPremium]);
|
|
|
|
|
|
+ }, [isPremium, lastUpdate]);
|
|
|
|
|
|
useFocusEffect(
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
useCallback(() => {
|
|
@@ -103,6 +135,21 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
}
|
|
}
|
|
}, [isPremium]);
|
|
}, [isPremium]);
|
|
|
|
|
|
|
|
+ const hasDownloadingMaps = () => {
|
|
|
|
+ if (!maps) return false;
|
|
|
|
+ return maps.some((map) => map?.status === 'downloading' && map?.progress < 100);
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const closeModal = () => {
|
|
|
|
+ setModalState({
|
|
|
|
+ visible: false,
|
|
|
|
+ type: 'confirm',
|
|
|
|
+ action: () => {},
|
|
|
|
+ message: '',
|
|
|
|
+ title: ''
|
|
|
|
+ });
|
|
|
|
+ };
|
|
|
|
+
|
|
const restoreOfflineMaps = async () => {
|
|
const restoreOfflineMaps = async () => {
|
|
try {
|
|
try {
|
|
const savedMapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
const savedMapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
@@ -112,18 +159,16 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
|
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
|
|
|
- for (const map of savedMaps.filter((m: any) => m.status === 'invalid')) {
|
|
|
|
|
|
+ for (const map of savedMaps.filter((m: any) => m.status === 'invalid' && m.progress < 100)) {
|
|
try {
|
|
try {
|
|
const pack = await offlineMapManager.getPack(map.id);
|
|
const pack = await offlineMapManager.getPack(map.id);
|
|
if (!pack) {
|
|
if (!pack) {
|
|
- console.log(`Pack ${map.id} not found, will be downloaded`);
|
|
|
|
-
|
|
|
|
if (map.bounds) {
|
|
if (map.bounds) {
|
|
await offlineMapManager.resumePackDownload(map.id);
|
|
await offlineMapManager.resumePackDownload(map.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
} catch (error) {
|
|
- console.log(`Error checking pack ${map.id}:`, error);
|
|
|
|
|
|
+ console.error(`Error checking pack ${map.id}:`, error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
@@ -147,54 +192,50 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
};
|
|
};
|
|
|
|
|
|
const handleDelete = (id: string, name: string) => {
|
|
const handleDelete = (id: string, name: string) => {
|
|
- Alert.alert('Delete Offline Map', `Are you sure you want to delete "${name}"?`, [
|
|
|
|
- { text: 'Cancel', style: 'cancel' },
|
|
|
|
- {
|
|
|
|
- text: 'Delete',
|
|
|
|
- style: 'destructive',
|
|
|
|
- onPress: async () => {
|
|
|
|
- try {
|
|
|
|
- const mapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
|
|
- const maps = mapsString ? JSON.parse(mapsString) : [];
|
|
|
|
- const map = maps.find((m: any) => m.id === id);
|
|
|
|
-
|
|
|
|
- if (map && map.status === 'downloading') {
|
|
|
|
- await offlineMapManager.cancelPackDownload(id);
|
|
|
|
- }
|
|
|
|
|
|
+ setModalState({
|
|
|
|
+ visible: true,
|
|
|
|
+ type: 'delete',
|
|
|
|
+ action: async () => {
|
|
|
|
+ try {
|
|
|
|
+ const mapsString = storage.get(OFFLINE_MAPS_KEY, StoreType.STRING) as string;
|
|
|
|
+ const maps = mapsString ? JSON.parse(mapsString) : [];
|
|
|
|
+ const map = maps.find((m: any) => m.id === id);
|
|
|
|
|
|
- await offlineMapManager.deletePack(id);
|
|
|
|
|
|
+ if (map && map.status === 'downloading') {
|
|
|
|
+ await offlineMapManager.cancelPackDownload(id);
|
|
|
|
+ }
|
|
|
|
|
|
- const updatedMaps = maps.filter((map) => map.id !== id);
|
|
|
|
- storage.set(OFFLINE_MAPS_KEY, JSON.stringify(updatedMaps));
|
|
|
|
|
|
+ await offlineMapManager.deletePack(id);
|
|
|
|
|
|
- setMaps(updatedMaps);
|
|
|
|
- } catch (error) {
|
|
|
|
- console.error('Error deleting offline map:', error);
|
|
|
|
- Alert.alert('Error', 'Failed to delete offline map');
|
|
|
|
- }
|
|
|
|
|
|
+ const updatedMaps = maps.filter((map: any) => map.id !== id);
|
|
|
|
+ storage.set(OFFLINE_MAPS_KEY, JSON.stringify(updatedMaps));
|
|
|
|
+
|
|
|
|
+ setMaps(updatedMaps);
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('Error deleting offline map:', error);
|
|
|
|
+ Alert.alert('Error', 'Failed to delete offline map');
|
|
}
|
|
}
|
|
- }
|
|
|
|
- ]);
|
|
|
|
|
|
+ },
|
|
|
|
+ message: `Are you sure you want to delete "${name}"?`,
|
|
|
|
+ title: 'Delete Offline Map'
|
|
|
|
+ });
|
|
};
|
|
};
|
|
|
|
|
|
- // TO DO
|
|
|
|
- // const handleUpdate = async (map) => {
|
|
|
|
- // try {
|
|
|
|
- // await offlineMapManager.updatePack(
|
|
|
|
- // map.id,
|
|
|
|
- // (progress) => {
|
|
|
|
- // console.log(`Update progress for ${map.id}:`, progress.percentage);
|
|
|
|
- // },
|
|
|
|
- // (error) => {
|
|
|
|
- // console.error(`Error updating map ${map.id}:`, error);
|
|
|
|
- // }
|
|
|
|
- // );
|
|
|
|
-
|
|
|
|
- // loadMaps();
|
|
|
|
- // } catch (error) {
|
|
|
|
- // console.error('Error updating offline map:', error);
|
|
|
|
- // }
|
|
|
|
- // };
|
|
|
|
|
|
+ const handleUpdate = async (map: any) => {
|
|
|
|
+ try {
|
|
|
|
+ await offlineMapManager.updatePack(
|
|
|
|
+ map.id,
|
|
|
|
+ (progress: any) => {},
|
|
|
|
+ (error: any) => {
|
|
|
|
+ console.error(`Error updating map ${map.id}:`, error);
|
|
|
|
+ }
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ loadMaps();
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('Error updating offline map:', error);
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
|
|
const handleEditName = (id, currentName) => {
|
|
const handleEditName = (id, currentName) => {
|
|
setEditingId(id);
|
|
setEditingId(id);
|
|
@@ -207,7 +248,7 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
return;
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
- const updatedMaps = maps.map((map) =>
|
|
|
|
|
|
+ const updatedMaps = maps.map((map: any) =>
|
|
map.id === editingId ? { ...map, name: editingName } : map
|
|
map.id === editingId ? { ...map, name: editingName } : map
|
|
);
|
|
);
|
|
|
|
|
|
@@ -245,17 +286,17 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
) : (
|
|
) : (
|
|
<View style={styles.mapInfoContainer}>
|
|
<View style={styles.mapInfoContainer}>
|
|
<Text style={styles.mapName}>{item.name}</Text>
|
|
<Text style={styles.mapName}>{item.name}</Text>
|
|
- <TouchableOpacity
|
|
|
|
|
|
+ {/* <TouchableOpacity
|
|
onPress={() => handleEditName(item.id, item.name)}
|
|
onPress={() => handleEditName(item.id, item.name)}
|
|
style={styles.iconButton}
|
|
style={styles.iconButton}
|
|
>
|
|
>
|
|
- <Ionicons name="pencil" size={16} color={Colors.DARK_BLUE} />
|
|
|
|
- </TouchableOpacity>
|
|
|
|
|
|
+ <EditIcon height={16} width={16} color={Colors.DARK_BLUE} />
|
|
|
|
+ </TouchableOpacity> */}
|
|
</View>
|
|
</View>
|
|
)}
|
|
)}
|
|
|
|
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
- <View>
|
|
|
|
|
|
+ <View style={{ gap: 2 }}>
|
|
<Text style={styles.mapDetails}>Size: {formatBytes(item.size)}</Text>
|
|
<Text style={styles.mapDetails}>Size: {formatBytes(item.size)}</Text>
|
|
<Text style={styles.mapDetails}>
|
|
<Text style={styles.mapDetails}>
|
|
Updated: {new Date(item.updatedAt).toLocaleDateString()}
|
|
Updated: {new Date(item.updatedAt).toLocaleDateString()}
|
|
@@ -301,8 +342,7 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
{isPremium && (
|
|
{isPremium && (
|
|
<View style={styles.actionsContainer}>
|
|
<View style={styles.actionsContainer}>
|
|
<TouchableOpacity
|
|
<TouchableOpacity
|
|
- // TO DO
|
|
|
|
- // onPress={() => handleUpdate(item)}
|
|
|
|
|
|
+ onPress={() => handleUpdate(item)}
|
|
style={[styles.actionButton, styles.updateButton]}
|
|
style={[styles.actionButton, styles.updateButton]}
|
|
disabled={!isPremium}
|
|
disabled={!isPremium}
|
|
>
|
|
>
|
|
@@ -342,17 +382,29 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
if (isPremium) {
|
|
if (isPremium) {
|
|
navigation.navigate(NAVIGATION_PAGES.OFFLINE_SELECT_REGIONS);
|
|
navigation.navigate(NAVIGATION_PAGES.OFFLINE_SELECT_REGIONS);
|
|
} else {
|
|
} else {
|
|
- Alert.alert(
|
|
|
|
- 'Premium Feature',
|
|
|
|
- 'Offline maps are available only with premium subscription.',
|
|
|
|
- [{ text: 'OK' }]
|
|
|
|
- );
|
|
|
|
|
|
+ setModalState({
|
|
|
|
+ visible: true,
|
|
|
|
+ type: 'success',
|
|
|
|
+ action: () => {},
|
|
|
|
+ message:
|
|
|
|
+ 'This feature is available to Premium users. Premium account settings can be managed on our website.',
|
|
|
|
+ title: 'Premium Feature'
|
|
|
|
+ });
|
|
}
|
|
}
|
|
}}
|
|
}}
|
|
>
|
|
>
|
|
<Text style={styles.buttonText}>Select regions</Text>
|
|
<Text style={styles.buttonText}>Select regions</Text>
|
|
</TouchableOpacity>
|
|
</TouchableOpacity>
|
|
|
|
|
|
|
|
+ {hasDownloadingMaps() && (
|
|
|
|
+ <View style={styles.downloadingNoticeContainer}>
|
|
|
|
+ <Text style={styles.downloadingNoticeText}>
|
|
|
|
+ Offline map downloading requires the app to stay open and active. Downloads will pause
|
|
|
|
+ if you leave the app or lock your screen.
|
|
|
|
+ </Text>
|
|
|
|
+ </View>
|
|
|
|
+ )}
|
|
|
|
+
|
|
{loading ? (
|
|
{loading ? (
|
|
<View style={styles.loadingContainer}>
|
|
<View style={styles.loadingContainer}>
|
|
<Loading />
|
|
<Loading />
|
|
@@ -372,11 +424,33 @@ export default function OfflineMapsScreen({ navigation }: { navigation: any }) {
|
|
/>
|
|
/>
|
|
)}
|
|
)}
|
|
</ScrollView>
|
|
</ScrollView>
|
|
|
|
+
|
|
|
|
+ <WarningModal
|
|
|
|
+ type={modalState.type}
|
|
|
|
+ isVisible={modalState.visible}
|
|
|
|
+ message={modalState.message}
|
|
|
|
+ title={modalState.title}
|
|
|
|
+ action={modalState.action}
|
|
|
|
+ onClose={() => closeModal()}
|
|
|
|
+ />
|
|
</PageWrapper>
|
|
</PageWrapper>
|
|
);
|
|
);
|
|
}
|
|
}
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
const styles = StyleSheet.create({
|
|
|
|
+ downloadingNoticeContainer: {
|
|
|
|
+ backgroundColor: Colors.FILL_LIGHT,
|
|
|
|
+ borderRadius: 8,
|
|
|
|
+ paddingHorizontal: 12,
|
|
|
|
+ paddingVertical: 8,
|
|
|
|
+ alignItems: 'center',
|
|
|
|
+ gap: 8
|
|
|
|
+ },
|
|
|
|
+ downloadingNoticeText: {
|
|
|
|
+ fontSize: getFontSize(12),
|
|
|
|
+ color: Colors.TEXT_GRAY,
|
|
|
|
+ flex: 1
|
|
|
|
+ },
|
|
container: {
|
|
container: {
|
|
flex: 1,
|
|
flex: 1,
|
|
backgroundColor: '#fff',
|
|
backgroundColor: '#fff',
|
|
@@ -406,7 +480,7 @@ const styles = StyleSheet.create({
|
|
},
|
|
},
|
|
listContainer: {
|
|
listContainer: {
|
|
paddingBottom: 16,
|
|
paddingBottom: 16,
|
|
- paddingTop: 10
|
|
|
|
|
|
+ paddingTop: 8
|
|
},
|
|
},
|
|
mapItem: {
|
|
mapItem: {
|
|
backgroundColor: Colors.FILL_LIGHT,
|
|
backgroundColor: Colors.FILL_LIGHT,
|
|
@@ -447,7 +521,8 @@ const styles = StyleSheet.create({
|
|
paddingHorizontal: 12,
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
paddingVertical: 6,
|
|
borderRadius: 4,
|
|
borderRadius: 4,
|
|
- marginLeft: 8
|
|
|
|
|
|
+ minWidth: 68,
|
|
|
|
+ alignItems: 'center'
|
|
},
|
|
},
|
|
updateButton: {
|
|
updateButton: {
|
|
backgroundColor: Colors.ORANGE
|
|
backgroundColor: Colors.ORANGE
|