ketr.photos/src/App.js
James Ketrenos d1fa906526 Clear input fields on New Identity
Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
2020-02-09 14:44:03 -08:00

443 lines
12 KiB
JavaScript
Executable File

/* Polyfills for IE */
import 'react-app-polyfill/ie11';
import 'core-js/features/array/find';
import 'core-js/features/array/includes';
import 'core-js/features/number/is-nan';
/* App starts here */
import React from "react";
import { withRouter, NavLink, Route, Switch } from "react-router-dom";
import Modal from "react-bootstrap/Modal";
import Button from "react-bootstrap/Button";
import "bootstrap/dist/css/bootstrap.min.css";
/* Custom components */
import './modest.css';
import "./App.css";
class Header extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
}
render() {
return (
<div className="Header">
<Logo/>
</div>
);
}
}
class Logo extends React.Component {
render() {
return <NavLink to="/"><div className="Logo"></div></NavLink>
}
}
class Footer extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div className="Footer">
<div className="Copyright">Copyright 2020 James Ketrenos</div>
</div>
);
}
}
function noChange() {};
class Identities extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
let params = window.location.search ? window.location.search.replace(/^\?/, "").split("&") : [],
id;
for (let i in params) {
let parts = params[i].split("=");
switch (parts[0]) {
case "id":
id = parseInt(parts[1]);
if (id != parts[1]) {
id = undefined;
}
break;
}
}
loadFace(id);
const newIdentity = document.getElementById("new-identity");
newIdentity.appendChild(createNewIdenityEditor());
}
render() {
return (
<div className="Identities">
<div>
<div id="face-editor" className="block"></div>
<div id="best-match" className="block"></div>
<div id="new-identity" className="block"></div>
</div>
<div id="identities" className="block"></div>
</div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
const location = this.props.history.location.pathname;
console.log(`App.mounted at ${location}`);
}
render() {
console.log("App");
return (
<div className="App" ref={ ref => (this.app = ref) }>
<Header/>
<div className="Body">
<div className="Main">
<Switch>
<Route path="/identities" render={ (props) => <Identities {...props}/> }/>
<Route path="/faces" render={ props => <div {...props}/> }/>
<Route path="/photos" render={ props => <div {...props}/> }/>
</Switch>
</div>
</div>
<Footer/>
</div>
);
}
}
export default withRouter(App);
/*
1. Query server for a face with no bound identity.
2. Query for faces related to that face
3. Put a UI that let's the user pick from existing identities.
4. Select / Deselect all faces that match
5. "Match" to bind the faces to the identity
6. Create new Identity
*/
function createFace(faceId, photoId, selectable) {
var div = document.createElement("div");
div.classList.add("face");
div.setAttribute("photo-id", photoId);
div.setAttribute("face-id", faceId);
div.style.backgroundImage = "url(face-data/" + (faceId % 100) + "/" + faceId + "-original.png)";
div.addEventListener("click", (event) => {
if (event.shiftKey) { /* identities */
let faceId = parseInt(event.currentTarget.getAttribute("face-id"));
if (faceId) {
window.location.href = "identities.html?id=" + faceId;
} else {
alert("No face id mapped to face.");
}
} else if (!selectable || event.ctrlKey) { /* face-explorer */
let photoId = parseInt(event.currentTarget.getAttribute("photo-id"));
if (photoId) {
window.open("face-explorer.html?" + photoId, "photo-" + photoId);
} else {
alert("No photo id mapped to face.");
}
} else {
if (event.currentTarget.hasAttribute("disabled")) {
event.currentTarget.removeAttribute("disabled");
} else {
event.currentTarget.setAttribute("disabled", "");
}
}
});
return div;
}
function loadFace(id) {
const faceEditorBlock = document.getElementById("face-editor");
faceEditorBlock.innerHTML = "";
window.fetch("api/v1/faces" + (id ? "/" + id : "")).then(res => res.json()).then((faces) => {
if (faces.length == 0) {
return;
}
const face = faces[0];
if (face.identityId) {
getIdentity(face.identityId);
} else {
getIdentities(face.id);
}
const editor = document.createElement("div");
editor.classList.add("editor");
editor.appendChild(createFace(face.id, face.photoId));
let row = document.createElement("div"),
label = document.createElement("div"),
a = document.createElement("a");
row.classList.add("editor-row");
label.textContent = "View photo: ";
row.appendChild(label);
a.setAttribute("target", "photo-" + face.photoId);
a.href = "face-explorer.html?" + face.photoId;
a.textContent = "photo " + face.photoId;
row.appendChild(a);
editor.appendChild(row);
row = document.createElement("div");
row.classList.add("editor-row");
let message = "Unassigned related faces: ";
if (face.relatedFaces.length > 0) {
message += face.relatedFaces.length + " related (tap to deselect)";
} else {
message += "No matches found.";
}
row.textContent = message;
editor.appendChild(row);
row = document.createElement("div");
row.classList.add("editor-row");
row.classList.add("related-faces");
editor.appendChild(row);
let button = document.createElement("button");
button.textContent = "show more";
button.addEventListener("click", (event) => {
showMore(row, face.relatedFaces);
});
editor.appendChild(button);
showMore(row, face.relatedFaces);
faceEditorBlock.appendChild(editor);
});
}
function showMore(row, faces) {
let index = row.querySelectorAll(".face").length,
max = Math.min(faces.length - index, 9);
let more = index + max < faces.length;
for (let i = index; i < index + max; i++) {
let relation = faces[i];
const distance = document.createElement("div"),
facePhoto = createFace(relation.faceId, relation.photoId, true);
distance.classList.add("distance");
distance.textContent = relation.distance.toFixed(2);
facePhoto.appendChild(distance);
row.appendChild(facePhoto);
}
if (!more) {
row.parentElement.querySelector("button").style.display = "none";
}
}
function getIdentity(identityId) {
const identitiesBlock = document.getElementById("identities");
identitiesBlock.innerHTML = "";
window.fetch("api/v1/identities/" + identityId).then(res => res.json()).then((identities) => {
identities.forEach((identity) => {
const block = createIdentityBlock(identity, true),
button = createUseThisIdentityButton(identity);
block.insertBefore(button, block.firstChild);
identitiesBlock.appendChild(block);
})
});
}
function getIdentities(faceId) {
const identitiesBlock = document.getElementById("identities");
identitiesBlock.innerHTML = "";
const search = faceId ? "?withScore=" + faceId : "";
window.fetch("api/v1/identities" + search).then(res => res.json()).then((identities) => {
identities.sort((a, b) => {
if (a.lastName == b.lastName) {
return a.firstName.localeCompare(b.firstName);
}
return a.lastName.localeCompare(b.lastName);
});
identities.forEach((identity) => {
const block = createIdentityBlock(identity),
button = createUseThisIdentityButton(identity);
block.insertBefore(button, block.firstChild);
identitiesBlock.appendChild(block);
});
});
}
function createUseThisIdentityButton(identity) {
const button = document.createElement("button");
button.textContent = "Use this identity";
button.addEventListener("click", (event) => {
const object = {
faces: []
};
Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
if (!face.hasAttribute("disabled")) {
object.faces.push(face.getAttribute("face-id"));
}
});
window.fetch("api/v1/identities/faces/add/" + identity.id, {
method: "PUT",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(object)
}).then(res => res.json()).then((identities) => {
console.log("Updated identity: ", identities[0]);
const face = document.body.querySelector("#face-editor .face");
loadFace(parseInt(face.getAttribute("face-id")));
});
});
return button;
}
function createIdentityBlock(identity, nolimit) {
const block = document.createElement("div");
block.classList.add("block");
let div = document.createElement("div");
div.textContent = identity.name;
block.appendChild(div);
div = document.createElement("div");
div.textContent = "Related faces " + identity.relatedFaces.length;
block.appendChild(div);
identity.relatedFaces.sort((a, b) => {
return a.distance - b.distance;
});
div = document.createElement("div");
div.classList.add("face-block");
if (nolimit) {
div.classList.add("face-block-nolimit");
}
let minDistance = {
distance: 1
};
for (let i = 0; i < identity.relatedFaces.length; i++) {
if (!nolimit && i >= 4) {
break;
}
const target = identity.relatedFaces[i];
const facePhoto = createFace(target.faceId, target.photoId),
distance = document.createElement("div");
if (target.distance < minDistance.distance) {
minDistance.distance = target.distance;
minDistance.photoId = target.photoId;
minDistance.faceId = target.faceId;
}
distance.classList.add("distance");
distance.textContent = target.distance.toFixed(2);
facePhoto.appendChild(distance);
div.appendChild(facePhoto);
}
if (minDistance.distance < 0.45) {
const bestMatch = document.getElementById("best-match");
const distance = document.createElement("div"),
facePhoto = createFace(minDistance.faceId, minDistance.photoId);
distance.classList.add("distance");
distance.textContent = minDistance.distance.toFixed(2);
facePhoto.appendChild(distance);
bestMatch.innerHTML = "";
bestMatch.appendChild(facePhoto);
} else {
const bestMatch = document.getElementById("best-match");
bestMatch.innerHTML = "No best guess for match.";
}
block.appendChild(div);
return block;
}
function createNewIdenityEditor() {
const block = document.createElement("div");
block.classList.add("block");
const editor = document.createElement("div");
editor.classList.add("editor");
const button = document.createElement("button");
button.textContent = "New Identity";
editor.appendChild(button);
[ "lastName", "firstName", "middleName", "name" ].forEach((key) => {
const row = document.createElement("div"),
left = document.createElement("div"),
right = document.createElement("input");
row.classList.add("editor-row");
left.classList.add("key");
left.textContent = key;
row.appendChild(left);
row.appendChild(right);
editor.appendChild(row);
});
button.addEventListener("click", (event) => {
const rows = event.currentTarget.parentElement.querySelectorAll(".editor-row"),
object = {};
Array.prototype.forEach.call(rows, (row) => {
object[row.firstChild.textContent] = row.lastChild.value;
});
object.faces = [];
Array.prototype.forEach.call(document.body.querySelectorAll("#face-editor .face"), (face) => {
if (!face.hasAttribute("disabled")) {
object.faces.push(face.getAttribute("face-id"));
}
});
console.log(object);
window.fetch("api/v1/identities", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(object)
}).then(res => res.json()).then((identities) => {
console.log("New identity: ", identities[0]);
let inputs = editor.querySelectorAll("input");
Array.prototype.forEach.call(inputs, (el) => {
if (el.value) {
el.value = "";
}
});
const face = document.body.querySelector("#face-editor .face");
loadFace(parseInt(face.getAttribute("face-id")));
});
});
block.appendChild(editor);
return block;
}