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.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
+ ```
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
@@ -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,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: mobilesnap
3
+ Version: 0.1.0
4
+ Requires-Python: >=3.8
5
+ Requires-Dist: typer>=0.9.0
6
+ Requires-Dist: playwright>=1.40.0
7
+ Requires-Dist: rich>=13.0.0
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
@@ -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,2 @@
1
+ [console_scripts]
2
+ mobilesnap = mobilesnap.main:app
@@ -0,0 +1,3 @@
1
+ typer>=0.9.0
2
+ playwright>=1.40.0
3
+ rich>=13.0.0
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ typer>=0.9.0
2
+ playwright>=1.40.0
3
+ rich>=13.0.0
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
+ )