screengraft 0.62.1 → 0.62.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.62.1",
3
+ "version": "0.62.3",
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",
@@ -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
- out = os.path.join(out_dir, f"{abs(hash(item['path']))}_{int(item.get('mtime') or 0)}_clip.jpg")
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
- limit = int(q.get("limit", ["40"])[0])
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
@@ -574,7 +574,6 @@
574
574
  .segbtn[disabled],.segbtn[disabled]:hover{background:var(--raise-low);color:var(--mute)}
575
575
  .segbtn:focus-visible{outline-offset:-2px}
576
576
  .segbtn svg{width:16px;height:16px;display:block;flex:none}
577
- .segbtn svg text{font:600 8px/1 -apple-system,BlinkMacSystemFont,"SF Pro",system-ui,sans-serif;fill:var(--ink)}
578
577
  .lrail{position:absolute;left:24px;top:48px;z-index:3;display:flex;flex-direction:column;gap:var(--s2)}
579
578
  .lrail .corners{margin-top:var(--s6)} /* 8 + 24 = the design's 32px between rails */
580
579
  .brail{position:absolute;left:0;right:0;bottom:8px;z-index:3;display:flex;justify-content:center;
@@ -1137,7 +1136,7 @@
1137
1136
  <div class="lrail" id="lrail">
1138
1137
  <div class="bgroup" role="group" aria-label="Zoom">
1139
1138
  <button class="segbtn" id="czIn" aria-label="Zoom in" title="Zoom in (⌘ + scroll zooms to the pointer)"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--icon)" d="M8.5 7.5H12.5V8.5H8.5V12.5H7.5V8.5H3.5V7.5H7.5V3.5H8.5V7.5Z"/></svg></button>
1140
- <button class="segbtn" id="cz100" aria-label="Actual size, 100%" title="Actual size — one photo pixel per screen pixel"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M14.1025 1.00488C14.6067 1.05621 15 1.48232 15 2V14L14.9951 14.1025C14.9472 14.573 14.573 14.9472 14.1025 14.9951L14 15H2C1.48232 15 1.05621 14.6067 1.00488 14.1025L1 14V2C1 1.44772 1.44772 1 2 1H14L14.1025 1.00488ZM2 14H14V2H2V14Z"/><text x="8" y="10.8" text-anchor="middle">1x</text></svg></button>
1139
+ <button class="segbtn" id="cz100" aria-label="Actual size, 100%" title="Actual size — one photo pixel per screen pixel"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--mute)" d="M14.1025 1.00488C14.6067 1.05621 15 1.48232 15 2V14L14.9951 14.1025C14.9472 14.573 14.573 14.9472 14.1025 14.9951L14 15H2C1.48232 15 1.05621 14.6067 1.00488 14.1025L1 14V2C1 1.44772 1.44772 1 2 1H14L14.1025 1.00488ZM2 14H14V2H2V14Z"/><g transform="translate(2.77 4.245)" fill="var(--ink)"><path d="M4.80713 7.75049L6.78906 4.85547L4.78564 1.96045H5.88672L7.26172 4.11426H7.34766L8.70654 1.96045H9.75391L7.78809 4.81787L9.78076 7.75049H8.68506L7.31543 5.56445H7.22949L5.85449 7.75049H4.80713Z"/><path d="M2.05713 7.75049V1.07422H1.97119L0 2.50293V1.48242L2.0625 0H3.02393V7.75049H2.05713Z"/></g></svg></button>
1141
1140
  <button class="segbtn" id="czOut" aria-label="Zoom out" title="Zoom out"><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="var(--icon)" d="M12.5 8.5H3.5V7.5H12.5V8.5Z"/></svg></button>
1142
1141
  </div>
1143
1142
  <div class="bgroup">