Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/manually edit progress #85

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 129 additions & 2 deletions js/components/DataManagement/DataManagement.react.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import React from 'react';
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
TouchableNativeFeedback,
Alert,
Modal,
TextInput,
Button,
} from 'react-native';
import {ScrollView} from 'react-native-gesture-handler';
import useStatusBarStyle from '../../hooks/useStatusBarStyle';
import CourseData from '../../course-data';
import DownloadManager from '../../download-manager';
import {genDeleteProgressForCourse} from '../../persistence';
import {genDeleteProgressForCourse, genSetProgressForCourse} from '../../persistence';
import {log} from '../../metrics';
import { useNavigation } from '@react-navigation/core';
import { MainNavigationProp } from '../App.react';

const DataManagement = ({route}: {route: any}) => {
useStatusBarStyle('white', 'dark-content');
const [modalVisible, setModalVisible] = useState(false);
const [lastLessonNumber, _setLastLessonNumber] = useState('');
const setLastLessonNumber = (val: string) => {
_setLastLessonNumber(val.replace(/[^0-9]/g, ''));
};
const {navigate} = useNavigation<MainNavigationProp<'Data Management'>>();

const {course} = route.params;
Expand Down Expand Up @@ -66,6 +74,89 @@ const DataManagement = ({route}: {route: any}) => {
</TouchableNativeFeedback>
</View>

<View style={styles.button}>
<TouchableNativeFeedback
onPress={async () => setModalVisible(true)}
// TODO: Replace with modal with number input and guide text. Added this for testing purposes.
useForeground={true}>
<View style={styles.buttonInner}>
<Text style={styles.buttonTextHeader}>
Set {courseTitle} progress
</Text>
<Text style={styles.buttonText}>
This will set all the lessons up to the given lesson number as
completed.
</Text>
</View>
</TouchableNativeFeedback>
<Modal
animationType="fade"
transparent={true}
visible={modalVisible}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
setModalVisible(!modalVisible);
// TODO: How do I do this better? I don't need the value of the input after the modal is closed so it can be discarded. A local variable would suffice.
setLastLessonNumber('');
}}>
<View style={styles.centeredView}>
<View style={styles.modalView}>
<Text style={styles.modalText}>
Set the lesson number you wish to set your course progress to:
</Text>
<TextInput
style={styles.input}
onChangeText={setLastLessonNumber}
value={lastLessonNumber}
placeholder="Lesson number"
keyboardType="numeric"
/>
<View>
<View>
<View style={styles.modalMargin}>
<Button
title="Cancel"
onPress={() => {
setModalVisible(!modalVisible);
// TODO: How do I do this better? I don't need the value of the input after the modal is closed so it can be discarded. A local variable would suffice.
setLastLessonNumber('');
}}
/>
</View>
<View>
<Button
disabled={!lastLessonNumber}
title="Set progress"
onPress={async () => {
const result = await genSetProgressForCourse(
course,
parseInt(lastLessonNumber, 10),
);

if (!result.hasPassed) {
Alert.alert(
'Error',
`Please enter a valid lesson number. The ${courseTitle} course consists of ${result.lessonCount} lessons.`,
);
} else {
Alert.alert(
'Success',
`The last completed lesson for this course is now set to lesson number ${lastLessonNumber}.`,
);
}
setModalVisible(!modalVisible);
// TODO: How do I do this better? I don't need the value of the input after the modal is closed so it can be discarded. A local variable would suffice.
setLastLessonNumber('');
}}
/>
</View>
</View>
</View>
</View>
</View>
</Modal>
</View>

<View style={styles.button}>
<TouchableNativeFeedback
onPress={async () => {
Expand Down Expand Up @@ -274,6 +365,42 @@ const styles = StyleSheet.create({
buttonText: {
fontSize: 16,
},
centeredView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
marginTop: 22,
backgroundColor: 'rgba(0, 0, 0, 0.55)',
},
modalView: {
margin: 20,
backgroundColor: 'white',
borderRadius: 20,
padding: 35,
alignItems: 'center',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
textAlign: 'center',
},
modalText: {
marginBottom: 15,
textAlign: 'center',
},
modalMargin: {
marginBottom: 15,
},
});

export default DataManagement;
4 changes: 3 additions & 1 deletion js/components/Listen/ListenBody.react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import TrackPlayer, {
} from 'react-native-track-player';
import {log} from '../../metrics';
import useIsLessonDownloaded from '../../hooks/useIsLessonDownloaded';
import useIsLessonFinished from '../../hooks/useIsLessonFinished';

interface Props {
course: Course,
Expand All @@ -38,6 +39,7 @@ const ListenBody = ({course, lesson, setBottomSheetOpen, skipBack, seekTo, toggl
const paused = playbackState === State.Paused;
const bottomSheet = useRef<RBSheet | null>(null);

const finished = useIsLessonFinished(course, lesson);
const downloaded = useIsLessonDownloaded(course, lesson);
if (downloaded === null) {
return (
Expand Down Expand Up @@ -151,7 +153,7 @@ const ListenBody = ({course, lesson, setBottomSheetOpen, skipBack, seekTo, toggl
});
setBottomSheetOpen(false);
}}>
<ListenBottomSheet course={course} lesson={lesson} downloaded={downloaded} />
<ListenBottomSheet course={course} lesson={lesson} downloaded={downloaded} finished={finished} />
</RBSheet>
</>
);
Expand Down
13 changes: 8 additions & 5 deletions js/components/Listen/ListenBottomSheet.react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {Icon} from 'react-native-elements';
import CourseData from '../../course-data';
import DownloadManager from '../../download-manager';
import {genMarkLessonFinished} from '../../persistence';
import {genMarkLessonFinished, genToggleLessonFinished} from '../../persistence';
import {genStopPlaying} from '../../audio-service';
import {useNavigation} from '@react-navigation/native';
import {useProgress} from 'react-native-track-player';
Expand All @@ -20,10 +20,11 @@ import { MainNavigationProp } from '../App.react';
interface Props {
course: Course,
lesson: number,
downloaded: boolean | null;
downloaded: boolean | null,
finished: boolean | null;
}

const ListenBottomSheet = ({course, lesson, downloaded}: Props) => {
const ListenBottomSheet = ({course, lesson, downloaded, finished}: Props) => {
const {position} = useProgress();
const {pop} = useNavigation<MainNavigationProp<'Listen'>>();

Expand All @@ -39,11 +40,13 @@ const ListenBottomSheet = ({course, lesson, downloaded}: Props) => {
position,
});

await genMarkLessonFinished(course, lesson);
await genToggleLessonFinished(course, lesson);
pop();
}}>
<View style={styles.bottomSheetRow}>
<Text style={styles.rowText}>Mark as finished</Text>
<Text style={styles.rowText}>
Mark as {finished ? 'unfinished' : 'finished'}
</Text>
<View style={styles.iconContainer}>
<Icon
style={styles.rowIcon}
Expand Down
17 changes: 17 additions & 0 deletions js/hooks/useIsLessonFinished.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {useState, useEffect} from 'react';
import {genProgressForLesson} from '../persistence';

export default function useIsLessonFinished(course: Course, lesson: number) {
const [finished, setFinished] = useState<boolean | null>(null);
useEffect(() => {
async function checkIfFinished() {
const resp = await genProgressForLesson(course, lesson);
setFinished(resp?.finished || null);
}

setFinished(null);
checkIfFinished();
}, [course, lesson]);

return finished;
}
79 changes: 79 additions & 0 deletions js/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,85 @@ export const genMarkLessonFinished = async (
}
};

export const genToggleLessonFinished = async (
course: Course,
lesson: number,
): Promise<void> => {
const progressObject = await genProgressForLesson(course, lesson);

await Promise.all([
AsyncStorage.setItem(
`@activity/${course}/${lesson}`,
JSON.stringify({
...progressObject,
finished: !progressObject?.finished,
}),
),
AsyncStorage.setItem(
`@activity/${course}/most-recent-lesson`,
lesson.toString(),
),
AsyncStorage.setItem('@activity/most-recent-course', course),
]);

if (
progressObject?.finished &&
(await genPreferenceAutoDeleteFinished()) &&
(await DownloadManager.genIsDownloaded(course, lesson))
) {
await DownloadManager.genDeleteDownload(course, lesson);
}
};

// TODO: Add return value in case user enters out of range lastLesson.
export const genSetProgressForCourse = async (
course: Course,
lastLesson: number,
): Promise<{hasPassed: boolean; lessonCount: number}> => {
const lastLessonIndex = lastLesson - 1;
const lessonCount = parseInt(CourseData.getFallbackLessonCount(course), 10);

if (lastLessonIndex >= lessonCount) {
return {hasPassed: false, lessonCount};
}

for (let lesson = 0; lesson <= lastLessonIndex; lesson++) {
const progressObject = await genProgressForLesson(course, lesson);

await AsyncStorage.setItem(
`@activity/${course}/${lesson}`,
JSON.stringify({
...progressObject,
finished: true,
}),
);
}

await Promise.all([
AsyncStorage.setItem(
`@activity/${course}/most-recent-lesson`,
lastLessonIndex.toString(),
),
AsyncStorage.setItem('@activity/most-recent-course', course),
]);

if (lastLessonIndex < lessonCount) {
for (let lesson = lastLessonIndex + 1; lesson < lessonCount; lesson++) {
const progressObject = await genProgressForLesson(course, lesson);

await AsyncStorage.setItem(
`@activity/${course}/${lesson}`,
JSON.stringify({
...progressObject,
finished: false,
}),
);
}
}

return {hasPassed: true, lessonCount};
};

export const genDeleteProgressForCourse = async (
course: Course,
): Promise<void> => {
Expand Down