Update web image editor (#588)
* Update web image editor * Delete type-assertions.ts * Re-add getKeys * Uncomment rotation code * Revert "Uncomment rotation code" This reverts commit 6269f3b928c2e5cacaf5d0ff5323fe975ee48eab. * Shuffle dependencies and update mobile resolution * Update ImageEditor modal layout for mobile * Avoid accidental closes of the EditImage modal --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
8f6b5d3df9
commit
b0ebb6c9d1
10 changed files with 642 additions and 16 deletions
|
@ -5,6 +5,7 @@ import {Image as RNImage} from 'react-native-image-crop-picker'
|
|||
import {openPicker} from 'lib/media/picker'
|
||||
import {getImageDim} from 'lib/media/manip'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
import {isNative} from 'platform/detection'
|
||||
|
||||
export class GalleryModel {
|
||||
images: ImageModel[] = []
|
||||
|
@ -37,7 +38,12 @@ export class GalleryModel {
|
|||
// Temporarily enforce uniqueness but can eventually also use index
|
||||
if (!this.images.some(i => i.path === image_.path)) {
|
||||
const image = new ImageModel(this.rootStore, image_)
|
||||
await image.compress()
|
||||
|
||||
if (!isNative) {
|
||||
await image.manipulate({})
|
||||
} else {
|
||||
await image.compress()
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.images.push(image)
|
||||
|
@ -45,6 +51,20 @@ export class GalleryModel {
|
|||
}
|
||||
}
|
||||
|
||||
async edit(image: ImageModel) {
|
||||
if (!isNative) {
|
||||
this.rootStore.shell.openModal({
|
||||
name: 'edit-image',
|
||||
image,
|
||||
gallery: this,
|
||||
})
|
||||
|
||||
return
|
||||
} else {
|
||||
this.crop(image)
|
||||
}
|
||||
}
|
||||
|
||||
async paste(uri: string) {
|
||||
if (this.size >= 4) {
|
||||
return
|
||||
|
@ -65,8 +85,8 @@ export class GalleryModel {
|
|||
})
|
||||
}
|
||||
|
||||
setAltText(image: ImageModel) {
|
||||
image.setAltText()
|
||||
setAltText(image: ImageModel, altText: string) {
|
||||
image.setAltText(altText)
|
||||
}
|
||||
|
||||
crop(image: ImageModel) {
|
||||
|
@ -78,6 +98,10 @@ export class GalleryModel {
|
|||
this.images.splice(index, 1)
|
||||
}
|
||||
|
||||
async previous(image: ImageModel) {
|
||||
image.previous()
|
||||
}
|
||||
|
||||
async pick() {
|
||||
const images = await openPicker(this.rootStore, {
|
||||
multiple: true,
|
||||
|
|
|
@ -1,13 +1,26 @@
|
|||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
import {compressAndResizeImageForPost} from 'lib/media/manip'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {openCropper} from 'lib/media/picker'
|
||||
import {POST_IMG_MAX} from 'lib/constants'
|
||||
import {scaleDownDimensions} from 'lib/media/util'
|
||||
import * as ImageManipulator from 'expo-image-manipulator'
|
||||
import {getDataUriSize, scaleDownDimensions} from 'lib/media/util'
|
||||
import {openCropper} from 'lib/media/picker'
|
||||
import {ActionCrop, FlipType, SaveFormat} from 'expo-image-manipulator'
|
||||
import {Position} from 'react-avatar-editor'
|
||||
import {compressAndResizeImageForPost} from 'lib/media/manip'
|
||||
|
||||
// TODO: EXIF embed
|
||||
// Cases to consider: ExternalEmbed
|
||||
|
||||
export interface ImageManipulationAttributes {
|
||||
rotate?: number
|
||||
scale?: number
|
||||
position?: Position
|
||||
flipHorizontal?: boolean
|
||||
flipVertical?: boolean
|
||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
||||
}
|
||||
|
||||
export class ImageModel implements RNImage {
|
||||
path: string
|
||||
mime = 'image/jpeg'
|
||||
|
@ -20,6 +33,17 @@ export class ImageModel implements RNImage {
|
|||
scaledWidth: number = POST_IMG_MAX.width
|
||||
scaledHeight: number = POST_IMG_MAX.height
|
||||
|
||||
// Web manipulation
|
||||
aspectRatio?: ImageManipulationAttributes['aspectRatio']
|
||||
position?: Position = undefined
|
||||
prev?: RNImage = undefined
|
||||
rotation?: number = 0
|
||||
scale?: number = 1
|
||||
flipHorizontal?: boolean = false
|
||||
flipVertical?: boolean = false
|
||||
|
||||
prevAttributes: ImageManipulationAttributes = {}
|
||||
|
||||
constructor(public rootStore: RootStoreModel, image: RNImage) {
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
|
@ -32,12 +56,55 @@ export class ImageModel implements RNImage {
|
|||
this.calcScaledDimensions()
|
||||
}
|
||||
|
||||
// TODO: Revisit compression factor due to updated sizing with zoom
|
||||
// get compressionFactor() {
|
||||
// const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
||||
|
||||
// return this.size < MAX_IMAGE_SIZE_IN_BYTES
|
||||
// ? 1
|
||||
// : MAX_IMAGE_SIZE_IN_BYTES / this.size
|
||||
// }
|
||||
|
||||
get ratioMultipliers() {
|
||||
return {
|
||||
'4:3': 4 / 3,
|
||||
'1:1': 1,
|
||||
'3:4': 3 / 4,
|
||||
None: this.width / this.height,
|
||||
}
|
||||
}
|
||||
|
||||
getDisplayDimensions(
|
||||
as: ImageManipulationAttributes['aspectRatio'] = '1:1',
|
||||
maxSide: number,
|
||||
) {
|
||||
const ratioMultiplier = this.ratioMultipliers[as]
|
||||
|
||||
if (ratioMultiplier === 1) {
|
||||
return {
|
||||
height: maxSide,
|
||||
width: maxSide,
|
||||
}
|
||||
}
|
||||
|
||||
if (ratioMultiplier < 1) {
|
||||
return {
|
||||
width: maxSide * ratioMultiplier,
|
||||
height: maxSide,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width: maxSide,
|
||||
height: maxSide / ratioMultiplier,
|
||||
}
|
||||
}
|
||||
|
||||
calcScaledDimensions() {
|
||||
const {width, height} = scaleDownDimensions(
|
||||
{width: this.width, height: this.height},
|
||||
POST_IMG_MAX,
|
||||
)
|
||||
|
||||
this.scaledWidth = width
|
||||
this.scaledHeight = height
|
||||
}
|
||||
|
@ -46,6 +113,7 @@ export class ImageModel implements RNImage {
|
|||
this.altText = altText
|
||||
}
|
||||
|
||||
// Only for mobile
|
||||
async crop() {
|
||||
try {
|
||||
const cropped = await openCropper(this.rootStore, {
|
||||
|
@ -55,15 +123,13 @@ export class ImageModel implements RNImage {
|
|||
width: this.scaledWidth,
|
||||
height: this.scaledHeight,
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
this.cropped = cropped
|
||||
this.compress()
|
||||
})
|
||||
} catch (err) {
|
||||
this.rootStore.log.error('Failed to crop photo', err)
|
||||
}
|
||||
|
||||
this.compress()
|
||||
}
|
||||
|
||||
async compress() {
|
||||
|
@ -74,6 +140,8 @@ export class ImageModel implements RNImage {
|
|||
: {width: this.width, height: this.height},
|
||||
POST_IMG_MAX,
|
||||
)
|
||||
|
||||
// TODO: Revisit this - currently iOS uses this as well
|
||||
const compressed = await compressAndResizeImageForPost({
|
||||
...(this.cropped === undefined ? this : this.cropped),
|
||||
width,
|
||||
|
@ -87,4 +155,99 @@ export class ImageModel implements RNImage {
|
|||
this.rootStore.log.error('Failed to compress photo', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Web manipulation
|
||||
async manipulate(
|
||||
attributes: {
|
||||
crop?: ActionCrop['crop']
|
||||
} & ImageManipulationAttributes,
|
||||
) {
|
||||
const {aspectRatio, crop, flipHorizontal, flipVertical, rotate, scale} =
|
||||
attributes
|
||||
const modifiers = []
|
||||
|
||||
if (flipHorizontal !== undefined) {
|
||||
this.flipHorizontal = flipHorizontal
|
||||
}
|
||||
|
||||
if (flipVertical !== undefined) {
|
||||
this.flipVertical = flipVertical
|
||||
}
|
||||
|
||||
if (this.flipHorizontal) {
|
||||
modifiers.push({flip: FlipType.Horizontal})
|
||||
}
|
||||
|
||||
if (this.flipVertical) {
|
||||
modifiers.push({flip: FlipType.Vertical})
|
||||
}
|
||||
|
||||
// TODO: Fix rotation -- currently not functional
|
||||
if (rotate !== undefined) {
|
||||
this.rotation = rotate
|
||||
}
|
||||
|
||||
if (this.rotation !== undefined) {
|
||||
modifiers.push({rotate: this.rotation})
|
||||
}
|
||||
|
||||
if (crop !== undefined) {
|
||||
modifiers.push({
|
||||
crop: {
|
||||
originX: crop.originX * this.width,
|
||||
originY: crop.originY * this.height,
|
||||
height: crop.height * this.height,
|
||||
width: crop.width * this.width,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (scale !== undefined) {
|
||||
this.scale = scale
|
||||
}
|
||||
|
||||
if (aspectRatio !== undefined) {
|
||||
this.aspectRatio = aspectRatio
|
||||
}
|
||||
|
||||
const ratioMultiplier = this.ratioMultipliers[this.aspectRatio ?? '1:1']
|
||||
|
||||
// TODO: Ollie - should support up to 2000 but smaller images that scale
|
||||
// up need an updated compression factor calculation. Use 1000 for now.
|
||||
const MAX_SIDE = 1000
|
||||
|
||||
const result = await ImageManipulator.manipulateAsync(
|
||||
this.path,
|
||||
[
|
||||
...modifiers,
|
||||
{resize: ratioMultiplier > 1 ? {width: MAX_SIDE} : {height: MAX_SIDE}},
|
||||
],
|
||||
{
|
||||
compress: 0.7, // TODO: revisit compression calculation
|
||||
format: SaveFormat.JPEG,
|
||||
},
|
||||
)
|
||||
|
||||
runInAction(() => {
|
||||
this.compressed = {
|
||||
mime: 'image/jpeg',
|
||||
path: result.uri,
|
||||
size: getDataUriSize(result.uri),
|
||||
...result,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
previous() {
|
||||
this.compressed = this.prev
|
||||
|
||||
const {flipHorizontal, flipVertical, rotate, position, scale} =
|
||||
this.prevAttributes
|
||||
|
||||
this.scale = scale
|
||||
this.rotation = rotate
|
||||
this.flipHorizontal = flipHorizontal
|
||||
this.flipVertical = flipVertical
|
||||
this.position = position
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import {ProfileModel} from '../content/profile'
|
|||
import {isObj, hasProp} from 'lib/type-guards'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {ImageModel} from '../media/image'
|
||||
import {GalleryModel} from '../media/gallery'
|
||||
|
||||
export interface ConfirmModal {
|
||||
name: 'confirm'
|
||||
|
@ -37,6 +38,12 @@ export interface ReportAccountModal {
|
|||
did: string
|
||||
}
|
||||
|
||||
export interface EditImageModal {
|
||||
name: 'edit-image'
|
||||
image: ImageModel
|
||||
gallery: GalleryModel
|
||||
}
|
||||
|
||||
export interface CropImageModal {
|
||||
name: 'crop-image'
|
||||
uri: string
|
||||
|
@ -102,6 +109,7 @@ export type Modal =
|
|||
// Posts
|
||||
| AltTextImageModal
|
||||
| CropImageModal
|
||||
| EditImageModal
|
||||
| ServerInputModal
|
||||
| RepostModal
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue