UNPKG

expo-image-multiple-picker

Version:

Fully customizable image picker for react native

631 lines 27.3 kB
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-native/no-inline-styles */ /* eslint-disable no-undef */ /* eslint-disable no-void */ /* eslint-disable eqeqeq */ /* eslint-disable no-shadow */ import * as MediaLibrary from 'expo-media-library'; import React, { Component, PureComponent, useEffect, useState, } from 'react'; import { ActivityIndicator, BackHandler, Dimensions, FlatList, Image, Pressable, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import Svg, { Path } from 'react-native-svg'; const screen = Dimensions.get('window'); const styles = StyleSheet.create({ root: { top: 0, left: 0, width: '100%', height: '100%', position: 'absolute', }, rootHeader: { width: '100%', backgroundColor: 'black', }, rootBody: { flex: 1, width: '100%', }, container: { flex: 1, }, defaultCheckedBg: { width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.2)', }, defaultCheckedContainer: { position: 'absolute', right: '10%', bottom: '20%', width: '30%', height: '30%', }, defaultVideoBg: { width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.1)', }, defaultVideoContainer: { position: 'absolute', left: '5%', bottom: '5%', width: '95%', height: 'auto', }, defaultAlbumContainer: { flex: 1, margin: 10, backgroundColor: 'white', height: 'auto', borderRadius: 5, alignItems: 'center', shadowColor: 'black', elevation: 1, }, defaultAlbumImage: { backgroundColor: 'black', width: '100%', minHeight: 200, resizeMode: 'contain', }, defaultHeaderContainer: { width: '100%', paddingTop: 80, padding: 20, height: 130, justifyContent: 'space-between', flexDirection: 'row', backgroundColor: 'white', elevation: 1, }, defaultHeaderButton: { alignItems: 'center', justifyContent: 'center', paddingVertical: 6, paddingHorizontal: 12, borderRadius: 4, elevation: 3, backgroundColor: 'black', }, defaultHeaderButtonText: { fontSize: 14, letterSpacing: 0.25, color: 'white', }, defaultSliderContainer: { position: 'absolute', alignItems: 'center', width: 50, right: -20, top: 30, }, defaultSliderButton: { height: 50, width: 50, backgroundColor: 'white', borderRadius: 40, }, defaultSliderBalloon: { right: 30, position: 'absolute', minWidth: 25, height: 25, backgroundColor: 'rgba(0,0,0,0.7)', borderRadius: 40, alignItems: 'center', }, defaultSliderTime: { position: 'absolute', borderRadius: 10, right: '105%', backgroundColor: 'white', padding: 10, minWidth: 150, alignItems: 'center', }, defaultSlider: { position: 'absolute', top: 0, height: '100%', width: 25, backgroundColor: 'rgba(255, 255, 255, 0.5)', borderRadius: 20, }, }); class ImageBox extends PureComponent { constructor() { super(...arguments); this._ismounted = false; this.state = { checked: this.props.item.isChecked() || false, }; } componentDidMount() { this._ismounted = true; } componentWillUnmount() { this._ismounted = false; } toggle() { const checked = !this.state.checked; const changed = this.props.item.onCheck(checked, { uncheck: () => { if (this._ismounted) this.setState({ checked: false }); }, asset: this.props.item.asset, }); if (changed) this.setState({ checked }); } getCheckedComponent() { return this.props.item.check ? (this.props.item.check()) : (React.createElement(View, { style: styles.defaultCheckedBg }, React.createElement(View, { style: styles.defaultCheckedContainer }, React.createElement(Svg, { viewBox: '0 0 512 512' }, React.createElement(Path, { fill: 'white', d: 'M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z' }))))); } getVideoComponent() { return this.props.item.video ? (this.props.item.video(this.props.item.asset)) : (React.createElement(View, { style: styles.defaultVideoBg }, React.createElement(View, { style: styles.defaultVideoContainer }, React.createElement(Text, { style: { fontSize: 6, color: 'white' } }, new Date(this.props.item.asset.duration * 1000) .toISOString() .slice(11, 19))))); } render() { return (React.createElement(TouchableOpacity, { onPress: this.toggle.bind(this), style: { width: this.props.item.size.width, height: this.props.item.size.height, } }, React.createElement(Image, { style: { width: this.props.item.size.width, resizeMode: 'cover', flex: 1, }, source: { uri: this.props.item.asset.uri } }), !!this.props.item.asset.duration && (React.createElement(View, { style: styles.root }, this.getVideoComponent())), React.createElement(View, { style: { ...styles.root, opacity: this.state.checked ? 1 : 0, } }, this.getCheckedComponent()))); } } export class ImagePickerCarousel extends Component { constructor() { super(...arguments); this.flatListRef = React.createRef(); this._unmounted = false; this.state = { selectedAssets: new Map(), data: [], currentIndex: 0, }; } getColumns() { return this.props.columns > 0 ? this.props.columns : 2; } isMultiple() { return this.props.multiple || false; } getImageSize() { const width = screen.width / this.getColumns(); return { width, height: width, }; } getItemsPerScreen() { const columns = this.getColumns(); const size = this.getImageSize(); return Math.ceil((screen.height / size.height) * columns); } selectedImage(checked, selected) { if (this.props.max && this.state.selectedAssets.size >= this.props.max && checked) { return false; } if (!this.props.multiple) { for (const sel of this.state.selectedAssets.values()) { sel.uncheck(); } this.state.selectedAssets.clear(); } if (checked) { this.state.selectedAssets.set(selected.asset.id, selected); } else { this.state.selectedAssets.delete(selected.asset.id); } this.setState({ selectedAssets: new Map(this.state.selectedAssets), }); if (this.props.onSelect) { this.props.onSelect([...this.state.selectedAssets.values()].map((s) => s.asset)); } return true; } exists(asset_id) { return this.state.data.find((data) => data.asset.id === asset_id); } isChecked(asset_id) { return this.state.selectedAssets.has(asset_id); } async fetchNextPage(stack) { var _a; const types = []; if (this.props.image) types.push(MediaLibrary.MediaType.photo); if (this.props.video) types.push(MediaLibrary.MediaType.video); const options = { album: this.props.albumID, first: stack, sortBy: [MediaLibrary.SortBy.modificationTime], mediaType: types, }; if (this.state.page) { options.after = this.state.page.endCursor; } const page = await MediaLibrary.getAssetsAsync(options); const isLastPage = page.endCursor == ((_a = this.state.page) === null || _a === void 0 ? void 0 : _a.endCursor); if (!this._unmounted && !isLastPage) { this.state.page = page; const size = this.getImageSize(); for (const asset of page.assets) { if (!this.exists(asset.id)) { this.state.data.push({ asset, size, isChecked: this.isChecked.bind(this, asset.id), onCheck: this.selectedImage.bind(this), check: this.props.check, video: this.props.videoComponent, }); } } this.setState({ data: [...this.state.data], page: this.state.page, }); return true; } return false; } async fillStartImages() { const needFetch = this.getItemsPerScreen(); const fetched = await this.fetchNextPage(needFetch < 100 ? needFetch : 100); if (fetched && this.state.data.length < needFetch) await this.fillStartImages(); } selectPropsImages() { var _a; const assets = this.state.selectedAssets; for (const selected of (_a = this.props.selected) !== null && _a !== void 0 ? _a : []) { assets.set(selected.id, { asset: selected, uncheck: () => { const assets = this.state.selectedAssets; assets.delete(selected.id); this.setState({ selectedAssets: new Map(assets) }); }, }); } this.setState({ selectedAssets: new Map(assets) }); } async componentDidMount() { if (this.props.selected && this.props.selected.length > 0) { this.selectPropsImages(); } if (this.state.data.length == 0) await this.fillStartImages(); } componentWillUnmount() { this._unmounted = true; } render() { return (React.createElement(React.Fragment, null, React.createElement(FlatList, { data: this.state.data, renderItem: ({ item }) => React.createElement(ImageBox, { item: item }), numColumns: this.getColumns(), keyExtractor: (item) => item.asset.id, initialNumToRender: this.getItemsPerScreen(), maxToRenderPerBatch: this.getItemsPerScreen(), onEndReached: () => this.fetchNextPage(this.getItemsPerScreen()), onMoveShouldSetResponderCapture: () => false, onStartShouldSetResponderCapture: () => false, ref: this.flatListRef, onScrollToIndexFailed: () => { }, onScroll: (e) => this.setState({ currentIndex: Math.ceil(e.nativeEvent.contentOffset.y / this.getImageSize().height), }), showsVerticalScrollIndicator: !this.props.timeSlider }), this.props.timeSlider && (React.createElement(ScrollTime, { data: this.state.data, flatList: this.flatListRef, galleryColumns: this.props.columns, currentIndex: this.state.currentIndex, selected: this.state.selectedAssets, height: this.props.timeSliderHeight, customSlider: this.props.slider })))); } } function ScrollTime({ data, flatList, galleryColumns, currentIndex, height = screen.height - 200, selected, customSlider, }) { var _a; const [balloons, setBalloons] = useState([]); const [button, setButton] = useState(); const [topTimeRelation, setTopTimeRelation] = useState({}); const [isMoving, setIsMoving] = useState(false); const [buttonProps, setButtonProps] = useState(); const [buttonLayout, setButtonLayout] = useState(); const scrollTo = (index, animated = true) => { var _a; (_a = flatList === null || flatList === void 0 ? void 0 : flatList.current) === null || _a === void 0 ? void 0 : _a.scrollToIndex({ index, animated, }); }; const getDay = (date) => { return new Date(date).toDateString(); }; useEffect(() => { if (data && data.length > 0) { const days = [ ...new Set(data.map((s) => getDay(s.asset.modificationTime))), ]; const relation = {}; for (let i = 0; i < days.length; i++) { const top = height * (i / (days.length - 1 || 1)); if (!isNaN(top)) { relation[days[i]] = top; } } setTopTimeRelation(relation); } else setTopTimeRelation({}); }, [data, height]); useEffect(() => { if (selected && selected.size > 0) { const balls = {}; for (const item of selected.values()) { const day = getDay(item.asset.modificationTime); const top = topTimeRelation[day]; if (top != undefined) { if (balls[top]) balls[top].quantity += 1; else { balls[top] = { top, date: new Date(), quantity: 1, styles: { top, position: 'absolute', }, }; } } } setBalloons(Object.values(balls)); } else setBalloons([]); }, [selected, topTimeRelation]); useEffect(() => { if (button && data && data.length > 0) { const item = data[currentIndex * galleryColumns]; if (item) { const day = getDay(item.asset.modificationTime); const top = topTimeRelation[day]; if (button.styles) button.styles.top = top; button.top = top; button.date = new Date(day); setButton({ ...button }); } } }, [currentIndex]); useEffect(() => { if (!isMoving && button) { const checked = data.findIndex((i) => getDay(i.asset.modificationTime) == getDay(button.date.getTime()) && i.isChecked()); const normal = checked == -1 && data.findIndex((i) => getDay(i.asset.modificationTime) == getDay(button.date.getTime())); const index = checked == -1 ? normal : checked; if (typeof index == 'number' && index != -1) { const scrollIndex = Math.ceil(index / galleryColumns); scrollTo(scrollIndex == 0 ? 0 : scrollIndex - 1); } } }, [isMoving]); const getClosestTop = (top) => { return Object.entries(topTimeRelation).reduce(([prevDay, prevTop], [currDay, currTop]) => { return Math.abs(currTop - top) < Math.abs(prevTop - top) ? [currDay, currTop] : [prevDay, prevTop]; }); }; const setSliderTop = (e) => { if (e && button && isMoving && Object.keys(topTimeRelation).length > 0) { const pageY = e.nativeEvent.pageY; e.currentTarget.measure((x, y, w, h, pX, pY) => { const [day, top] = getClosestTop(pageY - (pY - button.top)); if (top != button.top) { if (button.styles) button.styles.top = top; button.top = top; button.date = new Date(day); setButton({ ...button }); } }); } }; useEffect(() => { setButtonProps({ onResponderStart: () => setIsMoving(true), onResponderEnd: () => setIsMoving(false), onMoveShouldSetResponder: () => true, onStartShouldSetResponder: () => true, onResponderTerminationRequest: () => false, onResponderMove: setSliderTop, onLayout: (e) => setButtonLayout(e.nativeEvent.layout), }); }, [isMoving, topTimeRelation, button]); useEffect(() => { if (!button && data && data.length > 0) { setButton({ top: 0, date: new Date(data[0].asset.modificationTime), styles: { position: 'absolute', top: 0, }, }); } }, [button, data]); const realHeight = height + ((_a = buttonLayout === null || buttonLayout === void 0 ? void 0 : buttonLayout.height) !== null && _a !== void 0 ? _a : 0); if (customSlider) { const Slider = customSlider; return (React.createElement(Slider, { balloons: balloons, button: button, height: realHeight, isMoving: isMoving, buttonProps: buttonProps })); } return (React.createElement(View, { style: { ...styles.defaultSliderContainer, height: realHeight, } }, button && (React.createElement(View, { style: { ...styles.defaultSliderButton, opacity: isMoving ? 0.9 : 0.4, ...button.styles, }, ...buttonProps }, isMoving && (React.createElement(View, { style: styles.defaultSliderTime }, React.createElement(Text, null, button.date.toDateString()))), React.createElement(Svg, { viewBox: '0 0 562.392 562.391', fill: isMoving ? 'black' : 'white', width: 40, height: 40, style: { left: 5, top: 5 } }, React.createElement(Path, { d: 'M123.89,262.141h314.604c19.027,0,17.467-31.347,15.496-47.039c-0.605-4.841-3.636-11.971-6.438-15.967L303.965,16.533 c-12.577-22.044-32.968-22.044-45.551,0L114.845,199.111c-2.803,3.996-5.832,11.126-6.438,15.967 C106.43,230.776,104.863,262.141,123.89,262.141z' }), React.createElement(Path, { d: 'M114.845,363.274l143.569,182.584c12.577,22.044,32.968,22.044,45.551,0l143.587-182.609 c2.804-3.996,5.826-11.119,6.438-15.967c1.971-15.691,3.531-47.038-15.496-47.038H123.89c-19.027,0-17.46,31.365-15.483,47.062 C109.019,352.147,112.042,359.277,114.845,363.274z' })))), React.createElement(View, { style: { ...styles.defaultSlider, opacity: isMoving ? 1 : 0, }, pointerEvents: 'none' }), isMoving && balloons && balloons.map(({ top, quantity, styles: balloonStyles }) => (React.createElement(View, { key: top, style: { ...styles.defaultSliderBalloon, ...balloonStyles, }, pointerEvents: 'none' }, React.createElement(Text, { style: { color: 'white', fontSize: 17 } }, quantity)))))); } function DefaultAlbum(props) { return (React.createElement(TouchableOpacity, { style: styles.defaultAlbumContainer, onPress: () => props.goToGallery(props.album) }, React.createElement(Image, { style: styles.defaultAlbumImage, source: { uri: props.thumb.uri } }), React.createElement(Text, { style: { padding: 10, fontSize: 16 } }, props.album.title))); } function DefaultHeader(props) { return (React.createElement(View, { style: styles.defaultHeaderContainer }, props.view == 'gallery' && (React.createElement(React.Fragment, null, !props.noAlbums && (React.createElement(TouchableOpacity, { style: { width: 30, height: 30 }, onPress: props.goToAlbum }, React.createElement(Svg, { viewBox: '0 0 256 512', ...props }, React.createElement(Path, { fill: 'black', d: 'M192 448c-8.188 0-16.38-3.125-22.62-9.375l-160-160c-12.5-12.5-12.5-32.75 0-45.25l160-160c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25L77.25 256l137.4 137.4c12.5 12.5 12.5 32.75 0 45.25C208.4 444.9 200.2 448 192 448z' })))), props.imagesPicked == 0 && (React.createElement(React.Fragment, null, props.album && (React.createElement(Text, { style: { fontSize: 20 } }, props.album.title)), !props.album && props.multiple && (React.createElement(Text, { style: { fontSize: 20 } }, "Select the images")), !props.album && !props.multiple && (React.createElement(Text, { style: { fontSize: 20 } }, "Select an image")))), props.imagesPicked > 0 && (React.createElement(React.Fragment, null, props.multiple && (React.createElement(Text, { style: { fontSize: 20 } }, "Selected ", props.imagesPicked, " images")), !props.multiple && (React.createElement(Text, { style: { fontSize: 20 } }, "Selected")), React.createElement(Pressable, { style: styles.defaultHeaderButton, onPress: props.save }, React.createElement(Text, { style: styles.defaultHeaderButtonText }, "SAVE")))))), props.view == 'album' && (React.createElement(React.Fragment, null, React.createElement(Text, { style: { fontSize: 20 } }, "Select an album"))))); } export function ImagePicker(props) { var _a, _b, _c, _d, _e, _f; const [status, requestPermission] = MediaLibrary.usePermissions(); const [albums, setAlbums] = useState(); const [selectedAlbum, setSelectedAlbum] = useState(props.selectedAlbum); const [selectedAssets, setSelectedAssets] = useState((_a = props.selected) !== null && _a !== void 0 ? _a : []); async function askPermission() { let cancel = false; try { const permission = await requestPermission(); if (!permission.granted) cancel = true; } catch (_a) { cancel = true; } if (cancel && props.onCancel) { props.onCancel(); } } function goToAlbum() { setSelectedAlbum(undefined); setSelectedAssets([]); } function handleBackPress() { if (selectedAlbum) { goToAlbum(); } else if (props.onCancel) { props.onCancel(); } return true; } useEffect(() => { if (status && !status.granted) { if (status.canAskAgain) { askPermission(); } else if (props.onCancel) { props.onCancel(); } } }, [status]); useEffect(() => { const backHandler = BackHandler.addEventListener('hardwareBackPress', handleBackPress); return () => { backHandler.remove(); }; }, [selectedAlbum]); useEffect(() => { if (props.onSelectAlbum) props.onSelectAlbum(selectedAlbum); }, [selectedAlbum]); const noImages = props.image === false; async function getAlbums() { const data = []; const albums = await MediaLibrary.getAlbumsAsync({ includeSmartAlbums: true, }); for (const album of albums) { const types = []; if (!noImages) types.push(MediaLibrary.MediaType.photo); if (props.video) types.push(MediaLibrary.MediaType.video); const page = await MediaLibrary.getAssetsAsync({ first: 1, album, sortBy: [MediaLibrary.SortBy.modificationTime], mediaType: types, }); if (page.assets.length > 0) { data.push({ album, thumb: page.assets[0], goToGallery: setSelectedAlbum, }); } } setAlbums(data); } useEffect(() => { if (status && status.granted && !albums) { getAlbums(); } }, [status]); const Header = ((_b = props.theme) === null || _b === void 0 ? void 0 : _b.header) ? props.theme.header : DefaultHeader; const Album = ((_c = props.theme) === null || _c === void 0 ? void 0 : _c.album) ? props.theme.album : DefaultAlbum; if (props.noAlbums || selectedAssets.length > 0 || selectedAlbum) { return (React.createElement(View, { style: styles.root }, React.createElement(View, { style: styles.rootHeader }, React.createElement(Header, { view: 'gallery', imagesPicked: selectedAssets.length, picked: selectedAssets.length > 0, multiple: props.multiple || false, noAlbums: props.noAlbums || false, album: selectedAlbum, goToAlbum: goToAlbum, save: () => (props.onSave ? props.onSave(selectedAssets) : void 0) })), React.createElement(View, { style: styles.rootBody }, React.createElement(ImagePickerCarousel, { onSelect: setSelectedAssets, albumID: selectedAlbum ? selectedAlbum.id : undefined, multiple: props.multiple || false, columns: props.galleryColumns || 2, check: (_d = props.theme) === null || _d === void 0 ? void 0 : _d.check, selected: selectedAssets, max: props.limit, timeSlider: props.timeSlider, timeSliderHeight: props.timeSliderHeight, slider: (_e = props.theme) === null || _e === void 0 ? void 0 : _e.slider, video: props.video, videoComponent: (_f = props.theme) === null || _f === void 0 ? void 0 : _f.video, image: noImages ? false : true })))); } else { return (React.createElement(View, { style: styles.root }, React.createElement(View, { style: styles.rootHeader }, React.createElement(Header, { view: 'album', imagesPicked: 0, multiple: props.multiple || false, picked: false, noAlbums: props.noAlbums || false })), albums && (React.createElement(FlatList, { style: styles.rootBody, data: albums, numColumns: props.albumColumns || 2, keyExtractor: (d) => d.album.id, renderItem: ({ item }) => React.createElement(Album, { ...item }) })), !albums && (React.createElement(View, { style: { ...styles.rootBody, alignContent: 'center', justifyContent: 'center', } }, React.createElement(ActivityIndicator, { size: 48, color: 'blue' }))))); } } //# sourceMappingURL=index.js.map