/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import type {ImageId, ImageData, ImagesList} from './api.js'; import type {ImageEventWithId} from './index.js'; import { Toolbar, Button, Spacer, colors, FlexBox, FlexRow, FlexColumn, LoadingIndicator, styled, Select, ToggleButton, Text, } from 'flipper'; import MultipleSelect from './MultipleSelect.js'; import type {ImagesMap} from './ImagePool.js'; import {clipboard} from 'electron'; import {PureComponent} from 'react'; function formatMB(bytes: number) { return Math.floor(bytes / (1024 * 1024)) + 'MB'; } function formatKB(bytes: number) { return Math.floor(bytes / 1024) + 'KB'; } type ToggleProps = {| label: string, onClick?: (newValue: boolean) => void, toggled: boolean, |}; const ToolbarToggleButton = styled(ToggleButton)(_props => ({ alignSelf: 'center', marginRight: 4, minWidth: 30, })); const ToggleLabel = styled(Text)(_props => ({ whiteSpace: 'nowrap', })); function Toggle(props: ToggleProps) { return ( <> { props.onClick && props.onClick(!props.toggled); }} toggled={props.toggled} /> {props.label} ); } type ImagesCacheOverviewProps = { onColdStartChange: (checked: boolean) => void, coldStartFilter: boolean, allSurfacesOption: string, surfaceOptions: Set, selectedSurfaces: Set, onChangeSurface: (key: Set) => void, images: ImagesList, onClear: (type: string) => void, onTrimMemory: () => void, onRefresh: () => void, onEnableDebugOverlay: (enabled: boolean) => void, isDebugOverlayEnabled: boolean, onEnableAutoRefresh: (enabled: boolean) => void, isAutoRefreshEnabled: boolean, onImageSelected: (selectedImage: ImageId) => void, imagesMap: ImagesMap, events: Array, onTrackLeaks: (enabled: boolean) => void, isLeakTrackingEnabled: boolean, }; type ImagesCacheOverviewState = {| selectedImage: ?ImageId, size: number, |}; const StyledSelect = styled(Select)(props => ({ marginLeft: 6, marginRight: 6, height: '100%', maxWidth: 164, })); export default class ImagesCacheOverview extends PureComponent< ImagesCacheOverviewProps, ImagesCacheOverviewState, > { static Container = styled(FlexColumn)({ backgroundColor: colors.white, }); static Content = styled(FlexColumn)({ flex: 1, overflow: 'auto', }); static Empty = styled(FlexBox)({ alignItems: 'center', height: '100%', justifyContent: 'center', width: '100%', }); state = { selectedImage: undefined, size: 150, }; onImageSelected = (selectedImage: ImageId) => { this.setState({selectedImage}); this.props.onImageSelected(selectedImage); }; onKeyDown = (e: SyntheticKeyboardEvent<*>) => { const selectedImage = this.state.selectedImage; const imagesMap = this.props.imagesMap; if (selectedImage) { if ((e.ctrlKey || e.metaKey) && e.key === 'c') { clipboard.writeText(String(imagesMap[selectedImage])); e.preventDefault(); } } }; onEnableDebugOverlayToggled = () => { this.props.onEnableDebugOverlay(!this.props.isDebugOverlayEnabled); }; onEnableAutoRefreshToggled = () => { this.props.onEnableAutoRefresh(!this.props.isAutoRefreshEnabled); }; onChangeSize = (e: SyntheticInputEvent) => this.setState({size: parseInt(e.target.value, 10)}); onSurfaceOptionsChange = (selectedItem: string, checked: boolean) => { const {allSurfacesOption, surfaceOptions} = this.props; const selectedSurfaces = new Set([...this.props.selectedSurfaces]); if (checked && selectedItem === allSurfacesOption) { this.props.onChangeSurface(surfaceOptions); return; } if (!checked && selectedSurfaces.size === 1) { return; } if (selectedItem !== allSurfacesOption) { selectedSurfaces.delete(allSurfacesOption); if (checked) { selectedSurfaces.add(selectedItem); } else { selectedSurfaces.delete(selectedItem); } } if ( surfaceOptions.size - selectedSurfaces.size === 1 && !selectedSurfaces.has(allSurfacesOption) ) { selectedSurfaces.add(allSurfacesOption); } this.props.onChangeSurface(selectedSurfaces); }; render() { const hasImages = this.props.images.reduce( (c, cacheInfo) => c + cacheInfo.imageIds.length, 0, ) > 0; return ( {!hasImages ? ( ) : ( {this.props.images.map((data, index) => { const maxSize = data.maxSizeBytes; const subtitle = maxSize ? formatMB(data.sizeBytes) + ' / ' + formatMB(maxSize) : formatMB(data.sizeBytes); const onClear = data.clearKey ? () => this.props.onClear(data.clearKey) : null; return ( ); })} )} ); } } class ImageGrid extends PureComponent<{ title: string, subtitle: string, images: Array, selectedImage: ?ImageId, onImageSelected: (image: ImageId) => void, onClear: ?() => void, imagesMap: ImagesMap, size: number, events: Array, }> { static Content = styled('div')({ paddingLeft: 15, }); render() { const {images, onImageSelected, selectedImage} = this.props; if (images.length === 0) { return null; } return [ , {images.map(imageId => ( e.imageIds.includes(imageId)).length } /> ))} , ]; } } class ImageGridHeader extends PureComponent<{ title: string, subtitle: string, onClear: ?() => void, }> { static Container = styled(FlexRow)({ color: colors.dark70, paddingTop: 10, paddingBottom: 10, marginLeft: 15, marginRight: 15, marginBottom: 15, borderBottom: `1px solid ${colors.light10}`, flexShrink: 0, alignItems: 'center', position: 'sticky', top: 0, left: 0, right: 0, backgroundColor: colors.white, zIndex: 3, }); static Heading = styled('span')({ fontSize: 22, fontWeight: 600, }); static Subtitle = styled('span')({ fontSize: 22, fontWeight: 300, marginLeft: 15, }); static ClearButton = styled(Button)({ alignSelf: 'center', height: 30, marginLeft: 'auto', width: 100, }); render() { return ( {this.props.title} {this.props.subtitle} {this.props.onClear ? ( Clear Cache ) : null} ); } } class ImageItem extends PureComponent<{ imageId: ImageId, image: ?ImageData, selected: boolean, onSelected: (image: ImageId) => void, size: number, numberOfRequests: number, }> { static Container = styled(FlexBox)(({size}) => ({ float: 'left', alignItems: 'center', justifyContent: 'center', flexShrink: 0, height: size, width: size, borderRadius: 4, marginRight: 15, marginBottom: 15, backgroundColor: colors.light02, })); static Image = styled('img')({ borderRadius: 4, maxHeight: '100%', maxWidth: '100%', objectFit: 'contain', }); static Loading = styled('span')({ padding: '0 0', }); static SelectedHighlight = styled('div')(props => ({ borderColor: colors.highlight, borderStyle: 'solid', borderWidth: props.selected ? 3 : 0, borderRadius: 4, boxShadow: props.selected ? `inset 0 0 0 1px ${colors.white}` : 'none', bottom: 0, left: 0, position: 'absolute', right: 0, top: 0, })); static HoverOverlay = styled(FlexColumn)(props => ({ alignItems: 'center', backgroundColor: colors.whiteAlpha80, bottom: props.selected ? 4 : 0, fontSize: props.size > 100 ? 16 : 11, justifyContent: 'center', left: props.selected ? 4 : 0, opacity: 0, position: 'absolute', right: props.selected ? 4 : 0, top: props.selected ? 4 : 0, overflow: 'hidden', transition: '.1s opacity', '&:hover': { opacity: 1, }, })); static MemoryLabel = styled('span')({ fontWeight: 600, marginBottom: 6, }); static SizeLabel = styled('span')({ fontWeight: 300, }); static Events = styled('div')({ position: 'absolute', top: -5, right: -5, color: colors.white, backgroundColor: colors.highlight, fontWeight: 600, borderRadius: 10, fontSize: '0.85em', zIndex: 2, lineHeight: '20px', width: 20, textAlign: 'center', }); static defaultProps = { size: 150, }; onClick = () => { this.props.onSelected(this.props.imageId); }; render() { const {image, selected, size, numberOfRequests} = this.props; return ( {numberOfRequests > 0 && image != null && ( {numberOfRequests} )} {image != null ? ( ) : ( )} {image != null && ( {formatKB(image.sizeBytes)} {image.width}×{image.height} )} ); } }