diff --git a/README.md b/README.md index e7b8ef8..292f10a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ # ui-viewer +[![github actions](https://github.com/codematrixer/ui-viewer/actions/workflows/release.yml/badge.svg)](https://github.com/codematrixer/ui-viewer/actions) +[![pypi version](https://img.shields.io/pypi/v/uiviewer.svg)](https://pypi.python.org/pypi/uiviewer) +![python](https://img.shields.io/pypi/pyversions/uiviewer.svg) + UI hierarchy visualization tool, supporting Android, iOS, HarmonyOS NEXT. ![showcase](./docs/imgs/show.gif) # Installation +- python3.8+ + ```shell pip3 install -U uiviewer ``` diff --git a/pyproject.toml b/pyproject.toml index 03ca4bb..f63e8a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uiviewer" -version = "1.0.0" +version = "1.0.1" description = "UI hierarchy visualization tool, supporting Android, iOS, HarmonyOS NEXT." authors = ["codematrixer "] license = "MIT" diff --git a/uiviewer/__init__.py b/uiviewer/__init__.py index 29620bf..7c68785 100644 --- a/uiviewer/__init__.py +++ b/uiviewer/__init__.py @@ -1,19 +1 @@ -# -*- coding: utf-8 -*- - -import logging - -formatter = logging.Formatter('[%(asctime)s] %(filename)15s[line:%(lineno)4d] \ - [%(levelname)s] %(message)s', - datefmt='%Y-%m-%d %H:%M:%S') - -logger = logging.getLogger('hmdriver2') -logger.setLevel(logging.DEBUG) - -console_handler = logging.StreamHandler() -console_handler.setLevel(logging.DEBUG) -console_handler.setFormatter(formatter) - -logger.addHandler(console_handler) - - -__all__ = ['logger'] +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/uiviewer/__main__.py b/uiviewer/__main__.py index 1ed72c1..1cdeb9d 100644 --- a/uiviewer/__main__.py +++ b/uiviewer/__main__.py @@ -4,20 +4,12 @@ import webbrowser import uvicorn import threading -from typing import Union, Optional -from fastapi import FastAPI, Request, Query, HTTPException +from fastapi import FastAPI, Request, HTTPException from fastapi.staticfiles import StaticFiles -from fastapi.responses import JSONResponse, RedirectResponse - -from uiviewer._device import ( - list_serials, - init_device, - cached_devices, - AndroidDevice, - IosDevice, - HarmonyDevice -) +from fastapi.responses import JSONResponse + +from uiviewer.routers import api from uiviewer._models import ApiResponse @@ -29,6 +21,8 @@ app.mount("/static", StaticFiles(directory=static_dir), name="static") +app.include_router(api.router) + @app.exception_handler(Exception) def global_exception_handler(request: Request, exc: Exception): @@ -50,42 +44,6 @@ def open_browser(port): webbrowser.open_new(f"http://127.0.0.1:{port}") -@app.get("/") -def root(): - return RedirectResponse(url="/static/index.html") - - -@app.get("/health") -def health(): - return "ok" - - -@app.get("/{platform}/serials", response_model=ApiResponse) -def get_serials(platform: str): - serials = list_serials(platform) - return ApiResponse.doSuccess(serials) - - -@app.post("/{platform}/{serial}/connect", response_model=ApiResponse) -def connect(platform: str, serial: str, wdaUrl: Optional[str] = Query(None), maxDepth: Optional[int] = Query(None)): - ret = init_device(platform, serial, wdaUrl, maxDepth) - return ApiResponse.doSuccess(ret) - - -@app.get("/{platform}/{serial}/screenshot", response_model=ApiResponse) -def screenshot(platform: str, serial: str): - device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) - data = device.take_screenshot() - return ApiResponse.doSuccess(data) - - -@app.get("/{platform}/{serial}/hierarchy", response_model=ApiResponse) -def dump_hierarchy(platform: str, serial: str): - device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) - data = device.dump_hierarchy() - return ApiResponse.doSuccess(data) - - def run(port=8000): timer = threading.Timer(1.0, open_browser, args=[port]) timer.daemon = True diff --git a/uiviewer/_device.py b/uiviewer/_device.py index 1089203..b165222 100644 --- a/uiviewer/_device.py +++ b/uiviewer/_device.py @@ -2,7 +2,7 @@ import abc import tempfile -from typing import List, Dict, Union, Tuple +from typing import List, Dict, Union, Tuple, Optional from functools import cached_property # python3.8+ from PIL import Image @@ -101,9 +101,13 @@ class IosDevice(DeviceMeta): def __init__(self, udid: str, wda_url: str, max_depth: int) -> None: self.udid = udid self.wda_url = wda_url - self.max_depth = max_depth + self._max_depth = max_depth self.client = wda.Client(wda_url) + @property + def max_depth(self) -> int: + return int(self._max_depth) if self._max_depth else 30 + @cached_property def scale(self) -> int: return self.client.scale @@ -152,7 +156,7 @@ def get_device(platform: str, serial: str, wda_url: str, max_depth: int) -> Unio cached_devices = {} -def init_device(platform: str, serial: str, wda_url: str = None, max_depth: int = 30) -> bool: +def init_device(platform: str, serial: str, wda_url: str, max_depth: int): if serial not in list_serials(platform): raise HTTPException(status_code=500, detail=f"Device<{serial}> not found") diff --git a/uiviewer/_error.py b/uiviewer/_error.py deleted file mode 100644 index bcb6ef4..0000000 --- a/uiviewer/_error.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- - -class ScreenShotError(Exception): - pass diff --git a/uiviewer/_logger.py b/uiviewer/_logger.py new file mode 100644 index 0000000..29620bf --- /dev/null +++ b/uiviewer/_logger.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +import logging + +formatter = logging.Formatter('[%(asctime)s] %(filename)15s[line:%(lineno)4d] \ + [%(levelname)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S') + +logger = logging.getLogger('hmdriver2') +logger.setLevel(logging.DEBUG) + +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) + +logger.addHandler(console_handler) + + +__all__ = ['logger'] diff --git a/uiviewer/_version.py b/uiviewer/_version.py new file mode 100644 index 0000000..d8a93dd --- /dev/null +++ b/uiviewer/_version.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +import importlib.metadata + +__version__ = importlib.metadata.version('uiviewer') \ No newline at end of file diff --git a/uiviewer/routers/__init__.py b/uiviewer/routers/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/uiviewer/routers/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/uiviewer/routers/api.py b/uiviewer/routers/api.py new file mode 100644 index 0000000..1dbdf07 --- /dev/null +++ b/uiviewer/routers/api.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +from typing import Union, Optional + +from fastapi import APIRouter, Query +from fastapi.responses import RedirectResponse + +from uiviewer._device import ( + list_serials, + init_device, + cached_devices, + AndroidDevice, + IosDevice, + HarmonyDevice +) +from uiviewer._models import ApiResponse +from uiviewer._version import __version__ + + +router = APIRouter() + + +@router.get("/") +def root(): + return RedirectResponse(url="/static/index.html") + + +@router.get("/health") +def health(): + return "ok" + + +@router.get("/version", response_model=ApiResponse) +def get_version(): + return ApiResponse.doSuccess(__version__) + + +@router.get("/{platform}/serials", response_model=ApiResponse) +def get_serials(platform: str): + serials = list_serials(platform) + return ApiResponse.doSuccess(serials) + + +@router.post("/{platform}/{serial}/connect", response_model=ApiResponse) +def connect( + platform: str, + serial: str, + wdaUrl: Union[str, None] = Query(None), + maxDepth: Union[int, None] = Query(None) +): + ret = init_device(platform, serial, wdaUrl, maxDepth) + return ApiResponse.doSuccess(ret) + + +@router.get("/{platform}/{serial}/screenshot", response_model=ApiResponse) +def screenshot(platform: str, serial: str): + device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) + data = device.take_screenshot() + return ApiResponse.doSuccess(data) + + +@router.get("/{platform}/{serial}/hierarchy", response_model=ApiResponse) +def dump_hierarchy(platform: str, serial: str): + device: Union[AndroidDevice, IosDevice, HarmonyDevice] = cached_devices.get((platform, serial)) + data = device.dump_hierarchy() + return ApiResponse.doSuccess(data) \ No newline at end of file diff --git a/uiviewer/static/index.html b/uiviewer/static/index.html index 0a962ae..4b4d495 100644 --- a/uiviewer/static/index.html +++ b/uiviewer/static/index.html @@ -17,7 +17,7 @@
UI Viewer - 1.0.0 + {{version}}
- + diff --git a/uiviewer/static/js/api.js b/uiviewer/static/js/api.js index eb1bb42..94b416c 100644 --- a/uiviewer/static/js/api.js +++ b/uiviewer/static/js/api.js @@ -7,15 +7,37 @@ async function checkResponse(response) { return response.json(); } +export async function getVersion() { + const response = await fetch(`${API_HOST}version`); + return checkResponse(response); +} + export async function listDevices(platform) { const response = await fetch(`${API_HOST}${platform}/serials`); return checkResponse(response); } export async function connectDevice(platform, serial, wdaUrl, maxDepth) { - const response = await fetch(`${API_HOST}${platform}/${serial}/connect?wdaUrl=${wdaUrl}&maxDepth=${maxDepth}`, { + let url = `${API_HOST}${platform}/${serial}/connect`; + + if (platform === 'ios') { + const queryParams = []; + if (wdaUrl) { + queryParams.push(`wdaUrl=${encodeURIComponent(wdaUrl)}`); + } + if (maxDepth) { + queryParams.push(`maxDepth=${encodeURIComponent(maxDepth)}`); + } + + if (queryParams.length > 0) { + url += `?${queryParams.join('&')}`; + } + } + + const response = await fetch(url, { method: 'POST' }); + return checkResponse(response); } diff --git a/uiviewer/static/js/index.js b/uiviewer/static/js/index.js index 8f324b4..478aff1 100644 --- a/uiviewer/static/js/index.js +++ b/uiviewer/static/js/index.js @@ -1,18 +1,19 @@ import { saveToLocalStorage, getFromLocalStorage, copyToClipboard } from './utils.js'; -import { listDevices, connectDevice, fetchScreenshot, fetchHierarchy } from './api.js'; +import { getVersion, listDevices, connectDevice, fetchScreenshot, fetchHierarchy } from './api.js'; new Vue({ el: '#app', data() { return { + version: "", platform: getFromLocalStorage('platform', 'harmony'), serial: getFromLocalStorage('serial', ''), devices: [], isConnected: false, isConnecting: false, isDumping: false, - wdaUrl: getFromLocalStorage('wdaUrl', 'http://localhost:8100'), + wdaUrl: getFromLocalStorage('wdaUrl', ''), snapshotMaxDepth: getFromLocalStorage('snapshotMaxDepth', 30), packageName: getFromLocalStorage('packageName', ''), @@ -76,6 +77,9 @@ new Vue({ this.$refs.treeRef.filter(val); } }, + created() { + this.fetchVersion(); + }, mounted() { this.loadCachedScreenshot(); const canvas = this.$el.querySelector('#hierarchyCanvas'); @@ -88,12 +92,20 @@ new Vue({ this.serial = '' this.isConnected = false; }, + async fetchVersion() { + try { + const response = await getVersion(); + this.version = response.data; + } catch (err) { + console.error(err); + } + }, async listDevice() { try { const response = await listDevices(this.platform); this.devices = response.data; - } catch (error) { - console.error(error); + } catch (err) { + this.$message({ showClose: true, message: `Error: ${err.message}`, type: 'error' }); } }, async connectDevice() { @@ -102,6 +114,9 @@ new Vue({ if (!this.serial) { throw new Error('Please select device first'); } + if (this.platform === 'ios' && !this.wdaUrl) { + throw new Error('Please input wdaUrl first'); + } const response = await connectDevice(this.platform, this.serial, this.wdaUrl, this.snapshotMaxDepth); if (response.success) {