Update 'authenticate' email
Signed-off-by: James Ketrenos <james_gitlab@ketrenos.com>
This commit is contained in:
parent
d547ba5182
commit
09e07320a6
@ -118,7 +118,7 @@ const templates = {
|
||||
"<pre>{{notes}}</pre>",
|
||||
"",
|
||||
"<p>To authenticate:</p>",
|
||||
"<p>echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db</p>",
|
||||
"<p>echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 db/users.db</p>",
|
||||
"",
|
||||
"<p>Sincerely,<br>",
|
||||
"James</p>"
|
||||
@ -130,7 +130,7 @@ const templates = {
|
||||
"{{notes}}",
|
||||
"",
|
||||
"To authenticate:",
|
||||
"echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 users.db",
|
||||
"echo 'UPDATE users SET authenticated=1 WHERE id={{id}};' | sqlite3 db/users.db",
|
||||
"",
|
||||
"Sincerely,",
|
||||
"James"
|
||||
|
122
src/App.css
122
src/App.css
@ -188,4 +188,126 @@ a.Link {
|
||||
|
||||
.OnScreen {
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.more {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#face-editor {
|
||||
max-width: calc(128px * 4);
|
||||
}
|
||||
|
||||
#face-editor .related-faces {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#new-identity {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
#identities {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border: 1px solid black;
|
||||
padding: 0.25em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
#identities > div {
|
||||
border: 1px solid black;
|
||||
margin: 0.25em;
|
||||
padding: 0.25em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.more:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.face[disabled] {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.face-block {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0.5em;
|
||||
min-width: 128px;
|
||||
max-width: 128px;
|
||||
min-height: 128px;
|
||||
max-height: 128px;
|
||||
}
|
||||
|
||||
.face-block-nolimit {
|
||||
min-width: 128px;
|
||||
max-width: initial;
|
||||
min-height: 128px;
|
||||
max-height: initial;
|
||||
}
|
||||
|
||||
.face-block .face {
|
||||
max-width: 64px;
|
||||
max-height: 64px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: relative;
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background-size: contain;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
display: inline-block;
|
||||
border: 1px solid black;
|
||||
margin: 0.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.face:hover {
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.face .distance {
|
||||
position: absolute;
|
||||
background: black;
|
||||
opacity: 0.5;
|
||||
text-align: center;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.editor-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0.25em 0.5em;
|
||||
}
|
||||
|
||||
.editor-row .key {
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.Identities {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
}
|
||||
|
||||
.Identities > div:first-child {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
}
|
348
src/App.js
348
src/App.js
@ -57,6 +57,44 @@ class Footer extends React.Component {
|
||||
|
||||
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);
|
||||
@ -75,7 +113,7 @@ class App extends React.Component {
|
||||
<div className="Body">
|
||||
<div className="Main">
|
||||
<Switch>
|
||||
<Route path="/identities" render={ (props) => <div {...props}/> }/>
|
||||
<Route path="/identities" render={ (props) => <Identities {...props}/> }/>
|
||||
<Route path="/faces" render={ props => <div {...props}/> }/>
|
||||
<Route path="/photos" render={ props => <div {...props}/> }/>
|
||||
</Switch>
|
||||
@ -88,3 +126,311 @@ class App extends React.Component {
|
||||
}
|
||||
|
||||
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]);
|
||||
const face = document.body.querySelector("#face-editor .face");
|
||||
loadFace(parseInt(face.getAttribute("face-id")));
|
||||
});
|
||||
});
|
||||
block.appendChild(editor);
|
||||
|
||||
return block;
|
||||
}
|
@ -11,7 +11,13 @@ module.exports = merge(common, {
|
||||
publicPath: "http://localhost:8765/dist/",
|
||||
hotOnly: true,
|
||||
disableHostCheck: true,
|
||||
historyApiFallback: true
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
"/api": "http://localhost:8123/photos",
|
||||
"/photos/*": {
|
||||
target: "http://localhost:8123"
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [new webpack.HotModuleReplacementPlugin()]
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user