@fto-consult/expo-ui 8.46.3 → 8.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/create-app/App.js +62 -10
- package/bin/create-app/babel.config.js +1 -0
- package/bin/create-app/dependencies.js +2 -1
- package/bin/create-app/src/auth/index.js +61 -2
- package/bin/create-app/src/database/tables/getTable.js +10 -0
- package/bin/create-app/src/database/tables/index.js +57 -0
- package/bin/create-app.js +2 -1
- package/package.json +1 -1
- package/src/auth/Login.js +22 -16
- package/src/components/Image/Cropper/ExpoImageManipulator.js +311 -0
- package/src/components/Image/Cropper/ImageCropOverlay.js +219 -0
- package/src/components/Image/Cropper/index.js +83 -0
- package/src/components/Image/Editor/index.js +0 -1
- package/src/components/Image/index.js +27 -18
- package/src/screens/Help/openLibraries.js +1 -1
package/bin/create-app/App.js
CHANGED
@@ -10,6 +10,8 @@ import TableDataListScreen from "$screens/TableData/TableDataListScreen";
|
|
10
10
|
import TableDataScreen from "$screens/TableData/TableDataScreen";
|
11
11
|
import Notifications from "$components/Notifications";
|
12
12
|
import auth from "$src/auth";
|
13
|
+
import tablesData, { getTable as getTableData } from "$database/tables";
|
14
|
+
import {defaultStr} from "$cutils";
|
13
15
|
|
14
16
|
export default function AppMainEntry(){
|
15
17
|
return <ExpoUIProvider
|
@@ -22,8 +24,8 @@ export default function AppMainEntry(){
|
|
22
24
|
screens,
|
23
25
|
/** {object}, les options du composant Stack.Navigator, voir https://reactnavigation.org/docs/native-stack-navigator */
|
24
26
|
screenOptions : {},
|
25
|
-
drawerItems,
|
26
|
-
drawerSections,
|
27
|
+
drawerItems, //application main drawer items,
|
28
|
+
drawerSections, //les différentes sections du drawer principal de l'application
|
27
29
|
/***** mutate drawerItems before rendering
|
28
30
|
@param {object : {[drawerSection1]:{ label:section1Label,items:<Array>},[drawerSection2]:{}, ...[drawerSectionN]:{}}} drawerItems
|
29
31
|
@return {object}
|
@@ -34,6 +36,8 @@ export default function AppMainEntry(){
|
|
34
36
|
screenOptions : {},//les options du composant Stack.Navigator de react-navigation, voir https://reactnavigation.org/docs/native-stack-navigator/
|
35
37
|
}}
|
36
38
|
auth = {auth}
|
39
|
+
tablesData={tablesData}
|
40
|
+
getTableData={getTableData}
|
37
41
|
components = {{
|
38
42
|
/*** utilisé pour le renu du contenu des écran de type liste sur les tables de données */
|
39
43
|
TableDataListScreen,
|
@@ -63,16 +67,64 @@ export default function AppMainEntry(){
|
|
63
67
|
MainProvider : function({children,isLoaded,isLoading,isInitialized,hasGedStarted,...props}){
|
64
68
|
return children;
|
65
69
|
},
|
66
|
-
/***
|
67
|
-
|
70
|
+
/***
|
71
|
+
le composant en charge du rendu du logo de l'application
|
72
|
+
logo | Logo : ReactNode | ReactComponent | object {
|
73
|
+
image{ReactComponent} :,
|
74
|
+
text {ReactComponent}
|
75
|
+
},
|
68
76
|
},*/
|
69
|
-
logo : Logo
|
70
|
-
/****
|
71
|
-
|
72
|
-
|
77
|
+
logo : Logo,
|
78
|
+
/****
|
79
|
+
custom form fields
|
80
|
+
les form fields personnalisés doivent être définis ici
|
81
|
+
de la forme : {
|
82
|
+
[typeCustomField1] : <ReactComponent>,
|
83
|
+
...
|
84
|
+
[typeCustomFieldn] : <ReactComponent>
|
85
|
+
}
|
86
|
+
par exemple, si l'on souhaite définir un form field de type test, la déclaration sera de la forme :
|
87
|
+
{
|
88
|
+
test : Test, //ou test est le fom field associé au type test, ie le composant qui sera rendu pour ce type de Champ,
|
89
|
+
}
|
90
|
+
*/
|
91
|
+
customFormFields : {},
|
92
|
+
/***
|
93
|
+
la fonction permettant de muter les props du composant TableLink, permetant de lier les tables entre elles
|
94
|
+
Le composant TableLink permet de lier les données d'une tableData, L'usage dudit composant est définit dans la documentation de l'application
|
95
|
+
*/
|
73
96
|
tableLinkPropsMutator : (props)=>{
|
74
|
-
return
|
75
|
-
|
97
|
+
return {
|
98
|
+
...props,
|
99
|
+
/***
|
100
|
+
la fonction fetchForeignData est appelée lorsqu'on clique sur un élément du composant TableLink, permetant de lier un objet de la table table Data
|
101
|
+
foreignKeyTable {string} represente la table lié à la donnée
|
102
|
+
foreignKeyColumn {string} represenet le nom de la colonne qu'on souhaite récupérer la données
|
103
|
+
id {any}, represente la valeur actuelle sur laquelle on a cliqué
|
104
|
+
*/
|
105
|
+
fetchForeignData : ({foreignKeyTable,foreignKeyColumn,tableName,table,id,...args})=>{
|
106
|
+
const tableName = defaultStr(foreignKeyTable,table,tableName);
|
107
|
+
const tableObj = getTableData(tableName); //table object represente l'objet table, lié à la liste des tables data déclaré dans l'application
|
108
|
+
if (!tableObj) {
|
109
|
+
return Promise.reject({
|
110
|
+
message: `Impossible de récupérer la données associée à la table ${tableName}. Rassurez vous qu'elle figure dans la liste des tables supportées par l'application`,
|
111
|
+
});
|
112
|
+
}
|
113
|
+
//Vous pouvez dès cet instant accédes aux props de l'objet tableObj, notemment queryPath, qui permet de récupérer les données liés à la table data
|
114
|
+
const fieldName = defaultStr(foreignKeyColumn);
|
115
|
+
/*
|
116
|
+
implémenter votre logique pour récupérer l'objet associé à la table tableName, dont la colonne est fieldName, et la valeur est id.
|
117
|
+
//ajouter l'instruction d'importation de la fonction fetch : import fetch from "$capi/fetch";
|
118
|
+
exemple : return fetch(`${table.queryPath}/${id}${fieldName ? `?fieldName=${fieldName}`:""}`).then((resp) => resp.data);
|
119
|
+
*/
|
120
|
+
return Promise.resolve(null);
|
121
|
+
},
|
122
|
+
};
|
123
|
+
},
|
124
|
+
/***
|
125
|
+
({object})=><{object}>, la fonction permettant de muter les props du composant Fab, affiché dans les écrans par défaut
|
126
|
+
*/
|
127
|
+
fabPropsMutator : (props)=>props,
|
76
128
|
}}
|
77
129
|
/*** //for application initialization
|
78
130
|
@param {
|
@@ -7,6 +7,7 @@ module.exports = function(api) {
|
|
7
7
|
$components : path.resolve($src,"components"),
|
8
8
|
$navigation : path.resolve($src,"navigation"),
|
9
9
|
$screens : path.resolve($src,"screens"),
|
10
|
+
$database: path.resolve($src, "database"), //le repertoire dédié au données d'accès à la base de données
|
10
11
|
//...your custom module resolver alias, @see : https://www.npmjs.com/package/babel-plugin-module-resolver
|
11
12
|
}
|
12
13
|
return require("@fto-consult/expo-ui/babel.config")(api,{
|
@@ -1,6 +1,5 @@
|
|
1
1
|
export default {
|
2
2
|
enabled : false,//la gestion de l'authentification est désactivée par défaut
|
3
|
-
loginPropsMutator : (props)=>props,//({object})=><{object}>, la fonction permettant de muter les props du composant Login,
|
4
3
|
profilePropsMutator : ({fields,...props})=>({fields,...props}),//la fonction permettant de muter les champs liés à l'écran de mise à jour d'un profil utilisateur
|
5
4
|
signIn : ({user})=>Promise.resolve({message:"Connecté avec success"}), //la fonction permettant de connecter un utilisateur
|
6
5
|
signOut : ({user})=>Promise.resolve({message:"Déconnecté avec success"}),//la fonction permettant de déconnecter un utilisateur
|
@@ -24,5 +23,65 @@ export default {
|
|
24
23
|
getUserPseudo: (user) => user.pseudo,
|
25
24
|
getUserFirstName : (user)=>user.firstName,
|
26
25
|
getUserLastName : (user)=>user.lastName,
|
27
|
-
getUserFullName : (user)=> user.fullName || `${user.firstName && user.firstName ||''}${user.lastName && ` ${user.lastName}` ||''}
|
26
|
+
getUserFullName : (user)=> user.fullName || `${user.firstName && user.firstName ||''}${user.lastName && ` ${user.lastName}` ||''}`,
|
27
|
+
|
28
|
+
/****
|
29
|
+
Le composant à définir pour override le composant Login par défaut de l'application. example : Login : (porps)=><React.Component {...props}/>
|
30
|
+
Pour override l'interface de connexion par défaut, vous dévez définir le contenu de cette propriété comme étant un composant React qui sera rendu
|
31
|
+
rendu en lieu et place du composant de connexion par défaut : Ce composant aura comme props :
|
32
|
+
{
|
33
|
+
withPortal : {boolean}, //si le contenu de l'écran doit être rendu dans un portal
|
34
|
+
onSuccess <function> : (data)=><any>, la fonction appelée en cas de success
|
35
|
+
appBarProps <object>, les props à passer au composant ApppBar de l'écran de connexion, lorsque withPortal est à true
|
36
|
+
auth <object>, //le composant auth récupérer à l'aide du hook useAuth de $cauth. définit les fonctions suivantes :
|
37
|
+
{
|
38
|
+
signIn : (data)=><Promise>, la fonction permettant de connecter l'utilisateur
|
39
|
+
signOut : ()=><Promise>, la fonction permettant de déconnecter l'utilisateur, /
|
40
|
+
...rest,
|
41
|
+
}
|
42
|
+
}
|
43
|
+
*/
|
44
|
+
Login : null,
|
45
|
+
/*
|
46
|
+
la fonction loginPropsMutator de muter les props du composant Login par défaut, prise en compte lorsque le composant de connexion n'est pas remplacer par celui définit dans la prop login,
|
47
|
+
@param {object} props : les props de la fonction login, les props ont des propriétés suivantes :
|
48
|
+
{
|
49
|
+
onSuccess : ({object})=><Any>, la fonction appelée en cas de success
|
50
|
+
setState : (newState)=>(...newState),//la fonction utilisée pour update le state du composant. elle doit remplacer le state du composant
|
51
|
+
state : <Object: data,...rest>, le state actuel à l'instant t du composant
|
52
|
+
nextButton : <Object :
|
53
|
+
{
|
54
|
+
ref : nextButtonRef, //la référence vers le bouton next (le boutn Suivant)
|
55
|
+
isDisabled : x=> typeof buttonRef?.current?.isDisabled ==="function" && buttonRef.current?.isDisabled(),
|
56
|
+
enable : x=>typeof buttonRef?.current?.enable =="function" && buttonRef.current.enable(),
|
57
|
+
disable : x=> typeof buttonRef?.current?.disable =="function" && buttonRef?.current.disable(),
|
58
|
+
}
|
59
|
+
>,
|
60
|
+
prevButton : <Object :
|
61
|
+
{
|
62
|
+
ref : prevButtonRef, //la référence react ver le buton previous (le bouton Précédent)
|
63
|
+
isDisabled : x=> typeof buttonRef?.current?.isDisabled ==="function" && buttonRef.current?.isDisabled(),
|
64
|
+
enable : x=>typeof buttonRef?.current?.enable =="function" && buttonRef.current.enable(),
|
65
|
+
disable : x=> typeof buttonRef?.current?.disable =="function" && buttonRef?.current.disable(),
|
66
|
+
}
|
67
|
+
>,
|
68
|
+
showError <function> : (message,title)=><void>, la fonction permettant de notifier l'utilisateur en cas d'erreur
|
69
|
+
getData <function> : ()=><Object>, la fonction permettant de récupérer les données en cours de modification du formulaire de connextion à l'instant t
|
70
|
+
focusField <function> : (fieldName)=><void>, la fonction permettant d'activer le focus sur le champ fieldName à l'instant t
|
71
|
+
formName <string>, //le nom du formulaire Form, passé à la formData
|
72
|
+
nextButtonRef <{current:<any>}>, la référence vers le bouton next
|
73
|
+
previousButtonRef <{current:<any>}, la référence vers le bouton previous
|
74
|
+
}
|
75
|
+
@return <{object}>, l'objet a retourné doit être de la forme :
|
76
|
+
{
|
77
|
+
headerTopContent : <ReactComponent | ReactNode, le contenu a afficher au headerTop de l'interface de connexion
|
78
|
+
header : <ReactComponent| ReactNode>, le contenu du qui sera rendu immédiatement après le composant Header, par défaut, le texte "Connectez vous svp est affiché". Ce contenu doit être rendu si l'on souhaite override le texte "Connectez vous SVP"
|
79
|
+
containerProps : <object>, les props du composant <Surface/>, le composant qui est le wrapper du composant FormData en charge de récupérer les données de l'interface de connexion
|
80
|
+
withHeaderAvatar : <boolean>, si l'avatar ou l'iconne de connexion sera afficher à l'interface de connexion par défaut
|
81
|
+
...loginProps {object}, les composant Supplémentaires à passer au composant FormData utilisé pour le rendu du formulaire de connexion
|
82
|
+
}
|
83
|
+
*/
|
84
|
+
loginPropsMutator : (props)=>{
|
85
|
+
return props;
|
86
|
+
},
|
28
87
|
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
/*****
|
2
|
+
le contenu de cette fonction peut être généré automatiquement via les commandes suivantes (étant dans le repertoire de l'application)
|
3
|
+
npm run generate-getTable | npx @fto-consult/expo-ui generate-getTable
|
4
|
+
Notons que le script generate-getTable est définit comme étant l'un des scripts du package.json de l'application
|
5
|
+
@param {string} tableName, le nom de la table data
|
6
|
+
@return {object | null}, table, l'objet table associé
|
7
|
+
*/
|
8
|
+
export default function(tableName){
|
9
|
+
return null;
|
10
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
/****
|
2
|
+
la liste des tables de donnéees à exporter, de la forme :
|
3
|
+
[tableName1] : {
|
4
|
+
fields : { //la liste des champs liés à la table de données
|
5
|
+
[field1] : {
|
6
|
+
label | text : <string | ReactComponent>, le texte ou libelé lié au champ
|
7
|
+
type : <string>, le type de field par exemple : text,password, email, switch, checkbox, tel, select, et bien d'autres
|
8
|
+
|
9
|
+
//cette fonction est appelée à chaque fois que les premiers règles de validations ont été conclus lors de la validation du champ. Elle doit retourner soit un boolean, une chaine de caractère, un objet ou une promesse
|
10
|
+
//si une chaine de caractère est retournée, alors la validation du champ considère qu'il s'agit d'une erreur et le message est affiché comme erreur de validation du champ
|
11
|
+
//si un objet est retourné et cet objet contient un champ message | msg de type string, alors le validateur considère qu'il s'agit d'une erreur et le champ en question est affiché comme message de l'erreur
|
12
|
+
//si une promese est retournée, alors la fonction attendra que la promesse soit résolue
|
13
|
+
1. si la promesse resoud une chaine de caractère ou un objet idem au cas précédent, ladite chaine est condisérée comme une erreur
|
14
|
+
2. si la promesse renvoie une erreur, alors le validateur considère comme un échec de validation et le message lié à l'erreur est affiché comme message d'erreur de validation
|
15
|
+
//si false est retournée, alors rien n'est fait et le status de ce champ reste toujours à invalide. il sera donc impossible d'enregistrer le formulaire form data
|
16
|
+
//si true est retourneé, alors la formField est valide
|
17
|
+
onValidatorValid : ({value,context,....rest}) => <boolean | object {} | string | Promise <boolean | object : {} | string>>,
|
18
|
+
|
19
|
+
//Cette fonction est appélée à chaque échec de validation du form field
|
20
|
+
onValidatorNoValid : ({value,context,...rest}) => <any>
|
21
|
+
}
|
22
|
+
},
|
23
|
+
tableName <string>, le nom de la table name
|
24
|
+
label | text <string>, le titre à donner à la table data
|
25
|
+
icon : <string | ReactComponent>, l'icon de la table data,
|
26
|
+
queryPath <string>, //le chemin lié à l'api REST utilisée pour effectuer un query sur les données liés à la table Data en question
|
27
|
+
perms <object>, //l'objet perms définissant les permissions pouvant être assignés à l'utilisateur pour la table de donénes
|
28
|
+
showInFab <boolean | function()=><boolean>, //si la table de données sera affiché dans le layout Fab, le composant Fab qui est rendu pour les écrans dont la propriété withFab est à true
|
29
|
+
showInDrawer <boolean | function()=><boolean>, //détermine si la table data sera affichée dans le drawer principal de l'application
|
30
|
+
datagrid <object>, //les props à passer au composant datagrid lié à la table de données
|
31
|
+
drawerSection <string>, //le nom de la section associé au drawer dans lequel figurera le table data
|
32
|
+
print <function ({data,...settings})>=> <Promise<{content:[],...rest}>, //la fonction utile pour l'impression de la tabel de données suivant les recommandation de la libraririe pdfmake
|
33
|
+
printOptions <object>, //les options à passer à la fonction print,
|
34
|
+
|
35
|
+
databaseStatistics <function ()=> <boolean> | boolean>, //si la table data figurera dans les Statistiques en BD, validable si le composant DatabaseStatistics est appélé dans l'application
|
36
|
+
|
37
|
+
showInFab <boolean | function()=><boolean>, //spécifie si un bouton lié à la table sera affiché dans le composant Fab
|
38
|
+
|
39
|
+
//le champ fabProps doit retourner les props à appliquer au composant fab lié à la table data,si elle définit une propriété nomée actions de types tableau, alors, ces actions seront utilisées commes actions personnalisées du fab
|
40
|
+
// il doit s'agit d'un objet de la forme : {
|
41
|
+
actions : <array<Item> | object <Item>>. Item doit être un objet avec les propriétés suivantes :
|
42
|
+
Item : {
|
43
|
+
text | label <string> : "le texte à afficher au bouton fab",
|
44
|
+
icon <string | ReactComponent>, l'icone du bouton de fab,
|
45
|
+
backgroundColor <string, //la couleur d'arrière plan du bouton
|
46
|
+
color <string>, //la couleur du bouton de fab
|
47
|
+
onPress <func ()=>void>, //la fonction appelée lorsqu'on clique sur le bouton
|
48
|
+
}
|
49
|
+
}
|
50
|
+
fabProps { boolean<false> | object|function({tableName})}, //si fabProps vaux false ou retourne false, alors le table data ne s'affichera pas dans le composant Fab
|
51
|
+
}
|
52
|
+
*/
|
53
|
+
export default {
|
54
|
+
|
55
|
+
}
|
56
|
+
|
57
|
+
export {default as getTable} from "./getTable";
|
package/bin/create-app.js
CHANGED
@@ -31,8 +31,8 @@ module.exports = function(appName,{projectRoot:root}){
|
|
31
31
|
name,
|
32
32
|
version : "1.0.0",
|
33
33
|
"description": "",
|
34
|
-
"main": "index.js",
|
35
34
|
"main": "App.js",
|
35
|
+
"tablesDataPath": "./src/database/tables",
|
36
36
|
"scripts" : {
|
37
37
|
start : "npx expo start -c",
|
38
38
|
"dev" : "npx expo start --no-dev --minify -c",
|
@@ -40,6 +40,7 @@ module.exports = function(appName,{projectRoot:root}){
|
|
40
40
|
"build-web" : "npx expo export:web",
|
41
41
|
"build-android" : "npx eas build --platform android --profile preview",
|
42
42
|
"build-ios" : "eas build --platform ios",
|
43
|
+
"generate-getTable" : "nxp @fto-consult/expo-ui generate-getTable"
|
43
44
|
},
|
44
45
|
"dependencies" : {
|
45
46
|
[euModule] : packageObj.version,
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@fto-consult/expo-ui",
|
3
|
-
"version": "8.
|
3
|
+
"version": "8.48.0",
|
4
4
|
"description": "Bibliothèque de composants UI Expo,react-native",
|
5
5
|
"react-native-paper-doc": "https://github.com/callstack/react-native-paper/tree/main/docs/docs/guides",
|
6
6
|
"scripts": {
|
package/src/auth/Login.js
CHANGED
@@ -27,7 +27,7 @@ const WIDTH = 400;
|
|
27
27
|
|
28
28
|
export default function LoginComponent(props){
|
29
29
|
let {formName,step,appBarProps,onSuccess,withPortal,testID} = props;
|
30
|
-
const {auth:{loginPropsMutator}} = useContext();
|
30
|
+
const {auth:{loginPropsMutator,Login}} = useContext();
|
31
31
|
const loginTitle = getTitle();
|
32
32
|
testID = defaultStr(testID,"RN_Auth.LoginComponent");
|
33
33
|
formName = React.useRef(uniqid(defaultStr(formName,"login-formname"))).current;
|
@@ -74,7 +74,6 @@ export default function LoginComponent(props){
|
|
74
74
|
}
|
75
75
|
}
|
76
76
|
|
77
|
-
|
78
77
|
if(withPortal){
|
79
78
|
appBarProps = defaultObj(appBarProps);
|
80
79
|
appBarProps.backAction = false;
|
@@ -86,6 +85,27 @@ export default function LoginComponent(props){
|
|
86
85
|
},1000)
|
87
86
|
}
|
88
87
|
},[withPortal]);
|
88
|
+
React.useEffect(()=>{
|
89
|
+
Preloader.closeAll();
|
90
|
+
/*** pour initializer les cordonnées du composant login */
|
91
|
+
if(typeof initialize =='function'){
|
92
|
+
initialize();
|
93
|
+
}
|
94
|
+
},[]);
|
95
|
+
const prevStep = React.usePrevious(state.step);
|
96
|
+
React.useEffect(()=>{
|
97
|
+
/*** lorsque le state du composant change */
|
98
|
+
if(typeof onStepChange =='function'){
|
99
|
+
return onStepChange({...state,previousStep:prevStep,focusField,nextButtonRef})
|
100
|
+
}
|
101
|
+
},[state.step]);
|
102
|
+
if(React.isComponent(Login)) return <Login
|
103
|
+
{...props}
|
104
|
+
withPortal = {withPortal}
|
105
|
+
appBarProps = {appBarProps}
|
106
|
+
onSuccess = {onSuccess}
|
107
|
+
auth = {auth}
|
108
|
+
/>
|
89
109
|
const getButtonAction = (buttonRef)=>{
|
90
110
|
return {
|
91
111
|
ref : buttonRef,
|
@@ -122,20 +142,6 @@ export default function LoginComponent(props){
|
|
122
142
|
const containerProps = defaultObj(customContainerProps);
|
123
143
|
const contentProps = defaultObj(customContentProps);
|
124
144
|
|
125
|
-
React.useEffect(()=>{
|
126
|
-
Preloader.closeAll();
|
127
|
-
/*** pour initializer les cordonnées du composant login */
|
128
|
-
if(typeof initialize =='function'){
|
129
|
-
initialize();
|
130
|
-
}
|
131
|
-
},[]);
|
132
|
-
const prevStep = React.usePrevious(state.step);
|
133
|
-
React.useEffect(()=>{
|
134
|
-
/*** lorsque le state du composant change */
|
135
|
-
if(typeof onStepChange =='function'){
|
136
|
-
return onStepChange({...state,previousStep:prevStep,focusField,nextButtonRef})
|
137
|
-
}
|
138
|
-
},[state.step]);
|
139
145
|
/****la fonction à utiliser pour vérifier si l'on peut envoyer les données pour connextion
|
140
146
|
* par défaut, on envoie les données lorssqu'on est à l'étappe 2
|
141
147
|
* **/
|
@@ -0,0 +1,311 @@
|
|
1
|
+
import React, { Component } from 'react'
|
2
|
+
import {
|
3
|
+
Dimensions,
|
4
|
+
Image,
|
5
|
+
ScrollView,
|
6
|
+
Modal,
|
7
|
+
View,
|
8
|
+
Text,
|
9
|
+
SafeAreaView,
|
10
|
+
TouchableOpacity,
|
11
|
+
} from 'react-native'
|
12
|
+
import * as ImageManipulator from 'expo-image-manipulator'
|
13
|
+
import PropTypes from 'prop-types'
|
14
|
+
import AutoHeightImage from '../../Avatar/AutoHeightImage'
|
15
|
+
import Icon from "$ecomponents/Icon";
|
16
|
+
import {isIphoneX } from 'react-native-iphone-x-helper'
|
17
|
+
import ImageCropOverlay from './ImageCropOverlay'
|
18
|
+
|
19
|
+
const { width, height } = Dimensions.get('window')
|
20
|
+
|
21
|
+
|
22
|
+
class ExpoImageManipulator extends Component {
|
23
|
+
constructor(props) {
|
24
|
+
super(props)
|
25
|
+
const { squareAspect } = this.props
|
26
|
+
this.state = {
|
27
|
+
cropMode: true,
|
28
|
+
processing: false,
|
29
|
+
zoomScale: 1,
|
30
|
+
squareAspect,
|
31
|
+
}
|
32
|
+
|
33
|
+
this.scrollOffset = 0
|
34
|
+
|
35
|
+
this.currentPos = {
|
36
|
+
left: 0,
|
37
|
+
top: 0,
|
38
|
+
}
|
39
|
+
|
40
|
+
this.currentSize = {
|
41
|
+
width: 0,
|
42
|
+
height: 0,
|
43
|
+
}
|
44
|
+
|
45
|
+
this.maxSizes = {
|
46
|
+
width: 0,
|
47
|
+
height: 0,
|
48
|
+
}
|
49
|
+
|
50
|
+
this.actualSize = {
|
51
|
+
width: 0,
|
52
|
+
height: 0
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
async componentDidMount() {
|
57
|
+
await this.onConvertImageToEditableSize()
|
58
|
+
}
|
59
|
+
|
60
|
+
async onConvertImageToEditableSize() {
|
61
|
+
const { photo: { uri: rawUri } } = this.props
|
62
|
+
const { uri, width, height } = await ImageManipulator.manipulateAsync(rawUri,
|
63
|
+
[
|
64
|
+
{
|
65
|
+
resize: {
|
66
|
+
width: 1080,
|
67
|
+
},
|
68
|
+
},
|
69
|
+
])
|
70
|
+
this.setState({
|
71
|
+
uri,
|
72
|
+
})
|
73
|
+
this.actualSize.width = width
|
74
|
+
this.actualSize.height = height
|
75
|
+
}
|
76
|
+
onCropImage = () => {
|
77
|
+
this.setState({ processing: true })
|
78
|
+
const { uri } = this.state
|
79
|
+
Image.getSize(uri, async (actualWidth, actualHeight) => {
|
80
|
+
let cropObj = this.getCropBounds(actualWidth, actualHeight);
|
81
|
+
if (cropObj.height > 0 && cropObj.width > 0) {
|
82
|
+
let uriToCrop = uri
|
83
|
+
const { uri: uriCroped, base64, width: croppedWidth, height: croppedHeight } = await this.crop(cropObj, uriToCrop)
|
84
|
+
|
85
|
+
this.actualSize.width = croppedWidth
|
86
|
+
this.actualSize.height = croppedHeight
|
87
|
+
|
88
|
+
this.setState({
|
89
|
+
uri: uriCroped, base64, cropMode: false, processing: false,
|
90
|
+
})
|
91
|
+
} else {
|
92
|
+
this.setState({cropMode: false, processing: false})
|
93
|
+
}
|
94
|
+
})
|
95
|
+
}
|
96
|
+
|
97
|
+
onRotateImage = async () => {
|
98
|
+
const { uri } = this.state
|
99
|
+
let uriToCrop = uri
|
100
|
+
Image.getSize(uri, async (width2, height2) => {
|
101
|
+
const { uri: rotUri, base64 } = await this.rotate(uriToCrop, width2, height2)
|
102
|
+
this.setState({ uri: rotUri, base64 })
|
103
|
+
})
|
104
|
+
}
|
105
|
+
|
106
|
+
onFlipImage = async (orientation) => {
|
107
|
+
const { uri } = this.state
|
108
|
+
let uriToCrop = uri
|
109
|
+
Image.getSize(uri, async (width2, height2) => {
|
110
|
+
const { uri: rotUri, base64 } = await this.filp(uriToCrop, orientation)
|
111
|
+
this.setState({ uri: rotUri, base64 })
|
112
|
+
})
|
113
|
+
}
|
114
|
+
|
115
|
+
onHandleScroll = (event) => {
|
116
|
+
this.scrollOffset = event.nativeEvent.contentOffset.y
|
117
|
+
}
|
118
|
+
|
119
|
+
getCropBounds = (actualWidth, actualHeight) => {
|
120
|
+
let imageRatio = actualHeight / actualWidth
|
121
|
+
var originalHeight = Dimensions.get('window').height - 64
|
122
|
+
if (isIphoneX()) {
|
123
|
+
originalHeight = Dimensions.get('window').height - 122
|
124
|
+
}
|
125
|
+
let renderedImageWidth = imageRatio < (originalHeight / width) ? width : originalHeight / imageRatio
|
126
|
+
let renderedImageHeight = imageRatio < (originalHeight / width) ? width * imageRatio : originalHeight
|
127
|
+
|
128
|
+
let renderedImageY = (originalHeight - renderedImageHeight) / 2.0
|
129
|
+
let renderedImageX = (width - renderedImageWidth) / 2.0
|
130
|
+
|
131
|
+
const renderImageObj = {
|
132
|
+
left: renderedImageX,
|
133
|
+
top: renderedImageY,
|
134
|
+
width: renderedImageWidth,
|
135
|
+
height: renderedImageHeight,
|
136
|
+
}
|
137
|
+
const cropOverlayObj = {
|
138
|
+
left: this.currentPos.left,
|
139
|
+
top: this.currentPos.top,
|
140
|
+
width: this.currentSize.width,
|
141
|
+
height: this.currentSize.height,
|
142
|
+
}
|
143
|
+
|
144
|
+
var intersectAreaObj = {}
|
145
|
+
|
146
|
+
let x = Math.max(renderImageObj.left, cropOverlayObj.left);
|
147
|
+
let num1 = Math.min(renderImageObj.left + renderImageObj.width, cropOverlayObj.left + cropOverlayObj.width);
|
148
|
+
let y = Math.max(renderImageObj.top, cropOverlayObj.top);
|
149
|
+
let num2 = Math.min(renderImageObj.top + renderImageObj.height, cropOverlayObj.top + cropOverlayObj.height);
|
150
|
+
if (num1 >= x && num2 >= y)
|
151
|
+
intersectAreaObj = {
|
152
|
+
originX: (x - renderedImageX) * (actualWidth / renderedImageWidth) ,
|
153
|
+
originY: (y - renderedImageY) * (actualWidth / renderedImageWidth),
|
154
|
+
width: (num1 - x) * (actualWidth / renderedImageWidth),
|
155
|
+
height: (num2 - y) * (actualWidth / renderedImageWidth)
|
156
|
+
}
|
157
|
+
else {
|
158
|
+
intersectAreaObj = {
|
159
|
+
originX: x - renderedImageX,
|
160
|
+
originY: y - renderedImageY,
|
161
|
+
width: 0,
|
162
|
+
height: 0
|
163
|
+
}
|
164
|
+
}
|
165
|
+
return intersectAreaObj
|
166
|
+
}
|
167
|
+
|
168
|
+
filp = async (uri, orientation) => {
|
169
|
+
const { saveOptions } = this.props
|
170
|
+
const manipResult = await ImageManipulator.manipulateAsync(uri, [{
|
171
|
+
flip: orientation == 'vertical' ? ImageManipulator.FlipType.Vertical : ImageManipulator.FlipType.Horizontal
|
172
|
+
}],
|
173
|
+
saveOptions
|
174
|
+
);
|
175
|
+
return manipResult;
|
176
|
+
};
|
177
|
+
|
178
|
+
rotate = async (uri, width2, height2) => {
|
179
|
+
const { saveOptions } = this.props
|
180
|
+
const manipResult = await ImageManipulator.manipulateAsync(uri, [{
|
181
|
+
rotate: -90,
|
182
|
+
}, {
|
183
|
+
resize: {
|
184
|
+
width: this.trueWidth || width2,
|
185
|
+
// height: this.trueHeight || height2,
|
186
|
+
},
|
187
|
+
}], saveOptions)
|
188
|
+
return manipResult
|
189
|
+
}
|
190
|
+
|
191
|
+
crop = async (cropObj, uri) => {
|
192
|
+
const { saveOptions } = this.props
|
193
|
+
if (cropObj.height > 0 && cropObj.width > 0) {
|
194
|
+
const manipResult = await ImageManipulator.manipulateAsync(
|
195
|
+
uri,
|
196
|
+
[{
|
197
|
+
crop: cropObj,
|
198
|
+
}],
|
199
|
+
saveOptions,
|
200
|
+
)
|
201
|
+
return manipResult
|
202
|
+
}
|
203
|
+
return {
|
204
|
+
uri: null,
|
205
|
+
base64: null,
|
206
|
+
}
|
207
|
+
};
|
208
|
+
|
209
|
+
calculateMaxSizes = (event) => {
|
210
|
+
let w1 = event.nativeEvent.layout.width || 100
|
211
|
+
let h1 = event.nativeEvent.layout.height || 100
|
212
|
+
if (this.state.squareAspect) {
|
213
|
+
if (w1 < h1) h1 = w1
|
214
|
+
else w1 = h1
|
215
|
+
}
|
216
|
+
this.maxSizes.width = w1
|
217
|
+
this.maxSizes.height = h1
|
218
|
+
};
|
219
|
+
|
220
|
+
// eslint-disable-next-line camelcase
|
221
|
+
async UNSAFE_componentWillReceiveProps() {
|
222
|
+
await this.onConvertImageToEditableSize()
|
223
|
+
}
|
224
|
+
|
225
|
+
zoomImage() {
|
226
|
+
// this.refs.imageScrollView.zoomScale = 5
|
227
|
+
// this.setState({width: width})
|
228
|
+
// this.setState({zoomScale: 5})
|
229
|
+
|
230
|
+
// this.setState(curHeight)
|
231
|
+
}
|
232
|
+
|
233
|
+
render() {
|
234
|
+
const {
|
235
|
+
uri,
|
236
|
+
base64,
|
237
|
+
cropMode,
|
238
|
+
processing,
|
239
|
+
zoomScale
|
240
|
+
} = this.state
|
241
|
+
|
242
|
+
let imageRatio = this.actualSize.height / this.actualSize.width
|
243
|
+
var originalHeight = Dimensions.get('window').height - 64
|
244
|
+
if (isIphoneX()) {
|
245
|
+
originalHeight = Dimensions.get('window').height - 122
|
246
|
+
}
|
247
|
+
|
248
|
+
let cropRatio = originalHeight / width
|
249
|
+
|
250
|
+
let cropWidth = imageRatio < cropRatio ? width : originalHeight / imageRatio
|
251
|
+
let cropHeight = imageRatio < cropRatio ? width * imageRatio : originalHeight
|
252
|
+
|
253
|
+
let cropInitialTop = (originalHeight - cropHeight) / 2.0
|
254
|
+
let cropInitialLeft = (width - cropWidth) / 2.0
|
255
|
+
|
256
|
+
|
257
|
+
if (this.currentSize.width == 0 && cropMode) {
|
258
|
+
this.currentSize.width = cropWidth;
|
259
|
+
this.currentSize.height = cropHeight;
|
260
|
+
|
261
|
+
this.currentPos.top = cropInitialTop;
|
262
|
+
this.currentPos.left = cropInitialLeft;
|
263
|
+
}
|
264
|
+
return <>
|
265
|
+
<AutoHeightImage
|
266
|
+
style={{ backgroundColor: 'black' }}
|
267
|
+
source={{ uri }}
|
268
|
+
resizeMode={imageRatio >= 1 ? "contain" : 'contain'}
|
269
|
+
width={width}
|
270
|
+
height={originalHeight}
|
271
|
+
onLayout={this.calculateMaxSizes}
|
272
|
+
/>
|
273
|
+
{!!cropMode && (
|
274
|
+
<ImageCropOverlay onLayoutChanged={(top, left, width, height) => {
|
275
|
+
this.currentSize.width = width;
|
276
|
+
this.currentSize.height = height;
|
277
|
+
this.currentPos.top = top
|
278
|
+
this.currentPos.left = left
|
279
|
+
}} initialWidth={cropWidth} initialHeight={cropHeight} initialTop={cropInitialTop} initialLeft={cropInitialLeft} minHeight={100} minWidth={100} />
|
280
|
+
)}
|
281
|
+
</>
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
export default ExpoImageManipulator
|
286
|
+
|
287
|
+
ExpoImageManipulator.defaultProps = {
|
288
|
+
onPictureChoosed: ({ uri, base64 }) => console.log('URI:', uri, base64),
|
289
|
+
btnTexts: {
|
290
|
+
crop: 'Crop',
|
291
|
+
rotate: 'Rotate',
|
292
|
+
done: 'Done',
|
293
|
+
processing: 'Processing',
|
294
|
+
},
|
295
|
+
dragVelocity: 100,
|
296
|
+
resizeVelocity: 50,
|
297
|
+
saveOptions: {
|
298
|
+
compress: 1,
|
299
|
+
format: ImageManipulator.SaveFormat.PNG,
|
300
|
+
base64: false,
|
301
|
+
},
|
302
|
+
}
|
303
|
+
|
304
|
+
ExpoImageManipulator.propTypes = {
|
305
|
+
onPictureChoosed: PropTypes.func,
|
306
|
+
btnTexts: PropTypes.object,
|
307
|
+
saveOptions: PropTypes.object,
|
308
|
+
photo: PropTypes.object.isRequired,
|
309
|
+
dragVelocity: PropTypes.number,
|
310
|
+
resizeVelocity: PropTypes.number,
|
311
|
+
}
|
@@ -0,0 +1,219 @@
|
|
1
|
+
import React, { Component } from 'react'
|
2
|
+
import { View, PanResponder, Dimensions } from 'react-native';
|
3
|
+
|
4
|
+
class ImageCropOverlay extends React.Component {
|
5
|
+
|
6
|
+
state = {
|
7
|
+
draggingTL: false,
|
8
|
+
draggingTM: false,
|
9
|
+
draggingTR: false,
|
10
|
+
draggingML: false,
|
11
|
+
draggingMM: false,
|
12
|
+
draggingMR: false,
|
13
|
+
draggingBL: false,
|
14
|
+
draggingBM: false,
|
15
|
+
draggingBR: false,
|
16
|
+
initialTop: this.props.initialTop,
|
17
|
+
initialLeft: this.props.initialLeft,
|
18
|
+
initialWidth: this.props.initialWidth,
|
19
|
+
initialHeight: this.props.initialHeight,
|
20
|
+
|
21
|
+
offsetTop: 0,
|
22
|
+
offsetLeft: 0,
|
23
|
+
}
|
24
|
+
|
25
|
+
panResponder = {}
|
26
|
+
|
27
|
+
UNSAFE_componentWillMount() {
|
28
|
+
this.panResponder = PanResponder.create({
|
29
|
+
onStartShouldSetPanResponder: this.handleStartShouldSetPanResponder,
|
30
|
+
onPanResponderGrant: this.handlePanResponderGrant,
|
31
|
+
onPanResponderMove: this.handlePanResponderMove,
|
32
|
+
onPanResponderRelease: this.handlePanResponderEnd,
|
33
|
+
onPanResponderTerminate: this.handlePanResponderEnd,
|
34
|
+
})
|
35
|
+
}
|
36
|
+
|
37
|
+
render() {
|
38
|
+
const { draggingTL, draggingTM, draggingTR, draggingML, draggingMM, draggingMR, draggingBL, draggingBM, draggingBR, initialTop, initialLeft, initialHeight, initialWidth, offsetTop, offsetLeft} = this.state
|
39
|
+
const style = {}
|
40
|
+
|
41
|
+
style.top = initialTop + ((draggingTL || draggingTM || draggingTR || draggingMM) ? offsetTop : 0)
|
42
|
+
style.left = initialLeft + ((draggingTL || draggingML || draggingBL || draggingMM) ? offsetLeft : 0)
|
43
|
+
style.width = initialWidth + ((draggingTL || draggingML || draggingBL) ? - offsetLeft : (draggingTM || draggingMM || draggingBM) ? 0 : offsetLeft)
|
44
|
+
style.height = initialHeight + ((draggingTL || draggingTM || draggingTR) ? - offsetTop : (draggingML || draggingMM || draggingMR) ? 0 : offsetTop)
|
45
|
+
|
46
|
+
if (style.width > this.props.initialWidth) {
|
47
|
+
style.width = this.props.initialWidth
|
48
|
+
}
|
49
|
+
if (style.width < this.props.minWidth) {
|
50
|
+
style.width = this.props.minWidth
|
51
|
+
}
|
52
|
+
if (style.height > this.props.initialHeight) {
|
53
|
+
style.height = this.props.initialHeight
|
54
|
+
}
|
55
|
+
if (style.height < this.props.minHeight) {
|
56
|
+
style.height = this.props.minHeight
|
57
|
+
}
|
58
|
+
return (
|
59
|
+
<View {...this.panResponder.panHandlers} style={[{flex: 1, justifyContent: 'center', alignItems: 'center', position: 'absolute', borderStyle: 'solid', borderWidth: 2, borderColor: '#a4a4a4', backgroundColor: 'rgb(0,0,0,0.5)'}, style]}>
|
60
|
+
<View style={{flexDirection: 'row', width: '100%', flex: 1/3, backgroundColor: 'transparent'}}>
|
61
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingTL ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
62
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingTM ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
63
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingTR ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
64
|
+
</View>
|
65
|
+
<View style={{flexDirection: 'row', width: '100%', flex: 1/3, backgroundColor: 'transparent'}}>
|
66
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingML ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
67
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingMM ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
68
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingMR ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
69
|
+
</View>
|
70
|
+
<View style={{flexDirection: 'row', width: '100%', flex: 1/3, backgroundColor: 'transparent'}}>
|
71
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingBL ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
72
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingBM ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
73
|
+
<View style={{borderWidth: '#a4a4a4', borderWidth: 0, backgroundColor: draggingBR ? 'transparent' : 'transparent', flex: 1/3, height: '100%'}}></View>
|
74
|
+
</View>
|
75
|
+
<View style={{top: 0, left: 0, width: '100%', height: '100%', position: 'absolute', backgroundColor: 'rgba(0, 0, 0, 0.5)'}}>
|
76
|
+
<View style={{flex: 1/3, flexDirection: 'row'}}>
|
77
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
78
|
+
<View style={{ position: 'absolute', left: 5, top: 5, borderLeftWidth: 2, borderTopWidth: 2, height: 48, width: 48, borderColor: '#f4f4f4', borderStyle: 'solid' }}/>
|
79
|
+
</View>
|
80
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
81
|
+
</View>
|
82
|
+
<View style={{ flex: 3, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
83
|
+
<View style={{ position: 'absolute', right: 5, top: 5, borderRightWidth: 2, borderTopWidth: 2, height: 48, width: 48, borderColor: '#f4f4f4', borderStyle: 'solid' }}/>
|
84
|
+
</View>
|
85
|
+
</View>
|
86
|
+
<View style={{flex: 1/3, flexDirection: 'row'}}>
|
87
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
88
|
+
</View>
|
89
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
90
|
+
</View>
|
91
|
+
<View style={{ flex: 3, borderBottomWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
92
|
+
</View>
|
93
|
+
</View>
|
94
|
+
<View style={{flex: 1/3, flexDirection: 'row'}}>
|
95
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid', position: 'relative', }}>
|
96
|
+
<View style={{ position: 'absolute', left: 5, bottom: 5, borderLeftWidth: 2, borderBottomWidth: 2, height: 48, width: 48, borderColor: '#f4f4f4', borderStyle: 'solid' }}/>
|
97
|
+
</View>
|
98
|
+
<View style={{ flex: 3, borderRightWidth: 1, borderColor: '#c9c9c9', borderStyle: 'solid' }}>
|
99
|
+
</View>
|
100
|
+
<View style={{ flex: 3, position: 'relative' }}>
|
101
|
+
<View style={{ position: 'absolute', right: 5, bottom: 5, borderRightWidth: 2, borderBottomWidth: 2, height: 48, width: 48, borderColor: '#f4f4f4', borderStyle: 'solid' }}/>
|
102
|
+
</View>
|
103
|
+
</View>
|
104
|
+
</View>
|
105
|
+
</View>
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
getTappedItem(x, y) {
|
110
|
+
const { initialLeft, initialTop, initialWidth, initialHeight } = this.state
|
111
|
+
let xPos = parseInt((x - initialLeft) / (initialWidth / 3))
|
112
|
+
let yPos = parseInt((y - initialTop - 64) / (initialHeight / 3))
|
113
|
+
|
114
|
+
let index = yPos * 3 + xPos
|
115
|
+
if (index == 0) {
|
116
|
+
return 'tl';
|
117
|
+
} else if (index == 1) {
|
118
|
+
return 'tm';
|
119
|
+
} else if (index == 2) {
|
120
|
+
return 'tr';
|
121
|
+
} else if (index == 3) {
|
122
|
+
return 'ml';
|
123
|
+
} else if (index == 4) {
|
124
|
+
return 'mm';
|
125
|
+
} else if (index == 5) {
|
126
|
+
return 'mr';
|
127
|
+
} else if (index == 6) {
|
128
|
+
return 'bl';
|
129
|
+
} else if (index == 7) {
|
130
|
+
return 'bm';
|
131
|
+
} else if (index == 8) {
|
132
|
+
return 'br';
|
133
|
+
} else {
|
134
|
+
return '';
|
135
|
+
}
|
136
|
+
}
|
137
|
+
|
138
|
+
// Should we become active when the user presses down on the square?
|
139
|
+
handleStartShouldSetPanResponder = (event) => {
|
140
|
+
return true
|
141
|
+
}
|
142
|
+
|
143
|
+
// We were granted responder status! Let's update the UI
|
144
|
+
handlePanResponderGrant = (event) => {
|
145
|
+
// console.log(event.nativeEvent.locationX + ', ' + event.nativeEvent.locationY)
|
146
|
+
|
147
|
+
let selectedItem = this.getTappedItem(event.nativeEvent.pageX, event.nativeEvent.pageY)
|
148
|
+
if (selectedItem == 'tl') {
|
149
|
+
this.setState({draggingTL: true})
|
150
|
+
} else if (selectedItem == 'tm') {
|
151
|
+
this.setState({draggingTM: true})
|
152
|
+
} else if (selectedItem == 'tr') {
|
153
|
+
this.setState({draggingTR: true})
|
154
|
+
} else if (selectedItem == 'ml') {
|
155
|
+
this.setState({draggingML: true})
|
156
|
+
} else if (selectedItem == 'mm') {
|
157
|
+
this.setState({draggingMM: true})
|
158
|
+
} else if (selectedItem == 'mr') {
|
159
|
+
this.setState({draggingMR: true})
|
160
|
+
} else if (selectedItem == 'bl') {
|
161
|
+
this.setState({draggingBL: true})
|
162
|
+
} else if (selectedItem == 'bm') {
|
163
|
+
this.setState({draggingBM: true})
|
164
|
+
} else if (selectedItem == 'br') {
|
165
|
+
this.setState({draggingBR: true})
|
166
|
+
}
|
167
|
+
}
|
168
|
+
|
169
|
+
// Every time the touch/mouse moves
|
170
|
+
handlePanResponderMove = (e, gestureState) => {
|
171
|
+
// Keep track of how far we've moved in total (dx and dy)
|
172
|
+
this.setState({
|
173
|
+
offsetTop: gestureState.dy,
|
174
|
+
offsetLeft: gestureState.dx,
|
175
|
+
})
|
176
|
+
}
|
177
|
+
|
178
|
+
// When the touch/mouse is lifted
|
179
|
+
handlePanResponderEnd = (e, gestureState) => {
|
180
|
+
const {initialTop, initialLeft, initialWidth, initialHeight, draggingTL, draggingTM, draggingTR, draggingML, draggingMM, draggingMR, draggingBL, draggingBM, draggingBR } = this.state
|
181
|
+
|
182
|
+
const state = {
|
183
|
+
draggingTL: false,
|
184
|
+
draggingTM: false,
|
185
|
+
draggingTR: false,
|
186
|
+
draggingML: false,
|
187
|
+
draggingMM: false,
|
188
|
+
draggingMR: false,
|
189
|
+
draggingBL: false,
|
190
|
+
draggingBM: false,
|
191
|
+
draggingBR: false,
|
192
|
+
offsetTop: 0,
|
193
|
+
offsetLeft: 0,
|
194
|
+
}
|
195
|
+
|
196
|
+
state.initialTop = initialTop + ((draggingTL || draggingTM || draggingTR || draggingMM) ? gestureState.dy : 0)
|
197
|
+
state.initialLeft = initialLeft + ((draggingTL || draggingML || draggingBL || draggingMM) ? gestureState.dx : 0)
|
198
|
+
state.initialWidth = initialWidth + ((draggingTL || draggingML || draggingBL) ? - gestureState.dx : (draggingTM || draggingMM || draggingBM) ? 0 : gestureState.dx)
|
199
|
+
state.initialHeight = initialHeight + ((draggingTL || draggingTM || draggingTR) ? - gestureState.dy : (draggingML || draggingMM || draggingMR) ? 0 : gestureState.dy)
|
200
|
+
|
201
|
+
if (state.initialWidth > this.props.initialWidth) {
|
202
|
+
state.initialWidth = this.props.initialWidth
|
203
|
+
}
|
204
|
+
if (state.initialWidth < this.props.minWidth) {
|
205
|
+
state.initialWidth = this.props.minWidth
|
206
|
+
}
|
207
|
+
if (state.initialHeight > this.props.initialHeight) {
|
208
|
+
state.initialHeight = this.props.initialHeight
|
209
|
+
}
|
210
|
+
if (state.initialHeight < this.props.minHeight) {
|
211
|
+
state.initialHeight = this.props.minHeight
|
212
|
+
}
|
213
|
+
|
214
|
+
this.setState(state)
|
215
|
+
this.props.onLayoutChanged(state.initialTop, state.initialLeft, state.initialWidth, state.initialHeight)
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
export default ImageCropOverlay;
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import React, { useState, useEffect,useMemo,useRef } from '$react';
|
2
|
+
import { View, StyleSheet,ImageBackground} from 'react-native';
|
3
|
+
import theme from "$theme";
|
4
|
+
import ActivityIndicator from "$ecomponents/ActivityIndicator";
|
5
|
+
import Label from "$ecomponents/Label";
|
6
|
+
import PropTypes from "prop-types";
|
7
|
+
import { isNonNullString,defaultStr,defaultObj } from '$cutils';
|
8
|
+
import Button from "$ecomponents/Button";
|
9
|
+
import Dialog from "$ecomponents/Dialog";
|
10
|
+
import ExpoImageManipulator from './ExpoImageManipulator';
|
11
|
+
import ImageCropOverlay from './ImageCropOverlay';
|
12
|
+
|
13
|
+
|
14
|
+
/***@see : https://docs.expo.dev/versions/latest/sdk/bar-code-scanner/ */
|
15
|
+
export default function ImageCropperComponent({src,testID,onCancel,dialogProps}) {
|
16
|
+
testID = defaultStr(testID,"RN_ImageCropperComponent");
|
17
|
+
const [visible,setVisible] = useState(true);
|
18
|
+
dialogProps = Object.assign({},dialogProps);
|
19
|
+
const prevVisible = React.usePrevious(visible);
|
20
|
+
const cancelRef = React.useRef(false);
|
21
|
+
const cancel = ()=>{
|
22
|
+
cancelRef.current = true;
|
23
|
+
setVisible(false);
|
24
|
+
}
|
25
|
+
useEffect(()=>{
|
26
|
+
if(prevVisible === visible) return;
|
27
|
+
if(prevVisible && !visible && cancelRef.current && typeof onCancel =="function"){
|
28
|
+
onCancel();
|
29
|
+
}
|
30
|
+
cancelRef.current = false;
|
31
|
+
},[visible]);
|
32
|
+
return <Dialog
|
33
|
+
fullPage
|
34
|
+
actions={[]}
|
35
|
+
title = {`Rogner l'image`}
|
36
|
+
{...dialogProps}
|
37
|
+
onBackActionPress={cancel}
|
38
|
+
visible = {visible}
|
39
|
+
>
|
40
|
+
<ImageBackground
|
41
|
+
resizeMode="contain"
|
42
|
+
style={styles.imageBackground}
|
43
|
+
source={{ uri : src }}
|
44
|
+
>
|
45
|
+
<ImageCropOverlay onLayoutChanged={(top, left, width, height) => {
|
46
|
+
console.log(top,lef,width,height," is to lefff")
|
47
|
+
}} initialWidth={50} initialHeight={50}
|
48
|
+
initialTop={0} initialLeft={0}
|
49
|
+
minHeight={100} minWidth={100}
|
50
|
+
/>
|
51
|
+
</ImageBackground>
|
52
|
+
</Dialog>;
|
53
|
+
}
|
54
|
+
const styles = StyleSheet.create({
|
55
|
+
center : {
|
56
|
+
justifyContent : "center",
|
57
|
+
alignItems : "center",
|
58
|
+
flexDirection : "column",
|
59
|
+
flex : 1,
|
60
|
+
},
|
61
|
+
row : {
|
62
|
+
flexDirection : "row",
|
63
|
+
justifyContent : "center",
|
64
|
+
alignItems : "center",
|
65
|
+
flexWrap :"wrap",
|
66
|
+
},
|
67
|
+
imageBackground : {
|
68
|
+
flex : 1,
|
69
|
+
width : "100%",
|
70
|
+
height : "100%",
|
71
|
+
justifyContent: 'center',
|
72
|
+
padding: 20, alignItems: 'center',
|
73
|
+
}
|
74
|
+
});
|
75
|
+
|
76
|
+
/***
|
77
|
+
@see : https://docs.expo.dev/versions/latest/sdk/camera-next
|
78
|
+
*/
|
79
|
+
ImageCropperComponent.propTypes = {
|
80
|
+
onScan : PropTypes.func,
|
81
|
+
onGrantAccess : PropTypes.func, //lorsque la permission est allouée
|
82
|
+
onDenyAccess : PropTypes.func, //lorsque la permission est refusée
|
83
|
+
}
|
@@ -6,7 +6,6 @@ import {defaultObj} from "$cutils";
|
|
6
6
|
|
7
7
|
const ImageEditorComponent = React.forwardRef((props,ref)=>{
|
8
8
|
let {source,uri,onSuccess,imageUri,lockAspectRatio,dialogProps,onDismiss,visible,imageProps,...rest} = props;
|
9
|
-
const isMounted = React.useIsMounted();
|
10
9
|
const [context] = React.useState({});
|
11
10
|
imageProps = defaultObj(imageProps);
|
12
11
|
dialogProps = defaultObj(dialogProps);
|
@@ -3,15 +3,15 @@ import Menu from "$ecomponents/Menu";
|
|
3
3
|
import Avatar from "$ecomponents/Avatar";
|
4
4
|
import {isDecimal,setQueryParams,isValidURL,defaultDecimal,defaultStr as defaultString,isDataURL,isPromise,defaultBool,isObj,isNonNullString} from "$cutils";
|
5
5
|
import notify from "$enotify";
|
6
|
-
let maxWidthDiff =
|
6
|
+
let maxWidthDiff = 100, maxHeightDiff = 100;
|
7
7
|
import {StyleSheet} from "react-native";
|
8
8
|
import React from "$react";
|
9
9
|
import PropTypes from "prop-types";
|
10
10
|
import {isMobileNative} from "$cplatform";
|
11
|
+
import {uniqid} from "$cutils";
|
11
12
|
//import Signature from "$ecomponents/Signature";
|
12
13
|
import Label from "$ecomponents/Label";
|
13
|
-
//import
|
14
|
-
import {Component as CameraComponent} from "$emedia/camera";
|
14
|
+
//import Cropper from "./Cropper";
|
15
15
|
|
16
16
|
import {pickImage,nonZeroMin,canTakePhoto,takePhoto} from "$emedia";
|
17
17
|
import addPhoto from "$eassets/add_photo.png";
|
@@ -48,11 +48,8 @@ export const getUri = (src,onlySting)=>{
|
|
48
48
|
|
49
49
|
export default function ImageComponent(props){
|
50
50
|
const [src,setSrc] = React.useState(defaultVal(props.src));
|
51
|
+
const [cropWindowProp,setCropWindowProp] = React.useState(null);
|
51
52
|
const prevSrc = React.usePrevious(src);
|
52
|
-
/*const [editorProps,setEditorProps] = React.useState({
|
53
|
-
visible : false,
|
54
|
-
options : {}
|
55
|
-
})*/
|
56
53
|
const [isDrawing,setIsDrawing] = React.useState(false);
|
57
54
|
let {disabled,onMount,defaultSource,editable,onUnmount,label,text,labelProps,readOnly,beforeRemove,
|
58
55
|
onChange,draw,round,drawText,drawLabel,rounded,defaultSrc,
|
@@ -114,8 +111,8 @@ export default function ImageComponent(props){
|
|
114
111
|
if(!imageWidth && !imageHeight){
|
115
112
|
imageWidth = imageHeight = rest.size;
|
116
113
|
}
|
117
|
-
let cropWidth = nonZeroMin(cropProps.width,width)
|
118
|
-
let cropHeight = nonZeroMin(cropProps.height,height);
|
114
|
+
let cropWidth = nonZeroMin(cropProps.width,imageWidth,width,size)
|
115
|
+
let cropHeight = nonZeroMin(cropProps.height,imageHeight,height,size);
|
119
116
|
if(!cropWidth) cropWidth = undefined;
|
120
117
|
if(!cropHeight) cropHeight = undefined;
|
121
118
|
|
@@ -125,24 +122,26 @@ export default function ImageComponent(props){
|
|
125
122
|
if(cropWidth || cropHeight){
|
126
123
|
canCrop = true;
|
127
124
|
if(cropWidth) opts.width = cropWidth;
|
128
|
-
|
125
|
+
if(cropHeight) opts.height = cropHeight;
|
129
126
|
}
|
130
127
|
opts.allowsEditing = canCrop;
|
131
|
-
return opts;
|
128
|
+
return {...cropProps,...opts};
|
132
129
|
}
|
133
130
|
const handlePickedImage = (image,opts)=>{
|
134
131
|
opts = defaultObj(opts);
|
135
132
|
if(!isDataURL(image.dataURL)){
|
136
133
|
return notify.error(`Le fichier sélectionné est une image non valide`);
|
137
134
|
}
|
138
|
-
let diffWidth = image.width - cropWidth - maxWidthDiff,
|
139
|
-
diffHeight = image.height - cropHeight - maxHeightDiff;
|
140
|
-
let canCrop = isMobileNative()? false : ((width && diffWidth > 0) || (height && diffHeight > 0)? true : false);
|
141
135
|
const imageSrc = pickUri ? image.uri : image.dataURL;
|
142
|
-
if(
|
143
|
-
|
144
|
-
|
145
|
-
|
136
|
+
if(imageSrc){
|
137
|
+
const diffWidth = image.width - cropWidth - maxWidthDiff,diffHeight = image.height - cropHeight - maxHeightDiff;
|
138
|
+
const canCrop = isMobileNative()? false : ((diffWidth > 0) || (diffHeight > 0)? true : false);
|
139
|
+
if(canCrop){
|
140
|
+
const cProps = getCropProps(opts);
|
141
|
+
return context.cropImage({...cProps,source:image,uri:image.dataURL,src:imageSrc}).then((props)=>{
|
142
|
+
setSrc(imageSrc)
|
143
|
+
});
|
144
|
+
}
|
146
145
|
}
|
147
146
|
pickedImageRef.current = image;
|
148
147
|
setSrc(imageSrc);
|
@@ -170,6 +169,11 @@ export default function ImageComponent(props){
|
|
170
169
|
},[src]),
|
171
170
|
cropImage : (props)=>{
|
172
171
|
return Promise.resolve(props);
|
172
|
+
if(!isMobileNative()){
|
173
|
+
return new Promise((resolve,reject)=>{
|
174
|
+
setCropWindowProp(props);
|
175
|
+
});
|
176
|
+
}
|
173
177
|
return new Promise((resolve,reject)=>{
|
174
178
|
console.log({...editorProps,visible:true,...props},"is editor props");
|
175
179
|
setEditorProps({...editorProps,visible:true,...props})
|
@@ -263,6 +267,11 @@ export default function ImageComponent(props){
|
|
263
267
|
const _label = withLabel !== false ? defaultString(label) : "";
|
264
268
|
const isDisabled = menuItems.length > 0 ? true : false;
|
265
269
|
return <View testID={testID+"_FagmentContainer"}>
|
270
|
+
{false && src && !isMobileNative() && isObj(cropWindowProp) && Object.size(cropWindowProp,true) ? <Cropper
|
271
|
+
src={src}
|
272
|
+
{...cropWindowProp}
|
273
|
+
key = {uniqid("crop-image")}
|
274
|
+
/> : null}
|
266
275
|
{!createSignatureOnly ? (<Menu
|
267
276
|
{...menuProps}
|
268
277
|
disabled = {isDisabled}
|