diff options
Diffstat (limited to 'src/state/models')
-rw-r--r-- | src/state/models/cache/image-sizes.ts | 18 | ||||
-rw-r--r-- | src/state/models/content/profile.ts | 6 | ||||
-rw-r--r-- | src/state/models/media/gallery.ts | 85 | ||||
-rw-r--r-- | src/state/models/media/image.ts | 85 | ||||
-rw-r--r-- | src/state/models/ui/shell.ts | 4 |
5 files changed, 184 insertions, 14 deletions
diff --git a/src/state/models/cache/image-sizes.ts b/src/state/models/cache/image-sizes.ts index 2fd6e0013..bbfb9612b 100644 --- a/src/state/models/cache/image-sizes.ts +++ b/src/state/models/cache/image-sizes.ts @@ -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}), diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index 45d928c92..c26dc8749 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -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 || {} diff --git a/src/state/models/media/gallery.ts b/src/state/models/media/gallery.ts new file mode 100644 index 000000000..fbe6c92a0 --- /dev/null +++ b/src/state/models/media/gallery.ts @@ -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))) + } +} diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts new file mode 100644 index 000000000..584bf90cc --- /dev/null +++ b/src/state/models/media/image.ts @@ -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) + } + } +} diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts index dd5c899b3..47cc0aa82 100644 --- a/src/state/models/ui/shell.ts +++ b/src/state/models/ui/shell.ts @@ -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 { |