From 8d60d9776ca70ac383061e905adfb6a87f1cf214 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 14 Oct 2025 14:13:30 -0700 Subject: [PATCH] Add basic progress & LMS upload. --- app/jobs/_layout.tsx | 20 +-- app/settings.tsx | 15 ++- .../upload-progress/upload-progress.jsx | 117 +++++++++--------- redux/photos/photos.actions.js | 13 +- redux/photos/photos.reducer.js | 16 ++- redux/photos/photos.sagas.js | 117 ++++++++++-------- redux/photos/photos.selectors.js | 5 + redux/photos/photos.types.js | 2 + 8 files changed, 184 insertions(+), 121 deletions(-) diff --git a/app/jobs/_layout.tsx b/app/jobs/_layout.tsx index 73235ae..a34081f 100644 --- a/app/jobs/_layout.tsx +++ b/app/jobs/_layout.tsx @@ -14,16 +14,16 @@ function JobsStack() { { - router.setParams({ - search: event?.nativeEvent?.text, - }); - }, - }, + headerShown: false, + // headerSearchBarOptions: { + // placement: "automatic", + // placeholder: "Search", + // onChangeText: (event) => { + // router.setParams({ + // search: event?.nativeEvent?.text, + // }); + // }, + // }, }} /> Tab [Home|Settings] + + Using Local Media Server? {bodyshop?.uselocalmediaserver ? "Yes" : "No"} + ); diff --git a/components/upload-progress/upload-progress.jsx b/components/upload-progress/upload-progress.jsx index 63a730f..d5b57e3 100644 --- a/components/upload-progress/upload-progress.jsx +++ b/components/upload-progress/upload-progress.jsx @@ -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 { createStructuredSelector } from "reselect"; -import { selectPhotos } from "../../redux/photos/photos.selectors"; +import { + selectPhotos, + selectUploadProgress, +} from "../../redux/photos/photos.selectors"; const mapStateToProps = createStructuredSelector({ photos: selectPhotos, + photoUploadProgress: selectUploadProgress, }); export default connect(mapStateToProps, null)(UploadProgress); -export function UploadProgress({ photos }) { +export function UploadProgress({ photos, photoUploadProgress }) { if (photos?.length === 0) return null; return ( - Upload Progress. - {JSON.stringify(photos)} - {/* - - {Object.keys(progress.files).map((key) => ( - - - {progress.files[key].filename} - - - - - {`${formatBytes( - progress.files[key].loaded / - (((progress.files[key].uploadEnd || new Date()) - - progress.files[key].uploadStart) / - 1000) - )}/sec`} - {progress.files[key].percent === 1 && ( - <> - - Processing... - - )} - + + {Object.keys(photoUploadProgress).map((key) => ( + + + {photoUploadProgress[key].fileName} + + + + + {`${formatBytes( + photoUploadProgress[key].loaded / + (((photoUploadProgress[key].uploadEnd || new Date()) - + photoUploadProgress[key].uploadStart) / + 1000) + )}/sec`} + {photoUploadProgress[key].percent === 1 && ( + <> + + Processing... + + )} - ))} - - {progress.statusText ? ( - <> - - {progress.statusText} - - - ) : ( - <> - {`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`} - {`${formatBytes(progress.totalUploaded)} of ${formatBytes( - progress.totalToUpload - )} uploaded.`} - - )} - + ))} + + { + // progress.statusText ? ( + // <> + // + // {progress.statusText} + // + // + // ) : ( + // <> + // {`${progress.totalFilesCompleted} of ${progress.totalFiles} uploaded.`} + // {`${formatBytes(progress.totalUploaded)} of ${formatBytes( + // progress.totalToUpload + // )} uploaded.`} + // + // ) + } - */} + ); } diff --git a/redux/photos/photos.actions.js b/redux/photos/photos.actions.js index 7be5545..25ccdae 100644 --- a/redux/photos/photos.actions.js +++ b/redux/photos/photos.actions.js @@ -23,4 +23,15 @@ export const mediaUploadFailure = (error) => ({ export const mediaUploadSuccessOne = (photo) => ({ type: PhotosActionTypes.MEDIA_UPLOAD_SUCCESS_ONE, payload: photo, -}); \ No newline at end of file +}); + +export const mediaUploadProgressBulk = (info) => ({ + type: PhotosActionTypes.MEDIA_UPLOAD_PROGRESS_UPDATE_BULK, + payload: info, +}); + +export const mediaUploadCompleted = (photo) => ({ + type: PhotosActionTypes.MEDIA_UPLOAD_COMPLETED + //payload: photo, +}); + diff --git a/redux/photos/photos.reducer.js b/redux/photos/photos.reducer.js index 2892e71..42eafe0 100644 --- a/redux/photos/photos.reducer.js +++ b/redux/photos/photos.reducer.js @@ -2,7 +2,7 @@ import PhotosActionTypes from "./photos.types"; const INITIAL_STATE = { photos: [], - uploadInProgress: false, + uploadInProgress: true, uploadError: null, jobid: null, progress: {} @@ -35,7 +35,19 @@ const photosReducer = (state = INITIAL_STATE, action) => { ...state, 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: return state; } diff --git a/redux/photos/photos.sagas.js b/redux/photos/photos.sagas.js index 2c8b176..7b6b593 100644 --- a/redux/photos/photos.sagas.js +++ b/redux/photos/photos.sagas.js @@ -10,7 +10,9 @@ import { axiosAuthInterceptorId } from "../../util/CleanAxios"; import { fetchImageFromUri, replaceAccents } from '../../util/uploadUtils'; import { selectBodyshop, selectCurrentUser } from "../user/user.selectors"; import { + mediaUploadCompleted, mediaUploadFailure, + mediaUploadProgressBulk, mediaUploadProgressOne, mediaUploadStart, mediaUploadSuccessOne @@ -67,24 +69,29 @@ export function* mediaUploadStartAction({ payload: { photos, jobid } }) { const bodyshop = yield select(selectBodyshop); - // Process photos in batches to avoid overwhelming the system - const batchSize = 3; // Upload 3 photos concurrently - const batches = []; + if (bodyshop.uselocalmediaserver) { + yield call(uploadToLocalMediaServer, photos, bodyshop, jobid); + } + 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) { - batches.push(photos.slice(i, i + batchSize)); + for (let i = 0; i < photos.length; 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 - 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)); + yield put(mediaUploadCompleted()); } catch (error) { 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( photoBlob.data.name ).replace(/[^A-Z0-9]+/gi, "_")}-${new Date().getTime()}.${extension}` - - if (bodyshop.uselocalmediaserver) { - yield call(uploadToLocalMediaServer, photo, photoBlob, extension, key, bodyshop, jobid); - } else { - yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid); - } + yield call(uploadToImageProxy, photo, photoBlob, extension, key, bodyshop, jobid); yield put(mediaUploadSuccessOne(photo)); @@ -117,40 +119,56 @@ function* uploadSinglePhoto(photo, bodyshop, index, jobid) { } } -function* uploadToLocalMediaServer(photo, key) { +function* uploadToLocalMediaServer(photos, bodyshop, jobid) { 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(); - // formData.append('file', { - // uri: photo.uri, - // type: photo.type || 'image/jpeg', - // name: photo.fileName || `photo_${Date.now()}.jpg`, - // }); + const formData = new FormData(); + formData.append("jobid", jobid); - // 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', { - // method: 'POST', - // body: formData, - // headers: { - // 'Content-Type': 'multipart/form-data', - // }, - // }); + formData.append("skip_thumbnail", true); - // yield put(mediaUploadProgress({ photoId, status: 'uploading', progress: 75 })); + try { + const imexMediaServerResponse = yield call(axios.post, + `${bodyshop.localmediaserverhttp}/jobs/upload`, + formData, + options + ); - // if (!response.ok) { - // throw new Error(`Upload failed: ${response.status}`); - // } + if (imexMediaServerResponse.status !== 200) { + console.log("Error uploading documents:", JSON.stringify(imexMediaServerResponse, null, 2)); - // const result = yield call([response, 'json']); - // yield put(mediaUploadProgress({ photoId, status: 'completed', progress: 100 })); + } else { - // 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) { - 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.upload.onprogress = (e) => { - console.log("*** ~ awaitnewPromise ~ event:", e); if (e.lengthComputable) { - console.log(`Upload progress for ${photo.uri}:`, e.loaded / e.total); - put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total })); + put(mediaUploadProgressOne({ ...photo, progress: e.loaded / e.total, loaded: e.loaded })); } }; @@ -217,7 +233,7 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) const [hours, minutes, seconds] = time ? time.split(':') : []; const pictureMoment = moment(`${year}-${month}-${day}T${hours}:${minutes}:${seconds}`); - const documentInsert = yield call(client.mutate, ({ + yield call(client.mutate, ({ mutation: INSERT_NEW_DOCUMENT, variables: { docInput: [ @@ -236,7 +252,6 @@ function* uploadToImageProxy(photo, photoBlob, extension, key, bodyshop, jobid) ], }, })); - console.log("*** ~ uploadToImageProxy ~ documentInsert:", JSON.stringify(documentInsert, null, 2)); } } catch (error) { diff --git a/redux/photos/photos.selectors.js b/redux/photos/photos.selectors.js index f581003..b385ebc 100644 --- a/redux/photos/photos.selectors.js +++ b/redux/photos/photos.selectors.js @@ -14,3 +14,8 @@ export const selectUploadError = createSelector( [selectPhotosState], (photos) => photos.uploadError ); + +export const selectUploadProgress = createSelector( + [selectPhotosState], + (photos) => photos.progress +); \ No newline at end of file diff --git a/redux/photos/photos.types.js b/redux/photos/photos.types.js index b62d45d..e20a15c 100644 --- a/redux/photos/photos.types.js +++ b/redux/photos/photos.types.js @@ -4,5 +4,7 @@ const PhotosActionTypes = { MEDIA_UPLOAD_SUCCESS_ONE: "MEDIA_UPLOAD_SUCCESS_ONE", MEDIA_UPLOAD_FAILURE: "MEDIA_UPLOAD_FAILURE", 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;