Skip to content

Commit

Permalink
feat: show version on web page
Browse files Browse the repository at this point in the history
  • Loading branch information
codematrixer committed Oct 7, 2024
1 parent 8dd3954 commit 6ce25a5
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 83 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
license = "MIT"
Expand Down
20 changes: 1 addition & 19 deletions uiviewer/__init__.py
Original file line number Diff line number Diff line change
@@ -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 -*-
54 changes: 6 additions & 48 deletions uiviewer/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand All @@ -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
Expand Down
10 changes: 7 additions & 3 deletions uiviewer/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
4 changes: 0 additions & 4 deletions uiviewer/_error.py

This file was deleted.

19 changes: 19 additions & 0 deletions uiviewer/_logger.py
Original file line number Diff line number Diff line change
@@ -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']
5 changes: 5 additions & 0 deletions uiviewer/_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-

import importlib.metadata

__version__ = importlib.metadata.version('uiviewer')
1 change: 1 addition & 0 deletions uiviewer/routers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
66 changes: 66 additions & 0 deletions uiviewer/routers/api.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions uiviewer/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<div class="header">
<div style="margin-right: 20px;">
<span style="font-weight: bold; font-size: 20px;">UI Viewer</span>
<span> 1.0.0</span>
<span>{{version}}</span>
</div>
<el-select
v-model="platform"
Expand All @@ -42,7 +42,7 @@
</el-option>
</el-select>

<el-tooltip v-if="platform === 'ios'" content="Set WDA url, default value is http://localhost:8100" placement="top">
<el-tooltip v-if="platform === 'ios'" content="Set WDA url, such as http://localhost:8100" placement="top">
<el-input
class="custom-input"
v-model="wdaUrl"
Expand All @@ -55,7 +55,7 @@
<el-input
class="custom-input"
v-model="snapshotMaxDepth"
style="width: 100px; margin-right: 20px;"
style="width: 110px; margin-right: 20px;"
placeholder="maxDepth">
</el-input>
</el-tooltip>
Expand Down
24 changes: 23 additions & 1 deletion uiviewer/static/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
23 changes: 19 additions & 4 deletions uiviewer/static/js/index.js
Original file line number Diff line number Diff line change
@@ -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', ''),
Expand Down Expand Up @@ -76,6 +77,9 @@ new Vue({
this.$refs.treeRef.filter(val);
}
},
created() {
this.fetchVersion();
},
mounted() {
this.loadCachedScreenshot();
const canvas = this.$el.querySelector('#hierarchyCanvas');
Expand All @@ -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() {
Expand All @@ -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) {
Expand Down

0 comments on commit 6ce25a5

Please sign in to comment.