refactor: remove gallery mobx model and old modals
This commit is contained in:
@@ -1,9 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
|
||||||
|
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {ComposerImage} from '../gallery'
|
|
||||||
import {ThreadgateSetting} from '../queries/threadgate'
|
import {ThreadgateSetting} from '../queries/threadgate'
|
||||||
|
|
||||||
export interface EditProfileModal {
|
export interface EditProfileModal {
|
||||||
@@ -37,19 +35,6 @@ export interface ListAddRemoveUsersModal {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditImageModal {
|
|
||||||
name: 'edit-image'
|
|
||||||
image: ComposerImage
|
|
||||||
onChange: (next: ComposerImage) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CropImageModal {
|
|
||||||
name: 'crop-image'
|
|
||||||
uri: string
|
|
||||||
dimensions?: {width: number; height: number}
|
|
||||||
onSelect: (img?: RNImage) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DeleteAccountModal {
|
export interface DeleteAccountModal {
|
||||||
name: 'delete-account'
|
name: 'delete-account'
|
||||||
}
|
}
|
||||||
@@ -145,8 +130,6 @@ export type Modal =
|
|||||||
| ListAddRemoveUsersModal
|
| ListAddRemoveUsersModal
|
||||||
|
|
||||||
// Posts
|
// Posts
|
||||||
| CropImageModal
|
|
||||||
| EditImageModal
|
|
||||||
| RepostModal
|
| RepostModal
|
||||||
| SelfLabelModal
|
| SelfLabelModal
|
||||||
| ThreadgateModal
|
| ThreadgateModal
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
|
||||||
import {ImageModel} from './image'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {openPicker} from 'lib/media/picker'
|
|
||||||
import {getImageDim} from 'lib/media/manip'
|
|
||||||
|
|
||||||
interface InitialImageUri {
|
|
||||||
uri: string
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GalleryModel {
|
|
||||||
images: ImageModel[] = []
|
|
||||||
|
|
||||||
constructor(uris?: {uri: string; width: number; height: number}[]) {
|
|
||||||
makeAutoObservable(this)
|
|
||||||
|
|
||||||
if (uris) {
|
|
||||||
this.addFromUris(uris)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get isEmpty() {
|
|
||||||
return this.size === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
get size() {
|
|
||||||
return this.images.length
|
|
||||||
}
|
|
||||||
|
|
||||||
get needsAltText() {
|
|
||||||
return this.images.some(image => image.altText.trim() === '')
|
|
||||||
}
|
|
||||||
|
|
||||||
*add(image_: Omit<RNImage, 'size'>) {
|
|
||||||
if (this.size >= 4) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporarily enforce uniqueness but can eventually also use index
|
|
||||||
if (!this.images.some(i => i.path === image_.path)) {
|
|
||||||
const image = new ImageModel(image_)
|
|
||||||
|
|
||||||
// Initial resize
|
|
||||||
image.manipulate({})
|
|
||||||
this.images.push(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async paste(uri: string) {
|
|
||||||
if (this.size >= 4) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {width, height} = await getImageDim(uri)
|
|
||||||
|
|
||||||
const image = {
|
|
||||||
path: uri,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
mime: 'image/jpeg',
|
|
||||||
}
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.add(image)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
setAltText(image: ImageModel, altText: string) {
|
|
||||||
image.setAltText(altText)
|
|
||||||
}
|
|
||||||
|
|
||||||
crop(image: ImageModel) {
|
|
||||||
image.crop()
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(image: ImageModel) {
|
|
||||||
const index = this.images.findIndex(image_ => image_.path === image.path)
|
|
||||||
this.images.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
async previous(image: ImageModel) {
|
|
||||||
image.previous()
|
|
||||||
}
|
|
||||||
|
|
||||||
async pick() {
|
|
||||||
const images = await openPicker({
|
|
||||||
selectionLimit: 4 - this.size,
|
|
||||||
allowsMultipleSelection: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
return await Promise.all(
|
|
||||||
images.map(image => {
|
|
||||||
this.add(image)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async addFromUris(uris: InitialImageUri[]) {
|
|
||||||
for (const uriObj of uris) {
|
|
||||||
this.add({
|
|
||||||
mime: 'image/jpeg',
|
|
||||||
height: uriObj.height,
|
|
||||||
width: uriObj.width,
|
|
||||||
path: uriObj.uri,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {makeAutoObservable} from 'mobx'
|
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import {ActionCrop} from 'expo-image-manipulator'
|
|
||||||
import {Position} from 'react-avatar-editor'
|
|
||||||
import {Dimensions} from 'lib/media/types'
|
|
||||||
|
|
||||||
export interface ImageManipulationAttributes {
|
|
||||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
|
||||||
rotate?: number
|
|
||||||
scale?: number
|
|
||||||
position?: Position
|
|
||||||
flipHorizontal?: boolean
|
|
||||||
flipVertical?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ImageModel implements Omit<RNImage, 'size'> {
|
|
||||||
path: string
|
|
||||||
mime = 'image/jpeg'
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
altText = ''
|
|
||||||
cropped?: RNImage = undefined
|
|
||||||
compressed?: RNImage = undefined
|
|
||||||
|
|
||||||
// Web manipulation
|
|
||||||
prev?: RNImage
|
|
||||||
attributes: ImageManipulationAttributes = {
|
|
||||||
aspectRatio: 'None',
|
|
||||||
scale: 1,
|
|
||||||
flipHorizontal: false,
|
|
||||||
flipVertical: false,
|
|
||||||
rotate: 0,
|
|
||||||
}
|
|
||||||
prevAttributes: ImageManipulationAttributes = {}
|
|
||||||
|
|
||||||
constructor(image: Omit<RNImage, 'size'>) {
|
|
||||||
makeAutoObservable(this)
|
|
||||||
|
|
||||||
this.path = image.path
|
|
||||||
this.width = image.width
|
|
||||||
this.height = image.height
|
|
||||||
}
|
|
||||||
|
|
||||||
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
|
||||||
this.attributes.aspectRatio = aspectRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
setRotate(degrees: number) {
|
|
||||||
this.attributes.rotate = degrees
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
flipVertical() {
|
|
||||||
this.attributes.flipVertical = !this.attributes.flipVertical
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
flipHorizontal() {
|
|
||||||
this.attributes.flipHorizontal = !this.attributes.flipHorizontal
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
get ratioMultipliers() {
|
|
||||||
return {
|
|
||||||
'4:3': 4 / 3,
|
|
||||||
'1:1': 1,
|
|
||||||
'3:4': 3 / 4,
|
|
||||||
None: this.width / this.height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUploadDimensions(
|
|
||||||
dimensions: Dimensions,
|
|
||||||
maxDimensions: Dimensions = POST_IMG_MAX,
|
|
||||||
as: ImageManipulationAttributes['aspectRatio'] = 'None',
|
|
||||||
) {
|
|
||||||
const {width, height} = dimensions
|
|
||||||
const {width: maxWidth, height: maxHeight} = maxDimensions
|
|
||||||
|
|
||||||
return width < maxWidth && height < maxHeight
|
|
||||||
? {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}
|
|
||||||
: this.getResizedDimensions(as, POST_IMG_MAX.width)
|
|
||||||
}
|
|
||||||
|
|
||||||
getResizedDimensions(
|
|
||||||
as: ImageManipulationAttributes['aspectRatio'] = 'None',
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setAltText(altText: string) {
|
|
||||||
this.altText = altText.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only compress prior to upload
|
|
||||||
async compress() {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile
|
|
||||||
async crop() {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web manipulation
|
|
||||||
async manipulate(
|
|
||||||
_attributes: {
|
|
||||||
crop?: ActionCrop['crop']
|
|
||||||
} & ImageManipulationAttributes,
|
|
||||||
) {
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
resetCropped() {
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
previous() {
|
|
||||||
this.cropped = this.prev
|
|
||||||
this.attributes = this.prevAttributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {makeAutoObservable, runInAction} from 'mobx'
|
|
||||||
import {POST_IMG_MAX} from 'lib/constants'
|
|
||||||
import * as ImageManipulator from 'expo-image-manipulator'
|
|
||||||
import {getDataUriSize} 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 {Dimensions} from 'lib/media/types'
|
|
||||||
import {isIOS} from 'platform/detection'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
|
|
||||||
export interface ImageManipulationAttributes {
|
|
||||||
aspectRatio?: '4:3' | '1:1' | '3:4' | 'None'
|
|
||||||
rotate?: number
|
|
||||||
scale?: number
|
|
||||||
position?: Position
|
|
||||||
flipHorizontal?: boolean
|
|
||||||
flipVertical?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_IMAGE_SIZE_IN_BYTES = 976560
|
|
||||||
|
|
||||||
export class ImageModel implements Omit<RNImage, 'size'> {
|
|
||||||
path: string
|
|
||||||
mime = 'image/jpeg'
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
altText = ''
|
|
||||||
cropped?: RNImage = undefined
|
|
||||||
compressed?: RNImage = undefined
|
|
||||||
|
|
||||||
// Web manipulation
|
|
||||||
prev?: RNImage
|
|
||||||
attributes: ImageManipulationAttributes = {
|
|
||||||
aspectRatio: 'None',
|
|
||||||
scale: 1,
|
|
||||||
flipHorizontal: false,
|
|
||||||
flipVertical: false,
|
|
||||||
rotate: 0,
|
|
||||||
}
|
|
||||||
prevAttributes: ImageManipulationAttributes = {}
|
|
||||||
|
|
||||||
constructor(image: Omit<RNImage, 'size'>) {
|
|
||||||
makeAutoObservable(this)
|
|
||||||
|
|
||||||
this.path = image.path
|
|
||||||
this.width = image.width
|
|
||||||
this.height = image.height
|
|
||||||
}
|
|
||||||
|
|
||||||
setRatio(aspectRatio: ImageManipulationAttributes['aspectRatio']) {
|
|
||||||
this.attributes.aspectRatio = aspectRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
setRotate(degrees: number) {
|
|
||||||
this.attributes.rotate = degrees
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
flipVertical() {
|
|
||||||
this.attributes.flipVertical = !this.attributes.flipVertical
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
flipHorizontal() {
|
|
||||||
this.attributes.flipHorizontal = !this.attributes.flipHorizontal
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
get ratioMultipliers() {
|
|
||||||
return {
|
|
||||||
'4:3': 4 / 3,
|
|
||||||
'1:1': 1,
|
|
||||||
'3:4': 3 / 4,
|
|
||||||
None: this.width / this.height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getUploadDimensions(
|
|
||||||
dimensions: Dimensions,
|
|
||||||
maxDimensions: Dimensions = POST_IMG_MAX,
|
|
||||||
as: ImageManipulationAttributes['aspectRatio'] = 'None',
|
|
||||||
) {
|
|
||||||
const {width, height} = dimensions
|
|
||||||
const {width: maxWidth, height: maxHeight} = maxDimensions
|
|
||||||
|
|
||||||
return width < maxWidth && height < maxHeight
|
|
||||||
? {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}
|
|
||||||
: this.getResizedDimensions(as, POST_IMG_MAX.width)
|
|
||||||
}
|
|
||||||
|
|
||||||
getResizedDimensions(
|
|
||||||
as: ImageManipulationAttributes['aspectRatio'] = 'None',
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setAltText(altText: string) {
|
|
||||||
this.altText = altText.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only compress prior to upload
|
|
||||||
async compress() {
|
|
||||||
for (let i = 10; i > 0; i--) {
|
|
||||||
// Float precision
|
|
||||||
const factor = Math.round(i) / 10
|
|
||||||
const compressed = await ImageManipulator.manipulateAsync(
|
|
||||||
this.cropped?.path ?? this.path,
|
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
compress: factor,
|
|
||||||
base64: true,
|
|
||||||
format: SaveFormat.JPEG,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if (compressed.base64 !== undefined) {
|
|
||||||
const size = getDataUriSize(compressed.base64)
|
|
||||||
|
|
||||||
if (size < MAX_IMAGE_SIZE_IN_BYTES) {
|
|
||||||
runInAction(() => {
|
|
||||||
this.compressed = {
|
|
||||||
mime: 'image/jpeg',
|
|
||||||
path: compressed.uri,
|
|
||||||
size,
|
|
||||||
...compressed,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compression fails when removing redundant information is not possible.
|
|
||||||
// This can be tested with images that have high variance in noise.
|
|
||||||
throw new Error('Failed to compress image')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mobile
|
|
||||||
async crop() {
|
|
||||||
try {
|
|
||||||
// NOTE
|
|
||||||
// on ios, react-native-image-crop-picker gives really bad quality
|
|
||||||
// without specifying width and height. on android, however, the
|
|
||||||
// crop stretches incorrectly if you do specify it. these are
|
|
||||||
// both separate bugs in the library. we deal with that by
|
|
||||||
// providing width & height for ios only
|
|
||||||
// -prf
|
|
||||||
const {width, height} = this.getUploadDimensions({
|
|
||||||
width: this.width,
|
|
||||||
height: this.height,
|
|
||||||
})
|
|
||||||
|
|
||||||
const cropped = await openCropper({
|
|
||||||
mediaType: 'photo',
|
|
||||||
path: this.path,
|
|
||||||
freeStyleCropEnabled: true,
|
|
||||||
...(isIOS ? {width, height} : {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.cropped = cropped
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Failed to crop photo', {message: err})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web manipulation
|
|
||||||
async manipulate(
|
|
||||||
attributes: {
|
|
||||||
crop?: ActionCrop['crop']
|
|
||||||
} & ImageManipulationAttributes,
|
|
||||||
) {
|
|
||||||
let uploadWidth: number | undefined
|
|
||||||
let uploadHeight: number | undefined
|
|
||||||
|
|
||||||
const {aspectRatio, crop, position, scale} = attributes
|
|
||||||
const modifiers = []
|
|
||||||
|
|
||||||
if (this.attributes.flipHorizontal) {
|
|
||||||
modifiers.push({flip: FlipType.Horizontal})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attributes.flipVertical) {
|
|
||||||
modifiers.push({flip: FlipType.Vertical})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attributes.rotate !== undefined) {
|
|
||||||
modifiers.push({rotate: this.attributes.rotate})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (crop !== undefined) {
|
|
||||||
const croppedHeight = crop.height * this.height
|
|
||||||
const croppedWidth = crop.width * this.width
|
|
||||||
modifiers.push({
|
|
||||||
crop: {
|
|
||||||
originX: crop.originX * this.width,
|
|
||||||
originY: crop.originY * this.height,
|
|
||||||
height: croppedHeight,
|
|
||||||
width: croppedWidth,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const uploadDimensions = this.getUploadDimensions(
|
|
||||||
{width: croppedWidth, height: croppedHeight},
|
|
||||||
POST_IMG_MAX,
|
|
||||||
aspectRatio,
|
|
||||||
)
|
|
||||||
|
|
||||||
uploadWidth = uploadDimensions.width
|
|
||||||
uploadHeight = uploadDimensions.height
|
|
||||||
} else {
|
|
||||||
const uploadDimensions = this.getUploadDimensions(
|
|
||||||
{width: this.width, height: this.height},
|
|
||||||
POST_IMG_MAX,
|
|
||||||
aspectRatio,
|
|
||||||
)
|
|
||||||
|
|
||||||
uploadWidth = uploadDimensions.width
|
|
||||||
uploadHeight = uploadDimensions.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scale !== undefined) {
|
|
||||||
this.attributes.scale = scale
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position !== undefined) {
|
|
||||||
this.attributes.position = position
|
|
||||||
}
|
|
||||||
|
|
||||||
if (aspectRatio !== undefined) {
|
|
||||||
this.attributes.aspectRatio = aspectRatio
|
|
||||||
}
|
|
||||||
|
|
||||||
const ratioMultiplier =
|
|
||||||
this.ratioMultipliers[this.attributes.aspectRatio ?? '1:1']
|
|
||||||
|
|
||||||
const result = await ImageManipulator.manipulateAsync(
|
|
||||||
this.path,
|
|
||||||
[
|
|
||||||
...modifiers,
|
|
||||||
{
|
|
||||||
resize:
|
|
||||||
ratioMultiplier > 1 ? {width: uploadWidth} : {height: uploadHeight},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{
|
|
||||||
base64: true,
|
|
||||||
format: SaveFormat.JPEG,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.cropped = {
|
|
||||||
mime: 'image/jpeg',
|
|
||||||
path: result.uri,
|
|
||||||
size:
|
|
||||||
result.base64 !== undefined
|
|
||||||
? getDataUriSize(result.base64)
|
|
||||||
: MAX_IMAGE_SIZE_IN_BYTES + 999, // shouldn't hit this unless manipulation fails
|
|
||||||
...result,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resetCropped() {
|
|
||||||
this.manipulate({})
|
|
||||||
}
|
|
||||||
|
|
||||||
previous() {
|
|
||||||
this.cropped = this.prev
|
|
||||||
this.attributes = this.prevAttributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -143,12 +143,6 @@ const GalleryItem = ({
|
|||||||
cropImage(image).then(next => {
|
cropImage(image).then(next => {
|
||||||
onChange(next)
|
onChange(next)
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
// openModal({
|
|
||||||
// name: 'edit-image',
|
|
||||||
// image,
|
|
||||||
// gallery,
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,402 +0,0 @@
|
|||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
|
||||||
import {Pressable, StyleSheet, View} from 'react-native'
|
|
||||||
import {useWindowDimensions} from 'react-native'
|
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
|
||||||
import {MaterialIcons} from '@expo/vector-icons'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Slider} from '@miblanchard/react-native-slider'
|
|
||||||
import {observer} from 'mobx-react-lite'
|
|
||||||
import ImageEditor, {Position} from 'react-avatar-editor'
|
|
||||||
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {MAX_ALT_TEXT} from 'lib/constants'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
|
||||||
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
|
|
||||||
import {enforceLen} from 'lib/strings/helpers'
|
|
||||||
import {gradients, s} from 'lib/styles'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {getKeys} from 'lib/type-assertions'
|
|
||||||
import {GalleryModel} from 'state/models/media/gallery'
|
|
||||||
import {ImageModel} from 'state/models/media/image'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import {TextInput} from './util'
|
|
||||||
|
|
||||||
export const snapPoints = ['80%']
|
|
||||||
|
|
||||||
const RATIOS = {
|
|
||||||
'4:3': {
|
|
||||||
Icon: RectWideIcon,
|
|
||||||
},
|
|
||||||
'1:1': {
|
|
||||||
Icon: SquareIcon,
|
|
||||||
},
|
|
||||||
'3:4': {
|
|
||||||
Icon: RectTallIcon,
|
|
||||||
},
|
|
||||||
None: {
|
|
||||||
label: 'None',
|
|
||||||
Icon: MaterialIcons,
|
|
||||||
name: 'do-not-disturb-alt',
|
|
||||||
},
|
|
||||||
} as const
|
|
||||||
|
|
||||||
type AspectRatio = keyof typeof RATIOS
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
image: ImageModel
|
|
||||||
gallery: GalleryModel
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Component = observer(function EditImageImpl({
|
|
||||||
image,
|
|
||||||
gallery,
|
|
||||||
}: Props) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const windowDimensions = useWindowDimensions()
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
|
|
||||||
const {
|
|
||||||
aspectRatio,
|
|
||||||
// rotate = 0
|
|
||||||
} = image.attributes
|
|
||||||
|
|
||||||
const editorRef = useRef<ImageEditor>(null)
|
|
||||||
const [scale, setScale] = useState<number>(image.attributes.scale ?? 1)
|
|
||||||
const [position, setPosition] = useState<Position | undefined>(
|
|
||||||
image.attributes.position,
|
|
||||||
)
|
|
||||||
const [altText, setAltText] = useState(image?.altText ?? '')
|
|
||||||
|
|
||||||
const onFlipHorizontal = useCallback(() => {
|
|
||||||
image.flipHorizontal()
|
|
||||||
}, [image])
|
|
||||||
|
|
||||||
const onFlipVertical = useCallback(() => {
|
|
||||||
image.flipVertical()
|
|
||||||
}, [image])
|
|
||||||
|
|
||||||
// const onSetRotate = useCallback(
|
|
||||||
// (direction: 'left' | 'right') => {
|
|
||||||
// const rotation = (rotate + 90 * (direction === 'left' ? -1 : 1)) % 360
|
|
||||||
// image.setRotate(rotation)
|
|
||||||
// },
|
|
||||||
// [rotate, image],
|
|
||||||
// )
|
|
||||||
|
|
||||||
const onSetRatio = useCallback(
|
|
||||||
(ratio: AspectRatio) => {
|
|
||||||
image.setRatio(ratio)
|
|
||||||
},
|
|
||||||
[image],
|
|
||||||
)
|
|
||||||
|
|
||||||
const adjustments = useMemo(
|
|
||||||
() => [
|
|
||||||
// {
|
|
||||||
// name: 'rotate-left' as const,
|
|
||||||
// label: 'Rotate left',
|
|
||||||
// onPress: () => {
|
|
||||||
// onSetRotate('left')
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// name: 'rotate-right' as const,
|
|
||||||
// label: 'Rotate right',
|
|
||||||
// onPress: () => {
|
|
||||||
// onSetRotate('right')
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
name: 'flip' as const,
|
|
||||||
label: _(msg`Flip horizontal`),
|
|
||||||
onPress: onFlipHorizontal,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'flip' as const,
|
|
||||||
label: _(msg`Flip vertically`),
|
|
||||||
onPress: onFlipVertical,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[onFlipHorizontal, onFlipVertical, _],
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
image.prev = image.cropped
|
|
||||||
image.prevAttributes = image.attributes
|
|
||||||
image.resetCropped()
|
|
||||||
}, [image])
|
|
||||||
|
|
||||||
const onCloseModal = useCallback(() => {
|
|
||||||
closeModal()
|
|
||||||
}, [closeModal])
|
|
||||||
|
|
||||||
const onPressCancel = useCallback(async () => {
|
|
||||||
await gallery.previous(image)
|
|
||||||
onCloseModal()
|
|
||||||
}, [onCloseModal, gallery, image])
|
|
||||||
|
|
||||||
const onPressSave = useCallback(async () => {
|
|
||||||
image.setAltText(altText)
|
|
||||||
|
|
||||||
const crop = editorRef.current?.getCroppingRect()
|
|
||||||
|
|
||||||
await image.manipulate({
|
|
||||||
...(crop !== undefined
|
|
||||||
? {
|
|
||||||
crop: {
|
|
||||||
originX: crop.x,
|
|
||||||
originY: crop.y,
|
|
||||||
width: crop.width,
|
|
||||||
height: crop.height,
|
|
||||||
},
|
|
||||||
...(scale !== 1 ? {scale} : {}),
|
|
||||||
...(position !== undefined ? {position} : {}),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
image.prev = image.cropped
|
|
||||||
image.prevAttributes = image.attributes
|
|
||||||
onCloseModal()
|
|
||||||
}, [altText, image, position, scale, onCloseModal])
|
|
||||||
|
|
||||||
const getLabelIconSize = useCallback((as: AspectRatio) => {
|
|
||||||
switch (as) {
|
|
||||||
case 'None':
|
|
||||||
return 22
|
|
||||||
case '1:1':
|
|
||||||
return 32
|
|
||||||
default:
|
|
||||||
return 26
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (image.cropped === undefined) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const computedWidth =
|
|
||||||
windowDimensions.width > 500 ? 410 : windowDimensions.width - 80
|
|
||||||
const sideLength = isMobile ? computedWidth : 300
|
|
||||||
|
|
||||||
const dimensions = image.getResizedDimensions(aspectRatio, sideLength)
|
|
||||||
const imgContainerStyles = {width: sideLength, height: sideLength}
|
|
||||||
|
|
||||||
const imgControlStyles = {
|
|
||||||
alignItems: 'center' as const,
|
|
||||||
flexDirection: isMobile ? ('column' as const) : ('row' as const),
|
|
||||||
gap: isMobile ? 0 : 5,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
testID="editImageModal"
|
|
||||||
style={[
|
|
||||||
pal.view,
|
|
||||||
styles.container,
|
|
||||||
s.flex1,
|
|
||||||
{
|
|
||||||
paddingHorizontal: isMobile ? 16 : undefined,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text style={[styles.title, pal.text]}>
|
|
||||||
<Trans>Edit image</Trans>
|
|
||||||
</Text>
|
|
||||||
<View style={[styles.gap18, s.flexRow]}>
|
|
||||||
<View>
|
|
||||||
<View
|
|
||||||
style={[styles.imgContainer, pal.borderDark, imgContainerStyles]}>
|
|
||||||
<ImageEditor
|
|
||||||
ref={editorRef}
|
|
||||||
style={styles.imgEditor}
|
|
||||||
image={image.cropped.path}
|
|
||||||
scale={scale}
|
|
||||||
border={0}
|
|
||||||
position={position}
|
|
||||||
onPositionChange={setPosition}
|
|
||||||
{...dimensions}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<Slider
|
|
||||||
value={scale}
|
|
||||||
onValueChange={(v: number | number[]) =>
|
|
||||||
setScale(Array.isArray(v) ? v[0] : v)
|
|
||||||
}
|
|
||||||
minimumValue={1}
|
|
||||||
maximumValue={3}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
{!isMobile ? (
|
|
||||||
<Text type="sm-bold" style={pal.text}>
|
|
||||||
<Trans>Ratios</Trans>
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<View style={imgControlStyles}>
|
|
||||||
{getKeys(RATIOS).map(ratio => {
|
|
||||||
const {Icon, ...props} = RATIOS[ratio]
|
|
||||||
const labelIconSize = getLabelIconSize(ratio)
|
|
||||||
const isSelected = aspectRatio === ratio
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={ratio}
|
|
||||||
onPress={() => {
|
|
||||||
onSetRatio(ratio)
|
|
||||||
}}
|
|
||||||
accessibilityLabel={ratio}
|
|
||||||
accessibilityHint="">
|
|
||||||
<Icon
|
|
||||||
size={labelIconSize}
|
|
||||||
style={[styles.imgControl, isSelected ? s.blue3 : pal.text]}
|
|
||||||
color={(isSelected ? s.blue3 : pal.text).color}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
type={isSelected ? 'xs-bold' : 'xs-medium'}
|
|
||||||
style={[isSelected ? s.blue3 : pal.text, s.textCenter]}>
|
|
||||||
{ratio}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
{!isMobile ? (
|
|
||||||
<Text type="sm-bold" style={[pal.text, styles.subsection]}>
|
|
||||||
<Trans>Transformations</Trans>
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<View style={imgControlStyles}>
|
|
||||||
{adjustments.map(({label, name, onPress}) => (
|
|
||||||
<Pressable
|
|
||||||
key={label}
|
|
||||||
onPress={onPress}
|
|
||||||
accessibilityLabel={label}
|
|
||||||
accessibilityHint=""
|
|
||||||
style={styles.flipBtn}>
|
|
||||||
<MaterialIcons
|
|
||||||
name={name}
|
|
||||||
size={label?.startsWith('Flip') ? 22 : 24}
|
|
||||||
style={[
|
|
||||||
pal.text,
|
|
||||||
label === _(msg`Flip vertically`)
|
|
||||||
? styles.flipVertical
|
|
||||||
: undefined,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.gap18, styles.bottomSection, pal.border]}>
|
|
||||||
<Text type="sm-bold" style={pal.text} nativeID="alt-text">
|
|
||||||
<Trans>Accessibility</Trans>
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
testID="altTextImageInput"
|
|
||||||
style={[
|
|
||||||
styles.textArea,
|
|
||||||
pal.border,
|
|
||||||
pal.text,
|
|
||||||
{
|
|
||||||
maxHeight: isMobile ? 50 : undefined,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
multiline
|
|
||||||
value={altText}
|
|
||||||
onChangeText={text => setAltText(enforceLen(text, MAX_ALT_TEXT))}
|
|
||||||
accessibilityLabel={_(msg`Alt text`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
accessibilityLabelledBy="alt-text"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.btns}>
|
|
||||||
<Pressable onPress={onPressCancel} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={pal.link}>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable onPress={onPressSave} accessibilityRole="button">
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text type="xl-medium" style={s.white}>
|
|
||||||
<Trans context="action">Done</Trans>
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
gap: 18,
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
subsection: {marginTop: 12},
|
|
||||||
gap18: {gap: 18},
|
|
||||||
title: {
|
|
||||||
fontWeight: 'bold',
|
|
||||||
fontSize: 24,
|
|
||||||
},
|
|
||||||
btns: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
borderRadius: 4,
|
|
||||||
paddingVertical: 8,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
|
||||||
imgControl: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
height: 40,
|
|
||||||
},
|
|
||||||
imgEditor: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
},
|
|
||||||
imgContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
flipVertical: {
|
|
||||||
transform: [{rotate: '90deg'}],
|
|
||||||
},
|
|
||||||
flipBtn: {
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
textArea: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingTop: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
fontSize: 16,
|
|
||||||
height: 100,
|
|
||||||
textAlignVertical: 'top',
|
|
||||||
},
|
|
||||||
bottomSection: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
paddingTop: 18,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -12,9 +12,7 @@ import * as ChangeEmailModal from './ChangeEmail'
|
|||||||
import * as ChangeHandleModal from './ChangeHandle'
|
import * as ChangeHandleModal from './ChangeHandle'
|
||||||
import * as ChangePasswordModal from './ChangePassword'
|
import * as ChangePasswordModal from './ChangePassword'
|
||||||
import * as CreateOrEditListModal from './CreateOrEditList'
|
import * as CreateOrEditListModal from './CreateOrEditList'
|
||||||
import * as CropImageModal from './crop-image/CropImage.web'
|
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as EditImageModal from './EditImage'
|
|
||||||
import * as EditProfileModal from './EditProfile'
|
import * as EditProfileModal from './EditProfile'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
@@ -55,9 +53,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPressMask = () => {
|
const onPressMask = () => {
|
||||||
if (modal.name === 'crop-image' || modal.name === 'edit-image') {
|
|
||||||
return // dont close on mask presses during crop
|
|
||||||
}
|
|
||||||
closeModal()
|
closeModal()
|
||||||
}
|
}
|
||||||
const onInnerPress = () => {
|
const onInnerPress = () => {
|
||||||
@@ -74,8 +69,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <UserAddRemoveLists.Component {...modal} />
|
element = <UserAddRemoveLists.Component {...modal} />
|
||||||
} else if (modal.name === 'list-add-remove-users') {
|
} else if (modal.name === 'list-add-remove-users') {
|
||||||
element = <ListAddUserModal.Component {...modal} />
|
element = <ListAddUserModal.Component {...modal} />
|
||||||
} else if (modal.name === 'crop-image') {
|
|
||||||
element = <CropImageModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'delete-account') {
|
} else if (modal.name === 'delete-account') {
|
||||||
element = <DeleteAccountModal.Component />
|
element = <DeleteAccountModal.Component />
|
||||||
} else if (modal.name === 'repost') {
|
} else if (modal.name === 'repost') {
|
||||||
@@ -94,9 +87,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
} else if (modal.name === 'post-languages-settings') {
|
} else if (modal.name === 'post-languages-settings') {
|
||||||
element = <PostLanguagesSettingsModal.Component />
|
element = <PostLanguagesSettingsModal.Component />
|
||||||
} else if (modal.name === 'edit-image') {
|
|
||||||
// @ts-expect-error: todo
|
|
||||||
element = <EditImageModal.Component {...modal} />
|
|
||||||
} else if (modal.name === 'verify-email') {
|
} else if (modal.name === 'verify-email') {
|
||||||
element = <VerifyEmailModal.Component {...modal} />
|
element = <VerifyEmailModal.Component {...modal} />
|
||||||
} else if (modal.name === 'change-email') {
|
} else if (modal.name === 'change-email') {
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
|
||||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {Slider} from '@miblanchard/react-native-slider'
|
|
||||||
import ImageEditor from 'react-avatar-editor'
|
|
||||||
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {RectTallIcon, RectWideIcon, SquareIcon} from 'lib/icons'
|
|
||||||
import {Dimensions} from 'lib/media/types'
|
|
||||||
import {getDataUriSize} from 'lib/media/util'
|
|
||||||
import {gradients, s} from 'lib/styles'
|
|
||||||
import {Text} from 'view/com/util/text/Text'
|
|
||||||
import {calculateDimensions} from './cropImageUtil'
|
|
||||||
|
|
||||||
enum AspectRatio {
|
|
||||||
Square = 'square',
|
|
||||||
Wide = 'wide',
|
|
||||||
Tall = 'tall',
|
|
||||||
Custom = 'custom',
|
|
||||||
}
|
|
||||||
|
|
||||||
const DIMS: Record<string, Dimensions> = {
|
|
||||||
[AspectRatio.Square]: {width: 1000, height: 1000},
|
|
||||||
[AspectRatio.Wide]: {width: 1000, height: 750},
|
|
||||||
[AspectRatio.Tall]: {width: 750, height: 1000},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const snapPoints = ['0%']
|
|
||||||
|
|
||||||
export function Component({
|
|
||||||
uri,
|
|
||||||
dimensions,
|
|
||||||
onSelect,
|
|
||||||
}: {
|
|
||||||
uri: string
|
|
||||||
dimensions?: Dimensions
|
|
||||||
onSelect: (img?: RNImage) => void
|
|
||||||
}) {
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const defaultAspectStyle = dimensions
|
|
||||||
? AspectRatio.Custom
|
|
||||||
: AspectRatio.Square
|
|
||||||
const [as, setAs] = React.useState<AspectRatio>(defaultAspectStyle)
|
|
||||||
const [scale, setScale] = React.useState<number>(1)
|
|
||||||
const editorRef = React.useRef<ImageEditor>(null)
|
|
||||||
const imageEditorWidth = dimensions ? dimensions.width : DIMS[as].width
|
|
||||||
const imageEditorHeight = dimensions ? dimensions.height : DIMS[as].height
|
|
||||||
|
|
||||||
const doSetAs = (v: AspectRatio) => () => setAs(v)
|
|
||||||
|
|
||||||
const onPressCancel = () => {
|
|
||||||
onSelect(undefined)
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
const onPressDone = () => {
|
|
||||||
const canvas = editorRef.current?.getImageScaledToCanvas()
|
|
||||||
if (canvas) {
|
|
||||||
const dataUri = canvas.toDataURL('image/jpeg')
|
|
||||||
onSelect({
|
|
||||||
path: dataUri,
|
|
||||||
mime: 'image/jpeg',
|
|
||||||
size: getDataUriSize(dataUri),
|
|
||||||
width: imageEditorWidth,
|
|
||||||
height: imageEditorHeight,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
onSelect(undefined)
|
|
||||||
}
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
let cropperStyle
|
|
||||||
if (as === AspectRatio.Square) {
|
|
||||||
cropperStyle = styles.cropperSquare
|
|
||||||
} else if (as === AspectRatio.Wide) {
|
|
||||||
cropperStyle = styles.cropperWide
|
|
||||||
} else if (as === AspectRatio.Tall) {
|
|
||||||
cropperStyle = styles.cropperTall
|
|
||||||
} else if (as === AspectRatio.Custom) {
|
|
||||||
const cropperDimensions = calculateDimensions(
|
|
||||||
550,
|
|
||||||
imageEditorHeight,
|
|
||||||
imageEditorWidth,
|
|
||||||
)
|
|
||||||
cropperStyle = {
|
|
||||||
width: cropperDimensions.width,
|
|
||||||
height: cropperDimensions.height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View>
|
|
||||||
<View style={[styles.cropper, pal.borderDark, cropperStyle]}>
|
|
||||||
<ImageEditor
|
|
||||||
ref={editorRef}
|
|
||||||
style={styles.imageEditor}
|
|
||||||
image={uri}
|
|
||||||
width={imageEditorWidth}
|
|
||||||
height={imageEditorHeight}
|
|
||||||
scale={scale}
|
|
||||||
border={0}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={styles.ctrls}>
|
|
||||||
<Slider
|
|
||||||
value={scale}
|
|
||||||
onValueChange={(v: number | number[]) =>
|
|
||||||
setScale(Array.isArray(v) ? v[0] : v)
|
|
||||||
}
|
|
||||||
minimumValue={1}
|
|
||||||
maximumValue={3}
|
|
||||||
containerStyle={styles.slider}
|
|
||||||
/>
|
|
||||||
{as === AspectRatio.Custom ? null : (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={doSetAs(AspectRatio.Wide)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Wide`)}
|
|
||||||
accessibilityHint={_(msg`Sets image aspect ratio to wide`)}>
|
|
||||||
<RectWideIcon
|
|
||||||
size={24}
|
|
||||||
style={as === AspectRatio.Wide ? s.blue3 : pal.text}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={doSetAs(AspectRatio.Tall)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Tall`)}
|
|
||||||
accessibilityHint={_(msg`Sets image aspect ratio to tall`)}>
|
|
||||||
<RectTallIcon
|
|
||||||
size={24}
|
|
||||||
style={as === AspectRatio.Tall ? s.blue3 : pal.text}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={doSetAs(AspectRatio.Square)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Square`)}
|
|
||||||
accessibilityHint={_(msg`Sets image aspect ratio to square`)}>
|
|
||||||
<SquareIcon
|
|
||||||
size={24}
|
|
||||||
style={as === AspectRatio.Square ? s.blue3 : pal.text}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={styles.btns}>
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressCancel}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Cancel image crop`)}
|
|
||||||
accessibilityHint={_(msg`Exits image cropping process`)}>
|
|
||||||
<Text type="xl" style={pal.link}>
|
|
||||||
<Trans>Cancel</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressDone}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Save image crop`)}
|
|
||||||
accessibilityHint={_(msg`Saves image crop settings`)}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[gradients.blueLight.start, gradients.blueLight.end]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text type="xl-medium" style={s.white}>
|
|
||||||
<Trans>Done</Trans>
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
cropper: {
|
|
||||||
marginLeft: 'auto',
|
|
||||||
marginRight: 'auto',
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
cropperSquare: {
|
|
||||||
width: 400,
|
|
||||||
height: 400,
|
|
||||||
},
|
|
||||||
cropperWide: {
|
|
||||||
width: 400,
|
|
||||||
height: 300,
|
|
||||||
},
|
|
||||||
cropperTall: {
|
|
||||||
width: 300,
|
|
||||||
height: 400,
|
|
||||||
},
|
|
||||||
imageEditor: {
|
|
||||||
maxWidth: '100%',
|
|
||||||
},
|
|
||||||
ctrls: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
slider: {
|
|
||||||
flex: 1,
|
|
||||||
marginRight: 10,
|
|
||||||
},
|
|
||||||
btns: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
borderRadius: 4,
|
|
||||||
paddingVertical: 8,
|
|
||||||
paddingHorizontal: 24,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export const calculateDimensions = (
|
|
||||||
maxWidth: number,
|
|
||||||
originalHeight: number,
|
|
||||||
originalWidth: number,
|
|
||||||
) => {
|
|
||||||
const aspectRatio = originalWidth / originalHeight
|
|
||||||
const newHeight = maxWidth / aspectRatio
|
|
||||||
const newWidth = maxWidth
|
|
||||||
return {
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user