Add basic progress & LMS upload.
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,3 +24,14 @@ 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -14,3 +14,8 @@ export const selectUploadError = createSelector(
|
|||||||
[selectPhotosState],
|
[selectPhotosState],
|
||||||
(photos) => photos.uploadError
|
(photos) => photos.uploadError
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const selectUploadProgress = createSelector(
|
||||||
|
[selectPhotosState],
|
||||||
|
(photos) => photos.progress
|
||||||
|
);
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user