Add basic progress & LMS upload.

This commit is contained in:
Patrick Fic
2025-10-14 14:13:30 -07:00
parent 7fe1ea65f2
commit 8d60d9776c
8 changed files with 184 additions and 121 deletions

View File

@@ -14,16 +14,16 @@ function JobsStack() {
<Stack.Screen <Stack.Screen
name="index" name="index"
options={{ options={{
title: "Search", headerShown: false,
headerSearchBarOptions: { // headerSearchBarOptions: {
placement: "automatic", // placement: "automatic",
placeholder: "Search", // placeholder: "Search",
onChangeText: (event) => { // onChangeText: (event) => {
router.setParams({ // router.setParams({
search: event?.nativeEvent?.text, // search: event?.nativeEvent?.text,
}); // });
}, // },
}, // },
}} }}
/> />
<Stack.Screen <Stack.Screen

View File

@@ -1,9 +1,22 @@
import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component"; import SignOutButton from "@/components-old/sign-out-button/sign-out-button.component";
import { selectBodyshop } from "@/redux/user/user.selectors";
import { StyleSheet, Text, View } from "react-native"; import { StyleSheet, Text, View } from "react-native";
export default function Tab() { import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export default connect(mapStateToProps, null)(Tab);
function Tab({ bodyshop }) {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text>Tab [Home|Settings]</Text> <Text>Tab [Home|Settings]</Text>
<Text>
Using Local Media Server? {bodyshop?.uselocalmediaserver ? "Yes" : "No"}
</Text>
<SignOutButton /> <SignOutButton />
</View> </View>
); );

View File

@@ -1,75 +1,80 @@
import { StyleSheet, Text, View } from "react-native"; import { formatBytes } from "@/util/uploadUtils";
import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
import { ProgressBar } from "react-native-paper";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectPhotos } from "../../redux/photos/photos.selectors"; import {
selectPhotos,
selectUploadProgress,
} from "../../redux/photos/photos.selectors";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
photos: selectPhotos, photos: selectPhotos,
photoUploadProgress: selectUploadProgress,
}); });
export default connect(mapStateToProps, null)(UploadProgress); export default connect(mapStateToProps, null)(UploadProgress);
export function UploadProgress({ photos }) { export function UploadProgress({ photos, photoUploadProgress }) {
if (photos?.length === 0) return null; if (photos?.length === 0) return null;
return ( return (
<View style={styles.modalContainer}> <View style={styles.modalContainer}>
<Text>Upload Progress.</Text> <View style={styles.modal}>
<Text>{JSON.stringify(photos)}</Text> {Object.keys(photoUploadProgress).map((key) => (
{/* <View key={key} style={styles.progressItem}>
<View style={styles.modal}> <Text style={styles.progressText}>
{Object.keys(progress.files).map((key) => ( {photoUploadProgress[key].fileName}
<View key={key} style={styles.progressItem}> </Text>
<Text style={styles.progressText}> <View style={styles.progressBarContainer}>
{progress.files[key].filename} <ProgressBar
</Text> progress={photoUploadProgress[key].progress}
<View style={styles.progressBarContainer}> style={styles.progress}
<ProgressBar color={
progress={progress.files[key].percent} photoUploadProgress[key].progress === 1 ? "green" : "blue"
style={styles.progress} }
color={progress.files[key].percent === 1 ? "green" : "blue"} />
/> <View
<View style={{
style={{ display: "flex",
display: "flex", flexDirection: "row",
flexDirection: "row", alignItems: "center",
alignItems: "center", }}
}} >
> <Text>{`${formatBytes(
<Text>{`${formatBytes( photoUploadProgress[key].loaded /
progress.files[key].loaded / (((photoUploadProgress[key].uploadEnd || new Date()) -
(((progress.files[key].uploadEnd || new Date()) - photoUploadProgress[key].uploadStart) /
progress.files[key].uploadStart) / 1000)
1000) )}/sec`}</Text>
)}/sec`}</Text> {photoUploadProgress[key].percent === 1 && (
{progress.files[key].percent === 1 && ( <>
<> <ActivityIndicator style={{ marginLeft: 12 }} />
<ActivityIndicator style={{ marginLeft: 12 }} /> <Text style={{ marginLeft: 4 }}>Processing...</Text>
<Text style={{ marginLeft: 4 }}>Processing...</Text> </>
</> )}
)}
</View>
</View> </View>
</View> </View>
))}
<View style={styles.centeredView}>
{progress.statusText ? (
<>
<ActivityIndicator style={{ marginLeft: 12 }} />
<Text style={{ marginLeft: 4 }}>{progress.statusText}</Text>
<Divider />
</>
) : (
<>
<Text>{`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}</Text>
<Text>{`${formatBytes(progress.totalUploaded)} of ${formatBytes(
progress.totalToUpload
)} uploaded.`}</Text>
</>
)}
</View> </View>
))}
<View style={styles.centeredView}>
{
// progress.statusText ? (
// <>
// <ActivityIndicator style={{ marginLeft: 12 }} />
// <Text style={{ marginLeft: 4 }}>{progress.statusText}</Text>
// <Divider />
// </>
// ) : (
// <>
// <Text>{`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`}</Text>
// <Text>{`${formatBytes(progress.totalUploaded)} of ${formatBytes(
// progress.totalToUpload
// )} uploaded.`}</Text>
// </>
// )
}
</View> </View>
*/} </View>
</View> </View>
); );
} }

View File

@@ -23,4 +23,15 @@ export const mediaUploadFailure = (error) => ({
export const mediaUploadSuccessOne = (photo) => ({ export const mediaUploadSuccessOne = (photo) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE, type: PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE,
payload: photo, payload: photo,
}); });
export const mediaUploadProgressBulk = (info) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK,
payload: info,
});
export const mediaUploadCompleted = (photo) => ({
type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED
//payload: photo,
});

View File

@@ -2,7 +2,7 @@ import PhotosActionTypes from "./photos.types";
const INITIAL_STATE = { const INITIAL_STATE = {
photos: [], photos: [],
uploadInProgress: false, uploadInProgress: true,
uploadError: null, uploadError: null,
jobid: null, jobid: null,
progress: {} progress: {}
@@ -35,7 +35,19 @@ const photosReducer = (state = INITIAL_STATE, action) => {
...state, ...state,
progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed' } } progress: { ...state.progress, [action.payload.assetId]: { ...state.progress[action.payload.assetId], progress: 100, status: 'completed' } }
}; };
case PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK:
return {
...state,
progress: { Upload: action.payload }
};
case PhotosActionTypes.MEDIA_UPLOAD_COMPLETED:
return {
...state,
uploadInProgress: false,
uploadError: null,
photos: [],
progress: {}
};
default: default:
return state; return state;
} }

View File

@@ -10,7 +10,9 @@ import { axiosAuthInterceptorId } from "../../util/CleanAxios";
import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils'; import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils';
import { selectBodyshop, selectCurrentUser } from "../user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../user/user.selectors";
import { import {
mediaUploadCompleted,
mediaUploadFailure, mediaUploadFailure,
mediaUploadProgressBulk,
mediaUploadProgressOne, mediaUploadProgressOne,
mediaUploadStart, mediaUploadStart,
mediaUploadSuccessOne mediaUploadSuccessOne
@@ -67,24 +69,29 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) {
const bodyshop = yield select(selectBodyshop); const bodyshop = yield select(selectBodyshop);
// Process photos in batches to avoid overwhelming the system if (bodyshop.uselocalmediaserver) {
const batchSize = 3; // Upload 3 photos concurrently yield call(uploadToLocalMediaServer, photos, bodyshop, jobid);
const batches = []; }
else {
// Process photos in batches to avoid overwhelming the system
const batchSize = 3; // Upload 3 photos concurrently
const batches = [];
for (let i = 0; i < photos.length; i += batchSize) { for (let i = 0; i < photos.length; i += batchSize) {
batches.push(photos.slice(i, i + batchSize)); batches.push(photos.slice(i, i + batchSize));
}
// Process each batch sequentially, but photos within batch concurrently
for (const batch of batches) {
const uploadTasks = batch.map((photo, index) =>
fork(uploadSinglePhoto, photo, bodyshop, index, jobid)
);
// Wait for current batch to complete before starting next batch
yield all(uploadTasks);
// Small delay between batches to prevent overwhelming the server
yield delay(100);
}
} }
// Process each batch sequentially, but photos within batch concurrently yield put(mediaUploadCompleted());
for (const batch of batches) {
const uploadTasks = batch.map((photo, index) =>
fork(uploadSinglePhoto, photo, bodyshop, index, jobid)
);
// Wait for current batch to complete before starting next batch
yield all(uploadTasks);
// Small delay between batches to prevent overwhelming the server
yield delay(100);
}
//yield put(mediaUploadSuccess(photo));
} catch (error) { } catch (error) {
console.log("Saga Error: upload start", error, error.stack); console.log("Saga Error: upload start", error, error.stack);
@@ -102,12 +109,7 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
const key = `${bodyshop.id}/${jobid}/${replaceAccents( const key = `${bodyshop.id}/${jobid}/${replaceAccents(
photoBlob.data.name photoBlob.data.name
).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}`
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
if (bodyshop.uselocalmediaserver) {
yield call(uploadToLocalMediaServer, photo, photoBlob, extension, key, bodyshop, jobid);
} else {
yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid);
}
yield put(mediaUploadSuccessOne(photo)); yield put(mediaUploadSuccessOne(photo));
@@ -117,40 +119,56 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) {
} }
} }
function* uploadToLocalMediaServer(photo, key) { function* uploadToLocalMediaServer(photos, bodyshop, jobid) {
try { try {
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 25 })); const options = {
headers: {
"Content-Type": "multipart/form-data",
ims_token: bodyshop.localmediatoken,
},
onUploadProgress: (e) => {
put(mediaUploadProgressBulk({ progress: e.loaded / e.total, loaded: e.loaded }));
},
};
// const formData = new FormData(); const formData = new FormData();
// formData.append('file', { formData.append("jobid", jobid);
// uri: photo.uri,
// type: photo.type || 'image/jpeg',
// name: photo.fileName || `photo_${Date.now()}.jpg`,
// });
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 50 })); for (const file of photos) {
formData.append("file", {
uri: file.uri,
type: file.mimeType,
name: file.fileName,
});
}
// const response = yield call(fetch, 'YOUR_LOCAL_MEDIA_SERVER_ENDPOINT', { formData.append("skip_thumbnail", true);
// method: 'POST',
// body: formData,
// headers: {
// 'Content-Type': 'multipart/form-data',
// },
// });
// yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 75 })); try {
const imexMediaServerResponse = yield call(axios.post,
`${bodyshop.localmediaserverhttp}/jobs/upload`,
formData,
options
);
// if (!response.ok) { if (imexMediaServerResponse.status !== 200) {
// throw new Error(`Upload failed: ${response.status}`); console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2));
// }
// const result = yield call([response, 'json']); } else {
// yield put(mediaUploadProgress({ photoId, status: 'completed', progress: 100 }));
// return result; // onSuccess({
// duration: imexMediaServerResponse.headers["x-response-time"],
// });
}
} catch (error) {
console.log("Error uploading documents:", error.message, JSON.stringify(error, null, 2));
}
} catch (error) { } catch (error) {
throw new Error(`Local media server upload failed: ${error.message}`); console.log("Uncaught error", error);
} }
} }
@@ -182,10 +200,8 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
xhr.setRequestHeader("Content-Type", photoBlob.type); xhr.setRequestHeader("Content-Type", photoBlob.type);
xhr.upload.onprogress = (e) => { xhr.upload.onprogress = (e) => {
console.log("*** ~ awaitnewPromise ~ event:", e);
if (e.lengthComputable) { if (e.lengthComputable) {
console.log(`Upload progress for ${photo.uri}:`, e.loaded / e.total); put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded }));
put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total }));
} }
}; };
@@ -217,7 +233,7 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
const [hours, minutes, seconds] = time ? time.split(':') : []; const [hours, minutes, seconds] = time ? time.split(':') : [];
const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`); const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`);
const documentInsert = yield call(client.mutate, ({ yield call(client.mutate, ({
mutation: INSERT_NEW_DOCUMENT, mutation: INSERT_NEW_DOCUMENT,
variables: { variables: {
docInput: [ docInput: [
@@ -236,7 +252,6 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid)
], ],
}, },
})); }));
console.log("*** ~ uploadToImageProxy ~ documentInsert:", JSON.stringify(documentInsert, null, 2));
} }
} catch (error) { } catch (error) {

View File

@@ -14,3 +14,8 @@ export const selectUploadError = createSelector(
[selectPhotosState], [selectPhotosState],
(photos) => photos.uploadError (photos) => photos.uploadError
); );
export const selectUploadProgress = createSelector(
[selectPhotosState],
(photos) => photos.progress
);

View File

@@ -4,5 +4,7 @@ const PhotosActionTypes = {
MEDIA_UPLOAD_SUCCESS_ONE: "MEDIA_UPLOAD_SUCCESS_ONE", MEDIA_UPLOAD_SUCCESS_ONE: "MEDIA_UPLOAD_SUCCESS_ONE",
MEDIA_UPLOAD_FAILURE: "MEDIA_UPLOAD_FAILURE", MEDIA_UPLOAD_FAILURE: "MEDIA_UPLOAD_FAILURE",
MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE", MEDIA_UPLOAD_PROGRESS_UPDATE_ONE: "MEDIA_UPLOAD_PROGRESS_UPDATE_ONE",
MEDIA_UPLOAD_PROGRESS_UPDATE_BULK: "MEDIA_UPLOAD_PROGRESS_UPDATE_BULK",
MEDIA_UPLOAD_COMPLETED: "MEDIA_UPLOAD_COMPLETED",
}; };
export default PhotosActionTypes; export default PhotosActionTypes;