Split image cropping into secondary step (#473)
* Split image cropping into secondary step * Use ImageModel and GalleryModel * Add fix for pasting image URLs * Move models to state folder * Fix things that broke after rebase * Latest -- has image display bug * Remove contentFit * Fix iOS display in gallery * Tuneup the api signatures and implement compress/resize on web * Fix await * Lint fix and remove unused function * Fix android image pathing * Fix external embed x button on android * Remove min-height from composer (no longer useful and was mispositioning the composer on android) * Fix e2e picker --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
parent
91fadadb58
commit
2509290fdd
30 changed files with 875 additions and 833 deletions
18
src/state/models/cache/image-sizes.ts
vendored
18
src/state/models/cache/image-sizes.ts
vendored
|
@ -1,24 +1,24 @@
|
|||
import {Image} from 'react-native'
|
||||
import {Dim} from 'lib/media/manip'
|
||||
import type {Dimensions} from 'lib/media/types'
|
||||
|
||||
export class ImageSizesCache {
|
||||
sizes: Map<string, Dim> = new Map()
|
||||
activeRequests: Map<string, Promise<Dim>> = new Map()
|
||||
sizes: Map<string, Dimensions> = new Map()
|
||||
activeRequests: Map<string, Promise<Dimensions>> = new Map()
|
||||
|
||||
constructor() {}
|
||||
|
||||
get(uri: string): Dim | undefined {
|
||||
get(uri: string): Dimensions | undefined {
|
||||
return this.sizes.get(uri)
|
||||
}
|
||||
|
||||
async fetch(uri: string): Promise<Dim> {
|
||||
const dim = this.sizes.get(uri)
|
||||
if (dim) {
|
||||
return dim
|
||||
async fetch(uri: string): Promise<Dimensions> {
|
||||
const Dimensions = this.sizes.get(uri)
|
||||
if (Dimensions) {
|
||||
return Dimensions
|
||||
}
|
||||
const prom =
|
||||
this.activeRequests.get(uri) ||
|
||||
new Promise<Dim>(resolve => {
|
||||
new Promise<Dimensions>(resolve => {
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width: number, height: number) => resolve({width, height}),
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {PickedMedia} from 'lib/media/picker'
|
||||
import {
|
||||
ComAtprotoLabelDefs,
|
||||
AppBskyActorGetProfile as GetProfile,
|
||||
|
@ -10,6 +9,7 @@ import {RootStoreModel} from '../root-store'
|
|||
import * as apilib from 'lib/api/index'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
import {FollowState} from '../cache/my-follows'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
|
||||
export const ACTOR_TYPE_USER = 'app.bsky.system.actorUser'
|
||||
|
||||
|
@ -122,8 +122,8 @@ export class ProfileModel {
|
|||
|
||||
async updateProfile(
|
||||
updates: AppBskyActorProfile.Record,
|
||||
newUserAvatar: PickedMedia | undefined | null,
|
||||
newUserBanner: PickedMedia | undefined | null,
|
||||
newUserAvatar: RNImage | undefined | null,
|
||||
newUserBanner: RNImage | undefined | null,
|
||||
) {
|
||||
await this.rootStore.agent.upsertProfile(async existing => {
|
||||
existing = existing || {}
|
||||
|
|
85
src/state/models/media/gallery.ts
Normal file
85
src/state/models/media/gallery.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {RootStoreModel} from 'state/index'
|
||||
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'
|
||||
import {getDataUriSize} from 'lib/media/util'
|
||||
|
||||
export class GalleryModel {
|
||||
images: ImageModel[] = []
|
||||
|
||||
constructor(public rootStore: RootStoreModel) {
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
})
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this.size === 0
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.images.length
|
||||
}
|
||||
|
||||
get paths() {
|
||||
return this.images.map(image =>
|
||||
image.compressed === undefined ? image.path : image.compressed.path,
|
||||
)
|
||||
}
|
||||
|
||||
async add(image_: RNImage) {
|
||||
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(this.rootStore, image_)
|
||||
await image.compress()
|
||||
|
||||
runInAction(() => {
|
||||
this.images.push(image)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async paste(uri: string) {
|
||||
if (this.size >= 4) {
|
||||
return
|
||||
}
|
||||
|
||||
const {width, height} = await getImageDim(uri)
|
||||
|
||||
const image: RNImage = {
|
||||
path: uri,
|
||||
height,
|
||||
width,
|
||||
size: getDataUriSize(uri),
|
||||
mime: 'image/jpeg',
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
this.add(image)
|
||||
})
|
||||
}
|
||||
|
||||
crop(image: ImageModel) {
|
||||
image.crop()
|
||||
}
|
||||
|
||||
remove(image: ImageModel) {
|
||||
const index = this.images.findIndex(image_ => image_.path === image.path)
|
||||
this.images.splice(index, 1)
|
||||
}
|
||||
|
||||
async pick() {
|
||||
const images = await openPicker(this.rootStore, {
|
||||
multiple: true,
|
||||
maxFiles: 4 - this.images.length,
|
||||
})
|
||||
|
||||
await Promise.all(images.map(image => this.add(image)))
|
||||
}
|
||||
}
|
85
src/state/models/media/image.ts
Normal file
85
src/state/models/media/image.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
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'
|
||||
|
||||
// TODO: EXIF embed
|
||||
// Cases to consider: ExternalEmbed
|
||||
export class ImageModel implements RNImage {
|
||||
path: string
|
||||
mime = 'image/jpeg'
|
||||
width: number
|
||||
height: number
|
||||
size: number
|
||||
cropped?: RNImage = undefined
|
||||
compressed?: RNImage = undefined
|
||||
scaledWidth: number = POST_IMG_MAX.width
|
||||
scaledHeight: number = POST_IMG_MAX.height
|
||||
|
||||
constructor(public rootStore: RootStoreModel, image: RNImage) {
|
||||
makeAutoObservable(this, {
|
||||
rootStore: false,
|
||||
})
|
||||
|
||||
this.path = image.path
|
||||
this.width = image.width
|
||||
this.height = image.height
|
||||
this.size = image.size
|
||||
this.calcScaledDimensions()
|
||||
}
|
||||
|
||||
calcScaledDimensions() {
|
||||
const {width, height} = scaleDownDimensions(
|
||||
{width: this.width, height: this.height},
|
||||
POST_IMG_MAX,
|
||||
)
|
||||
|
||||
this.scaledWidth = width
|
||||
this.scaledHeight = height
|
||||
}
|
||||
|
||||
async crop() {
|
||||
try {
|
||||
const cropped = await openCropper(this.rootStore, {
|
||||
mediaType: 'photo',
|
||||
path: this.path,
|
||||
freeStyleCropEnabled: true,
|
||||
width: this.scaledWidth,
|
||||
height: this.scaledHeight,
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
this.cropped = cropped
|
||||
})
|
||||
} catch (err) {
|
||||
this.rootStore.log.error('Failed to crop photo', err)
|
||||
}
|
||||
|
||||
this.compress()
|
||||
}
|
||||
|
||||
async compress() {
|
||||
try {
|
||||
const {width, height} = scaleDownDimensions(
|
||||
this.cropped
|
||||
? {width: this.cropped.width, height: this.cropped.height}
|
||||
: {width: this.width, height: this.height},
|
||||
POST_IMG_MAX,
|
||||
)
|
||||
const compressed = await compressAndResizeImageForPost({
|
||||
...(this.cropped === undefined ? this : this.cropped),
|
||||
width,
|
||||
height,
|
||||
})
|
||||
|
||||
runInAction(() => {
|
||||
this.compressed = compressed
|
||||
})
|
||||
} catch (err) {
|
||||
this.rootStore.log.error('Failed to compress photo', err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import {RootStoreModel} from '../root-store'
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {ProfileModel} from '../content/profile'
|
||||
import {isObj, hasProp} from 'lib/type-guards'
|
||||
import {PickedMedia} from 'lib/media/types'
|
||||
import {Image} from 'lib/media/types'
|
||||
|
||||
export interface ConfirmModal {
|
||||
name: 'confirm'
|
||||
|
@ -38,7 +38,7 @@ export interface ReportAccountModal {
|
|||
export interface CropImageModal {
|
||||
name: 'crop-image'
|
||||
uri: string
|
||||
onSelect: (img?: PickedMedia) => void
|
||||
onSelect: (img?: Image) => void
|
||||
}
|
||||
|
||||
export interface DeleteAccountModal {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue