├── .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 | [](https://travis-ci.org/piximi/application)
7 | [](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 |

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 |
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 |
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 =
;
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 |
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 |
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 |
--------------------------------------------------------------------------------