mobile-snap 1.0.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.html +862 -0
- package/README.md +105 -0
- package/__pycache__/setup.cpython-312.pyc +0 -0
- package/bin/cli.js +143 -0
- package/mobilesnap/__init__.py +1 -0
- package/mobilesnap/__pycache__/main.cpython-312.pyc +0 -0
- package/mobilesnap/main.py +192 -0
- package/mobilesnap.egg-info/PKG-INFO +9 -0
- package/mobilesnap.egg-info/SOURCES.txt +10 -0
- package/mobilesnap.egg-info/dependency_links.txt +1 -0
- package/mobilesnap.egg-info/entry_points.txt +2 -0
- package/mobilesnap.egg-info/requires.txt +3 -0
- package/mobilesnap.egg-info/top_level.txt +1 -0
- package/package.json +26 -0
- package/requirements.txt +3 -0
- package/setup.py +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# MobileSnap 📸
|
|
2
|
+
|
|
3
|
+
MobileSnap adalah alat CLI (Command Line Interface) berbasis Node.js yang dirancang untuk mengotomatisasi pengambilan tangkapan layar (screenshot) App Store & Google Play Store dengan presisi piksel tinggi langsung dari server pengembangan lokal (seperti Astro, Next.js, React, atau Vue).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🏗️ Arsitektur Sistem
|
|
8
|
+
|
|
9
|
+
MobileSnap dirancang dengan fokus pada efisiensi, keandalan, dan kemudahan penggunaan. Berikut adalah diagram alur kerja utama aplikasi:
|
|
10
|
+
|
|
11
|
+
```mermaid
|
|
12
|
+
graph TD
|
|
13
|
+
A[Pengguna menjalankan perintah CLI npx] --> B[Commander CLI Parser]
|
|
14
|
+
B --> C{Validasi Argumen & URL}
|
|
15
|
+
C -->|URL Valid| D[Inisialisasi Playwright Async]
|
|
16
|
+
C -->|URL Tidak Valid| E[Console Error & Exit]
|
|
17
|
+
D --> F[Buka Headless Chromium Browser]
|
|
18
|
+
F --> G[Looping berdasarkan Pilihan Platform: iOS / Android / Both]
|
|
19
|
+
G --> H[Konfigurasi Context & Viewport Emulator Perangkat]
|
|
20
|
+
H --> I[Looping berdasarkan Jalur/Rute URL]
|
|
21
|
+
I --> J[Navigasi Halaman & Tunggu State Network Idle]
|
|
22
|
+
J --> K[Ambil Tangkapan Layar & Simpan File]
|
|
23
|
+
K --> L[Tutup Context Browser]
|
|
24
|
+
L --> M[Selesai & Tampilkan Ringkasan]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Komponen Utama
|
|
28
|
+
|
|
29
|
+
1. **Parser CLI ([bin/cli.js](file:///d:/Deweb/MobileSnap/bin/cli.js))**: Menggunakan library `Commander` untuk memproses input parameter dari pengguna secara intuitif.
|
|
30
|
+
2. **Mesin Otomatisasi Browser**: Berbasis `Playwright` untuk menjalankan proses Chromium tanpa kepala (*headless*).
|
|
31
|
+
3. **Pengaturan Emulator Presisi**:
|
|
32
|
+
- **Skala Perangkat (DPI)**: Ditetapkan ke `deviceScaleFactor: 3` untuk menghasilkan kualitas tangkapan layar yang sangat tajam (Retina/High DPI) sesuai standar rilis.
|
|
33
|
+
- **Agen Pengguna (User Agent)**: Dikonfigurasi dinamis sesuai platform target (iOS menggunakan user agent iPhone, Android menggunakan user agent Google Pixel 7).
|
|
34
|
+
4. **Sinkronisasi Hidrasi Web**: Menggunakan `page.waitForLoadState("networkidle")` untuk mendeteksi ketika semua aset selesai dimuat sebelum tangkapan layar diambil. Ini sangat penting untuk framework modern seperti Astro.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 📱 Spesifikasi Dimensi Target
|
|
39
|
+
|
|
40
|
+
MobileSnap secara otomatis mengambil gambar untuk perangkat berikut berdasarkan platform yang dipilih:
|
|
41
|
+
|
|
42
|
+
### iOS (Apple App Store)
|
|
43
|
+
| Nama Layar | Resolusi (Piksel) | Rasio Aspek | Output File Contoh |
|
|
44
|
+
| :--- | :--- | :--- | :--- |
|
|
45
|
+
| **6.7" Display** | 1290 x 2796 | 19.5:9 | `6.7_inch_home.png` |
|
|
46
|
+
| **6.5" Display** | 1242 x 2688 | 19.5:9 | `6.5_inch_home.png` |
|
|
47
|
+
|
|
48
|
+
### Android (Google Play Store)
|
|
49
|
+
| Nama Layar | Resolusi (Piksel) | Rasio Aspek | Output File Contoh |
|
|
50
|
+
| :--- | :--- | :--- | :--- |
|
|
51
|
+
| **Android Phone** | 1080 x 2400 | 20:9 | `android_phone_home.png` |
|
|
52
|
+
| **Android Tablet (10")** | 1600 x 2560 | 16:10 | `android_tablet_home.png` |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🚀 Cara Penggunaan Instan (NPX)
|
|
57
|
+
|
|
58
|
+
Anda tidak perlu menginstal apa pun secara permanen. Cukup jalankan perintah menggunakan `npx`:
|
|
59
|
+
|
|
60
|
+
```powershell
|
|
61
|
+
# 1. Jalankan langsung dari server lokal Anda
|
|
62
|
+
npx mobile-snap --url http://localhost:4321
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> [!NOTE]
|
|
66
|
+
> Jika ini adalah pertama kalinya Anda menjalankan Playwright, Anda mungkin perlu mengunduh browser binaries dengan menjalankan perintah:
|
|
67
|
+
> ```powershell
|
|
68
|
+
> npx playwright install chromium
|
|
69
|
+
> ```
|
|
70
|
+
|
|
71
|
+
Jika Anda ingin menginstalnya secara global di sistem Anda:
|
|
72
|
+
```powershell
|
|
73
|
+
npm install -g mobile-snap
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 💻 Panduan Penggunaan CLI
|
|
79
|
+
|
|
80
|
+
Aplikasi ini menerima 4 opsi utama:
|
|
81
|
+
|
|
82
|
+
| Parameter | Singkatan | Deskripsi | Standar (Default) | Pilihan |
|
|
83
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
84
|
+
| `--url` | `-u` | **(Wajib)** URL server lokal. | - | - |
|
|
85
|
+
| `--paths` | `-p` | Jalur/rute halaman yang dipisahkan tanda koma. | `/` | - |
|
|
86
|
+
| `--output`| `-o` | Nama direktori tempat menyimpan gambar. | `mobilesnap_output` | - |
|
|
87
|
+
| `--platform`| `-l`| Platform target tangkapan layar. | `ios` | `ios`, `android`, `both` |
|
|
88
|
+
|
|
89
|
+
### Contoh Perintah
|
|
90
|
+
|
|
91
|
+
#### 1. Pengambilan Halaman iOS Saja (Default)
|
|
92
|
+
```powershell
|
|
93
|
+
npx mobile-snap --url http://localhost:4321
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### 2. Pengambilan Halaman Android Saja
|
|
97
|
+
```powershell
|
|
98
|
+
npx mobile-snap --url http://localhost:4321 --platform android
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
#### 3. Pengambilan 2 Platform Sekaligus (iOS & Android)
|
|
102
|
+
Mengambil gambar halaman utama `/` dan halaman `/scan` untuk kedua platform sekaligus ke folder `hasil_store`:
|
|
103
|
+
```powershell
|
|
104
|
+
npx mobile-snap --url http://localhost:4321 --paths "/, /scan" --platform both --output hasil_store
|
|
105
|
+
```
|
|
Binary file
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { chromium } from 'playwright';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
// Device configurations grouped by platform
|
|
11
|
+
const DEVICE_CONFIGS = {
|
|
12
|
+
ios: {
|
|
13
|
+
devices: {
|
|
14
|
+
"6.7_inch": { width: 1290, height: 2796 },
|
|
15
|
+
"6.5_inch": { width: 1242, height: 2688 }
|
|
16
|
+
},
|
|
17
|
+
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
|
18
|
+
},
|
|
19
|
+
android: {
|
|
20
|
+
devices: {
|
|
21
|
+
"android_phone": { width: 1080, height: 2400 },
|
|
22
|
+
"android_tablet": { width: 1600, height: 2560 }
|
|
23
|
+
},
|
|
24
|
+
userAgent: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36"
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function safeFilename(route) {
|
|
29
|
+
const cleanPath = route.replace(/^\/+|\/+$/g, '');
|
|
30
|
+
if (!cleanPath) return 'home';
|
|
31
|
+
return cleanPath.replace(/[^a-zA-Z0-9_\-]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function captureScreenshots(url, paths, outputDir, platform) {
|
|
35
|
+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
36
|
+
url = 'http://' + url;
|
|
37
|
+
}
|
|
38
|
+
url = url.replace(/\/+$/, '');
|
|
39
|
+
|
|
40
|
+
let targetPlatforms = [];
|
|
41
|
+
if (platform === 'both') {
|
|
42
|
+
targetPlatforms = ['ios', 'android'];
|
|
43
|
+
} else {
|
|
44
|
+
targetPlatforms = [platform];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(pc.bold(pc.blue('Starting MobileSnap screenshot automation...')));
|
|
48
|
+
console.log(`Target Server: ${pc.cyan(url)}`);
|
|
49
|
+
console.log(`Platform(s): ${pc.cyan(targetPlatforms.join(', ').toUpperCase())}`);
|
|
50
|
+
console.log(`Output Directory: ${pc.cyan(path.resolve(outputDir))}\n`);
|
|
51
|
+
|
|
52
|
+
// Ensure directory exists
|
|
53
|
+
if (!fs.existsSync(outputDir)) {
|
|
54
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let browser;
|
|
58
|
+
const launchSpinner = ora('Launching Chromium browser...').start();
|
|
59
|
+
try {
|
|
60
|
+
browser = await chromium.launch({ headless: true });
|
|
61
|
+
launchSpinner.succeed('Chromium browser launched successfully');
|
|
62
|
+
} catch (err) {
|
|
63
|
+
launchSpinner.fail(pc.red('Failed to launch Chromium browser'));
|
|
64
|
+
console.error(pc.red(err.message));
|
|
65
|
+
console.log(pc.yellow('\n💡 Tips: Jalankan "npx playwright install chromium" untuk mengunduh browser binaries.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const plat of targetPlatforms) {
|
|
70
|
+
const config = DEVICE_CONFIGS[plat];
|
|
71
|
+
console.log(pc.bold(pc.blue(`💻 Platform: ${plat.toUpperCase()}`)));
|
|
72
|
+
|
|
73
|
+
for (const [deviceName, size] of Object.entries(config.devices)) {
|
|
74
|
+
console.log(pc.magenta(` 📱 Processing ${deviceName} (${size.width}x${size.height}px)...`));
|
|
75
|
+
|
|
76
|
+
const context = await browser.newContext({
|
|
77
|
+
viewport: { width: size.width, height: size.height },
|
|
78
|
+
userAgent: config.userAgent,
|
|
79
|
+
deviceScaleFactor: 3, // High DPI for crisp screenshots
|
|
80
|
+
isMobile: true,
|
|
81
|
+
hasTouch: true
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const page = await context.newPage();
|
|
85
|
+
|
|
86
|
+
for (const route of paths) {
|
|
87
|
+
const normalizedPath = '/' + route.replace(/^\/+/, '');
|
|
88
|
+
const targetUrl = `${url}${normalizedPath}`;
|
|
89
|
+
const nameSnippet = safeFilename(normalizedPath);
|
|
90
|
+
const filename = `${deviceName}_${nameSnippet}.png`;
|
|
91
|
+
const outputPath = path.join(outputDir, filename);
|
|
92
|
+
|
|
93
|
+
const pageSpinner = ora(` Navigating to ${normalizedPath}...`).start();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await page.goto(targetUrl, { timeout: 30000 });
|
|
97
|
+
pageSpinner.text = ` Waiting for network idle on ${normalizedPath}...`;
|
|
98
|
+
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
|
99
|
+
|
|
100
|
+
// Wait a brief moment for layout/dynamic scripts to settle
|
|
101
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
102
|
+
|
|
103
|
+
pageSpinner.text = ` Saving screenshot ${filename}...`;
|
|
104
|
+
await page.screenshot({ path: outputPath, fullPage: false });
|
|
105
|
+
pageSpinner.succeed(pc.green(` ✔ Saved ${filename}`));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
pageSpinner.fail(pc.red(` ✘ Failed to capture ${normalizedPath}: ${err.message}`));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await context.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
await browser.close();
|
|
116
|
+
console.log(pc.bold(pc.green(`\n🎉 Selesai! Semua tangkapan layar disimpan di '${outputDir}'.`)));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.name('mobile-snap')
|
|
121
|
+
.description('⚡ MobileSnap CLI: Automate App Store & Google Play Store screenshots')
|
|
122
|
+
.version('1.0.0')
|
|
123
|
+
.requiredOption('-u, --url <url>', 'Base URL of the local development server (e.g. localhost:3000)')
|
|
124
|
+
.option('-p, --paths <paths>', 'Comma-separated list of routes to capture', '/')
|
|
125
|
+
.option('-o, --output <output>', 'Output directory to save screenshots', 'mobilesnap_output')
|
|
126
|
+
.option('-l, --platform <platform>', 'Target platform: "ios", "android", or "both"', 'ios')
|
|
127
|
+
.action((options) => {
|
|
128
|
+
const pathList = options.paths.split(',').map(p => p.trim()).filter(Boolean);
|
|
129
|
+
const finalPaths = pathList.length ? pathList : ['/'];
|
|
130
|
+
|
|
131
|
+
const platformVal = options.platform.toLowerCase();
|
|
132
|
+
if (!['ios', 'android', 'both'].includes(platformVal)) {
|
|
133
|
+
console.error(pc.red(`Error: Platform '${options.platform}' tidak valid. Pilih antara 'ios', 'android', atau 'both'.`));
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
captureScreenshots(options.url, finalPaths, options.output, platformVal).catch(err => {
|
|
138
|
+
console.error(pc.red(`Terjadi kesalahan tidak terduga: ${err.message}`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# MobileSnap package
|
|
Binary file
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
import typer
|
|
8
|
+
from playwright.async_api import async_playwright, Error as PlaywrightError
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
# Initialize Typer app and Rich Console
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="mobilesnap",
|
|
14
|
+
help="⚡ MobileSnap: Automate pixel-precise App Store & Google Play screenshots from local web servers.",
|
|
15
|
+
add_completion=False,
|
|
16
|
+
)
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Enum for Platform choices
|
|
21
|
+
class Platform(str, Enum):
|
|
22
|
+
ios = "ios"
|
|
23
|
+
android = "android"
|
|
24
|
+
both = "both"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Device configurations grouped by platform
|
|
28
|
+
DEVICE_CONFIGS = {
|
|
29
|
+
"ios": {
|
|
30
|
+
"devices": {
|
|
31
|
+
"6.7_inch": {"width": 1290, "height": 2796},
|
|
32
|
+
"6.5_inch": {"width": 1242, "height": 2688},
|
|
33
|
+
},
|
|
34
|
+
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1",
|
|
35
|
+
},
|
|
36
|
+
"android": {
|
|
37
|
+
"devices": {
|
|
38
|
+
"android_phone": {"width": 1080, "height": 2400},
|
|
39
|
+
"android_tablet": {"width": 1600, "height": 2560},
|
|
40
|
+
},
|
|
41
|
+
"user_agent": "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.36",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def safe_filename(path: str) -> str:
|
|
47
|
+
"""Converts a URL path into a safe, descriptive file name snippet."""
|
|
48
|
+
# Strip leading/trailing slashes
|
|
49
|
+
clean_path = path.strip("/")
|
|
50
|
+
if not clean_path:
|
|
51
|
+
return "home"
|
|
52
|
+
# Replace non-alphanumeric characters with underscores
|
|
53
|
+
clean_path = re.sub(r"[^a-zA-Z0-9_\-]", "_", clean_path)
|
|
54
|
+
# Replace multiple consecutive underscores with a single one
|
|
55
|
+
clean_path = re.sub(r"_+", "_", clean_path)
|
|
56
|
+
return clean_path.strip("_")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def capture_screenshots(url: str, paths: List[str], output_dir: Path, platform: Platform):
|
|
60
|
+
"""Core async capture logic using Playwright."""
|
|
61
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
|
|
63
|
+
# Normalize url (ensure it has a scheme)
|
|
64
|
+
if not url.startswith(("http://", "https://")):
|
|
65
|
+
url = "http://" + url
|
|
66
|
+
url = url.rstrip("/")
|
|
67
|
+
|
|
68
|
+
# Determine which platforms to target
|
|
69
|
+
if platform == Platform.both:
|
|
70
|
+
target_platforms = ["ios", "android"]
|
|
71
|
+
else:
|
|
72
|
+
target_platforms = [platform.value]
|
|
73
|
+
|
|
74
|
+
console.print(f"[bold blue]Starting MobileSnap screenshot automation...[/bold blue]")
|
|
75
|
+
console.print(f"Target Server: [cyan]{url}[/cyan]")
|
|
76
|
+
console.print(f"Platform(s): [cyan]{', '.join(target_platforms).upper()}[/cyan]")
|
|
77
|
+
console.print(f"Output Directory: [cyan]{output_dir.resolve()}[/cyan]\n")
|
|
78
|
+
|
|
79
|
+
async with async_playwright() as p:
|
|
80
|
+
# Launch headless Chromium
|
|
81
|
+
with console.status("[yellow]Launching Chromium browser...[/yellow]") as status:
|
|
82
|
+
try:
|
|
83
|
+
browser = await p.chromium.launch(headless=True)
|
|
84
|
+
except Exception as e:
|
|
85
|
+
console.print(f"[bold red]Failed to launch Chromium browser: {e}[/bold red]")
|
|
86
|
+
raise typer.Exit(code=1)
|
|
87
|
+
|
|
88
|
+
# Loop through each target platform
|
|
89
|
+
for plat in target_platforms:
|
|
90
|
+
config = DEVICE_CONFIGS[plat]
|
|
91
|
+
user_agent = config["user_agent"]
|
|
92
|
+
devices = config["devices"]
|
|
93
|
+
|
|
94
|
+
console.print(f"[bold blue]💻 Platform: {plat.upper()}[/bold blue]")
|
|
95
|
+
|
|
96
|
+
for device_name, size in devices.items():
|
|
97
|
+
width, height = size["width"], size["height"]
|
|
98
|
+
console.print(f" [bold magenta]📱 Processing {device_name} ({width}x{height}px)...[/bold magenta]")
|
|
99
|
+
|
|
100
|
+
# Set up context
|
|
101
|
+
context = await browser.new_context(
|
|
102
|
+
viewport={"width": width, "height": height},
|
|
103
|
+
user_agent=user_agent,
|
|
104
|
+
device_scale_factor=3, # High DPI for crisp screenshots
|
|
105
|
+
is_mobile=True,
|
|
106
|
+
has_touch=True,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
page = await context.new_page()
|
|
110
|
+
|
|
111
|
+
for path in paths:
|
|
112
|
+
normalized_path = "/" + path.lstrip("/")
|
|
113
|
+
target_url = f"{url}{normalized_path}"
|
|
114
|
+
name_snippet = safe_filename(normalized_path)
|
|
115
|
+
filename = f"{device_name}_{name_snippet}.png"
|
|
116
|
+
output_path = output_dir / filename
|
|
117
|
+
|
|
118
|
+
with console.status(f" Navigating to {normalized_path}...") as status:
|
|
119
|
+
try:
|
|
120
|
+
await page.goto(target_url, timeout=30000)
|
|
121
|
+
status.update(f" Waiting for network idle on {normalized_path}...")
|
|
122
|
+
await page.wait_for_load_state("networkidle", timeout=15000)
|
|
123
|
+
await asyncio.sleep(0.5)
|
|
124
|
+
|
|
125
|
+
status.update(f" Saving screenshot {filename}...")
|
|
126
|
+
await page.screenshot(path=str(output_path), full_page=False)
|
|
127
|
+
|
|
128
|
+
console.print(
|
|
129
|
+
f" [green]✔[/green] Saved [bold green]{filename}[/bold green]"
|
|
130
|
+
)
|
|
131
|
+
except PlaywrightError as err:
|
|
132
|
+
console.print(
|
|
133
|
+
f" [red]✘[/red] Failed to capture [cyan]{normalized_path}[/cyan]: {err.message}"
|
|
134
|
+
)
|
|
135
|
+
except Exception as err:
|
|
136
|
+
console.print(
|
|
137
|
+
f" [red]✘[/red] Error on [cyan]{normalized_path}[/cyan]: {err}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
await context.close()
|
|
141
|
+
|
|
142
|
+
await browser.close()
|
|
143
|
+
|
|
144
|
+
console.print(f"\n[bold green]🎉 Selesai! Semua tangkapan layar disimpan di '{output_dir}'.[/bold green]")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.command()
|
|
148
|
+
def main(
|
|
149
|
+
url: str = typer.Option(
|
|
150
|
+
...,
|
|
151
|
+
"--url",
|
|
152
|
+
"-u",
|
|
153
|
+
help="Base URL of the local development server (e.g., localhost:3000 or http://127.0.0.1:4321)",
|
|
154
|
+
),
|
|
155
|
+
paths: str = typer.Option(
|
|
156
|
+
"/",
|
|
157
|
+
"--paths",
|
|
158
|
+
"-p",
|
|
159
|
+
help="Comma-separated list of paths/routes to capture (e.g., '/, /scan, /profile')",
|
|
160
|
+
),
|
|
161
|
+
output: Path = typer.Option(
|
|
162
|
+
Path("mobilesnap_output"),
|
|
163
|
+
"--output",
|
|
164
|
+
"-o",
|
|
165
|
+
help="Output directory to save the screenshots",
|
|
166
|
+
),
|
|
167
|
+
platform: Platform = typer.Option(
|
|
168
|
+
Platform.ios,
|
|
169
|
+
"--platform",
|
|
170
|
+
"-l",
|
|
171
|
+
help="Target platform: 'ios', 'android', or 'both'",
|
|
172
|
+
),
|
|
173
|
+
):
|
|
174
|
+
"""
|
|
175
|
+
⚡ MobileSnap CLI: Generate pixel-precise App Store & Google Play screenshots from a local web server automatically.
|
|
176
|
+
"""
|
|
177
|
+
# Parse paths from comma-separated string
|
|
178
|
+
path_list = [p.strip() for p in paths.split(",") if p.strip()]
|
|
179
|
+
if not path_list:
|
|
180
|
+
path_list = ["/"]
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
# Run the async core loop
|
|
184
|
+
asyncio.run(capture_screenshots(url=url, paths=path_list, output_dir=output, platform=platform))
|
|
185
|
+
except Exception as e:
|
|
186
|
+
console.print(f"[bold red]An unexpected error occurred during execution: {e}[/bold red]")
|
|
187
|
+
raise typer.Exit(code=1)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
app()
|
|
192
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
mobilesnap/__init__.py
|
|
4
|
+
mobilesnap/main.py
|
|
5
|
+
mobilesnap.egg-info/PKG-INFO
|
|
6
|
+
mobilesnap.egg-info/SOURCES.txt
|
|
7
|
+
mobilesnap.egg-info/dependency_links.txt
|
|
8
|
+
mobilesnap.egg-info/entry_points.txt
|
|
9
|
+
mobilesnap.egg-info/requires.txt
|
|
10
|
+
mobilesnap.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mobilesnap
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mobile-snap",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Automate pixel-precise App Store & Google Play screenshots from a local web server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "bin/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mobile-snap": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"screenshot",
|
|
12
|
+
"app store",
|
|
13
|
+
"play store",
|
|
14
|
+
"playwright",
|
|
15
|
+
"cli",
|
|
16
|
+
"automation"
|
|
17
|
+
],
|
|
18
|
+
"author": "First Ryan",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"commander": "^11.1.0",
|
|
22
|
+
"ora": "^7.0.1",
|
|
23
|
+
"picocolors": "^1.0.0",
|
|
24
|
+
"playwright": "^1.40.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/requirements.txt
ADDED
package/setup.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="mobilesnap",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[
|
|
8
|
+
"typer>=0.9.0",
|
|
9
|
+
"playwright>=1.40.0",
|
|
10
|
+
"rich>=13.0.0",
|
|
11
|
+
],
|
|
12
|
+
entry_points={
|
|
13
|
+
"console_scripts": [
|
|
14
|
+
"mobilesnap=mobilesnap.main:app",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
python_requires=">=3.8",
|
|
18
|
+
)
|