device-shots 0.4.2 → 0.5.0

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.
Files changed (3) hide show
  1. package/README.md +7 -9
  2. package/dist/index.js +41 -67
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,10 +7,9 @@ A CLI tool that captures screenshots from running iOS simulators and Android emu
7
7
  1. **Discovers devices** — Finds all running iOS simulators and Android emulators that have your app installed
8
8
  2. **Cleans up status bars** — Sets a uniform status bar (9:41, full battery, full signal) on all devices before capturing
9
9
  3. **Captures screenshots** — Takes screenshots from every discovered device in one go
10
- 4. **Transparent Android status bar** — Automatically makes the Android status bar area transparent using ImageMagick
11
- 5. **Frames iOS screenshots** — Wraps iOS screenshots in device bezels (iPhone/iPad frames) using [device-frames-core](https://pypi.org/project/device-frames-core/)
12
- 6. **Frames Android screenshots** — Adds a black border with rounded corners for a clean device-like look (requires ImageMagick)
13
- 7. **Organizes by screen size** — Saves screenshots into store-aligned size buckets (e.g. `6.9`, `6.3`, `phone`) instead of device names
10
+ 4. **Frames iOS screenshots** — Wraps iOS screenshots in device bezels (iPhone/iPad frames) using [device-frames-core](https://pypi.org/project/device-frames-core/)
11
+ 5. **Frames Android screenshots** — Adds uniform black padding with matching rounded corners on both the screenshot and frame (requires ImageMagick)
12
+ 6. **Organizes by screen size** — Saves screenshots into store-aligned size buckets (e.g. `6.9`, `6.3`, `phone`) instead of device names
14
13
 
15
14
  ## Use cases
16
15
 
@@ -29,7 +28,7 @@ A CLI tool that captures screenshots from running iOS simulators and Android emu
29
28
 
30
29
  ### Optional
31
30
 
32
- - **ImageMagick** — needed for Android status bar transparency and Android framing (black border + rounded corners). Install with `brew install imagemagick`
31
+ - **ImageMagick** — needed for Android screenshot framing (black border + rounded corners). Install with `brew install imagemagick`
33
32
  - **Python 3** — needed for iOS screenshot framing with device bezels. The tool auto-creates a virtual environment at `~/.device-shots/.venv` and installs `device-frames-core` and `Pillow` automatically on first use
34
33
 
35
34
  ## Install
@@ -210,15 +209,14 @@ Uses [device-frames-core](https://pypi.org/project/device-frames-core/) to wrap
210
209
 
211
210
  ### Android — Black frame with rounded corners
212
211
 
213
- Uses ImageMagick to clip the screenshot to rounded corners (overflow hidden) and place it on a black rounded rectangle with uniform padding on all sides. Dimensions scale proportionally to the screenshot:
212
+ Uses ImageMagick to clip the screenshot to rounded corners (overflow hidden) and place it on a black rounded rectangle with uniform padding on all sides. The screenshot and frame share the same border radius. Dimensions scale proportionally:
214
213
 
215
214
  | Property | Value |
216
215
  |----------|-------|
217
216
  | Padding | ~2.5% of image width (equal on all sides) |
218
- | Inner corner radius | ~4% of image width |
219
- | Outer corner radius | inner radius + padding |
217
+ | Border radius | ~4% of image width (same for screenshot and frame) |
220
218
 
221
- For a 1080px wide screenshot, this produces ~27px padding with ~43px inner rounded corners.
219
+ For a 1080px wide screenshot, this produces ~27px uniform padding with ~43px rounded corners.
222
220
 
223
221
  ## License
224
222
 
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { writeFileSync as writeFileSync2, existsSync as existsSync5 } from "fs";
5
+ import { writeFileSync as writeFileSync2, existsSync as existsSync5, rmSync } from "fs";
6
6
  import pc3 from "picocolors";
7
7
  import prompts2 from "prompts";
8
8
 
@@ -374,49 +374,6 @@ async function captureAndroidScreenshot(serial, outputPath) {
374
374
  return false;
375
375
  }
376
376
  }
377
- async function makeStatusBarTransparent(serial, imagePath) {
378
- if (!await commandExists("magick")) return false;
379
- const adb = getAdbPath();
380
- const STATUS_BAR_HEIGHT_DP = 24;
381
- const { stdout: densityOutput } = await run(adb, [
382
- "-s",
383
- serial,
384
- "shell",
385
- "wm",
386
- "density"
387
- ]);
388
- const densityMatch = densityOutput.match(/(\d+)\s*$/m);
389
- if (!densityMatch) return false;
390
- const density = parseInt(densityMatch[1], 10);
391
- const statusBarPx = Math.ceil(STATUS_BAR_HEIGHT_DP * density / 160);
392
- const { stdout: identify } = await run("magick", [
393
- "identify",
394
- "-format",
395
- "%w",
396
- imagePath
397
- ]);
398
- const imgWidth = identify.trim();
399
- if (!imgWidth) return false;
400
- try {
401
- await runOrFail("magick", [
402
- imagePath,
403
- "-region",
404
- `${imgWidth}x${statusBarPx}+0+0`,
405
- "-alpha",
406
- "set",
407
- "-channel",
408
- "A",
409
- "-evaluate",
410
- "set",
411
- "0",
412
- "+channel",
413
- imagePath
414
- ]);
415
- return true;
416
- } catch {
417
- return false;
418
- }
419
- }
420
377
 
421
378
  // src/devices/discover.ts
422
379
  async function discoverDevices(bundleId, platform = "both") {
@@ -523,20 +480,12 @@ async function frameAndroidScreenshot(inputPath, outputPath) {
523
480
  const width = parseInt(match[1], 10);
524
481
  const height = parseInt(match[2], 10);
525
482
  const padding = Math.round(width * 0.025);
526
- const innerRadius = Math.round(width * 0.04);
527
- const outerRadius = innerRadius + padding;
483
+ const radius = Math.round(width * 0.04);
528
484
  const totalW = width + padding * 2;
529
485
  const totalH = height + padding * 2;
530
486
  try {
487
+ const tmpRounded = inputPath.replace(".png", "_rounded_tmp.png");
531
488
  await runOrFail("magick", [
532
- // 1. Create black rounded rectangle background (outer frame)
533
- "-size",
534
- `${totalW}x${totalH}`,
535
- "xc:none",
536
- "-draw",
537
- `fill black roundrectangle 0,0 ${totalW - 1},${totalH - 1} ${outerRadius},${outerRadius}`,
538
- // 2. Load screenshot and clip to rounded corners (overflow hidden)
539
- "(",
540
489
  inputPath,
541
490
  "-alpha",
542
491
  "set",
@@ -547,13 +496,20 @@ async function frameAndroidScreenshot(inputPath, outputPath) {
547
496
  "-draw",
548
497
  `fill black color 0,0 reset`,
549
498
  "-draw",
550
- `fill white roundrectangle 0,0 ${width - 1},${height - 1} ${innerRadius},${innerRadius}`,
499
+ `fill white roundrectangle 0,0 ${width - 1},${height - 1} ${radius},${radius}`,
551
500
  ")",
552
501
  "-compose",
553
502
  "DstIn",
554
503
  "-composite",
555
- ")",
556
- // 3. Place rounded screenshot centered on the black frame
504
+ tmpRounded
505
+ ]);
506
+ await runOrFail("magick", [
507
+ "-size",
508
+ `${totalW}x${totalH}`,
509
+ "xc:none",
510
+ "-draw",
511
+ `fill black roundrectangle 0,0 ${totalW - 1},${totalH - 1} ${radius},${radius}`,
512
+ tmpRounded,
557
513
  "-gravity",
558
514
  "center",
559
515
  "-compose",
@@ -561,6 +517,11 @@ async function frameAndroidScreenshot(inputPath, outputPath) {
561
517
  "-composite",
562
518
  outputPath
563
519
  ]);
520
+ try {
521
+ const { unlinkSync: unlinkSync2 } = await import("fs");
522
+ unlinkSync2(tmpRounded);
523
+ } catch {
524
+ }
564
525
  return true;
565
526
  } catch {
566
527
  return false;
@@ -719,15 +680,6 @@ Detected ${devices.length} device(s) with ${bundleIdDisplay}:`)
719
680
  success = await captureIosScreenshot(device.captureId, tmpPath);
720
681
  } else {
721
682
  success = await captureAndroidScreenshot(device.captureId, tmpPath);
722
- if (success) {
723
- const transparent = await makeStatusBarTransparent(
724
- device.captureId,
725
- tmpPath
726
- );
727
- if (transparent) {
728
- s.text = `${icon}: Captured from ${device.displayName} (status bar transparent)`;
729
- }
730
- }
731
683
  }
732
684
  if (success) {
733
685
  captured.push({
@@ -877,7 +829,7 @@ async function frameCommand(dir, options) {
877
829
  var program = new Command();
878
830
  program.name("device-shots").description(
879
831
  "Capture and frame mobile app screenshots from iOS simulators and Android emulators"
880
- ).version("0.4.2");
832
+ ).version("0.5.0");
881
833
  program.command("capture").description("Capture screenshots from running devices").argument("[name]", "Screenshot name").option("-b, --bundle-id <id>", "App bundle ID").option("-o, --output <dir>", "Output directory").option("-p, --platform <platform>", "ios, android, or both").option("--no-frame", "Skip framing after capture").option("--time <time>", "Status bar time", "9:41").action(async (name, opts) => {
882
834
  await captureCommand({ name, ...opts });
883
835
  });
@@ -911,4 +863,26 @@ program.command("init").description("Create a .device-shotsrc.json config file")
911
863
  writeFileSync2(configPath, config + "\n");
912
864
  console.log(pc3.green(`Created ${configPath}`));
913
865
  });
866
+ program.command("clean").description("Delete all screenshots and metadata").option("-o, --output <dir>", "Screenshots directory to clean").option("-y, --yes", "Skip confirmation").action(async (opts) => {
867
+ const config = await loadConfig();
868
+ const outputDir = opts.output || config.output;
869
+ if (!existsSync5(outputDir)) {
870
+ console.log(pc3.dim(`Nothing to clean \u2014 ${outputDir} does not exist.`));
871
+ return;
872
+ }
873
+ if (!opts.yes) {
874
+ const response = await prompts2({
875
+ type: "confirm",
876
+ name: "confirm",
877
+ message: `Delete everything in ${outputDir}?`,
878
+ initial: false
879
+ });
880
+ if (!response.confirm) {
881
+ console.log("Aborting.");
882
+ return;
883
+ }
884
+ }
885
+ rmSync(outputDir, { recursive: true, force: true });
886
+ console.log(pc3.green(`Cleaned ${outputDir}`));
887
+ });
914
888
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "device-shots",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Capture and frame mobile app screenshots from iOS simulators and Android emulators",
5
5
  "type": "module",
6
6
  "bin": {