Upload Image from url (#1193)

This commit is contained in:
WithoutPants 2021-03-11 12:56:34 +11:00 committed by GitHub
parent b3966b3c76
commit 55aee21cff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 205 additions and 26 deletions

View File

@ -26,7 +26,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.FrontImage != nil {
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
if err != nil {
return nil, err
}
@ -34,7 +34,7 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
// Process the base 64 encoded image string
if input.BackImage != nil {
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
backimageData, err = utils.ProcessImageInput(*input.BackImage)
if err != nil {
return nil, err
}
@ -126,7 +126,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
var frontimageData []byte
frontImageIncluded := translator.hasField("front_image")
if input.FrontImage != nil {
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
frontimageData, err = utils.ProcessImageInput(*input.FrontImage)
if err != nil {
return nil, err
}
@ -134,7 +134,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
backImageIncluded := translator.hasField("back_image")
var backimageData []byte
if input.BackImage != nil {
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
backimageData, err = utils.ProcessImageInput(*input.BackImage)
if err != nil {
return nil, err
}
@ -189,7 +189,7 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
// HACK - if front image is null and back image is not null, then set the front image
// to the default image since we can't have a null front image and a non-null back image
if frontimageData == nil && backimageData != nil {
_, frontimageData, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
frontimageData, _ = utils.ProcessImageInput(models.DefaultMovieImage)
}
if err := qb.UpdateImages(movie.ID, frontimageData, backimageData); err != nil {

View File

@ -18,7 +18,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
var err error
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
}
if err != nil {
@ -139,7 +139,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
var err error
imageIncluded := translator.hasField("image")
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}

View File

@ -80,7 +80,7 @@ func (r *mutationResolver) sceneUpdate(input models.SceneUpdateInput, translator
if input.CoverImage != nil && *input.CoverImage != "" {
var err error
_, coverImageData, err = utils.ProcessBase64Image(*input.CoverImage)
coverImageData, err = utils.ProcessImageInput(*input.CoverImage)
if err != nil {
return nil, err
}

View File

@ -20,7 +20,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
// Process the base 64 encoded image string
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}
@ -96,7 +96,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
imageIncluded := translator.hasField("image")
if input.Image != nil {
var err error
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
}

View File

@ -24,7 +24,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input models.TagCreate
var err error
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err
@ -82,7 +82,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
imageIncluded := translator.hasField("image")
if input.Image != nil {
_, imageData, err = utils.ProcessBase64Image(*input.Image)
imageData, err = utils.ProcessImageInput(*input.Image)
if err != nil {
return nil, err

View File

@ -4,11 +4,66 @@ import (
"crypto/md5"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strings"
"time"
)
// Timeout to get the image. Includes transfer time. May want to make this
// configurable at some point.
const imageGetTimeout = time.Second * 60
const base64RE = `^data:.+\/(.+);base64,(.*)$`
// ProcessImageInput transforms an image string either from a base64 encoded
// string, or from a URL, and returns the image as a byte slice
func ProcessImageInput(imageInput string) ([]byte, error) {
regex := regexp.MustCompile(base64RE)
if regex.MatchString(imageInput) {
_, d, err := ProcessBase64Image(imageInput)
return d, err
}
// assume input is a URL. Read it.
return ReadImageFromURL(imageInput)
}
// ReadImageFromURL returns image data from a URL
func ReadImageFromURL(url string) ([]byte, error) {
client := &http.Client{
Timeout: imageGetTimeout,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
// assume is a URL for now
// set the host of the URL as the referer
if req.URL.Scheme != "" {
req.Header.Set("Referer", req.URL.Scheme+"://"+req.Host+"/")
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// ProcessBase64Image transforms a base64 encoded string from a form post and returns the MD5 hash of the data and the
// image itself as a byte slice.
func ProcessBase64Image(imageString string) (string, []byte, error) {
@ -16,7 +71,7 @@ func ProcessBase64Image(imageString string) (string, []byte, error) {
return "", nil, fmt.Errorf("empty image string")
}
regex := regexp.MustCompile(`^data:.+\/(.+);base64,(.*)$`)
regex := regexp.MustCompile(base64RE)
matches := regex.FindStringSubmatch(imageString)
var encodedString string
if len(matches) > 2 {

View File

@ -2,6 +2,7 @@
* Added Performer tags.
### 🎨 Improvements
* Allow scene/performer/studio image upload via URL.
* Add button to hide unmatched scenes in Tagger view.
* Hide create option in dropdowns when searching in filters.
* Add scrape gallery from fragment to UI

View File

@ -571,8 +571,10 @@ export const Movie: React.FC = () => {
onToggleEdit={onToggleEdit}
onSave={onSave}
onImageChange={onFrontImageChange}
onImageChangeURL={onFrontImageLoad}
onClearImage={onClearFrontImage}
onBackImageChange={onBackImageChange}
onBackImageChangeURL={onBackImageLoad}
onClearBackImage={onClearBackImage}
onDelete={onDelete}
/>

View File

@ -418,6 +418,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
ImageUtils.onImageChange(event, onImageLoad);
}
function onImageChangeURL(url: string) {
formik.setFieldValue("image", url);
}
function onDisplayScrapeDialog(scraper: GQL.Scraper) {
setIsDisplayingScraperDialog(scraper);
}
@ -533,7 +537,12 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
);
return (
<OverlayTrigger trigger="click" placement="top" overlay={popover}>
<OverlayTrigger
trigger="click"
placement="top"
overlay={popover}
rootClose
>
<Button variant="secondary" className="mr-2">
Scrape with...
</Button>
@ -636,7 +645,11 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
""
)}
{renderScraperMenu()}
<ImageInput isEditing onImageChange={onImageChangeHandler} />
<ImageInput
isEditing
onImageChange={onImageChangeHandler}
onImageURL={onImageChangeURL}
/>
<Button
className="mx-2"
variant="danger"

View File

@ -723,7 +723,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
alt="Scene cover"
/>
)}
<ImageInput isEditing onImageChange={onCoverImageChange} />
<ImageInput
isEditing
onImageChange={onCoverImageChange}
onImageURL={onImageLoad}
/>
</Form.Group>
</div>
</div>

View File

@ -12,6 +12,8 @@ interface IProps {
onAutoTag?: () => void;
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
onBackImageChange?: (event: React.FormEvent<HTMLInputElement>) => void;
onImageChangeURL?: (url: string) => void;
onBackImageChangeURL?: (url: string) => void;
onClearImage?: () => void;
onClearBackImage?: () => void;
acceptSVG?: boolean;
@ -65,6 +67,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
isEditing={props.isEditing}
text="Back image..."
onImageChange={props.onBackImageChange}
onImageURL={props.onBackImageChangeURL}
/>
);
}
@ -116,6 +119,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
isEditing={props.isEditing}
text={props.onBackImageChange ? "Front image..." : undefined}
onImageChange={props.onImageChange}
onImageURL={props.onImageChangeURL}
acceptSVG={props.acceptSVG ?? false}
/>
{props.isEditing && props.onClearImage ? (

View File

@ -1,10 +1,20 @@
import React from "react";
import { Button, Form } from "react-bootstrap";
import React, { useState } from "react";
import {
Button,
Col,
Form,
OverlayTrigger,
Popover,
Row,
} from "react-bootstrap";
import { Modal } from ".";
import Icon from "./Icon";
interface IImageInput {
isEditing: boolean;
text?: string;
onImageChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onImageURL?: (url: string) => void;
acceptSVG?: boolean;
}
@ -12,18 +22,107 @@ export const ImageInput: React.FC<IImageInput> = ({
isEditing,
text,
onImageChange,
onImageURL,
acceptSVG = false,
}) => {
const [isShowDialog, setIsShowDialog] = useState(false);
const [url, setURL] = useState("");
if (!isEditing) return <div />;
if (!onImageURL) {
// just return the file input
return (
<Form.Label className="image-input">
<Button variant="secondary">{text ?? "Browse for image..."}</Button>
<Form.Control
type="file"
onChange={onImageChange}
accept={`.jpg,.jpeg,.png${acceptSVG ? ",.svg" : ""}`}
/>
</Form.Label>
);
}
function onConfirmURL() {
if (!onImageURL) {
return;
}
setIsShowDialog(false);
onImageURL(url);
}
function renderDialog() {
return (
<Modal
show={!!isShowDialog}
onHide={() => setIsShowDialog(false)}
header="Image URL"
accept={{ onClick: onConfirmURL, text: "Confirm" }}
>
<div className="dialog-content">
<Form.Group controlId="url" as={Row}>
<Form.Label column xs={3}>
URL
</Form.Label>
<Col xs={9}>
<Form.Control
className="text-input"
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setURL(event.currentTarget.value)
}
value={url}
placeholder="URL"
/>
</Col>
</Form.Group>
</div>
</Modal>
);
}
const popover = (
<Popover id="set-image-popover">
<Popover.Content>
<>
<div>
<Form.Label className="image-input">
<Button variant="secondary">
<Icon icon="file" className="fa-fw" />
<span>From file...</span>
</Button>
<Form.Control
type="file"
onChange={onImageChange}
accept={`.jpg,.jpeg,.png${acceptSVG ? ",.svg" : ""}`}
/>
</Form.Label>
</div>
<div>
<Button className="minimal" onClick={() => setIsShowDialog(true)}>
<Icon icon="link" className="fa-fw" />
<span>From URL...</span>
</Button>
</div>
</>
</Popover.Content>
</Popover>
);
return (
<Form.Label className="image-input">
<Button variant="secondary">{text ?? "Browse for image..."}</Button>
<Form.Control
type="file"
onChange={onImageChange}
accept={`.jpg,.jpeg,.png${acceptSVG ? ",.svg" : ""}`}
/>
</Form.Label>
<>
{renderDialog()}
<OverlayTrigger
trigger="click"
placement="top"
overlay={popover}
rootClose
>
<Button variant="secondary" className="mr-2">
{text ?? "Set image..."}
</Button>
</OverlayTrigger>
</>
);
};

View File

@ -319,6 +319,7 @@ export const Studio: React.FC = () => {
onToggleEdit={onToggleEdit}
onSave={onSave}
onImageChange={onImageChangeHandler}
onImageChangeURL={onImageLoad}
onClearImage={() => {
onClearImage();
}}