Skip to content

Commit 6dec117

Browse files
author
Tom De Smedt
committed
Save button lights up with unsaved changes
and some simple Python functions for repetitive maintenance stuff (merging tag sets)
1 parent 03ee919 commit 6dec117

File tree

2 files changed

+138
-69
lines changed

2 files changed

+138
-69
lines changed

picky.html

Lines changed: 88 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<!doctype html>
22
<html>
33
<head>
4+
<title>Picky</title>
5+
<meta name="author" content="Textgain">
46
<meta charset="utf-8">
57

68
<style>
79
* {
8-
font: 12px Arial;
9-
line-height: 1em;
1010
color: #555;
11+
font: 12px/1em Arial;
1112
}
1213
body {
1314
margin: 25px;
@@ -18,47 +19,57 @@
1819
.var a {
1920
text-decoration: none;
2021
}
21-
label {
22+
label { /* " LABEL " */
2223
display: inline-block;
23-
margin-right: 5px;
2424
width: 40px;
25+
margin-right: 5px;
26+
font-size: 0.75em;
2527
text-align: right;
2628
text-transform: uppercase;
27-
font-size: 0.75em;
2829
}
29-
input, select, textarea, button {
30+
input, select, textarea, button, .button {
3031
outline: none;
3132
}
3233
input[type="text"],
3334
input[type="number"] {
35+
padding: 2px 0;
3436
border: 0;
3537
border-bottom: 1px solid #ccc;
36-
padding: 2px 0;
3738
}
3839
input[type="text"],
3940
input[type="number"],
4041
textarea {
42+
box-sizing: border-box;
4143
width: 180px;
4244
}
4345
textarea {
4446
height: 90px;
45-
border: 1px solid #ccc;
4647
padding: 4px;
48+
border: 1px solid #ccc;
4749
}
4850
.button {
4951
display: inline-block;
50-
padding: 6px 8px;
51-
background: dodgerblue;
52-
color: white;
52+
padding: 8px;
53+
border: 1px solid #ccc;
54+
border-radius: 8px;
55+
color: inherit;
56+
background: #fff;
5357
text-decoration: none;
5458
}
59+
.button:hover,
60+
.button.hot {
61+
color: white;
62+
background: rgb(0,150,255);
63+
border: 1px solid rgb(0,150,255);
64+
}
5565
.view {
5666
position: absolute;
5767
top: 40px;
5868
left: 320px;
5969
width: 720px;
6070
height: 480px;
6171
text-align: center;
72+
/* border: 1px dotted red; */
6273
}
6374
.view div {
6475
position: relative;
@@ -74,12 +85,13 @@
7485
.view a {
7586
position: absolute;
7687
display: block;
77-
min-width: 50px;
88+
min-width: 45px;
7889
min-height: 1em;
7990
padding: 8px;
80-
border: 1px solid rgba(255,255,255,0.25);
81-
background: rgba(0,0,0,0.75);
91+
border: 1px solid rgba(255,255,255,0.6);
92+
border-radius: 8px;
8293
color: white;
94+
background: rgba(0,0,0,0.6);
8395
text-decoration: none;
8496
}
8597
.view a#T { top: 0%; left: 50%; transform: translate(-50%, -50%); }
@@ -115,26 +127,26 @@
115127

116128
var images = []; // [File1, File2, ...]
117129
var texts = {}; // {image: text}
118-
var labels = {}; // {image: [label1, label2, ...]}
130+
var tags = {}; // {image: [tag1, tag2, ...]}
119131
var index = 0;
120132

121133
var controls = {
122-
'T' : 'text+image',
123-
'L' : 'text',
124-
'R' : 'image',
134+
'T' : '',
135+
'L' : 'bad',
136+
'R' : 'good',
125137
'B' : ''
126138
};
127139

128140
function $(id) {
129141
return document.getElementById(id);
130142
}
131143

132-
function readAsJSON(file, v) {
144+
function readAsJSON(file, o) {
133145
// Parse JSON-file & assign to object.
134-
var f;
146+
let f;
135147
f = new FileReader();
136148
f.onload = function() {
137-
window[v] = JSON.parse(f.result);
149+
window[o] = JSON.parse(f.result);
138150
refresh();
139151
};
140152
f.readAsText(file);
@@ -150,28 +162,28 @@
150162
readAsJSON(input.files[0], 'texts');
151163
}
152164

153-
function loadLabels(input) {
154-
readAsJSON(input.files[0], 'labels');
165+
function loadTags(input) {
166+
readAsJSON(input.files[0], 'tags');
155167
}
156168

157169
function refresh() {
158170
show(index);
159171
}
160172

161173
function show(i) {
162-
// Show image with given index + labels.
163-
$('index' ).value = '';
164-
$('name' ).value = '';
165-
$('labels').value = '';
166-
$('text' ).innerHTML = '';
167-
$('image' ).src = '#';
174+
// Show image with given index + tags.
175+
$('index').value = '';
176+
$('name' ).value = '';
177+
$('tags' ).value = '';
178+
$('text' ).innerHTML = '';
179+
$('image').src = '#';
168180

169-
var img = images[i];
181+
let img = images[i];
170182
if (img) {
171-
$('index' ).value = i;
172-
$('name' ).value = img.name;
173-
$('labels').value = (labels[img.name] || []).join(', ');
174-
$('text' ).innerHTML = (texts[ img.name] || '');
183+
$('index').value = i;
184+
$('name' ).value = img.name;
185+
$('tags' ).value = (tags[img.name] || []).join(', ');
186+
$('text' ).innerHTML = (texts[ img.name] || '');
175187
f = new FileReader();
176188
f.onload = function() {
177189
try {
@@ -184,12 +196,12 @@
184196
}
185197

186198
function customize(k, v) {
187-
// Assign label v to control k (T/L/R/B).
199+
// Assign tag v to control k (T/L/R/B).
188200
controls[k] = $(k).innerHTML = v;
189201
}
190202

191203
function bwd() {
192-
// Show previous image (if any).
204+
// Show prev image (if any).
193205
index = Math.max(index-1, -1);
194206
show(index);
195207
}
@@ -202,55 +214,62 @@
202214

203215
function goto(i) {
204216
// Show image with index i.
205-
index = Math.min(Math.max(parseInt(i) || 0, 0), images.length-1);
217+
index = Math.max(parseInt(i) || 0, 0);
218+
index = Math.min(index, images.length-1);
206219
show(index);
207220
}
208221

209222
function tag(v) {
210-
// 1) Add label v to current image.
223+
// 1) Add tag v to current image.
211224
// 2) Show next image.
212-
var img = images[index];
225+
let img = images[index];
213226
if (img && v) {
214-
var k = img.name;
215-
if (labels[k] == undefined) {
216-
labels[k] = [];
227+
let k = img.name;
228+
if (tags[k] == undefined) {
229+
tags[k] = [];
217230
}
218-
if (labels[k].includes(v) == false) {
219-
labels[k].push(v);
231+
if (tags[k].includes(v) == false) {
232+
tags[k].push(v);
220233
}
221234
}
222235
fwd();
236+
237+
$('save').classList.add('hot'); // (unsaved changes)
223238
}
224239

225240
function edit(s) {
226-
// Replace labels on current image,
241+
// Replace tags on current image,
227242
// from comma-separated string.
228-
var img = images[index];
243+
let img = images[index];
229244
if (img) {
230-
var k = img.name;
231-
var a = s.split(',');
232-
for (var i=0; i < a.length; i++) {
245+
let k = img.name;
246+
let a = s.split(',');
247+
for (let i=0; i < a.length; i++) {
233248
a[i] = a[i].trim();
234249
}
235-
labels[k] = a;
250+
tags[k] = a;
236251
}
252+
253+
$('save').classList.add('hot'); // (unsaved changes)
237254
}
238255

239256
function save(a) {
240-
// Generate JSON-file from labels at <a>.
241-
var f;
242-
f = JSON.stringify(labels);
257+
// Generate JSON-file from tags at <a>.
258+
let f;
259+
f = JSON.stringify(tags);
243260
f = new File([f], {type: 'text/plain'});
244261
f = URL.createObjectURL(f);
245-
a.download = 'labels.json';
262+
a.download = 'tags.json';
246263
a.href = f;
264+
265+
$('save').classList.remove('hot');
247266
}
248267

249268
window.onload = function() {
250269

251270
// Controls:
252271
// 1) Backspace & tab show previous/next image.
253-
// 2) Up, down, left, right add label to image.
272+
// 2) Up, down, left, right add tasg to image.
254273
// 3) As long as no field has the focus.
255274
document.addEventListener('keydown', function(e) {
256275
if (['a', 'input', 'textarea'].includes(e.target.tagName.toLowerCase())) {
@@ -298,21 +317,21 @@
298317
</div>
299318

300319
<div class="var">
301-
<label>labels</label>
302-
<input type="file" onchange="loadLabels(this);" />
320+
<label>tags</label>
321+
<input type="file" onchange="loadTags(this);" />
303322
</div>
304323

305324
<div class="var">
306325
<label>&#9650;</label>
307-
<input type="text" onchange="customize('T', this.value);" value="text+image" />
326+
<input type="text" onchange="customize('T', this.value);" value="" />
308327
</div>
309328
<div class="var">
310329
<label>&#9664;</label>
311-
<input type="text" onchange="customize('L', this.value);" value="text" />
330+
<input type="text" onchange="customize('L', this.value);" value="bad" />
312331
</div>
313332
<div class="var">
314333
<label>&#9654;</label>
315-
<input type="text" onchange="customize('R', this.value);" value="image" />
334+
<input type="text" onchange="customize('R', this.value);" value="good" />
316335
</div>
317336
<div class="var">
318337
<label>&#9660;</label>
@@ -325,36 +344,36 @@
325344
<a href="#" onclick="bwd(); return false;">&laquo;</a>
326345
<a href="#" onclick="fwd(); return false;">&raquo;</a>
327346
</div>
328-
347+
329348
<div class="var">
330349
<label>name</label>
331350
<input type="text" id="name" disabled />
332351
</div>
333-
352+
334353
<br />
335354
<div class="var">
336355
<label></label>
337-
<textarea id="labels" onchange="edit(this.value);"></textarea>
356+
<textarea id="tags" onchange="edit(this.value);"></textarea>
338357
</div>
339-
358+
340359
<div class="var">
341360
<label></label>
342361
<a class="button" id="save" href="" onclick="save(this);">Save</a>
343362
<input type="checkbox" id="auto" /> auto
344-
<div>
363+
</div>
345364

346365
<div class="view">
347366
<div>
348367
<img id="image" src="" /><br />
349-
<a id="L" href="#" onclick="tag(this.innerHTML); return false;">text</a>
350-
<a id="T" href="#" onclick="tag(this.innerHTML); return false;">text+image</a>
351-
<a id="R" href="#" onclick="tag(this.innerHTML); return false;">image</a>
368+
<a id="L" href="#" onclick="tag(this.innerHTML); return false;">bad</a>
369+
<a id="T" href="#" onclick="tag(this.innerHTML); return false;"></a>
370+
<a id="R" href="#" onclick="tag(this.innerHTML); return false;">good</a>
352371
<a id="B" href="#" onclick="tag(this.innerHTML); return false;"></a>
353372
</div>
354373
</div>
355374

356375
<p id="text"></p>
357-
376+
358377
</body>
359378

360379
</html>

picky.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import os
2+
import shutil
3+
import json
4+
5+
class Tags(dict):
6+
7+
def __init__(self, path, *args, **kwargs):
8+
""" Returns the given JSON export from Picky
9+
as a (path, [tags])-dict
10+
"""
11+
if os.path.exists(path or ''):
12+
self.update(json.load(open(path, 'rb')))
13+
self.update(*args)
14+
self.update(kwargs)
15+
self.path = path
16+
17+
def save(self):
18+
json.dump(self, open(self.path, 'w'))
19+
20+
def find(self, f=lambda v: True):
21+
return {k: v for k, v in self.items() if f(v)}
22+
23+
def merge(*tags):
24+
""" Returns a dict of the merged Tags.
25+
"""
26+
m = {}
27+
for t in tags:
28+
for k, v in t.items():
29+
m.setdefault(k, []).extend(v)
30+
return m
31+
32+
# Merging two sets of Tags:
33+
34+
# t1 = Tags(None, {'1.jpg': ['cat'], '2.jpg': ['dog']})
35+
# t2 = Tags(None, {'1.jpg': ['hat']})
36+
# t3 = Tags(None, merge(t1, t2))
37+
# print(t3)
38+
39+
# Finding a subset of Tags:
40+
41+
# t1 = Tags(None, {
42+
# '1.jpg': ['+'],
43+
# '2.jpg': ['+'],
44+
# '3.jpg': ['-'],
45+
# })
46+
# for f1 in t1.find(lambda tags: '+' in tags):
47+
# f2 = os.path.basename(f1)
48+
# f2 = os.path.join('subset', f2)
49+
# shutil.copy(f1, f2)
50+
# print(f1)

0 commit comments

Comments
 (0)