leva icon indicating copy to clipboard operation
leva copied to clipboard

Audio, video or file input

Open enzoferey opened this issue 4 years ago • 9 comments

Hi !

I was wondering if you intend to do an audio, video, or a more generic file type. Right now, images are supported as ObjectURL, but I wonder why wouldn't we want a similar component using react-dropzone as you currently do but that supports other types of files, most likely sending back a File object.

I can understand custom plugins could be covering the use case, but it doesn't seem you are providing a FileInput primitive within the { Components } from leva/plugin namespace neither, so it's pretty hard to build it while still matching Leva styles (and even less if you modified the default Leva theme).

enzoferey avatar Jun 08 '21 19:06 enzoferey

Hi Enzo, feel free to propose a PR over the existing Image input. That would work right?

dbismut avatar Jun 08 '21 22:06 dbismut

@dbismut do you want me to modify the Image input to make it file generic ? Wouldn't that be a breaking for people expecting an image there ? Plus of course the preview square only being useful for images, not other types of files.

There are only 1 thing to be changed in the Image component => the mimetype accepted and not normalizing the value to an ObjectURL, that would already make it support any kind of file.

However I guess we want a generic one and then being able to create other ones like the Image one on top of it.

enzoferey avatar Jun 09 '21 08:06 enzoferey

Hi Enzo, I think we should try to refactor the Image component so that it can support multiple formats, but I agree that we shouldn't break the image API.

So maybe we can have:

useControls(
  { myImage: { image: image.jpg } },
  { myAudio: { audio: sound.mp4 } },
  { myFile : { file: someFile.docx } }
)

And update the preview accordingly?

dbismut avatar Jun 09 '21 10:06 dbismut

I will give it a shot in the upcoming days as it becomes a priority for me.

enzoferey avatar Jun 09 '21 11:06 enzoferey

To speed things up, I hacked together a patch using https://github.com/ds300/patch-package that removes the preview and accepts all type of files returning a File value instead.

I will eventually work on a proper solution to contribute to the repo, but this is an escape patch in case you just need whatever generic file as I do.

The patch
diff --git a/node_modules/leva/dist/leva.cjs.dev.js b/node_modules/leva/dist/leva.cjs.dev.js
index e80df53..40a516c 100644
--- a/node_modules/leva/dist/leva.cjs.dev.js
+++ b/node_modules/leva/dist/leva.cjs.dev.js
@@ -691,16 +691,16 @@ const sanitize$1 = v => {
 
   if (v instanceof File) {
     try {
-      return URL.createObjectURL(v);
+      return v;
     } catch (e) {
       return undefined;
     }
   }
 
-  if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
-  throw Error(`Invalid image format [undefined | blob | File].`);
+  if (typeof v === 'string') return v;
+  throw Error(`Invalid image format [undefined | string | File].`);
 };
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
 const normalize$1 = ({
   image
 }) => {
@@ -719,7 +719,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
 const ImageContainer = vectorPlugin.styled('div', {
   position: 'relative',
   display: 'grid',
-  gridTemplateColumns: '$sizes$rowHeight auto 20px',
+  gridTemplateColumns: 'auto auto',
   columnGap: '$colGap',
   alignItems: 'center'
 });
@@ -838,30 +838,11 @@ function ImageComponent() {
     isDragAccept
   } = reactDropzone.useDropzone({
     maxFiles: 1,
-    accept: 'image/*',
     onDrop
   });
   return React__default['default'].createElement(vectorPlugin.Row, {
     input: true
-  }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(ImagePreview, {
-    ref: popinRef,
-    hasImage: !!value,
-    onPointerDown: () => !!value && show(),
-    onPointerUp: hide,
-    style: {
-      backgroundImage: value ? `url(${value})` : 'none'
-    }
-  }), shown && !!value && React__default['default'].createElement(vectorPlugin.Portal, null, React__default['default'].createElement(vectorPlugin.Overlay, {
-    onPointerUp: hide,
-    style: {
-      cursor: 'pointer'
-    }
-  }), React__default['default'].createElement(ImageLargePreview, {
-    ref: wrapperRef,
-    style: {
-      backgroundImage: `url(${value})`
-    }
-  })), React__default['default'].createElement(DropZone, getRootProps({
+  }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(DropZone, getRootProps({
     isDragAccept
   }), React__default['default'].createElement("input", getInputProps()), React__default['default'].createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React__default['default'].createElement(Remove, {
     onClick: clear,
diff --git a/node_modules/leva/dist/leva.cjs.prod.js b/node_modules/leva/dist/leva.cjs.prod.js
index c9c06ba..eb3033b 100644
--- a/node_modules/leva/dist/leva.cjs.prod.js
+++ b/node_modules/leva/dist/leva.cjs.prod.js
@@ -691,16 +691,16 @@ const sanitize$1 = v => {
 
   if (v instanceof File) {
     try {
-      return URL.createObjectURL(v);
+      return v;
     } catch (e) {
       return undefined;
     }
   }
 
-  if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
-  throw Error(`Invalid image format [undefined | blob | File].`);
+  if (typeof v === 'string') return v;
+  throw Error(`Invalid image format [undefined | string | File].`);
 };
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
 const normalize$1 = ({
   image
 }) => {
@@ -719,7 +719,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
 const ImageContainer = vectorPlugin.styled('div', {
   position: 'relative',
   display: 'grid',
-  gridTemplateColumns: '$sizes$rowHeight auto 20px',
+  gridTemplateColumns: 'auto auto',
   columnGap: '$colGap',
   alignItems: 'center'
 });
@@ -838,30 +838,11 @@ function ImageComponent() {
     isDragAccept
   } = reactDropzone.useDropzone({
     maxFiles: 1,
-    accept: 'image/*',
     onDrop
   });
   return React__default['default'].createElement(vectorPlugin.Row, {
     input: true
-  }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(ImagePreview, {
-    ref: popinRef,
-    hasImage: !!value,
-    onPointerDown: () => !!value && show(),
-    onPointerUp: hide,
-    style: {
-      backgroundImage: value ? `url(${value})` : 'none'
-    }
-  }), shown && !!value && React__default['default'].createElement(vectorPlugin.Portal, null, React__default['default'].createElement(vectorPlugin.Overlay, {
-    onPointerUp: hide,
-    style: {
-      cursor: 'pointer'
-    }
-  }), React__default['default'].createElement(ImageLargePreview, {
-    ref: wrapperRef,
-    style: {
-      backgroundImage: `url(${value})`
-    }
-  })), React__default['default'].createElement(DropZone, getRootProps({
+  }, React__default['default'].createElement(vectorPlugin.Label, null, label), React__default['default'].createElement(ImageContainer, null, React__default['default'].createElement(DropZone, getRootProps({
     isDragAccept
   }), React__default['default'].createElement("input", getInputProps()), React__default['default'].createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React__default['default'].createElement(Remove, {
     onClick: clear,
diff --git a/node_modules/leva/dist/leva.esm.js b/node_modules/leva/dist/leva.esm.js
index 0cdf36d..1d2f2bf 100644
--- a/node_modules/leva/dist/leva.esm.js
+++ b/node_modules/leva/dist/leva.esm.js
@@ -678,16 +678,16 @@ const sanitize$1 = v => {
 
   if (v instanceof File) {
     try {
-      return URL.createObjectURL(v);
+      return v;
     } catch (e) {
       return undefined;
     }
   }
 
-  if (typeof v === 'string' && v.indexOf('blob:') === 0) return v;
-  throw Error(`Invalid image format [undefined | blob | File].`);
+  if (typeof v === 'string') return v;
+  throw Error(`Invalid image format [undefined | string | File].`);
 };
-const schema$1 = (_o, s) => typeof s === 'object' && 'image' in s;
+const schema$1 = (_o, s) => s instanceof File || typeof s === "string";
 const normalize$1 = ({
   image
 }) => {
@@ -706,7 +706,7 @@ var props$1 = /*#__PURE__*/Object.freeze({
 const ImageContainer = styled('div', {
   position: 'relative',
   display: 'grid',
-  gridTemplateColumns: '$sizes$rowHeight auto 20px',
+  gridTemplateColumns: 'auto auto',
   columnGap: '$colGap',
   alignItems: 'center'
 });
@@ -825,30 +825,11 @@ function ImageComponent() {
     isDragAccept
   } = useDropzone({
     maxFiles: 1,
-    accept: 'image/*',
     onDrop
   });
   return React.createElement(Row, {
     input: true
-  }, React.createElement(Label, null, label), React.createElement(ImageContainer, null, React.createElement(ImagePreview, {
-    ref: popinRef,
-    hasImage: !!value,
-    onPointerDown: () => !!value && show(),
-    onPointerUp: hide,
-    style: {
-      backgroundImage: value ? `url(${value})` : 'none'
-    }
-  }), shown && !!value && React.createElement(Portal, null, React.createElement(Overlay, {
-    onPointerUp: hide,
-    style: {
-      cursor: 'pointer'
-    }
-  }), React.createElement(ImageLargePreview, {
-    ref: wrapperRef,
-    style: {
-      backgroundImage: `url(${value})`
-    }
-  })), React.createElement(DropZone, getRootProps({
+  }, React.createElement(Label, null, label), React.createElement(ImageContainer, null, React.createElement(DropZone, getRootProps({
     isDragAccept
   }), React.createElement("input", getInputProps()), React.createElement(Instructions, null, isDragAccept ? 'drop image' : 'click or drop')), React.createElement(Remove, {
     onClick: clear,
diff --git a/node_modules/leva/src/components/Image/Image.tsx b/node_modules/leva/src/components/Image/Image.tsx
index 75d8840..98bce81 100644
--- a/node_modules/leva/src/components/Image/Image.tsx
+++ b/node_modules/leva/src/components/Image/Image.tsx
@@ -8,7 +8,6 @@ import type { ImageProps } from './image-types'
 
 export function ImageComponent() {
   const { label, value, onUpdate } = useInputContext<ImageProps>()
-  const { popinRef, wrapperRef, shown, show, hide } = usePopin()
 
   const onDrop = useCallback(
     (acceptedFiles) => {
@@ -25,26 +24,13 @@ export function ImageComponent() {
     [onUpdate]
   )
 
-  const { getRootProps, getInputProps, isDragAccept } = useDropzone({ maxFiles: 1, accept: 'image/*', onDrop })
+  const { getRootProps, getInputProps, isDragAccept } = useDropzone({ maxFiles: 1, onDrop })
 
   // TODO fix any in DropZone
   return (
     <Row input>
       <Label>{label}</Label>
       <ImageContainer>
-        <ImagePreview
-          ref={popinRef}
-          hasImage={!!value}
-          onPointerDown={() => !!value && show()}
-          onPointerUp={hide}
-          style={{ backgroundImage: value ? `url(${value})` : 'none' }}
-        />
-        {shown && !!value && (
-          <Portal>
-            <Overlay onPointerUp={hide} style={{ cursor: 'pointer' }} />
-            <ImageLargePreview ref={wrapperRef} style={{ backgroundImage: `url(${value})` }} />
-          </Portal>
-        )}
         <DropZone {...(getRootProps({ isDragAccept }) as any)}>
           <input {...getInputProps()} />
           <Instructions>{isDragAccept ? 'drop image' : 'click or drop'}</Instructions>
diff --git a/node_modules/leva/src/components/Image/image-plugin.ts b/node_modules/leva/src/components/Image/image-plugin.ts
index 887fecd..75d4c18 100644
--- a/node_modules/leva/src/components/Image/image-plugin.ts
+++ b/node_modules/leva/src/components/Image/image-plugin.ts
@@ -1,19 +1,21 @@
 import type { ImageInput } from '../../types'
 
-export const sanitize = (v: any): string | undefined => {
+export const sanitize = (v: any): File | string | undefined => {
   if (v === undefined) return undefined
   if (v instanceof File) {
     try {
-      return URL.createObjectURL(v)
+      return v;
     } catch (e) {
       return undefined
     }
   }
-  if (typeof v === 'string' && v.indexOf('blob:') === 0) return v
-  throw Error(`Invalid image format [undefined | blob | File].`)
+  if(typeof v === "string") {
+    return v;
+  }
+  throw Error(`Invalid image format [undefined | string | File].`)
 }
 
-export const schema = (_o: any, s: any) => typeof s === 'object' && 'image' in s
+export const schema = (_o: any, s: any) => s instanceof File || typeof s === "string";
 
 export const normalize = ({ image }: ImageInput) => {
   return { value: image }
diff --git a/node_modules/leva/src/components/Image/image-types.ts b/node_modules/leva/src/components/Image/image-types.ts
index c9f3b96..a3ef8f2 100644
--- a/node_modules/leva/src/components/Image/image-types.ts
+++ b/node_modules/leva/src/components/Image/image-types.ts
@@ -1,3 +1,3 @@
 import type { LevaInputProps } from '../../types'
 
-export type ImageProps = LevaInputProps<string | undefined>
+export type ImageProps = LevaInputProps<File | string | undefined>
diff --git a/node_modules/leva/src/types/public.ts b/node_modules/leva/src/types/public.ts
index 06a55e1..708df91 100644
--- a/node_modules/leva/src/types/public.ts
+++ b/node_modules/leva/src/types/public.ts
@@ -93,7 +93,7 @@ export type Vector3dInput = MergedInputWithSettings<Vector3d, Vector3dSettings>
 
 export type IntervalInput = { value: [number, number]; min: number; max: number }
 
-export type ImageInput = { image: undefined | string }
+export type ImageInput = { image: undefined | string | File }
 
 type SelectInput = { options: any[] | Record<string, any>; value?: any }

enzoferey avatar Jun 10 '21 20:06 enzoferey

Any news on this?

TiagoCavalcante avatar Nov 25 '22 23:11 TiagoCavalcante

Hey @TiagoCavalcante , in case you were asking about updates on my side, I'm using leva on a project that is no longer a priority for me. Feel free to use my patch above or contribute yourself 👍🏻

enzoferey avatar Nov 26 '22 11:11 enzoferey

Hello. I'm recently implemented File input plugin, so I can share it.

mishazawa avatar Apr 14 '23 19:04 mishazawa

https://github.com/mishazawa/leva-file-picker

mishazawa avatar Apr 25 '23 14:04 mishazawa