device-shots 0.3.0 → 0.4.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.
- package/README.md +53 -8
- package/dist/index.js +41 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# device-shots
|
|
2
2
|
|
|
3
|
-
A CLI tool that captures screenshots from running iOS simulators and Android emulators, then
|
|
3
|
+
A CLI tool that captures screenshots from running iOS simulators and Android emulators, then frames them for store-ready use. iOS screenshots get real device bezels, Android screenshots get a clean black border with rounded corners. Built for developers who need store-ready screenshots without manual work.
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
@@ -9,7 +9,8 @@ A CLI tool that captures screenshots from running iOS simulators and Android emu
|
|
|
9
9
|
3. **Captures screenshots** — Takes screenshots from every discovered device in one go
|
|
10
10
|
4. **Transparent Android status bar** — Automatically makes the Android status bar area transparent using ImageMagick
|
|
11
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. **
|
|
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
|
|
13
14
|
|
|
14
15
|
## Use cases
|
|
15
16
|
|
|
@@ -28,8 +29,8 @@ A CLI tool that captures screenshots from running iOS simulators and Android emu
|
|
|
28
29
|
|
|
29
30
|
### Optional
|
|
30
31
|
|
|
31
|
-
- **ImageMagick** — needed
|
|
32
|
-
- **Python 3** — needed for iOS screenshot framing. The tool auto-creates a virtual environment at `~/.device-shots/.venv` and installs `device-frames-core` and `Pillow` automatically on first use
|
|
32
|
+
- **ImageMagick** — needed for Android status bar transparency and Android framing (black border + rounded corners). Install with `brew install imagemagick`
|
|
33
|
+
- **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
|
|
33
34
|
|
|
34
35
|
## Install
|
|
35
36
|
|
|
@@ -67,7 +68,7 @@ device-shots capture checkout -b com.example.myapp --time "10:30"
|
|
|
67
68
|
### Frame existing screenshots
|
|
68
69
|
|
|
69
70
|
```bash
|
|
70
|
-
# Frame all unframed iOS
|
|
71
|
+
# Frame all unframed screenshots (iOS + Android) in ./.screenshots
|
|
71
72
|
device-shots frame
|
|
72
73
|
|
|
73
74
|
# Frame from a specific directory
|
|
@@ -83,11 +84,11 @@ device-shots frame --force
|
|
|
83
84
|
device-shots init
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
Creates a `.device-shotsrc.json` in the current directory so you don't have to pass `--bundle-id` every time:
|
|
87
|
+
Creates a `.device-shotsrc.json` in the current directory so you don't have to pass `--bundle-id` every time. It will ask for both production and dev bundle IDs:
|
|
87
88
|
|
|
88
89
|
```json
|
|
89
90
|
{
|
|
90
|
-
"bundleId": "com.example.myapp",
|
|
91
|
+
"bundleId": ["com.example.myapp", "com.example.myapp.dev"],
|
|
91
92
|
"output": "./.screenshots",
|
|
92
93
|
"platform": "both",
|
|
93
94
|
"time": "9:41",
|
|
@@ -95,6 +96,14 @@ Creates a `.device-shotsrc.json` in the current directory so you don't have to p
|
|
|
95
96
|
}
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
A single string also works if you only have one bundle ID:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"bundleId": "com.example.myapp"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
98
107
|
## Config file
|
|
99
108
|
|
|
100
109
|
The tool uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig), so you can configure it in any of these ways:
|
|
@@ -105,6 +114,20 @@ The tool uses [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig), so you
|
|
|
105
114
|
- `device-shots.config.js`
|
|
106
115
|
- `"device-shots"` key in `package.json`
|
|
107
116
|
|
|
117
|
+
## Multiple bundle IDs
|
|
118
|
+
|
|
119
|
+
Most apps have separate bundle IDs for production and development builds. You can pass an array to `bundleId` in the config:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"bundleId": ["com.example.myapp", "com.example.myapp.dev"]
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
During discovery, the tool tries each bundle ID on every running device and captures from whichever build is installed. This means it works regardless of whether you have the dev or production build running.
|
|
128
|
+
|
|
129
|
+
The CLI `--bundle-id` flag accepts a single ID for quick one-off captures.
|
|
130
|
+
|
|
108
131
|
## Output structure
|
|
109
132
|
|
|
110
133
|
Screenshots are organized by platform and screen size bucket, matching App Store and Play Store requirements:
|
|
@@ -125,7 +148,9 @@ Screenshots are organized by platform and screen size bucket, matching App Store
|
|
|
125
148
|
├── android/
|
|
126
149
|
│ └── phone/ # Pixel 9 Pro, etc.
|
|
127
150
|
│ ├── dashboard.png
|
|
128
|
-
│
|
|
151
|
+
│ ├── dashboard_framed.png
|
|
152
|
+
│ ├── settings.png
|
|
153
|
+
│ └── settings_framed.png
|
|
129
154
|
└── metadata.json
|
|
130
155
|
```
|
|
131
156
|
|
|
@@ -175,6 +200,26 @@ Tracks which physical device was used for each size bucket:
|
|
|
175
200
|
}
|
|
176
201
|
```
|
|
177
202
|
|
|
203
|
+
## Framing
|
|
204
|
+
|
|
205
|
+
Both platforms get framed automatically after capture (disable with `--no-frame`).
|
|
206
|
+
|
|
207
|
+
### iOS — Device bezels
|
|
208
|
+
|
|
209
|
+
Uses [device-frames-core](https://pypi.org/project/device-frames-core/) to wrap screenshots in realistic Apple device frames (iPhone, iPad). The device model is auto-detected from the screenshot resolution. Requires Python 3 (venv is managed automatically).
|
|
210
|
+
|
|
211
|
+
### Android — Black border with rounded corners
|
|
212
|
+
|
|
213
|
+
Uses ImageMagick to add a black bezel-like border with rounded corners. Dimensions scale proportionally to the screenshot:
|
|
214
|
+
|
|
215
|
+
| Property | Value |
|
|
216
|
+
|----------|-------|
|
|
217
|
+
| Border width | ~1.8% of image width |
|
|
218
|
+
| Inner corner radius | ~4.5% of image width |
|
|
219
|
+
| Outer corner radius | inner radius + border width |
|
|
220
|
+
|
|
221
|
+
For a 1080px wide screenshot, this produces a ~19px border with ~49px rounded corners.
|
|
222
|
+
|
|
178
223
|
## License
|
|
179
224
|
|
|
180
225
|
MIT
|
package/dist/index.js
CHANGED
|
@@ -416,14 +416,28 @@ async function makeStatusBarTransparent(serial, imagePath) {
|
|
|
416
416
|
|
|
417
417
|
// src/devices/discover.ts
|
|
418
418
|
async function discoverDevices(bundleId, platform = "both") {
|
|
419
|
+
const ids = Array.isArray(bundleId) ? bundleId : [bundleId];
|
|
419
420
|
const results = [];
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
421
|
+
const seen = /* @__PURE__ */ new Set();
|
|
422
|
+
for (const id of ids) {
|
|
423
|
+
if (platform === "ios" || platform === "both") {
|
|
424
|
+
const iosDevices = await discoverIosDevices(id);
|
|
425
|
+
for (const d of iosDevices) {
|
|
426
|
+
if (!seen.has(d.captureId)) {
|
|
427
|
+
seen.add(d.captureId);
|
|
428
|
+
results.push(d);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (platform === "android" || platform === "both") {
|
|
433
|
+
const androidDevices = await discoverAndroidDevices(id);
|
|
434
|
+
for (const d of androidDevices) {
|
|
435
|
+
if (!seen.has(d.captureId)) {
|
|
436
|
+
seen.add(d.captureId);
|
|
437
|
+
results.push(d);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
427
441
|
}
|
|
428
442
|
return results;
|
|
429
443
|
}
|
|
@@ -594,12 +608,13 @@ async function frameAllAndroidScreenshots(screenshotsDir, force = false) {
|
|
|
594
608
|
async function captureCommand(options) {
|
|
595
609
|
const config = await loadConfig();
|
|
596
610
|
const bundleId = options.bundleId || config.bundleId;
|
|
597
|
-
if (!bundleId) {
|
|
611
|
+
if (!bundleId || Array.isArray(bundleId) && bundleId.length === 0) {
|
|
598
612
|
console.error(
|
|
599
613
|
pc.red("Bundle ID is required. Use --bundle-id or set it in config.")
|
|
600
614
|
);
|
|
601
615
|
process.exit(1);
|
|
602
616
|
}
|
|
617
|
+
const bundleIdDisplay = Array.isArray(bundleId) ? bundleId.join(", ") : bundleId;
|
|
603
618
|
const outputDir = options.output || config.output;
|
|
604
619
|
const platform = options.platform || config.platform;
|
|
605
620
|
const time = options.time || config.time;
|
|
@@ -608,13 +623,13 @@ async function captureCommand(options) {
|
|
|
608
623
|
const devices = await discoverDevices(bundleId, platform);
|
|
609
624
|
spinner.stop();
|
|
610
625
|
if (devices.length === 0) {
|
|
611
|
-
console.error(pc.red(`No devices found with ${
|
|
626
|
+
console.error(pc.red(`No devices found with ${bundleIdDisplay} installed.`));
|
|
612
627
|
console.error("Start a simulator/emulator and install the app first.");
|
|
613
628
|
process.exit(1);
|
|
614
629
|
}
|
|
615
630
|
console.log(
|
|
616
631
|
pc.bold(`
|
|
617
|
-
Detected ${devices.length} device(s) with ${
|
|
632
|
+
Detected ${devices.length} device(s) with ${bundleIdDisplay}:`)
|
|
618
633
|
);
|
|
619
634
|
for (const device of devices) {
|
|
620
635
|
const sizeDir = join4(outputDir, device.platform, device.screenSize);
|
|
@@ -842,11 +857,11 @@ async function frameCommand(dir, options) {
|
|
|
842
857
|
var program = new Command();
|
|
843
858
|
program.name("device-shots").description(
|
|
844
859
|
"Capture and frame mobile app screenshots from iOS simulators and Android emulators"
|
|
845
|
-
).version("0.
|
|
860
|
+
).version("0.4.0");
|
|
846
861
|
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) => {
|
|
847
862
|
await captureCommand({ name, ...opts });
|
|
848
863
|
});
|
|
849
|
-
program.command("frame").description("Frame existing
|
|
864
|
+
program.command("frame").description("Frame existing screenshots with device bezels or borders").argument("[dir]", "Screenshots directory").option("-i, --input <dir>", "Screenshots directory").option("-f, --force", "Re-frame existing screenshots").action(async (dir, opts) => {
|
|
850
865
|
await frameCommand(dir, opts);
|
|
851
866
|
});
|
|
852
867
|
program.command("init").description("Create a .device-shotsrc.json config file").action(async () => {
|
|
@@ -855,16 +870,24 @@ program.command("init").description("Create a .device-shotsrc.json config file")
|
|
|
855
870
|
console.log(pc3.yellow(`${configPath} already exists.`));
|
|
856
871
|
return;
|
|
857
872
|
}
|
|
858
|
-
const response = await prompts2(
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
873
|
+
const response = await prompts2([
|
|
874
|
+
{
|
|
875
|
+
type: "text",
|
|
876
|
+
name: "bundleId",
|
|
877
|
+
message: "Production bundle ID (e.g. com.example.myapp)"
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
type: "text",
|
|
881
|
+
name: "devBundleId",
|
|
882
|
+
message: "Dev bundle ID (leave empty to skip)"
|
|
883
|
+
}
|
|
884
|
+
]);
|
|
863
885
|
if (!response.bundleId) {
|
|
864
886
|
console.log("Aborting.");
|
|
865
887
|
return;
|
|
866
888
|
}
|
|
867
|
-
const
|
|
889
|
+
const bundleId = response.devBundleId ? [response.bundleId, response.devBundleId] : response.bundleId;
|
|
890
|
+
const config = createDefaultConfig(bundleId);
|
|
868
891
|
writeFileSync2(configPath, config + "\n");
|
|
869
892
|
console.log(pc3.green(`Created ${configPath}`));
|
|
870
893
|
});
|