screengraft 0.62.0 → 0.62.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/recents.py +13 -0
- package/scripts/ui.py +14 -2
- package/ui/index.html +6 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screengraft",
|
|
3
|
-
"version": "0.62.
|
|
3
|
+
"version": "0.62.2",
|
|
4
4
|
"description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mockup",
|
package/scripts/recents.py
CHANGED
|
@@ -39,7 +39,16 @@ def _load() -> dict:
|
|
|
39
39
|
if not isinstance(d, dict):
|
|
40
40
|
raise ValueError
|
|
41
41
|
except (OSError, ValueError):
|
|
42
|
+
# No store (or an unreadable one): build it from the sessions on disk
|
|
43
|
+
# and WRITE it, or every read until the first record() walks them again.
|
|
42
44
|
d = _seed()
|
|
45
|
+
for r in ROLES:
|
|
46
|
+
d.setdefault(r, [])
|
|
47
|
+
try:
|
|
48
|
+
_save(d)
|
|
49
|
+
except OSError:
|
|
50
|
+
pass
|
|
51
|
+
return d
|
|
43
52
|
for r in ROLES:
|
|
44
53
|
d.setdefault(r, [])
|
|
45
54
|
return d
|
|
@@ -52,6 +61,10 @@ def _save(d: dict):
|
|
|
52
61
|
with open(tmp, "w") as f:
|
|
53
62
|
json.dump(d, f, indent=1)
|
|
54
63
|
os.replace(tmp, p)
|
|
64
|
+
try:
|
|
65
|
+
os.chmod(p, 0o600) # paths under the user's home: owner-only
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
55
68
|
|
|
56
69
|
|
|
57
70
|
def _seed() -> dict:
|
package/scripts/ui.py
CHANGED
|
@@ -28,6 +28,7 @@ stdlib only on the server side; OpenCV via detect/warp.
|
|
|
28
28
|
"""
|
|
29
29
|
import argparse
|
|
30
30
|
import atexit
|
|
31
|
+
import hashlib
|
|
31
32
|
import json
|
|
32
33
|
import mimetypes
|
|
33
34
|
import os
|
|
@@ -733,7 +734,10 @@ def _thumb_for(item):
|
|
|
733
734
|
out_dir = os.path.join(SESSION.dir, "thumbs")
|
|
734
735
|
if item.get("ext") in VIDEO_EXT:
|
|
735
736
|
os.makedirs(out_dir, exist_ok=True)
|
|
736
|
-
|
|
737
|
+
# A stable name: hash() is salted per process, so a name built on it
|
|
738
|
+
# misses the cache on every launch.
|
|
739
|
+
key = hashlib.sha1(item["path"].encode()).hexdigest()[:16]
|
|
740
|
+
out = os.path.join(out_dir, f"{key}_{int(item.get('mtime') or 0)}_clip.jpg")
|
|
737
741
|
if not os.path.exists(out):
|
|
738
742
|
try:
|
|
739
743
|
fr = W.read_frame_at(item["path"], 0)
|
|
@@ -981,7 +985,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
981
985
|
# The folder scan (scan.py) still exists for the CLI and for a
|
|
982
986
|
# first run with nothing used yet; ?role= selects the list.
|
|
983
987
|
role = (q.get("role") or ["photo"])[0]
|
|
984
|
-
|
|
988
|
+
try:
|
|
989
|
+
limit = max(1, min(200, int(q.get("limit", ["40"])[0])))
|
|
990
|
+
except ValueError:
|
|
991
|
+
limit = 40
|
|
985
992
|
items = RECENT.items(role, limit)
|
|
986
993
|
for it in items:
|
|
987
994
|
it["thumb"] = _thumb_for(it)
|
|
@@ -1546,6 +1553,11 @@ def _publish_current(payload):
|
|
|
1546
1553
|
"""
|
|
1547
1554
|
os.makedirs(os.path.dirname(CURRENT), exist_ok=True)
|
|
1548
1555
|
_write_json_atomic(CURRENT, payload)
|
|
1556
|
+
# The pointer carries the session token: owner-only, like a credential.
|
|
1557
|
+
try:
|
|
1558
|
+
os.chmod(CURRENT, 0o600)
|
|
1559
|
+
except OSError:
|
|
1560
|
+
pass
|
|
1549
1561
|
|
|
1550
1562
|
def clear():
|
|
1551
1563
|
cur = None
|
package/ui/index.html
CHANGED
|
@@ -461,7 +461,12 @@
|
|
|
461
461
|
make it visible", 12 Sep 2026). Its own keyboard stop with its own name
|
|
462
462
|
("Remove photo"), distinct from the fit bar's Reset -- that resets the
|
|
463
463
|
QUAD; this un-chooses a SOURCE. Only present while the chip is filled. */
|
|
464
|
-
|
|
464
|
+
/* Above the scrim (z 50) and beside the popover (z 51): the chip whose
|
|
465
|
+
popover is open stays visible and lit, so it is clear which source is
|
|
466
|
+
being chosen; the other chip stays clickable to switch (14 Sep 2026). The
|
|
467
|
+
top bar creates no stacking context of its own, so this z-index reaches
|
|
468
|
+
the root. */
|
|
469
|
+
.chipwrap{position:relative;display:inline-flex;min-width:0;z-index:52}
|
|
465
470
|
.chipwrap .inputchip:not(.is-empty){padding-right:30px}
|
|
466
471
|
.chipclear{position:absolute;right:5px;top:50%;transform:translateY(-50%);
|
|
467
472
|
width:22px;height:22px;padding:0;border-radius:999px;flex:none;
|