├── .gitignore ├── .storybook ├── addons.ts ├── config.ts └── webpack.config.js ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile.dev ├── LICENSE ├── README.md ├── docker-compose.yml ├── package.json ├── public ├── cyto.png ├── favicon.ico ├── index.html └── manifest.json ├── pull_request_template.md ├── src ├── App.tsx ├── components │ └── Image │ │ └── Image.js ├── constants.ts ├── containers │ ├── ConnectedApplication.ts │ ├── ConnectedDeleteImageDialog.ts │ ├── ConnectedGallery.ts │ ├── ConnectedImageViewer.ts │ ├── ConnectedImportImagesButton.ts │ ├── ConnectedItem.ts │ ├── ConnectedItemCategoryMenu.ts │ ├── ConnectedItemLabel.ts │ └── index.ts ├── demos │ ├── cifar10.cyto │ ├── mnist.cyto │ └── worms.piximi ├── i18n.js ├── index.css ├── index.tsx ├── pages │ ├── image │ │ ├── BrightnessSlider │ │ │ ├── BrightnessSlider.css.tsx │ │ │ └── BrightnessSlider.tsx │ │ ├── ChannelSelection │ │ │ └── ChannelSelection.js │ │ ├── ContrastSlider │ │ │ ├── ContrastSlider.css.tsx │ │ │ └── ContrastSlider.tsx │ │ ├── ImageHistogram │ │ │ ├── ImageHistogram.css.tsx │ │ │ └── ImageHistogram.js │ │ ├── ImageViewer │ │ │ ├── ImageViewer.css.tsx │ │ │ └── ImageViewer.tsx │ │ ├── ImageViewerDialog │ │ │ ├── ImageViewerDialog.css.tsx │ │ │ └── ImageViewerDialog.tsx │ │ ├── ImageViewerExposureDrawer │ │ │ ├── ImageViewerExposureDrawer.css.tsx │ │ │ └── ImageViewerExposureDrawer.tsx │ │ └── index.ts │ └── images │ │ ├── Application │ │ ├── Application.css.ts │ │ └── Application.tsx │ │ ├── DeleteButton │ │ ├── DeleteButton.css.ts │ │ └── DeleteButton.tsx │ │ ├── DeleteImageDialog │ │ └── DeleteImageDialog.tsx │ │ ├── Gallery │ │ ├── Gallery.css │ │ └── Gallery.js │ │ ├── GalleryCustomDragLayer │ │ ├── GalleryCustomDragLayer.css │ │ └── GalleryCustomDragLayer.js │ │ ├── GalleryItem │ │ └── GalleryItem.tsx │ │ ├── GalleryItemCategoryMenu │ │ └── GalleryItemCategoryMenu.tsx │ │ ├── GalleryItemLabel │ │ ├── GalleryItemLabel.css.ts │ │ └── GalleryItemLabel.tsx │ │ ├── GalleryItems │ │ └── GalleryItems.tsx │ │ ├── GallerySelectionBox │ │ └── GallerySelectionBox.tsx │ │ ├── ImageSearch │ │ └── ImageSearch.tsx │ │ ├── ImportImagesButton │ │ ├── ImportImagesButton.css.ts │ │ ├── ImportImagesButton.stories.tsx │ │ └── ImportImagesButton.tsx │ │ ├── Logo │ │ ├── Logo.test.tsx │ │ └── Logo.tsx │ │ ├── PrimaryAppBar │ │ ├── ConnectedPrimaryAppBar.ts │ │ ├── PrimaryAppBar.css.ts │ │ └── PrimaryAppBar.tsx │ │ ├── helper.ts │ │ └── index.ts ├── react-app-env.d.ts ├── selectors │ └── images.js ├── serviceWorker.ts └── setupTests.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env.development.local 3 | .env.local 4 | .env.production.local 5 | .env.test.local 6 | .idea 7 | /build 8 | /coverage 9 | /node_modules 10 | npm-debug.log* 11 | package-lock.json 12 | yarn-debug.log* 13 | yarn-error.log* 14 | -------------------------------------------------------------------------------- /.storybook/addons.ts: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-viewport/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.ts: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | const req = require.context('../src', true, /\.stories\.tsx$/); 4 | 5 | function stories() { 6 | req.keys().forEach(req); 7 | } 8 | 9 | configure(stories, module); 10 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ config, mode }) => { 2 | config.module.rules.push({ 3 | test: /\.(ts|tsx)$/, 4 | loader: require.resolve('babel-loader'), 5 | options: { 6 | presets: [['react-app', { flow: false, typescript: true }]], 7 | }, 8 | }); 9 | 10 | config.resolve.extensions.push('.ts', '.tsx'); 11 | 12 | return config; 13 | }; 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '12' 4 | 5 | env: 6 | - NODE_OPTIONS=--max-old-space-size=4096 7 | 8 | warnings_are_errors: false 9 | 10 | stages: 11 | - Testing 12 | - name: Dev Deployment 13 | if: branch = develop 14 | - name: Prod Deployment 15 | if: branch = production 16 | 17 | cache: npm 18 | before_deploy: | 19 | function keep_alive() { 20 | while true; do 21 | echo -en "\a" 22 | sleep 60 23 | done 24 | } 25 | keep_alive & 26 | 27 | jobs: 28 | include: 29 | - stage: Testing 30 | script: npm test 31 | 32 | - stage: Dev Deployment 33 | script: if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then CI=false npm run build && ls; else echo "Skip build for Pull Request"; fi 34 | deploy: 35 | provider: gcs 36 | on: 37 | branch: develop 38 | access_key_id: GOOGAPI2JYRMRONGVOXT 39 | acl: public-read 40 | bucket: master.piximi.app # TODO: request for new domain: dev.piximi.app and change this 41 | cache_control: "max-age=no-cache" 42 | detect_encoding: true 43 | repo: piximi/application 44 | secret_access_key: 45 | secure: Xv/fFzUooy3JjZG/296tkoJW40zgIQM/xTURF/m7tOegruReVXyt8AIuSeVoTBMeUoDIfvj1ZoIIFFFaAkihgib8ixFJZUl51lv1v2q45GzW0aqztKyLYDfXhkdebVK5VaEw1ggdD1hlHxzadwKLESUIAsQzPCsGyMbR59LaSRoNzY4xWOHtBl+e/KWPjpoT9F5K/5YTv45UMnMvVuQ+7gvfAUPybh+xQ8FbZl5y1BGS91Fa9PFfclRCdZOiu1S62Z0Evouj4ECRE+vWsihm6AsAm49EEeoLJQxyGzppmlSb+lTHtOv6EaQHJ8fwGTjDXlCqMI5sfG2pvK1DjXitXuNTyMgijFdyvgm/yQanXamKxPBqRTsvz6Shop+MWJ7MnC8SRExbYC3wHkgoqNfI9T8IMezxeN6KXJGBua2dsNBNZ/iVzeB4IClNAc0qguFEJiWTjXMGjAxSqGtnEMGe8gPsv6bdaZXnt3TWZqHiN7KtkhtepsM9NTWfGb/plHLnnhrMWeBwI6BsYyY6nRBkIpA3bCvMFyb5SWvamXMspBWKpMLGiqseceElL7kD9hfSlmB7VDt2piyuESUsvgp9eR9CGTq9BXFiLfAPonGPLArqxLLnIr9bQhrjp6kRcEPtUkE86pq/5avK3i1itq8pr2EdIIUhZCeVJbqjSmlDv7U= 46 | skip_cleanup: true 47 | 48 | - stage: Prod Deployment 49 | script: if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then CI=false npm run build && ls; else echo "Skip build for Pull Request"; fi 50 | deploy: 51 | provider: gcs 52 | on: 53 | branch: production 54 | access_key_id: GOOGAPI2JYRMRONGVOXT 55 | acl: public-read 56 | bucket: www.piximi.app 57 | cache_control: "max-age=no-cache" 58 | detect_encoding: true 59 | repo: piximi/application 60 | secret_access_key: 61 | secure: Xv/fFzUooy3JjZG/296tkoJW40zgIQM/xTURF/m7tOegruReVXyt8AIuSeVoTBMeUoDIfvj1ZoIIFFFaAkihgib8ixFJZUl51lv1v2q45GzW0aqztKyLYDfXhkdebVK5VaEw1ggdD1hlHxzadwKLESUIAsQzPCsGyMbR59LaSRoNzY4xWOHtBl+e/KWPjpoT9F5K/5YTv45UMnMvVuQ+7gvfAUPybh+xQ8FbZl5y1BGS91Fa9PFfclRCdZOiu1S62Z0Evouj4ECRE+vWsihm6AsAm49EEeoLJQxyGzppmlSb+lTHtOv6EaQHJ8fwGTjDXlCqMI5sfG2pvK1DjXitXuNTyMgijFdyvgm/yQanXamKxPBqRTsvz6Shop+MWJ7MnC8SRExbYC3wHkgoqNfI9T8IMezxeN6KXJGBua2dsNBNZ/iVzeB4IClNAc0qguFEJiWTjXMGjAxSqGtnEMGe8gPsv6bdaZXnt3TWZqHiN7KtkhtepsM9NTWfGb/plHLnnhrMWeBwI6BsYyY6nRBkIpA3bCvMFyb5SWvamXMspBWKpMLGiqseceElL7kD9hfSlmB7VDt2piyuESUsvgp9eR9CGTq9BXFiLfAPonGPLArqxLLnIr9bQhrjp6kRcEPtUkE86pq/5avK3i1itq8pr2EdIIUhZCeVJbqjSmlDv7U= 62 | skip_cleanup: true 63 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## How to Contribute 20 | 21 | 1. Fork the repository 22 | 2. Make the fix 23 | 3. Submit a pull request to the project owner. 24 | 25 | ### Fork the repository 26 | 27 | Forking a repository is a simple two-step process. 28 | 1. On GitHub, navigate to the piximi/application repository. 29 | 2. In the top-right corner of the page, click Fork. 30 | 31 | ### Keep your fork synced 32 | 33 | You might fork a project in order to propose changes to the upstream, or original, repository. In this case, it's good practice to regularly sync your fork with the upstream repository. To do this, you'll need to use Git on the command line. You can practice setting the upstream repository using the same piximi/application repository you just forked! 34 | 35 | #### Step 1: Create a local clone of your fork 36 | 37 | 1. On GitHub, navigate to your fork of the application repository. 38 | 39 | 2. Under the repository name, click Clone or download. 40 | 41 | 3. In the Clone with HTTPs section, click the copy to clipboard symbol to copy the clone URL for the repository. 42 | 43 | 4. Open Terminal. 44 | 45 | 5. Type git clone, and then paste the URL you copied in Step 2. It will look like this, with your GitHub username instead of YOUR-USERNAME: 46 | ``` 47 | git clone https://github.com/YOUR-USERNAME/application 48 | ``` 49 | 6. Press Enter. Your local clone will be created. 50 | 51 | Now, you have a local copy of your fork of the application repository! 52 | 53 | #### Step 2: Configure Git to sync your fork with the original application repository 54 | 55 | When you fork a project in order to propose changes to the original repository, you can configure Git to pull changes from the original, or upstream, repository into the local clone of your fork. 56 | 57 | 1. On GitHub, navigate to the piximi/application repository. 58 | 59 | 2. Under the repository name, click Clone or download 60 | 61 | 3. In the Clone with HTTPs section, click the copy to clipboard symbol to copy the clone URL for the repository. 62 | 63 | 4. Open Terminal. 64 | 65 | 5. Change directories to the location of the fork you cloned in **Step 1: Create a local clone of your fork**. 66 | 67 | 6. Type ``` git remote -v ``` and press Enter. You'll see the current configured remote repository for your fork. 68 | 69 | 7. Type ``` git remote add upstream ```, and then paste the URL you copied in Step 2 and press Enter. It will look like this: 70 | ``` git remote add upstream https://github.com/piximi/application.git ``` 71 | 72 | 8. To verify the new upstream repository you've specified for your fork, type ``` git remote -v ``` again. You should see the URL for your fork as origin, and the URL for the original repository as upstream. 73 | 74 | Now, you can keep your fork synced with the upstream repository with a few Git commands. 75 | 76 | 1. To pull from the original CYTO AI repository use: ``` git pull upstream master ``` 77 | 2. To push to your own fork use: ``` git push origin master ``` 78 | 79 | ## Code of Conduct 80 | 81 | ### Our Pledge 82 | 83 | In the interest of fostering an open and welcoming environment, we as 84 | contributors and maintainers pledge to making participation in our project and 85 | our community a harassment-free experience for everyone, regardless of age, body 86 | size, disability, ethnicity, gender identity and expression, level of experience, 87 | nationality, personal appearance, race, religion, or sexual identity and 88 | orientation. 89 | 90 | ### Our Standards 91 | 92 | Examples of behavior that contributes to creating a positive environment 93 | include: 94 | 95 | * Using welcoming and inclusive language 96 | * Being respectful of differing viewpoints and experiences 97 | * Gracefully accepting constructive criticism 98 | * Focusing on what is best for the community 99 | * Showing empathy towards other community members 100 | 101 | Examples of unacceptable behavior by participants include: 102 | 103 | * The use of sexualized language or imagery and unwelcome sexual attention or 104 | advances 105 | * Trolling, insulting/derogatory comments, and personal or political attacks 106 | * Public or private harassment 107 | * Publishing others' private information, such as a physical or electronic 108 | address, without explicit permission 109 | * Other conduct which could reasonably be considered inappropriate in a 110 | professional setting 111 | 112 | ### Our Responsibilities 113 | 114 | Project maintainers are responsible for clarifying the standards of acceptable 115 | behavior and are expected to take appropriate and fair corrective action in 116 | response to any instances of unacceptable behavior. 117 | 118 | Project maintainers have the right and responsibility to remove, edit, or 119 | reject comments, commits, code, wiki edits, issues, and other contributions 120 | that are not aligned to this Code of Conduct, or to ban temporarily or 121 | permanently any contributor for other behaviors that they deem inappropriate, 122 | threatening, offensive, or harmful. 123 | 124 | ### Scope 125 | 126 | This Code of Conduct applies both within project spaces and in public spaces 127 | when an individual is representing the project or its community. Examples of 128 | representing a project or community include using an official project e-mail 129 | address, posting via an official social media account, or acting as an appointed 130 | representative at an online or offline event. Representation of a project may be 131 | further defined and clarified by project maintainers. 132 | 133 | ### Enforcement 134 | 135 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 136 | reported by contacting allen.goodman@icloud.com or any other project team member 137 | with whom you feel comfortable communicating. All 138 | complaints will be reviewed and investigated and will result in a response that 139 | is deemed necessary and appropriate to the circumstances. The project team is 140 | obligated to maintain confidentiality with regard to the reporter of an incident. 141 | Further details of specific enforcement policies may be posted separately. 142 | 143 | Project maintainers who do not follow or enforce the Code of Conduct in good 144 | faith may face temporary or permanent repercussions as determined by other 145 | members of the project's leadership. 146 | 147 | ### Attribution 148 | 149 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 150 | available at [http://contributor-covenant.org/version/1/4][version] 151 | 152 | [homepage]: http://contributor-covenant.org 153 | [version]: http://contributor-covenant.org/version/1/4/ 154 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /application 4 | 5 | ADD package-lock.json /application/ 6 | ADD package.json /application/ 7 | 8 | RUN npm install 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD 3-Clause License 2 | 3 | Copyright © 2019 Cyto. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, 9 | this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the Broad Institute, Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED “AS IS.” BROAD MAKES NO EXPRESS OR IMPLIED 20 | REPRESENTATIONS OR WARRANTIES OF ANY KIND REGARDING THE SOFTWARE AND 21 | COPYRIGHT, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, 22 | FITNESS FOR A PARTICULAR PURPOSE, CONFORMITY WITH ANY DOCUMENTATION, 23 | NON-INFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, WHETHER OR NOT 24 | DISCOVERABLE. IN NO EVENT SHALL BROAD, THE COPYRIGHT HOLDERS, OR CONTRIBUTORS 25 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO PROCUREMENT OF SUBSTITUTE 27 | GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 28 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 29 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 30 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF, HAVE REASON TO KNOW, OR IN 31 | FACT SHALL KNOW OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | If, by operation of law or otherwise, any of the aforementioned warranty 34 | disclaimers are determined inapplicable, your sole remedy, regardless of the 35 | form of action, including, but not limited to, negligence and strict 36 | liability, shall be replacement of the software with an updated version if one 37 | exists. 38 | 39 | Development of CellProfiler has been funded in whole or in part with federal 40 | funds from the National Institutes of Health, the National Science Foundation, 41 | and the Human Frontier Science Program. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | Old piximi monorepo -> see piximi/piximi 3 | 4 | # Piximi 5 | 6 | [![Travis (.org) branch](https://img.shields.io/travis/piximi/application/develop.svg?label=Develop%20Build%20on%20Travis%20CI%20&style=flat-square&logo=Travis)](https://travis-ci.org/piximi/application) 7 | [![Travis (.org) branch](https://img.shields.io/travis/piximi/application/production.svg?label=Production%20Build%20on%20Travis%20CI%20&style=flat-square&logo=Travis)](https://travis-ci.org/piximi/application) 8 | 9 | A web-based deep learning tool for classification of human cells, created with Tensorflow.js and React. 10 | https://www.piximi.app 11 | 12 | **Please make sure to clean your browser cache, since we are continuously developing new features:)** 13 | 14 | ## Getting Started 15 | 16 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system. 17 | 18 | ## Development 19 | 20 | ### Set up your development environment 21 | 22 | #### Option 1: Develop locally without Docker 23 | 1. Install npm and Node.js: https://www.npmjs.com/get-npm 24 | 2. Verify the installation by: `node --version` and `npm -v` 25 | 3. Install the node dependencies with `npm install` 26 | 4. Clone the project: `git clone https://github.com/piximi/application.git` and move into the repo with `cd application` 27 | 5. Run `npm start` from within the root directory of the repo, which should pop up a new tab your browser pointing at http://localhost:3000 28 | 29 | #### Option 2: Develop using Docker-Compose 30 | 1. Clone the project: `git clone https://github.com/piximi/application.git` and move into the repo with `cd application` 31 | 2. Make sure you have docker-compose installed: `docker-compose version` 32 | 3. From within the root directory, run `docker-compose up`. (Note you might want to run `docker-compose up --build` whenever you make changes to the `Dockerfile.dev`.) 33 | 4. You should be able to see the application page at http://localhost:3000. _You won't need to restart the server as long as the docker-compose is running, the app will be dynamically relaoded._ 34 | 35 | ### Development process 36 | 37 | There is no _master_ branch in this repo, and `develop` is the default branch that you should branch off when you want to make changes. Although in some rare cases the maintainers of the repo could merge hot-fix PRs directly to `production` branch, 38 | in general developers follow the process: 39 | 40 | 1. Branch off the latest remote `develop` branch either in a forked repo or the same repo (requries write access to the repo). 41 | 2. Make changes, commit the changes and create a PR against the `develop` branch. 42 | 3. Make sure all of the tests pass. 43 | 4. Ask the maintainers of the repo to merge the PR for you, usually they will be the reviewers of your PR. 44 | 5. PRs merged to `develop` will trigger a deployment to the dev server. 45 | 46 | Maintainers can merge the `develop` to `production` to trigger a deployment/promotion to the prod server. External contributors will need to follow the CONTRIBUTING guide. 47 | 48 | ## Deployment 49 | 50 | You can deploy by running: ``` npm deploy ``` 51 | 52 | You can define where to deploy in the package.json file under: ** homepage ** 53 | 54 | ## Contributing 55 | 56 | Please read [CONTRIBUTING.md](https://github.com/piximi/application/blob/develop/CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us. 57 | 58 | ## Versioning 59 | 60 | We use [SemVer](http://semver.org/) for versioning. 61 | 62 | ## License 63 | 64 | This project is licensed under the BSD 3-Clause License - see the [LICENSE.md](LICENSE) file for details 65 | 66 | ## Acknowledgments 67 | 68 | * Hat tip to anyone whose code was used 69 | * Inspiration 70 | * etc 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | application: 4 | container_name: piximi-application 5 | build: 6 | context: . 7 | dockerfile: Dockerfile.dev 8 | command: 9 | ["npm", "start"] 10 | volumes: 11 | # Only mount a whitelist of top-level files/directories; specifically 12 | # exclude node_modules here. 13 | - ./src:/application/src 14 | - ./public:/application/public 15 | ports: 16 | - "3000:3000" 17 | environment: 18 | - NODE_ENV=dev 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "piximi", 3 | "version": "0.0.0", 4 | "private": false, 5 | "dependencies": { 6 | "@material-ui/core": "4.5.0", 7 | "@material-ui/icons": "4.5.1", 8 | "@material-ui/styles": "4.5.0", 9 | "@piximi/autotuner": "^1.0.2", 10 | "@piximi/components": "^0.1.16", 11 | "@piximi/hooks": "^0.1.11", 12 | "@piximi/navigation-drawer": "^1.0.11", 13 | "@piximi/store": "^0.1.30", 14 | "@piximi/translations": "^0.1.2", 15 | "@piximi/types": "^0.1.8", 16 | "@storybook/addon-actions": "^5.1.9", 17 | "@storybook/addon-links": "^5.0.11", 18 | "@storybook/addon-viewport": "^5.1.9", 19 | "@storybook/addons": "^5.0.11", 20 | "@storybook/react": "^5.0.11", 21 | "@storybook/storybook-deployer": "^2.8.1", 22 | "@tensorflow/tfjs": "^1.2.11", 23 | "@tensorflow/tfjs-vis": "^1.2.0", 24 | "axios": "^0.19.0", 25 | "base64-arraybuffer": "^0.2.0", 26 | "classnames": "^2.2.6", 27 | "color": "^3.1.2", 28 | "d3": "^5.12.0", 29 | "file-saver": "^2.0.2", 30 | "filtrex": "^2.0.0", 31 | "i18next": "^17.1.0", 32 | "image-js": "^0.21.7", 33 | "js-file-download": "^0.4.7", 34 | "json2csv": "^4.5.4", 35 | "localforage": "^1.7.3", 36 | "lodash": "^4.17.11", 37 | "react": "^16.12.0", 38 | "react-color": "^2.17.3", 39 | "react-dnd": "^9.4.0", 40 | "react-dnd-html5-backend": "^9.4.0", 41 | "react-dom": "^16.12.0", 42 | "react-i18next": "^10.13.1", 43 | "react-portal": "^4.2.0", 44 | "react-redux": "^7.1.1", 45 | "react-scripts": "^3.2.0", 46 | "react-spinners": "^0.6.1", 47 | "react-virtualized": "^9.21.1", 48 | "redux": "^4.0.4", 49 | "redux-persist": "^6.0.0", 50 | "redux-starter-kit": "^0.7.0", 51 | "redux-thunk": "^2.3.0", 52 | "reselect": "^4.0.0", 53 | "string-hash": "^1.1.3", 54 | "styled-components": "^4.4.0", 55 | "upgrade": "^1.1.0", 56 | "uuid": "^3.3.3" 57 | }, 58 | "scripts": { 59 | "storybook": "start-storybook", 60 | "analyze": "source-map-explorer build/static/js/main.*", 61 | "build": "react-scripts build", 62 | "deploy": "gh-pages -d build", 63 | "eject": "react-scripts eject", 64 | "flow": "flow", 65 | "precommit": "lint-staged", 66 | "predeploy": "npm run build", 67 | "start": "react-scripts start", 68 | "test": "react-scripts test" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.7.2", 72 | "@storybook/react": "^5.2.6", 73 | "@types/classnames": "^2.2.7", 74 | "@types/color": "^3.0.0", 75 | "@types/enzyme": "^3.9.4", 76 | "@types/enzyme-adapter-react-16": "^1.0.5", 77 | "@types/file-saver": "^2.0.1", 78 | "@types/jest": "^24.0.15", 79 | "@types/lodash": "^4.14.134", 80 | "@types/node": "^12.0.10", 81 | "@types/react": "^16.9.5", 82 | "@types/react-color": "^3.0.1", 83 | "@types/react-dom": "^16.9.1", 84 | "@types/react-portal": "^4.0.2", 85 | "@types/react-redux": "^7.1.4", 86 | "@types/react-virtualized": "^9.21.4", 87 | "@types/string-hash": "^1.1.1", 88 | "@types/styled-components": "^4.1.16", 89 | "@types/uuid": "^3.4.4", 90 | "babel-core": "^7.0.0-bridge.0", 91 | "babel-loader": "^8.0.6", 92 | "babel-runtime": "^6.26.0", 93 | "enzyme": "^3.10.0", 94 | "enzyme-adapter-react-16": "^1.14.0", 95 | "enzyme-to-json": "^3.3.4", 96 | "gh-pages": "^2.0.1", 97 | "husky": "^2.5.0", 98 | "jest-enzyme": "^7.0.2", 99 | "lint-staged": "^8.2.1", 100 | "prettier": "^1.18.2", 101 | "react-testing-library": "^8.0.1", 102 | "redux-devtools-extension": "^2.13.7", 103 | "source-map-explorer": "^2.0.1", 104 | "typescript": "^3.6.4" 105 | }, 106 | "homepage": ".", 107 | "lint-staged": { 108 | "src/**/*.{css,js,json,jsx,ts,tsx}": [ 109 | "prettier --single-quote --write", 110 | "git add" 111 | ] 112 | }, 113 | "jest": { 114 | "snapshotSerializers": [ 115 | "enzyme-to-json/serializer" 116 | ] 117 | }, 118 | "browserslist": [ 119 | ">0.2%", 120 | "not dead", 121 | "not ie <= 11", 122 | "not op_mini all" 123 | ] 124 | } 125 | -------------------------------------------------------------------------------- /public/cyto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piximi/application_archived/48b0388b4037ba50b8399bf3c829e651a44eebd4/public/cyto.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piximi/application_archived/48b0388b4037ba50b8399bf3c829e651a44eebd4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 28 | 29 | Piximi 30 | 31 | 32 | 33 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Piximi", 3 | "name": "Piximi", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Purpose 2 | 3 | 4 | - No issue is linked to this PR. 5 | 6 | ### Changes 7 | 8 | 9 | - No changes. 10 | 11 | ### Review Instructions 12 | 13 | 14 | - No instructions. -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ConnectedApplication } from './containers'; 3 | import { createMuiTheme } from '@material-ui/core/styles'; 4 | import { ThemeProvider } from '@material-ui/styles'; 5 | 6 | const theme = createMuiTheme({ 7 | palette: { 8 | type: 'light' 9 | } 10 | }); 11 | 12 | const App = () => { 13 | return ( 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/components/Image/Image.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const Image = props => { 4 | const { src, openImageViewerDialog, id } = props; 5 | 6 | const [imageStatus, setImageStatus] = React.useState('loading'); 7 | const [image, setImage] = React.useState(null); 8 | const [imageHeight, setImageHeight] = React.useState(null); 9 | const [imageWidth, setImageWidth] = React.useState(null); 10 | 11 | let canvasRef = React.useRef(); 12 | 13 | const onLoad = e => { 14 | const image = e.target; 15 | const width = image.width; 16 | const height = image.height; 17 | setImageStatus('loaded'); 18 | setImage(image); 19 | setImageHeight(height); 20 | setImageWidth(width); 21 | image.style.height = '0px'; 22 | }; 23 | 24 | // Draw canvas 25 | const draw = () => { 26 | if (imageStatus === 'loaded') { 27 | const canvas = canvasRef.current; 28 | const context = canvas.getContext('2d'); 29 | canvas.height = props.height * 0.9; 30 | canvas.width = props.width * 0.9; 31 | const ratio = Math.min( 32 | canvas.width / imageWidth, 33 | canvas.height / imageHeight 34 | ); 35 | canvas.height = imageHeight * ratio; 36 | canvas.width = imageWidth * ratio; 37 | 38 | // Apply filters to context 39 | context.filter = 40 | 'brightness(' + 41 | props.brightness + 42 | '%) contrast(' + 43 | props.contrast + 44 | '%)'; 45 | 46 | context.drawImage(image, 0, 0, canvas.width, canvas.height); 47 | 48 | image.crossOrigin = 'Anonymous'; 49 | 50 | image.setAttribute('crossOrigin', ''); 51 | 52 | // FIXME: Sat Jun 15 (Allen) 53 | // Apply selected channel filter 54 | // const pixel = context.getImageData(0, 0, canvas.width, canvas.height); 55 | // let data = pixel.data; 56 | // selectVisibleChannels(data, props.unselectedChannels); 57 | // context.putImageData(pixel, 0, 0); 58 | } 59 | }; 60 | 61 | // const selectVisibleChannels = (imageData, nonVisibleChannels) => { 62 | // for (let i = 0; i < imageData.length; i += 4) { 63 | // for (let j = 0; j < 4; j += 1) { 64 | // if (nonVisibleChannels.includes(j)) imageData[j + i] = 0; 65 | // } 66 | // } 67 | // }; 68 | 69 | React.useEffect(() => { 70 | draw(); 71 | }); 72 | 73 | return ( 74 |
75 | 84 | foo 90 |
91 | ); 92 | }; 93 | 94 | Image.defaultProps = { 95 | brightness: 100, 96 | contrast: 100, 97 | unselectedChannels: [] 98 | }; 99 | 100 | export default Image; 101 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const fields = [ 2 | { 3 | label: 'image_checksum', 4 | value: 'id' 5 | }, 6 | { 7 | label: 'image_pathname', 8 | value: 'pathname' 9 | }, 10 | { 11 | label: 'image_shape_t', 12 | value: 'image_shape_t' 13 | }, 14 | { 15 | label: 'image_shape_p', 16 | value: 'image_shape_p' 17 | }, 18 | { 19 | label: 'image_shape_r', 20 | value: 'height' 21 | }, 22 | { 23 | label: 'image_shape_c', 24 | value: 'width' 25 | }, 26 | { 27 | label: 'image_shape_channels', 28 | value: 'channels' 29 | }, 30 | { 31 | label: 'object_index', 32 | value: 'object_index' 33 | }, 34 | { 35 | label: 'object_bounding_box_minimum_t', 36 | value: 'object_bounding_box_minimum_t' 37 | }, 38 | { 39 | label: 'object_bounding_box_minimum_p', 40 | value: 'object_bounding_box_minimum_p' 41 | }, 42 | { 43 | label: 'object_bounding_box_minimum_r', 44 | value: 'object_bounding_box_minimum_r' 45 | }, 46 | { 47 | label: 'object_bounding_box_minimum_c', 48 | value: 'object_bounding_box_minimum_c' 49 | }, 50 | { 51 | label: 'object_bounding_box_maximum_t', 52 | value: 'object_bounding_box_maximum_t' 53 | }, 54 | { 55 | label: 'object_bounding_box_maximum_p', 56 | value: 'object_bounding_box_maximum_p' 57 | }, 58 | { 59 | label: 'object_bounding_box_maximum_r', 60 | value: 'height' 61 | }, 62 | { 63 | label: 'object_bounding_box_maximum_c', 64 | value: 'width' 65 | }, 66 | { 67 | label: 'object_mask_checksum', 68 | value: 'object_mask_checksum' 69 | }, 70 | { 71 | label: 'object_mask_pathname', 72 | value: 'object_mask_pathname' 73 | }, 74 | { 75 | label: 'object_category', 76 | value: 'category' 77 | }, 78 | { 79 | label: 'object_category_name', 80 | value: 'categoryName' 81 | } 82 | ]; 83 | 84 | export const colors = [ 85 | 'rgb(193, 53, 19)', // r, 60s 86 | 'rgb(248, 52, 35)', // r, 70s 87 | 'rgb(251, 0, 66)', // r, 80s 88 | 'rgb(159, 40, 20)', // r, 90s 89 | 'rgb(218, 22, 69)', // r, 00s 90 | 'rgb(251, 31, 94)', // r, 10s 91 | 'rgb( 99, 123, 38)', // g, 60s 92 | 'rgb(100, 145, 65)', // g, 70s 93 | 'rgb( 34, 227, 219)', // g, 80s 94 | 'rgb( 34, 107, 141)', // g, 90s 95 | 'rgb( 40, 209, 17)', // g, 00s 96 | 'rgb( 44, 208, 83)', // g, 10s 97 | 'rgb( 36, 98, 121)', // b, 60s 98 | 'rgb( 16, 143, 200)', // b, 70s 99 | 'rgb( 44, 80, 191)', // b, 80s 100 | 'rgb( 19, 55, 160)', // b, 90s 101 | 'rgb( 54, 133, 213)', // b, 00s 102 | 'rgb( 12, 103, 254)' // b, 10s 103 | ]; 104 | -------------------------------------------------------------------------------- /src/containers/ConnectedApplication.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Application } from '../pages/images'; 3 | import { Dispatch } from 'redux'; 4 | import { Classifier } from '@piximi/types'; 5 | import { updateImageCategoryAction } from '@piximi/store'; 6 | 7 | type State = { 8 | classifier: Classifier; 9 | settings: any; 10 | }; 11 | 12 | const mapStateToProps = (state: State) => { 13 | return { 14 | categories: state.classifier.categories, 15 | images: state.classifier.images 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = (dispatch: Dispatch) => { 20 | return { 21 | updateImageCategory: (identifier: string, categoryIdentifier: string) => { 22 | const payload = { 23 | identifier: identifier, 24 | categoryIdentifier: categoryIdentifier 25 | }; 26 | 27 | const action = updateImageCategoryAction(payload); 28 | 29 | dispatch(action); 30 | } 31 | }; 32 | }; 33 | 34 | const ConnectedApplication = connect( 35 | mapStateToProps, 36 | mapDispatchToProps 37 | )(Application); 38 | 39 | export default ConnectedApplication; 40 | -------------------------------------------------------------------------------- /src/containers/ConnectedDeleteImageDialog.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { DeleteImageDialog } from '../pages/images'; 3 | import { deleteImageAction } from '@piximi/store'; 4 | import { Dispatch } from 'redux'; 5 | import { Classifier } from '@piximi/types'; 6 | 7 | type State = { 8 | classifier: Classifier; 9 | }; 10 | 11 | const mapStateToProps = (state: State) => { 12 | return { 13 | images: state.classifier.images 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch: Dispatch) => { 18 | return { 19 | deleteImages: (identifiers: string[]) => { 20 | for (let identifier of identifiers) { 21 | const payload = { 22 | identifier: identifier 23 | }; 24 | 25 | const action = deleteImageAction(payload); 26 | 27 | dispatch(action); 28 | } 29 | } 30 | }; 31 | }; 32 | 33 | const ConnectedDeleteImageDialog = connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(DeleteImageDialog); 37 | 38 | export default ConnectedDeleteImageDialog; 39 | -------------------------------------------------------------------------------- /src/containers/ConnectedGallery.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { Gallery } from '../pages/images'; 3 | import { Classifier } from '@piximi/types'; 4 | 5 | type State = { 6 | classifier: Classifier; 7 | }; 8 | 9 | const mapStateToProps = (state: State) => { 10 | return { 11 | images: state.classifier.images, 12 | categories: state.classifier.categories 13 | }; 14 | }; 15 | 16 | const ConnectedGallery = connect(mapStateToProps)(Gallery); 17 | 18 | export default ConnectedGallery; 19 | -------------------------------------------------------------------------------- /src/containers/ConnectedImageViewer.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { ImageViewer } from '../pages/image'; 3 | import { 4 | updateImageBrightnessAction, 5 | updateImageContrastAction 6 | } from '@piximi/store'; 7 | import { Dispatch } from 'redux'; 8 | import { Classifier, Image } from '@piximi/types'; 9 | 10 | type State = { 11 | classifier: Classifier; 12 | }; 13 | 14 | const mapStateToProps = (state: State) => { 15 | return { 16 | images: state.classifier.images 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = (dispatch: Dispatch) => { 21 | return { 22 | saveEdits: (identifier: string, brightness: number, contrast: number) => { 23 | const brightnessPayload = { 24 | identifier: identifier, 25 | brightness: brightness 26 | }; 27 | 28 | const brightnessAction = updateImageBrightnessAction(brightnessPayload); 29 | 30 | dispatch(brightnessAction); 31 | 32 | const contrastPayload = { 33 | identifier: identifier, 34 | contrast: contrast 35 | }; 36 | 37 | const contrastAction = updateImageContrastAction(contrastPayload); 38 | 39 | dispatch(contrastAction); 40 | }, 41 | saveEditsGlobally: ( 42 | images: Image[], 43 | brightness: number, 44 | contrast: number 45 | ) => { 46 | for (let image of images) { 47 | const brightnessPayload = { 48 | identifier: image.identifier, 49 | brightness: brightness 50 | }; 51 | 52 | const brightnessAction = updateImageBrightnessAction(brightnessPayload); 53 | 54 | dispatch(brightnessAction); 55 | 56 | const contrastPayload = { 57 | identifier: image.identifier, 58 | contrast: contrast 59 | }; 60 | 61 | const contrastAction = updateImageContrastAction(contrastPayload); 62 | 63 | dispatch(contrastAction); 64 | } 65 | } 66 | }; 67 | }; 68 | 69 | const ConnectedImageViewer = connect( 70 | mapStateToProps, 71 | mapDispatchToProps 72 | )(ImageViewer); 73 | 74 | export default ConnectedImageViewer; 75 | -------------------------------------------------------------------------------- /src/containers/ConnectedImportImagesButton.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { createImagesAction } from '@piximi/store'; 3 | import * as uuid from 'uuid'; 4 | import { ImportImagesButton } from '../pages/images'; 5 | import { Dispatch } from 'redux'; 6 | import { Classifier, Image, Partition } from '@piximi/types'; 7 | 8 | type State = { 9 | classifier: Classifier; 10 | }; 11 | 12 | type imageProps = { 13 | checksum: string; 14 | data: string; 15 | }; 16 | 17 | const mapStateToProps = (state: State) => { 18 | return { 19 | images: state.classifier.images 20 | }; 21 | }; 22 | 23 | const mapDispatchToProps = (dispatch: Dispatch) => { 24 | return { 25 | createImages: (imagePropsArray: imageProps[]) => { 26 | const images: Image[] = imagePropsArray.map((imageProps: imageProps) => { 27 | const image: Image = { 28 | categoryIdentifier: '00000000-0000-0000-0000-000000000000', 29 | checksum: imageProps.checksum, 30 | data: imageProps.data, 31 | identifier: uuid.v4(), 32 | partition: Partition.Training, 33 | scores: [], 34 | visualization: { 35 | brightness: 0, 36 | contrast: 0, 37 | visible: true, 38 | visibleChannels: [] 39 | } 40 | }; 41 | return image; 42 | }); 43 | 44 | const payload = { 45 | images: images 46 | }; 47 | 48 | const action = createImagesAction(payload); 49 | 50 | dispatch(action); 51 | } 52 | }; 53 | }; 54 | 55 | const ConnectedImportImagesButton = connect( 56 | mapStateToProps, 57 | mapDispatchToProps 58 | )(ImportImagesButton); 59 | 60 | export default ConnectedImportImagesButton; 61 | -------------------------------------------------------------------------------- /src/containers/ConnectedItem.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { GalleryItem } from '../pages/images'; 3 | import { updateImageCategoryAction } from '@piximi/store'; 4 | import { Dispatch } from 'redux'; 5 | import { Classifier } from '@piximi/types'; 6 | 7 | type State = { 8 | classifier: Classifier; 9 | }; 10 | 11 | const mapStateToProps = (state: State) => { 12 | return { 13 | images: state.classifier.images 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch: Dispatch) => { 18 | return { 19 | updateImageCategory: (identifier: string, categoryIdentifier: string) => { 20 | const payload = { 21 | categoryIdentifier: categoryIdentifier, 22 | identifier: identifier 23 | }; 24 | 25 | const action = updateImageCategoryAction(payload); 26 | 27 | dispatch(action); 28 | } 29 | }; 30 | }; 31 | 32 | const ConnectedItem = connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(GalleryItem); 36 | 37 | export default ConnectedItem; 38 | -------------------------------------------------------------------------------- /src/containers/ConnectedItemCategoryMenu.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { GalleryItemCategoryMenu } from '../pages/images'; 3 | import { updateImageCategoryAction } from '@piximi/store'; 4 | import { Dispatch } from 'redux'; 5 | import { Classifier } from '@piximi/types'; 6 | 7 | type State = { 8 | classifier: Classifier; 9 | }; 10 | 11 | const mapStateToProps = (state: State) => { 12 | return { 13 | categories: state.classifier.categories 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch: Dispatch) => { 18 | return { 19 | updateImageCategory: (identifier: string, categoryIdentifier: string) => { 20 | const payload = { 21 | identifier: identifier, 22 | categoryIdentifier: categoryIdentifier 23 | }; 24 | 25 | const action = updateImageCategoryAction(payload); 26 | 27 | dispatch(action); 28 | } 29 | }; 30 | }; 31 | 32 | const ConnectedItemCategoryMenu = connect( 33 | mapStateToProps, 34 | mapDispatchToProps 35 | )(GalleryItemCategoryMenu); 36 | 37 | export default ConnectedItemCategoryMenu; 38 | -------------------------------------------------------------------------------- /src/containers/ConnectedItemLabel.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { GalleryItemLabel } from '../pages/images'; 3 | import { Classifier } from '@piximi/types'; 4 | 5 | type State = { 6 | classifier: Classifier; 7 | }; 8 | 9 | const mapStateToProps = (state: State) => { 10 | return { 11 | categories: state.classifier.categories 12 | }; 13 | }; 14 | 15 | const ConnectedItemLabel = connect(mapStateToProps)(GalleryItemLabel); 16 | 17 | export default ConnectedItemLabel; 18 | -------------------------------------------------------------------------------- /src/containers/index.ts: -------------------------------------------------------------------------------- 1 | import ConnectedApplication from './ConnectedApplication'; 2 | import ConnectedDeleteImageDialog from './ConnectedDeleteImageDialog'; 3 | import ConnectedGallery from './ConnectedGallery'; 4 | import ConnectedImageViewer from './ConnectedImageViewer'; 5 | import ConnectedImportImagesButton from './ConnectedImportImagesButton'; 6 | import ConnectedItem from './ConnectedItem'; 7 | import ConnectedItemCategoryMenu from './ConnectedItemCategoryMenu'; 8 | import ConnectedItemLabel from './ConnectedItemLabel'; 9 | 10 | export { 11 | ConnectedApplication, 12 | ConnectedDeleteImageDialog, 13 | ConnectedGallery, 14 | ConnectedImageViewer, 15 | ConnectedImportImagesButton, 16 | ConnectedItem, 17 | ConnectedItemCategoryMenu, 18 | ConnectedItemLabel 19 | }; 20 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import translations from '@piximi/translations'; 4 | 5 | i18n 6 | .use(initReactI18next) // passes i18n down to react-i18next 7 | .init({ 8 | resources: translations, 9 | lng: 'en', 10 | keySeparator: false, // we do not use keys in form messages.welcome 11 | interpolation: { 12 | escapeValue: false // react already safes from xss 13 | } 14 | }); 15 | 16 | export default i18n; 17 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | .noselect { 2 | -webkit-touch-callout: none; /* iOS Safari */ 3 | -webkit-user-select: none; /* Safari */ 4 | -khtml-user-select: none; /* Konqueror HTML */ 5 | -moz-user-select: none; /* Firefox */ 6 | -ms-user-select: none; /* Internet Explorer/Edge */ 7 | user-select: none; /* Non-prefixed version, currently 8 | supported by Chrome and Opera */ 9 | } 10 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { Provider } from 'react-redux'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | import './i18n'; 9 | import { store, persistor } from '@piximi/store'; 10 | 11 | ReactDOM.render( 12 | 13 | 14 | 15 | 16 | , 17 | document.getElementById('root') as HTMLElement 18 | ); 19 | 20 | serviceWorker.register(); 21 | 22 | export { store }; 23 | -------------------------------------------------------------------------------- /src/pages/image/BrightnessSlider/BrightnessSlider.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | root: { 6 | padding: '24px 24px 0 24px', 7 | width: 'calc(100% - 48px)' 8 | }, 9 | slider: { 10 | padding: '22px 0px', 11 | color: 'white' 12 | } 13 | }); 14 | 15 | export default styles; 16 | -------------------------------------------------------------------------------- /src/pages/image/BrightnessSlider/BrightnessSlider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './BrightnessSlider.css'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import Slider from '@material-ui/core/Slider'; 5 | import { makeStyles } from '@material-ui/styles'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | type Props = { 10 | brightness: number; 11 | setBrightness: (brightness: number) => void; 12 | }; 13 | 14 | const BrightnessSlider = (props: Props) => { 15 | const classes = useStyles({}); 16 | 17 | const { brightness, setBrightness } = props; 18 | 19 | const onChange = (event: any, value: any) => { 20 | setBrightness(value); 21 | }; 22 | 23 | return ( 24 |
25 | 26 | Brightness 27 | 28 | 29 | 39 |
40 | ); 41 | }; 42 | 43 | export default BrightnessSlider; 44 | -------------------------------------------------------------------------------- /src/pages/image/ChannelSelection/ChannelSelection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Checkbox from '@material-ui/core/Checkbox'; 3 | 4 | class ChannelSelection extends React.Component { 5 | state = { 6 | 0: true, 7 | 1: true, 8 | 2: true 9 | }; 10 | 11 | static getDerivedStateFromProps(props, state) { 12 | let channelState = { ...state }; 13 | for (let channel of props.unselectedChannels) { 14 | if (channelState[channel] === true) channelState[channel] = false; 15 | } 16 | return channelState; 17 | } 18 | 19 | onChange = name => event => { 20 | const selectState = { ...this.state }; 21 | let currentlyUnselected = []; 22 | selectState[Number(name)] = !selectState[Number(name)]; 23 | for (let channel in selectState) { 24 | if (selectState[channel] === false) 25 | currentlyUnselected.push(Number(channel)); 26 | } 27 | this.setState(selectState); 28 | this.props.setUnselectedChannels(currentlyUnselected); 29 | }; 30 | 31 | render() { 32 | return ( 33 |
34 | 39 | 44 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | export default ChannelSelection; 55 | -------------------------------------------------------------------------------- /src/pages/image/ContrastSlider/ContrastSlider.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | root: { 6 | padding: '24px 24px 0 24px', 7 | width: 'calc(100% - 48px)' 8 | }, 9 | slider: { 10 | padding: '22px 0px' 11 | } 12 | }); 13 | 14 | export default styles; 15 | -------------------------------------------------------------------------------- /src/pages/image/ContrastSlider/ContrastSlider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import Slider from '@material-ui/core/Slider'; 4 | import styles from './ContrastSlider.css'; 5 | import { makeStyles } from '@material-ui/styles'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | type Props = { 10 | contrast: number; 11 | setContrast: (contrast: number) => void; 12 | }; 13 | 14 | const ContrastSlider = (props: Props) => { 15 | const { contrast, setContrast } = props; 16 | 17 | const classes = useStyles({}); 18 | 19 | const onChange = (event: any, value: any) => { 20 | setContrast(value); 21 | }; 22 | 23 | return ( 24 |
25 | 26 | Contrast 27 | 28 | 29 | 38 |
39 | ); 40 | }; 41 | 42 | export default ContrastSlider; 43 | -------------------------------------------------------------------------------- /src/pages/image/ImageHistogram/ImageHistogram.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | xyplot: { 6 | width: 'calc(100% - 48px)' 7 | } 8 | }); 9 | 10 | export default styles; 11 | -------------------------------------------------------------------------------- /src/pages/image/ImageHistogram/ImageHistogram.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as d3 from 'd3'; 3 | 4 | const ImageHistogram = props => { 5 | const canvasRef = React.useRef(); 6 | const nodeRef = React.useRef(); 7 | 8 | const { channels, src, brightness, contrast } = props; 9 | 10 | const [data, setData] = React.useState([]); // eslint-disable-line no-unused-vars 11 | 12 | React.useEffect(() => { 13 | const createHistogram = imgData => { 14 | let W = 300; 15 | let H = 300; 16 | const svg = d3.select(nodeRef.current); 17 | const margin = { top: 20, right: 44, bottom: 20, left: 0 }; 18 | const width = W - margin.left - margin.right; 19 | const height = H - margin.top - margin.bottom; 20 | let yAxis = true; 21 | let q = document.querySelector('svg'); 22 | q.style.width = width; 23 | q.style.height = height; 24 | if (yAxis) { 25 | d3.selectAll('g.y-axis').remove(); 26 | } 27 | 28 | const graphComponent = (imgData, color) => { 29 | const data = Object.keys(imgData).map(function(key) { 30 | return { freq: imgData[key], idx: +key }; 31 | }); 32 | const x = d3 33 | .scaleLinear() 34 | .range([0, width]) 35 | .domain([ 36 | 0, 37 | d3.max(data, function(d) { 38 | return d.idx; 39 | }) 40 | ]); 41 | const y = d3 42 | .scaleLinear() 43 | .range([height, 0]) 44 | .domain([ 45 | 0, 46 | d3.max(data, function(d) { 47 | return d.freq; 48 | }) 49 | ]); 50 | const g = svg 51 | .append('g') 52 | .attr( 53 | 'transform', 54 | 'translate(' + margin.left + ',' + margin.top + ')' 55 | ); 56 | if (!yAxis) { 57 | yAxis = true; 58 | g.append('g') 59 | .attr('class', 'y-axis') 60 | .attr('transform', 'translate(' + -5 + ',0)') 61 | .call( 62 | d3 63 | .axisLeft(y) 64 | .ticks(10) 65 | .tickSizeInner(10) 66 | .tickSizeOuter(2) 67 | ); 68 | } 69 | 70 | g.selectAll('.bar-' + color) 71 | .data(data) 72 | .enter() 73 | .append('rect') 74 | .attr('class', 'bar-' + color) 75 | .attr('fill', color) 76 | .attr('x', function(d) { 77 | return x(d.idx); 78 | }) 79 | .attr('y', function(d) { 80 | return y(d.freq); 81 | }) 82 | .attr('width', 2) 83 | .attr('opacity', 0.8) 84 | .attr('height', function(d) { 85 | return height - y(d.freq); 86 | }); 87 | }; 88 | 89 | if (channels.includes(0)) { 90 | d3.selectAll('.bar-red').remove(); 91 | } else { 92 | graphComponent(imgData.rD, 'red'); 93 | } 94 | 95 | if (channels.includes(1)) { 96 | d3.selectAll('.bar-green').remove(); 97 | } else { 98 | graphComponent(imgData.gD, 'green'); 99 | } 100 | 101 | if (channels.includes(2)) { 102 | d3.selectAll('.bar-blue').remove(); 103 | } else { 104 | graphComponent(imgData.bD, 'blue'); 105 | } 106 | }; 107 | 108 | const createPlottableData = imageData => { 109 | let rD = {}, 110 | gD = {}, 111 | bD = {}; 112 | for (let i = 0; i < 256; i++) { 113 | rD[i] = 0; 114 | gD[i] = 0; 115 | bD[i] = 0; 116 | } 117 | 118 | for (let j = 0; j < imageData.length; j += 4) { 119 | rD[imageData[j]]++; 120 | gD[imageData[j + 1]]++; 121 | bD[imageData[j + 2]]++; 122 | } 123 | return { rD, gD, bD }; 124 | }; 125 | 126 | let image = new Image(); 127 | 128 | image.src = src; 129 | 130 | image.onload = e => { 131 | const img = e.target; 132 | const canvas = canvasRef.current; 133 | canvas.width = img.width; 134 | canvas.height = img.height; 135 | 136 | const context = canvas.getContext('2d'); 137 | context.drawImage(img, 0, 0, img.width, img.height); 138 | 139 | img.crossOrigin = 'Anonymous'; 140 | 141 | const imageData = context.getImageData(0, 0, img.width, img.height).data; 142 | 143 | const plottableData = createPlottableData(imageData); 144 | 145 | // Create Histogram 146 | createHistogram(plottableData); 147 | 148 | setData(plottableData); 149 | }; 150 | }, [channels, src, brightness, contrast]); 151 | 152 | return ( 153 |
154 | 160 | 161 |
162 | ); 163 | }; 164 | 165 | export default ImageHistogram; 166 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewer/ImageViewer.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | root: { 6 | flexGrow: 1, 7 | backgroundColor: '#000' 8 | }, 9 | container: { 10 | bottom: '16px', 11 | position: 'absolute', 12 | top: 0 13 | }, 14 | flex: { 15 | flex: 1 16 | }, 17 | menuButton: { 18 | marginLeft: -12, 19 | marginRight: 20, 20 | color: '#FFF' 21 | }, 22 | globalButton: { 23 | marginLeft: -12, 24 | marginRight: 20, 25 | color: '#2196f3' 26 | }, 27 | 28 | saveButton: { 29 | left: 'calc(100% - 560px)' 30 | }, 31 | 32 | undoButton: { 33 | left: 'calc(100% - 580px)' 34 | }, 35 | 36 | appbar: { 37 | backgroundColor: '#000', 38 | boxShadow: 'none', 39 | width: '100%' 40 | }, 41 | grow: { 42 | flexGrow: 1 43 | } 44 | }); 45 | 46 | export default styles; 47 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewer/ImageViewer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './ImageViewer.css'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Grid from '@material-ui/core/Grid'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import Toolbar from '@material-ui/core/Toolbar'; 7 | import Button from '@material-ui/core/Button'; 8 | import Tooltip from '@material-ui/core/Tooltip'; 9 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 10 | import EqualizerIcon from '@material-ui/icons/Equalizer'; 11 | import PublicIcon from '@material-ui/icons/Public'; 12 | import { ImageViewerExposureDrawer } from '..'; 13 | import Image from '../../../components/Image/Image'; 14 | import { makeStyles } from '@material-ui/styles'; 15 | 16 | const useStyles = makeStyles(styles); 17 | 18 | type Props = { 19 | src: any; 20 | imgIdentifier: any; 21 | saveEditsGlobally: any; 22 | onClose: any; 23 | images: any; 24 | }; 25 | 26 | const ImageViewer = (props: Props) => { 27 | const classes = useStyles({}); 28 | 29 | const [applySettingsGlobally, setApplySettingsGlobally] = React.useState( 30 | false 31 | ); 32 | const [exposureDrawerToggled, setExposureDrawerToggled] = React.useState( 33 | true 34 | ); 35 | const [brightness, setBrightness] = React.useState(100); 36 | const [contrast, setContrast] = React.useState(100); 37 | const [unselectedChannels, setUnselectedChannels] = React.useState([]); 38 | 39 | const { src, imgIdentifier, onClose, images } = props; 40 | 41 | const toggleExposureDrawer = () => { 42 | setExposureDrawerToggled(!exposureDrawerToggled); 43 | }; 44 | 45 | const saveEdits = () => {}; 46 | 47 | const undoEdits = () => { 48 | const initialBrightness = images[imgIdentifier].brightness; 49 | 50 | const initialContrast = images[imgIdentifier].contrast; 51 | 52 | setBrightness(initialBrightness); 53 | setContrast(initialContrast); 54 | }; 55 | 56 | return ( 57 |
58 | 65 | 66 | 74 | 75 | 76 | 77 | 78 | 79 | 85 | 86 | 87 | 88 | 89 | setApplySettingsGlobally(!applySettingsGlobally)} 91 | className={ 92 | applySettingsGlobally 93 | ? classes.globalButton 94 | : classes.menuButton 95 | } 96 | color="inherit" 97 | aria-label="Menu" 98 | > 99 | 100 | 101 | 102 | 103 | {exposureDrawerToggled ? ( 104 | 111 | ) : null} 112 | 113 | {exposureDrawerToggled ? ( 114 | 121 | ) : null} 122 | 123 |
124 | 125 | 130 | 131 | 132 | 133 | 134 | 135 | 147 |
148 | ); 149 | }; 150 | 151 | export default ImageViewer; 152 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewerDialog/ImageViewerDialog.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | root: { 6 | flexGrow: 1 7 | } 8 | }); 9 | 10 | export default styles; 11 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewerDialog/ImageViewerDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './ImageViewerDialog.css'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import { ConnectedImageViewer } from '../../../containers'; 5 | import { makeStyles } from '@material-ui/styles'; 6 | 7 | const useStyles = makeStyles(styles); 8 | 9 | const ImageViewerDialog = (props: any) => { 10 | const classes = useStyles({}); 11 | 12 | const { onClose, open, src, imgIdentifier } = props; 13 | 14 | return ( 15 | 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default ImageViewerDialog; 26 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewerExposureDrawer/ImageViewerExposureDrawer.css.tsx: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const drawerWidth = 280; 4 | 5 | const styles = () => 6 | createStyles({ 7 | drawerPaper: { 8 | width: drawerWidth, 9 | backgroundColor: '#202124' 10 | }, 11 | content: { 12 | width: '400px' 13 | } 14 | }); 15 | 16 | export default styles; 17 | -------------------------------------------------------------------------------- /src/pages/image/ImageViewerExposureDrawer/ImageViewerExposureDrawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './ImageViewerExposureDrawer.css'; 3 | import Drawer from '@material-ui/core/Drawer'; 4 | import { 5 | BrightnessSlider, 6 | ChannelSelection, 7 | ContrastSlider, 8 | ImageHistogram 9 | } from '..'; 10 | import { makeStyles } from '@material-ui/styles'; 11 | 12 | const useStyles = makeStyles(styles); 13 | 14 | const ImageViewerExposureDrawer = (props: any) => { 15 | const { 16 | onClose, 17 | open, 18 | src, 19 | setBrightness, 20 | brightness, 21 | setContrast, 22 | contrast, 23 | setUnselectedChannels, 24 | unselectedChannels 25 | } = props; 26 | 27 | const classes = useStyles({}); 28 | 29 | return ( 30 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export default ImageViewerExposureDrawer; 52 | -------------------------------------------------------------------------------- /src/pages/image/index.ts: -------------------------------------------------------------------------------- 1 | import BrightnessSlider from './BrightnessSlider/BrightnessSlider'; 2 | import ChannelSelection from './ChannelSelection/ChannelSelection'; 3 | import ContrastSlider from './ContrastSlider/ContrastSlider'; 4 | import ImageHistogram from './ImageHistogram/ImageHistogram'; 5 | import ImageViewer from './ImageViewer/ImageViewer'; 6 | import ImageViewerDialog from './ImageViewerDialog/ImageViewerDialog'; 7 | import ImageViewerExposureDrawer from './ImageViewerExposureDrawer/ImageViewerExposureDrawer'; 8 | 9 | export { 10 | BrightnessSlider, 11 | ChannelSelection, 12 | ContrastSlider, 13 | ImageHistogram, 14 | ImageViewer, 15 | ImageViewerDialog, 16 | ImageViewerExposureDrawer 17 | }; 18 | -------------------------------------------------------------------------------- /src/pages/images/Application/Application.css.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | import { Theme } from '@material-ui/core'; 3 | 4 | const drawerWidth = 280; 5 | 6 | const styles = (theme: Theme) => 7 | createStyles({ 8 | root: { 9 | flexGrow: 1 10 | }, 11 | appFrame: { 12 | zIndex: 1, 13 | overflow: 'hidden', 14 | display: 'flex', 15 | width: '100%' 16 | }, 17 | appBar: { 18 | position: 'absolute', 19 | transition: theme.transitions.create(['margin', 'width'], { 20 | easing: theme.transitions.easing.sharp, 21 | duration: theme.transitions.duration.leavingScreen 22 | }) 23 | }, 24 | appBarShift: { 25 | transition: theme.transitions.create(['margin', 'width'], { 26 | easing: theme.transitions.easing.easeOut, 27 | duration: theme.transitions.duration.enteringScreen 28 | }) 29 | }, 30 | appBarShiftLeft: { 31 | marginLeft: drawerWidth 32 | }, 33 | menuButton: { 34 | marginLeft: 12, 35 | marginRight: 20 36 | }, 37 | hide: { 38 | display: 'none' 39 | }, 40 | drawerPaper: { 41 | position: 'relative', 42 | width: drawerWidth 43 | }, 44 | drawerHeader: { 45 | display: 'flex', 46 | alignItems: 'center', 47 | justifyContent: 'flex-end', 48 | toolbar: theme.mixins.toolbar 49 | }, 50 | content: { 51 | flexGrow: 1, 52 | transition: theme.transitions.create('margin', { 53 | easing: theme.transitions.easing.sharp, 54 | duration: theme.transitions.duration.leavingScreen 55 | }) 56 | }, 57 | contentLeft: { 58 | marginLeft: 0 59 | }, 60 | contentShift: { 61 | transition: theme.transitions.create('margin', { 62 | easing: theme.transitions.easing.easeOut, 63 | duration: theme.transitions.duration.enteringScreen 64 | }) 65 | }, 66 | contentShiftLeft: { 67 | marginLeft: drawerWidth 68 | }, 69 | fab: { 70 | position: 'absolute', 71 | bottom: theme.spacing(2), 72 | right: theme.spacing(2) 73 | }, 74 | unlabeledToggled: { 75 | '&:hover': { 76 | background: 'rgba(150,150,150,1)' 77 | }, 78 | background: 'rgba(200,200,200,1)', 79 | position: 'absolute', 80 | bottom: theme.spacing(2), 81 | right: theme.spacing(2) 82 | }, 83 | unlabeledUntoggled: { 84 | background: 'rgba(200,50,50,1)', 85 | position: 'absolute', 86 | bottom: theme.spacing(2), 87 | right: theme.spacing(2) 88 | }, 89 | pacmanLoader: { 90 | position: 'fixed', 91 | top: '50%', 92 | left: '50%' 93 | } 94 | }); 95 | 96 | export default styles; 97 | -------------------------------------------------------------------------------- /src/pages/images/Application/Application.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './Application.css'; 3 | import classNames from 'classnames'; 4 | import { ConnectedPrimaryAppBar } from '..'; 5 | import HTML5Backend from 'react-dnd-html5-backend'; 6 | import { DndProvider } from 'react-dnd'; 7 | import { useDrawer } from '@piximi/hooks'; 8 | import { makeStyles } from '@material-ui/styles'; 9 | import { ConnectedGallery } from '../../../containers'; 10 | import { NavigationDrawer } from '@piximi/navigation-drawer'; 11 | 12 | const useStyles = makeStyles(styles); 13 | 14 | type Props = { 15 | updateImageCategory: any; 16 | }; 17 | 18 | export const Application = (props: Props) => { 19 | const classes = useStyles({}); 20 | 21 | const [selectedImages, setSelectedImages] = React.useState([]); 22 | const { openedDrawer, toggleDrawer } = useDrawer(); 23 | 24 | const { updateImageCategory } = props; 25 | 26 | return ( 27 | 28 |
29 | 35 | 36 | 37 | 38 |
44 |
45 | 46 | 53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/pages/images/DeleteButton/DeleteButton.css.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | button: { 6 | padding: '8px' 7 | }, 8 | icon: { 9 | padding: '4px 8px' 10 | } 11 | }); 12 | 13 | export default styles; 14 | -------------------------------------------------------------------------------- /src/pages/images/DeleteButton/DeleteButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './DeleteButton.css'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import Tooltip from '@material-ui/core/Tooltip'; 5 | import Delete from '@material-ui/icons/Delete'; 6 | import { ConnectedDeleteImageDialog } from '../../../containers'; 7 | import { makeStyles } from '@material-ui/styles'; 8 | import { useDialog } from '@piximi/hooks'; 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | const useStyles = makeStyles(styles); 12 | 13 | export const DeleteButton = (props: any) => { 14 | const { t: translation } = useTranslation(); 15 | 16 | const { openedDialog, openDialog, closeDialog } = useDialog(); 17 | 18 | const classes = useStyles({}); 19 | 20 | const { selectedImages, setSelectedImages } = props; 21 | 22 | return ( 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /src/pages/images/DeleteImageDialog/DeleteImageDialog.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import Dialog from '@material-ui/core/Dialog'; 4 | import DialogActions from '@material-ui/core/DialogActions'; 5 | import DialogContent from '@material-ui/core/DialogContent'; 6 | import DialogTitle from '@material-ui/core/DialogTitle'; 7 | import DialogContentText from '@material-ui/core/DialogContentText'; 8 | 9 | import { useTranslation } from 'react-i18next'; 10 | 11 | export const DeleteImageDialog = (props: any) => { 12 | const { onClose, open, selectedImages } = props; 13 | 14 | function onClickDeleteButton() { 15 | props.setSelectedImages([]); 16 | props.deleteImages(props.selectedImages); 17 | props.onClose(); 18 | } 19 | 20 | const noSelectedImages = selectedImages.length; 21 | let title = 22 | noSelectedImages > 0 23 | ? 'Do you want to delete the ' + noSelectedImages + ' selected images?' 24 | : 'Please select images before clicking on this button'; 25 | let text = 26 | noSelectedImages > 0 27 | ? 'Please confirm that you want to delete the currently selected images' 28 | : null; 29 | 30 | if (noSelectedImages === 1) { 31 | title = 'Do you want to delete the selected image'; 32 | text = 'Please confirm you want to delete this image'; 33 | } 34 | 35 | const { t: translation } = useTranslation(); 36 | 37 | return ( 38 | 39 | {title} 40 | 41 | 42 | 43 | {text} 44 | 45 | 46 | 47 | 48 | 51 | 52 | {noSelectedImages > 0 ? ( 53 | 56 | ) : null} 57 | 58 | 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/pages/images/Gallery/Gallery.css: -------------------------------------------------------------------------------- 1 | ul > li { 2 | display: inline-block; 3 | /* You can also add some margins here to make it look prettier */ 4 | zoom: 1; 5 | *display: inline; 6 | /* this fix is needed for IE7- */ 7 | margin: 32px; 8 | padding: 1px; 9 | } 10 | 11 | .target { 12 | background-color: #f3f3f3; 13 | width: 256px; 14 | height: 256px; 15 | } 16 | 17 | .selected { 18 | border: 2px solid; 19 | border-radius: 4px; 20 | border-color: #4a90e2; 21 | display: table; 22 | } 23 | 24 | .unselected { 25 | border: 2px solid; 26 | border-radius: 4px; 27 | border-color: transparent; 28 | display: table; 29 | } 30 | 31 | .container { 32 | padding-top: 12px; 33 | padding-left: 12px; 34 | position: fixed; 35 | z-index: 1202; 36 | width: 100%; 37 | height: 95%; 38 | } 39 | 40 | .noselect { 41 | -webkit-touch-callout: none; /* iOS Safari */ 42 | -webkit-user-select: none; /* Safari */ 43 | -khtml-user-select: none; /* Konqueror HTML */ 44 | -moz-user-select: none; /* Firefox */ 45 | -ms-user-select: none; /* Internet Explorer/Edge */ 46 | user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */ 47 | } 48 | -------------------------------------------------------------------------------- /src/pages/images/Gallery/Gallery.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './Gallery.css'; 3 | import { GalleryCustomDragLayer, GalleryItems, GallerySelectionBox } from '..'; 4 | import { collisionDetection } from '../helper'; 5 | 6 | export const Gallery = props => { 7 | const { images, categories, imagesPerRow, decreaseWidth } = props; 8 | 9 | const visibleCategories = categories 10 | .filter(category => category.visualization.visible) 11 | .map(category => category.identifier); 12 | 13 | const imageIsVisible = image => { 14 | return ( 15 | visibleCategories.includes(image.categoryIdentifier) && 16 | image.visualization.visible 17 | ); 18 | }; 19 | 20 | const visibleImages = 21 | images.length > 0 ? images.filter(image => imageIsVisible(image)) : images; 22 | 23 | const [selected, setSelected] = React.useState([]); 24 | const [collisions, setCollisions] = React.useState([]); 25 | const [selectionBoxCoordinates, setSelectionBoxCoordinates] = React.useState({ 26 | x1: 0, 27 | x2: 0, 28 | y1: 0, 29 | y2: 0 30 | }); 31 | const [selectionBoxVisibility, setSelectionBoxVisibility] = React.useState( 32 | 'hidden' 33 | ); 34 | const [currentlyDraggedItem, setCurrentlyDraggedItem] = React.useState(null); 35 | const [shiftKeyPressed, setShiftKeyPressed] = React.useState(false); 36 | const [altKeyPressed, setAltKeyPressed] = React.useState(false); 37 | const [mouseDown, setMouseDown] = React.useState(false); 38 | const [windowWidth, setWindowWidth] = React.useState(window.innerWidth); 39 | 40 | React.useEffect(() => { 41 | document.addEventListener('keydown', keyEvent); 42 | document.addEventListener('keyup', keyEvent); 43 | window.addEventListener('resize', windowResizeEvent); 44 | }, []); 45 | 46 | const onmousedown = e => { 47 | let currentSelectionBoxCoordinates = { 48 | ...selectionBoxCoordinates 49 | }; 50 | currentSelectionBoxCoordinates.x1 = e.clientX; //Set the initial X 51 | currentSelectionBoxCoordinates.y1 = e.clientY; //Set the initial Y 52 | currentSelectionBoxCoordinates.x2 = e.clientX; //Set the initial X 53 | currentSelectionBoxCoordinates.y2 = e.clientY; //Set the initial Y 54 | 55 | setMouseDown(true); 56 | setSelectionBoxCoordinates(currentSelectionBoxCoordinates); 57 | 58 | // Only activate selection box when not dragging on a selectable item 59 | if (e.target.getAttribute('type') !== 'selectableElement') { 60 | setSelectionBoxVisibility('visible'); 61 | } 62 | }; 63 | 64 | const onmousemove = e => { 65 | // Always update coordinates based on mouse position 66 | let currentSelectionBoxCoordinates = { 67 | ...selectionBoxCoordinates 68 | }; 69 | currentSelectionBoxCoordinates.x2 = e.clientX; 70 | currentSelectionBoxCoordinates.y2 = e.clientY; 71 | if (mouseDown) { 72 | setSelectionBoxCoordinates(currentSelectionBoxCoordinates); 73 | } 74 | // Only check for collisions if selection box is active 75 | if (selectionBoxVisibility === 'visible') { 76 | const collisions = collisionDetection(currentSelectionBoxCoordinates); 77 | setSelected(collisions); 78 | setCollisions(collisions); 79 | props.setSelectedImages(collisions); 80 | } 81 | }; 82 | 83 | const onmouseup = e => { 84 | // Check if no collisions occured and mouseup event is outside of a selectable item 85 | if ( 86 | e.target.getAttribute('type') !== 'selectableElement' && 87 | collisions.length === 0 88 | ) { 89 | // if so unselect all items 90 | setSelected([]); 91 | props.setSelectedImages([]); 92 | } 93 | // Hide selection box und reset collisions 94 | setMouseDown(false); 95 | setSelectionBoxVisibility('hidden'); 96 | setCollisions([]); 97 | }; 98 | 99 | const selectItem = imgId => { 100 | let selectedItems = [...selected]; 101 | const noSelectedItems = selectedItems.length; 102 | // Check if clicked on an already selected item 103 | if (selectedItems.includes(imgId)) { 104 | return; 105 | } 106 | // Check if shiftkey is pressed 107 | if (shiftKeyPressed) { 108 | selectedItems.push(imgId); 109 | } 110 | // Check if alt keys is pressed 111 | else if (altKeyPressed) { 112 | // Select a range of images 113 | let selectOthers = false; 114 | const lastSelected = selectedItems[selectedItems.length - 1]; 115 | for (let image of props.images) { 116 | if (image.id === imgId || image.id === lastSelected) { 117 | selectedItems.push(image.id); 118 | selectOthers = !selectOthers; 119 | } 120 | if (selectOthers && noSelectedItems !== 0) selectedItems.push(image.id); 121 | } 122 | } 123 | // No special key pressed 124 | else { 125 | selectedItems = [imgId]; 126 | } 127 | // Set selected state 128 | props.setSelectedImages(selectedItems); 129 | setSelected(selectedItems); 130 | }; 131 | 132 | const keyEvent = e => { 133 | setShiftKeyPressed(e.shiftKey); 134 | setAltKeyPressed(e.altKey); 135 | }; 136 | 137 | const windowResizeEvent = e => { 138 | setWindowWidth(e.target.innerWidth); 139 | }; 140 | 141 | // Check if no images are visible or available 142 | if (!images || !images.length) { 143 | return null; 144 | } 145 | 146 | return ( 147 |
154 | 155 | 159 | 168 |
169 | ); 170 | }; 171 | 172 | Gallery.defaultProps = { 173 | decreaseWidth: 0, 174 | imagesPerRow: 10 175 | }; 176 | -------------------------------------------------------------------------------- /src/pages/images/GalleryCustomDragLayer/GalleryCustomDragLayer.css: -------------------------------------------------------------------------------- 1 | div#drag-layer img { 2 | width: 64px; 3 | box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.5); 4 | position: absolute; 5 | opacity: 0.7; 6 | margin: 16px; 7 | } 8 | 9 | div#drag-layer img:nth-child(1) { 10 | transform: rotate(-9deg); 11 | z-index: 3; 12 | } 13 | 14 | div#drag-layer img:nth-child(2) { 15 | transform: rotate(-5deg); 16 | z-index: 2; 17 | } 18 | 19 | div#drag-layer img:nth-child(3) { 20 | transform: rotate(7deg); 21 | z-index: 1; 22 | } 23 | 24 | div#drag-layer img:nth-child(4) { 25 | z-index: 0; 26 | } 27 | 28 | div#drag-layer span { 29 | font: 0.5em sans-serif; 30 | background-color: #ff0000; 31 | padding: 8px; 32 | border-radius: 64px; 33 | min-width: 8px; 34 | min-height: 8px; 35 | z-index: 5; 36 | position: absolute; 37 | color: #ffffff; 38 | text-align: center; 39 | } 40 | -------------------------------------------------------------------------------- /src/pages/images/GalleryCustomDragLayer/GalleryCustomDragLayer.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import './GalleryCustomDragLayer.css'; 3 | 4 | const layerStyles = { 5 | position: 'fixed', 6 | pointerEvents: 'none', 7 | zIndex: 9999, 8 | left: 0, 9 | top: 0, 10 | width: '100%', 11 | height: '100%' 12 | }; 13 | 14 | const getItemStyles = props => { 15 | const { currentOffset } = props; 16 | 17 | if (!currentOffset) { 18 | return { 19 | display: 'none' 20 | }; 21 | } 22 | 23 | const { x, y } = currentOffset; 24 | 25 | const transform = `translate(${x}px, ${y}px)`; 26 | 27 | return { 28 | transform: transform, 29 | WebkitTransform: transform 30 | }; 31 | }; 32 | 33 | let swapArrayElements = (arr, indexA, indexB) => { 34 | const temp = arr[indexA]; 35 | 36 | arr[indexA] = arr[indexB]; 37 | arr[indexB] = temp; 38 | }; 39 | 40 | export const GalleryCustomDragLayer = props => { 41 | const { item, itemType, isDragging } = props; 42 | 43 | // const spec = { 44 | // collect: (monitor, props) => { 45 | // return { 46 | // item: monitor.getItem(), 47 | // itemType: monitor.getItemType(), 48 | // currentOffset: monitor.getClientOffset(), 49 | // isDragging: monitor.isDragging() 50 | // }; 51 | // } 52 | // }; 53 | // 54 | // const collectedProps = useDragLayer(spec); 55 | 56 | const renderItem = (type, item) => { 57 | const list = document.getElementsByClassName('selected'); 58 | 59 | let imgSources = []; 60 | 61 | let draggedIndex = 0; 62 | 63 | for (let i = 0; i < list.length; i = i + 1) { 64 | const element = list[i]; 65 | 66 | const imgElement = list[i].childNodes[2]; 67 | 68 | let img = foo; 69 | 70 | imgSources.push(img); 71 | 72 | if (element.getAttribute('imgid') === item.item.identifier) { 73 | draggedIndex = i; 74 | } 75 | } 76 | 77 | swapArrayElements(imgSources, draggedIndex, 0); 78 | 79 | return ( 80 |
81 | {imgSources} {list.length}{' '} 82 |
83 | ); 84 | }; 85 | 86 | if (!isDragging) { 87 | return null; 88 | } 89 | 90 | return ( 91 |
92 |
{renderItem(itemType, item)}
93 |
94 | ); 95 | }; 96 | 97 | // function collect(monitor) { 98 | // return { 99 | // item: monitor.getItem(), 100 | // itemType: monitor.getItemType(), 101 | // currentOffset: monitor.getClientOffset(), 102 | // isDragging: monitor.isDragging() 103 | // }; 104 | // } 105 | -------------------------------------------------------------------------------- /src/pages/images/GalleryItem/GalleryItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ImageViewerDialog } from '../../image'; 3 | import Image from '../../../components/Image/Image'; 4 | import { useDialog } from '@piximi/hooks'; 5 | import { ConnectedItemLabel } from '../../../containers'; 6 | import { ImageDragSource } from '@piximi/components'; 7 | 8 | export const GalleryItem = (props: any) => { 9 | // item = image 10 | const { selectedItems, onmousedown, containerStyle, item } = props; 11 | 12 | const { openedDialog, openDialog, closeDialog } = useDialog(); 13 | 14 | const unselectedChannels = item.visualization.visibleChannels; 15 | 16 | return ( 17 | 22 | 23 | 24 | 35 | 36 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/pages/images/GalleryItemCategoryMenu/GalleryItemCategoryMenu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import LabelIcon from '@material-ui/icons/Label'; 3 | import * as _ from 'lodash'; 4 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 5 | import ListItemText from '@material-ui/core/ListItemText'; 6 | import MenuList from '@material-ui/core/MenuList'; 7 | import MenuItem from '@material-ui/core/MenuItem'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Popover from '@material-ui/core/Popover'; 10 | 11 | export const GalleryItemCategoryMenu = (props: any) => { 12 | const { 13 | anchorEl, 14 | categories, 15 | image, 16 | onClose, 17 | open, 18 | updateImageCategory 19 | } = props; 20 | 21 | const anchorPosition = { 22 | top: open ? anchorEl.getBoundingClientRect().bottom - 3 : 0, 23 | left: open ? anchorEl.getBoundingClientRect().left + 14 : 0 24 | }; 25 | 26 | const onMenuItemClick = (category: any) => { 27 | updateImageCategory(image.identifier, category.identifier); 28 | 29 | onClose(); 30 | }; 31 | 32 | const [unknown, known] = _.partition(categories, category => { 33 | if (category.identifier === '00000000-0000-0000-0000-000000000000') { 34 | return category; 35 | } 36 | }); 37 | 38 | let sortedCategories = _.concat(_.sortBy(known, 'description'), unknown); 39 | 40 | const items = sortedCategories.map((category: any) => ( 41 | onMenuItemClick(category)} 44 | > 45 | 46 | 47 | 48 | 49 | 50 | 51 | )); 52 | 53 | return ( 54 | 60 | 61 | {items} 62 | 63 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /src/pages/images/GalleryItemLabel/GalleryItemLabel.css.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | 3 | const styles = () => 4 | createStyles({ 5 | iconButton: { 6 | padding: '8px', 7 | position: 'absolute', 8 | '&:hover': { 9 | background: 'none' 10 | } 11 | } 12 | }); 13 | 14 | export default styles; 15 | -------------------------------------------------------------------------------- /src/pages/images/GalleryItemLabel/GalleryItemLabel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import LabelIcon from '@material-ui/icons/Label'; 3 | import LabelImportantIcon from '@material-ui/icons/LabelImportant'; 4 | import styles from './GalleryItemLabel.css'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import { ConnectedItemCategoryMenu } from '../../../containers'; 7 | import { makeStyles } from '@material-ui/styles'; 8 | import { useMenu } from '@piximi/hooks'; 9 | import { Image, Category, Score } from '@piximi/types'; 10 | 11 | const useStyles = makeStyles(styles); 12 | 13 | const getLableIndex = (scores: Score[]) => { 14 | let maxScore = 0; 15 | let lableIndex = 0; 16 | for (let i = 0; i < scores.length; i++) { 17 | if (scores[i].probability > maxScore) { 18 | maxScore = scores[i].probability; 19 | lableIndex = i; 20 | } 21 | } 22 | return scores[lableIndex].categoryIdentifier; 23 | }; 24 | 25 | type GalleryItemLabelProps = { 26 | categories: Category[]; 27 | image: Image; 28 | }; 29 | 30 | export const GalleryItemLabel = (props: GalleryItemLabelProps) => { 31 | const { anchorEl, openedMenu, openMenu, closeMenu } = useMenu(); 32 | 33 | const { categories, image } = props; 34 | 35 | const classes = useStyles({}); 36 | 37 | const predictedImage: boolean = 38 | image.categoryIdentifier === '00000000-0000-0000-0000-000000000000' && 39 | image.scores.length !== 0; 40 | const predictedCategoryIdentifier: string = predictedImage 41 | ? getLableIndex(image.scores) 42 | : '00000000-0000-0000-0000-000000000000'; 43 | 44 | const findCategoryColor = (categoryIdentifier: string) => { 45 | const index = categories.findIndex((category: any) => { 46 | return category.identifier === categoryIdentifier; 47 | }); 48 | 49 | if (index > -1) { 50 | return categories[index].visualization.color; 51 | } else { 52 | return '#000'; 53 | } 54 | }; 55 | 56 | return ( 57 | 58 | 64 | {!predictedImage ? ( 65 | 68 | ) : ( 69 | 72 | )} 73 | 74 | 75 | 81 | 82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/pages/images/GalleryItems/GalleryItems.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Grid, AutoSizer } from 'react-virtualized'; 3 | import { ConnectedItem } from '../../../containers'; 4 | 5 | type GalleryItemsProps = { 6 | decreaseWidth: any; 7 | selectItem: any; 8 | images: any; 9 | selectedItems: any; 10 | ondrag: any; 11 | asyncImgLoadingFunc: any; 12 | callOnDragEnd: any; 13 | imagesPerRow: any; 14 | windowWidth: any; 15 | }; 16 | 17 | export const GalleryItems = (props: GalleryItemsProps) => { 18 | const { 19 | decreaseWidth, 20 | selectItem, 21 | images, 22 | selectedItems, 23 | ondrag, 24 | asyncImgLoadingFunc, 25 | callOnDragEnd, 26 | imagesPerRow, 27 | windowWidth 28 | } = props; 29 | 30 | const onmousedown = (imgId: any) => { 31 | selectItem(imgId); 32 | }; 33 | 34 | let cellRenderer: ({ 35 | columnIndex, 36 | key, 37 | rowIndex, 38 | style 39 | }: { 40 | columnIndex: any; 41 | key: any; 42 | rowIndex: any; 43 | style: any; 44 | }) => undefined | any; 45 | 46 | cellRenderer = ({ columnIndex, key, rowIndex, style }) => { 47 | let newStyle = { ...style }; 48 | const index = picturesPerRow * rowIndex - 1 + columnIndex + 1; 49 | 50 | if (index > noImages - 1) { 51 | return; 52 | } 53 | 54 | return ( 55 |
56 | 66 |
67 | ); 68 | }; 69 | 70 | let picturesPerRow = imagesPerRow; 71 | 72 | // Media queries 73 | if (windowWidth > 0) { 74 | if (windowWidth - decreaseWidth < 900) picturesPerRow = 5; 75 | if (windowWidth - decreaseWidth < 850) picturesPerRow = 4; 76 | if (windowWidth - decreaseWidth < 700) picturesPerRow = 3; 77 | if (windowWidth - decreaseWidth < 450) picturesPerRow = 2; 78 | if (windowWidth - decreaseWidth < 200) picturesPerRow = 1; 79 | } 80 | 81 | const noImages = images.length; 82 | const quotient = Math.floor(noImages / picturesPerRow); 83 | const remainder = noImages % picturesPerRow; 84 | 85 | let rowCount = quotient; 86 | 87 | if (remainder !== 0) { 88 | rowCount = rowCount + 1; 89 | } 90 | 91 | return ( 92 | 93 | {({ height, width }) => { 94 | const calculatedWidth = width - decreaseWidth; 95 | const columnWidth = calculatedWidth / picturesPerRow; 96 | const columnCount = 97 | picturesPerRow > noImages ? noImages : picturesPerRow; 98 | 99 | return ( 100 | 110 | ); 111 | }} 112 | 113 | ); 114 | }; 115 | -------------------------------------------------------------------------------- /src/pages/images/GallerySelectionBox/GallerySelectionBox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '../Gallery/Gallery.css'; 3 | import { reCalc } from '../helper'; 4 | 5 | type GallerySelectionBoxProps = { 6 | selectionBoxCoordinates: { 7 | x1: number; 8 | x2: number; 9 | y1: number; 10 | y2: number; 11 | }; 12 | visibility: any; 13 | }; 14 | 15 | export const GallerySelectionBox = (props: GallerySelectionBoxProps) => { 16 | const { selectionBoxCoordinates, visibility } = props; 17 | 18 | const [style, setStyle] = React.useState({ 19 | zIndex: 1, 20 | position: 'fixed' as 'fixed', 21 | background: '#eaeaea', 22 | opacity: 0.4, 23 | border: '0.1em solid', 24 | borderColor: '#AAAAAA' 25 | }); 26 | 27 | const styleFromBoxCoordinates = reCalc(selectionBoxCoordinates); 28 | 29 | const newStyle = { 30 | ...style, 31 | ...styleFromBoxCoordinates, 32 | visibility: visibility 33 | }; 34 | 35 | React.useEffect(() => { 36 | setStyle(newStyle); 37 | }, [selectionBoxCoordinates, visibility]); 38 | 39 | return
; 40 | }; 41 | -------------------------------------------------------------------------------- /src/pages/images/ImageSearch/ImageSearch.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Partition, Category } from '@piximi/types'; 2 | import { compileExpression } from 'filtrex'; 3 | 4 | let changeImagesVisibilityFunction: ( 5 | itentifiers: string[], 6 | visibility: boolean 7 | ) => void; 8 | let invisibleImages: string[] = []; 9 | 10 | const categoryDict: { [identifier: string]: string } = {}; 11 | const flattendedImages: { 12 | identifier: string; 13 | category: string; 14 | probability: number; 15 | prediction: string; 16 | partition: string; 17 | }[] = []; 18 | 19 | export const ImageSearch = (searchInput: string) => { 20 | try { 21 | var searchFunction = compileExpression(searchInput); 22 | } catch (error) { 23 | alert('invalid search input'); 24 | return true; 25 | } 26 | 27 | const negativeSearchResults: string[] = []; 28 | const positiveSearchResults: string[] = []; 29 | flattendedImages.forEach(image => { 30 | if (searchFunction(image) === 0) { 31 | negativeSearchResults.push(image.identifier); 32 | } else { 33 | positiveSearchResults.push(image.identifier); 34 | } 35 | }); 36 | 37 | changeImagesVisibilityFunction(negativeSearchResults, false); 38 | changeImagesVisibilityFunction(positiveSearchResults, true); 39 | 40 | invisibleImages = negativeSearchResults; 41 | return invisibleImages.length !== 0; 42 | }; 43 | 44 | export const ClearSearch = () => { 45 | changeImagesVisibilityFunction(invisibleImages, true); 46 | }; 47 | 48 | export const InitializeSearch = ( 49 | categories: Category[], 50 | images: Image[], 51 | changeImagesVisibility: (itentifiers: string[], visibility: boolean) => void 52 | ) => { 53 | changeImagesVisibilityFunction = changeImagesVisibility; 54 | 55 | categories.forEach((category: Category) => { 56 | categoryDict[category.identifier] = category.description; 57 | }); 58 | images.forEach((image: Image) => flattenImage(image)); 59 | }; 60 | 61 | const getPrediction = (image: Image) => { 62 | let maxScore = 0; 63 | let lableIndex = 0; 64 | for (let i = 0; i < image.scores.length; i++) { 65 | if (image.scores[i].probability > maxScore) { 66 | maxScore = image.scores[i].probability; 67 | lableIndex = i; 68 | } 69 | } 70 | return { 71 | prediction: image.scores[lableIndex].categoryIdentifier, 72 | probability: image.scores[lableIndex].probability 73 | }; 74 | }; 75 | 76 | const flattenImage = (image: Image) => { 77 | let probability: number; 78 | let prediction: string; 79 | if (image.scores.length === 0) { 80 | probability = -1; 81 | prediction = 'none'; 82 | } else { 83 | const imagePrediction = getPrediction(image); 84 | probability = imagePrediction.probability; 85 | prediction = imagePrediction.prediction; 86 | } 87 | 88 | const category = categoryDict[image.categoryIdentifier]; 89 | const partition = Partition[image.partition].toLowerCase(); 90 | 91 | flattendedImages.push({ 92 | identifier: image.identifier, 93 | category: category, 94 | probability: probability, 95 | prediction: prediction, 96 | partition: partition 97 | }); 98 | }; 99 | -------------------------------------------------------------------------------- /src/pages/images/ImportImagesButton/ImportImagesButton.css.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | import { Theme } from '@material-ui/core'; 3 | 4 | const styles = (theme: Theme) => 5 | createStyles({ 6 | button: { 7 | textTransform: 'none', 8 | fontSize: '1rem', 9 | borderRadius: theme.spacing(1), 10 | padding: theme.spacing(1), 11 | fontWeight: 'inherit', 12 | letterSpacing: 'inherit' 13 | }, 14 | icon: { 15 | paddingRight: theme.spacing(1), 16 | paddingTop: '4px', 17 | paddingBottom: '4px' 18 | } 19 | }); 20 | 21 | export default styles; 22 | -------------------------------------------------------------------------------- /src/pages/images/ImportImagesButton/ImportImagesButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { ImportImagesButton } from './ImportImagesButton'; 4 | import { ThemeProvider } from '@material-ui/styles'; 5 | import { createMuiTheme } from '@material-ui/core'; 6 | import { store } from '@piximi/store'; 7 | import { Provider } from 'react-redux'; 8 | import HTML5Backend from 'react-dnd-html5-backend'; 9 | import { DndProvider } from 'react-dnd'; 10 | 11 | const theme = createMuiTheme({ 12 | palette: { 13 | type: 'light' 14 | } 15 | }); 16 | 17 | const toggle = () => {}; 18 | 19 | storiesOf('ImportImagesButton', module).add('example', () => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | )); 28 | -------------------------------------------------------------------------------- /src/pages/images/ImportImagesButton/ImportImagesButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import hash from 'string-hash'; 3 | import { withStyles } from '@material-ui/core/styles'; 4 | import Button from '@material-ui/core/Button'; 5 | import Menu, { MenuProps } from '@material-ui/core/Menu'; 6 | import MenuItem from '@material-ui/core/MenuItem'; 7 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 8 | import ListItemText from '@material-ui/core/ListItemText'; 9 | import FolderOpen from '@material-ui/icons/FolderOpen'; 10 | import CropOriginal from '@material-ui/icons/CropOriginal'; 11 | import AddPhotoAlternateIcon from '@material-ui/icons/AddPhotoAlternate'; 12 | import CircularProgress from '@material-ui/core/CircularProgress'; 13 | import { useTranslation } from 'react-i18next'; 14 | import { makeStyles } from '@material-ui/styles'; 15 | import styles from './ImportImagesButton.css'; 16 | 17 | const useStyles = makeStyles(styles); 18 | 19 | const StyledMenu = withStyles({ 20 | paper: { 21 | border: '1px solid #d3d4d5' 22 | } 23 | })((props: MenuProps) => ( 24 | 37 | )); 38 | 39 | export function ImportImagesButton(props: any) { 40 | const { createImages } = props; 41 | const { t: translation } = useTranslation(); 42 | const classes = useStyles({}); 43 | const inputElFolder = React.useRef(null); 44 | const inputElFile = React.useRef(null); 45 | const [anchorEl, setAnchorEl] = React.useState(null); 46 | const [reading, setReading] = useState(false); 47 | 48 | type imageProps = { 49 | checksum: string; 50 | data: string; 51 | }; 52 | 53 | const handleClick = (event: React.MouseEvent) => { 54 | setAnchorEl(event.currentTarget); 55 | }; 56 | 57 | const handleClickInputFolder = (event: any) => { 58 | if (inputElFolder.current) { 59 | inputElFolder.current.click(); 60 | } 61 | handleClose(); 62 | }; 63 | 64 | const handleClickInputFile = (event: any) => { 65 | if (inputElFile.current) { 66 | inputElFile.current.click(); 67 | } 68 | handleClose(); 69 | }; 70 | 71 | const handleClose = () => { 72 | setAnchorEl(null); 73 | }; 74 | 75 | const onInputChange = (event: any) => { 76 | const files = event.target.files; 77 | const imageProps: imageProps[] = []; 78 | setReading(true); 79 | let counter = 0; 80 | for (const file of files) { 81 | const reader: FileReader = new FileReader(); 82 | reader.onload = (reader: any) => { 83 | const data = reader.target.result as string; 84 | const checksum = String(hash(data as string)); 85 | imageProps.push({ checksum, data }); 86 | counter += 1; 87 | if (counter === files.length) { 88 | setReading(false); 89 | createImages(imageProps); 90 | } 91 | }; 92 | reader.readAsDataURL(file); 93 | } 94 | }; 95 | 96 | // prettier-ignore 97 | //@ts-ignore 98 | const inputElement = 99 | 100 | return ( 101 |
102 | 118 | 125 | 126 | 127 | 128 | 129 | 130 | {inputElement} 131 | 132 | 133 | 134 | 135 | 136 | 137 | 145 | 146 | 147 |
148 | ); 149 | } 150 | -------------------------------------------------------------------------------- /src/pages/images/Logo/Logo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | it('renders without crashing', () => { 5 | shallow(
); 6 | }); 7 | -------------------------------------------------------------------------------- /src/pages/images/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Typography from '@material-ui/core/Typography'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | export const Logo = () => { 6 | const { t: translation } = useTranslation(); 7 | 8 | return ( 9 | 10 | {translation('Piximi')} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/pages/images/PrimaryAppBar/ConnectedPrimaryAppBar.ts: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { updateImageVisibilityAction } from '@piximi/store'; 3 | import { Dispatch } from 'redux'; 4 | import { Classifier } from '@piximi/types'; 5 | import { PrimaryAppBar } from './PrimaryAppBar'; 6 | 7 | type State = { 8 | classifier: Classifier; 9 | }; 10 | 11 | const mapStateToProps = (state: State) => { 12 | return { 13 | images: state.classifier.images, 14 | categories: state.classifier.categories 15 | }; 16 | }; 17 | 18 | const mapDispatchToProps = (dispatch: Dispatch) => { 19 | return { 20 | changeImagesVisibility: (identifiers: string[], visibility: boolean) => { 21 | const payload = { 22 | identifiers: identifiers, 23 | visible: visibility 24 | }; 25 | 26 | const action = updateImageVisibilityAction(payload); 27 | 28 | dispatch(action); 29 | } 30 | }; 31 | }; 32 | 33 | export const ConnectedPrimaryAppBar = connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(PrimaryAppBar); 37 | -------------------------------------------------------------------------------- /src/pages/images/PrimaryAppBar/PrimaryAppBar.css.ts: -------------------------------------------------------------------------------- 1 | import { createStyles } from '@material-ui/styles'; 2 | import { Theme } from '@material-ui/core'; 3 | 4 | const drawerWidth = 280; 5 | 6 | const styles = (theme: Theme) => 7 | createStyles({ 8 | appBar: { 9 | backgroundColor: 'rgba(0, 0, 0, 0)', 10 | borderBottom: '1px solid rgba(0, 0, 0, 0.12)', 11 | boxShadow: 'none', 12 | transition: theme.transitions.create(['margin', 'width'], { 13 | easing: theme.transitions.easing.sharp, 14 | duration: theme.transitions.duration.leavingScreen 15 | }) 16 | }, 17 | appBarShift: { 18 | transition: theme.transitions.create(['margin', 'width'], { 19 | easing: theme.transitions.easing.easeOut, 20 | duration: theme.transitions.duration.enteringScreen 21 | }) 22 | }, 23 | appBarShiftLeft: { 24 | marginLeft: drawerWidth 25 | }, 26 | toolBar: { 27 | justifyContent: 'space-between' 28 | }, 29 | menuButton: { 30 | marginLeft: 12, 31 | marginRight: 20 32 | }, 33 | hide: { 34 | display: 'none' 35 | }, 36 | search: { 37 | marginLeft: theme.spacing(1), 38 | marginRight: theme.spacing(1) 39 | }, 40 | padding: { 41 | marginRight: theme.spacing(1) 42 | }, 43 | slider: { 44 | root: { 45 | width: 10 46 | }, 47 | slider: { 48 | padding: '22px 0px' 49 | } 50 | } 51 | }); 52 | 53 | export default styles; 54 | -------------------------------------------------------------------------------- /src/pages/images/PrimaryAppBar/PrimaryAppBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './PrimaryAppBar.css'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import Tooltip from '@material-ui/core/Tooltip'; 7 | import Paper from '@material-ui/core/Paper'; 8 | import InputBase from '@material-ui/core/InputBase'; 9 | import classNames from 'classnames'; 10 | import MenuIcon from '@material-ui/icons/Menu'; 11 | import SearchIcon from '@material-ui/icons/Search'; 12 | import Clear from '@material-ui/icons/Clear'; 13 | import { 14 | ImageSearch, 15 | InitializeSearch, 16 | ClearSearch 17 | } from '../ImageSearch/ImageSearch'; 18 | import { ConnectedImportImagesButton } from '../../../containers'; 19 | import { DeleteButton, Logo } from '..'; 20 | import { makeStyles } from '@material-ui/styles'; 21 | 22 | const useStyles = makeStyles(styles); 23 | 24 | export const PrimaryAppBar = (props: any) => { 25 | const classes = useStyles({}); 26 | 27 | const { 28 | toggle, 29 | toggled, 30 | selectedImages, 31 | setSelectedImages, 32 | images, 33 | categories, 34 | changeImagesVisibility 35 | } = props; 36 | 37 | const [searchInput, setSearchInput] = React.useState(''); 38 | const [clearSearchResults, setClearSearchResults] = React.useState( 39 | false 40 | ); 41 | const onSearchInputChange = (event: React.FormEvent) => { 42 | const target = event.target as HTMLInputElement; 43 | setSearchInput(target.value); 44 | }; 45 | 46 | const onSearchIconClick = () => { 47 | InitializeSearch(categories, images, changeImagesVisibility); 48 | const searchResultsToClear: boolean = ImageSearch(searchInput); 49 | setClearSearchResults(searchResultsToClear); 50 | }; 51 | 52 | const onClearImageSearchClick = () => { 53 | ClearSearch(); 54 | setSearchInput(''); 55 | setClearSearchResults(false); 56 | }; 57 | 58 | const onKeyPress = (ev: any) => { 59 | if (ev.key === 'Enter') { 60 | onSearchIconClick(); 61 | } 62 | }; 63 | 64 | return ( 65 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | 89 | 90 | 96 | 97 | {clearSearchResults && ( 98 | 103 | 104 | 105 | )} 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | 116 | 117 |
118 | 119 | 123 | 124 | 125 | ); 126 | }; 127 | -------------------------------------------------------------------------------- /src/pages/images/helper.ts: -------------------------------------------------------------------------------- 1 | const collisionWithRectangle = ( 2 | rectangle1: { x: any; y: any; width: any; height: any }, 3 | rectangle2: any 4 | ) => { 5 | // Check if two rectangles overlap 6 | return !!( 7 | rectangle1.x < rectangle2.x + rectangle2.width && 8 | rectangle1.x + rectangle1.width > rectangle2.x && 9 | rectangle1.y < rectangle2.y + rectangle2.height && 10 | rectangle1.y + rectangle1.height > rectangle2.y 11 | ); 12 | }; 13 | 14 | const reCalcWithoutPixelString = (mousePosition: { 15 | x1: any; 16 | x2: any; 17 | y1: any; 18 | y2: any; 19 | }) => { 20 | let x3 = Math.min(mousePosition.x1, mousePosition.x2); //Smaller X 21 | let x4 = Math.max(mousePosition.x1, mousePosition.x2); //Larger X 22 | let y3 = Math.min(mousePosition.y1, mousePosition.y2); //Smaller Y 23 | let y4 = Math.max(mousePosition.y1, mousePosition.y2); //Larger Y 24 | let left = x3; 25 | let top = y3; 26 | let width = x4 - x3; 27 | let height = y4 - y3; 28 | return { x: left, y: top, width: width, height: height }; 29 | }; 30 | 31 | const collisionDetection = (mousePosition: { 32 | x1: number; 33 | x2: number; 34 | y1: number; 35 | y2: number; 36 | }) => { 37 | // Check if any selectable item is overlapping with mouse selection box 38 | const rectangle1 = reCalcWithoutPixelString(mousePosition); 39 | const elements = document.getElementsByTagName('canvas'); // Check collisions with selectable elements 40 | let collisions = []; 41 | for (let i = 0; i < elements.length; i++) { 42 | const element = elements[i]; 43 | const rectangle2 = element.getBoundingClientRect(); 44 | const imageId = element.getAttribute('imgid'); 45 | const collisionDetected = collisionWithRectangle(rectangle1, rectangle2); 46 | if (collisionDetected) { 47 | collisions.push(imageId); 48 | } 49 | } 50 | return collisions; 51 | }; 52 | 53 | const reCalc = (mousePosition: { 54 | x1: number; 55 | x2: number; 56 | y1: number; 57 | y2: number; 58 | }) => { 59 | // Calculate rectangle position 60 | let x3 = Math.min(mousePosition.x1, mousePosition.x2); //Smaller X 61 | let x4 = Math.max(mousePosition.x1, mousePosition.x2); //Larger X 62 | let y3 = Math.min(mousePosition.y1, mousePosition.y2); //Smaller Y 63 | let y4 = Math.max(mousePosition.y1, mousePosition.y2); //Larger Y 64 | let left = x3 + 'px'; 65 | let top = y3 + 'px'; 66 | let width = x4 - x3 + 'px'; 67 | let height = y4 - y3 + 'px'; 68 | return { left: left, top: top, width: width, height: height }; 69 | }; 70 | 71 | export { collisionDetection, reCalc, reCalcWithoutPixelString }; 72 | -------------------------------------------------------------------------------- /src/pages/images/index.ts: -------------------------------------------------------------------------------- 1 | export { Application } from './Application/Application'; 2 | export { DeleteButton } from './DeleteButton/DeleteButton'; 3 | export { DeleteImageDialog } from './DeleteImageDialog/DeleteImageDialog'; 4 | export { Gallery } from './Gallery/Gallery'; 5 | export { 6 | GalleryCustomDragLayer 7 | } from './GalleryCustomDragLayer/GalleryCustomDragLayer'; 8 | export { GalleryItem } from './GalleryItem/GalleryItem'; 9 | export { 10 | GalleryItemCategoryMenu 11 | } from './GalleryItemCategoryMenu/GalleryItemCategoryMenu'; 12 | export { GalleryItemLabel } from './GalleryItemLabel/GalleryItemLabel'; 13 | export { GalleryItems } from './GalleryItems/GalleryItems'; 14 | export { GallerySelectionBox } from './GallerySelectionBox/GallerySelectionBox'; 15 | export { ImportImagesButton } from './ImportImagesButton/ImportImagesButton'; 16 | export { Logo } from './Logo/Logo'; 17 | export { ConnectedPrimaryAppBar } from './PrimaryAppBar/ConnectedPrimaryAppBar'; 18 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/selectors/images.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | 3 | const getImages = images => images; 4 | 5 | // Calculate no of visible categories for memoization 6 | const getVisibleCategories = state => { 7 | let noOfVisibleCategories = 0; 8 | for (let category of state.categories) { 9 | if (category.visible) noOfVisibleCategories += 1; 10 | } 11 | return noOfVisibleCategories; 12 | }; 13 | 14 | export const selectVisibleImages = state => { 15 | let result = {}; 16 | for (let key in state.images) { 17 | if (state.images[key].visible) result[key] = state.images[key]; 18 | } 19 | return result; 20 | }; 21 | 22 | export const getVisibleImages = createSelector( 23 | [getImages, getVisibleCategories], 24 | images => selectVisibleImages(images) 25 | ); 26 | 27 | export default getVisibleImages; 28 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config?: any) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl: any, config: any) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl: RequestInfo, config: any) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import { configure } from 'enzyme'; 2 | 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | configure({ adapter: new Adapter() }); 6 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "preserve" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | --------------------------------------------------------------------------------