feat: 分发包改为字节码,并补上 MuMu 探测和下载列表滚动
This commit is contained in:
@@ -180,6 +180,37 @@ def copy_server(stage: Path) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hide_server_source(stage: Path) -> None:
|
||||||
|
server = stage / "server"
|
||||||
|
python = stage / "runtime" / "python" / "python.exe"
|
||||||
|
if not python.is_file():
|
||||||
|
raise SystemExit("还没有内置 Python,不能编译服务端")
|
||||||
|
run(
|
||||||
|
[
|
||||||
|
str(python),
|
||||||
|
"-m",
|
||||||
|
"compileall",
|
||||||
|
"-b",
|
||||||
|
"-q",
|
||||||
|
"-s",
|
||||||
|
str(server),
|
||||||
|
"-p",
|
||||||
|
"server",
|
||||||
|
str(server),
|
||||||
|
],
|
||||||
|
server,
|
||||||
|
)
|
||||||
|
for path in server.rglob("*.py"):
|
||||||
|
path.unlink()
|
||||||
|
for path in list(server.rglob("__pycache__")):
|
||||||
|
if path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
if any(server.rglob("*.py")):
|
||||||
|
raise SystemExit("服务端源码没有从分发包里去掉")
|
||||||
|
if not any(server.rglob("*.pyc")):
|
||||||
|
raise SystemExit("服务端没有编译出字节码")
|
||||||
|
|
||||||
|
|
||||||
def make_zip(stage: Path) -> Path:
|
def make_zip(stage: Path) -> Path:
|
||||||
suffix = "windows-x64" if sys.platform == "win32" else platform.system().lower()
|
suffix = "windows-x64" if sys.platform == "win32" else platform.system().lower()
|
||||||
archive = DIST / f"{STAGE_NAME}-{suffix}.zip"
|
archive = DIST / f"{STAGE_NAME}-{suffix}.zip"
|
||||||
@@ -220,6 +251,7 @@ def main() -> None:
|
|||||||
ui_dest = stage / "dist" / "ui"
|
ui_dest = stage / "dist" / "ui"
|
||||||
shutil.copytree(DIST / "ui", ui_dest)
|
shutil.copytree(DIST / "ui", ui_dest)
|
||||||
bundle_python(stage)
|
bundle_python(stage)
|
||||||
|
hide_server_source(stage)
|
||||||
bundle_adb(stage)
|
bundle_adb(stage)
|
||||||
bundle_ffmpeg(stage)
|
bundle_ffmpeg(stage)
|
||||||
bundle_webview(stage)
|
bundle_webview(stage)
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ router = APIRouter(prefix="/api/adb")
|
|||||||
@router.get("/devices")
|
@router.get("/devices")
|
||||||
def devices():
|
def devices():
|
||||||
return call(lambda: {"devices": Adb.locate().list_devices()})
|
return call(lambda: {"devices": Adb.locate().list_devices()})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/probe-mumu")
|
||||||
|
def probe_mumu():
|
||||||
|
return call(lambda: Adb.locate().probe_mumu())
|
||||||
|
|||||||
@@ -59,6 +59,14 @@ class Adb:
|
|||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
|
|
||||||
|
def probe_mumu(self) -> dict:
|
||||||
|
ports = sorted(_listening_local_ports() & _mumu_ports())
|
||||||
|
if not ports:
|
||||||
|
raise RuntimeError("没有发现正在运行的 MuMu。请先打开模拟器,再探测。")
|
||||||
|
for port in ports:
|
||||||
|
self._run(["connect", f"127.0.0.1:{port}"], timeout=8)
|
||||||
|
return {"devices": self.list_devices(), "ports": ports}
|
||||||
|
|
||||||
def read_profile(self, serial: str) -> DeviceProfile:
|
def read_profile(self, serial: str) -> DeviceProfile:
|
||||||
serial = self._ready(serial)
|
serial = self._ready(serial)
|
||||||
profile = DeviceProfile.builtin()
|
profile = DeviceProfile.builtin()
|
||||||
@@ -192,3 +200,44 @@ class Adb:
|
|||||||
|
|
||||||
def _run(self, args: list[str], timeout: int = 20) -> subprocess.CompletedProcess:
|
def _run(self, args: list[str], timeout: int = 20) -> subprocess.CompletedProcess:
|
||||||
return subprocess.run([self.executable, *args], capture_output=True, timeout=timeout)
|
return subprocess.run([self.executable, *args], capture_output=True, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
def _mumu_ports() -> set[int]:
|
||||||
|
ports = {7555}
|
||||||
|
for index in range(8):
|
||||||
|
base = 16384 + index * 32
|
||||||
|
ports.add(base)
|
||||||
|
ports.add(base + 1)
|
||||||
|
port = 5555
|
||||||
|
while port <= 5599:
|
||||||
|
ports.add(port)
|
||||||
|
port += 2
|
||||||
|
return ports
|
||||||
|
|
||||||
|
|
||||||
|
def _listening_local_ports() -> set[int]:
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
["netstat", "-ano", "-p", "tcp"],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=8,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
return set()
|
||||||
|
text = proc.stdout.decode("utf-8", "replace")
|
||||||
|
if "LISTENING" not in text.upper() and "监听" not in text:
|
||||||
|
text = proc.stdout.decode("gbk", "replace")
|
||||||
|
ports = set()
|
||||||
|
for line in text.splitlines():
|
||||||
|
upper = line.upper()
|
||||||
|
if "LISTENING" not in upper and "监听" not in line:
|
||||||
|
continue
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) < 2 or ":" not in parts[1]:
|
||||||
|
continue
|
||||||
|
host, _, port_text = parts[1].rpartition(":")
|
||||||
|
if host not in ("127.0.0.1", "0.0.0.0") or not port_text.isdigit():
|
||||||
|
continue
|
||||||
|
ports.add(int(port_text))
|
||||||
|
return ports
|
||||||
|
|||||||
+10
-6
@@ -11,6 +11,7 @@ struct Backend {
|
|||||||
child: Mutex<Option<Child>>,
|
child: Mutex<Option<Child>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
fn repo_root() -> PathBuf {
|
fn repo_root() -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("..")
|
.join("..")
|
||||||
@@ -19,14 +20,17 @@ fn repo_root() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn app_root() -> PathBuf {
|
fn app_root() -> PathBuf {
|
||||||
if cfg!(debug_assertions) {
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
return repo_root();
|
return repo_root();
|
||||||
}
|
}
|
||||||
std::env::current_exe()
|
#[cfg(not(debug_assertions))]
|
||||||
.ok()
|
{
|
||||||
.and_then(|path| path.parent().map(Path::to_path_buf))
|
std::env::current_exe()
|
||||||
.filter(|path| path.join("server").is_dir())
|
.ok()
|
||||||
.unwrap_or_else(repo_root)
|
.and_then(|path| path.parent().map(Path::to_path_buf))
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn python_command(root: &Path) -> PathBuf {
|
fn python_command(root: &Path) -> PathBuf {
|
||||||
|
|||||||
+40
-6
@@ -470,6 +470,15 @@ function deviceLabel(item: Device) {
|
|||||||
return `${item.model} · ${item.serial} · ${mark}`;
|
return `${item.model} · ${item.serial} · ${mark}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyDeviceList(list: Device[]) {
|
||||||
|
devices.value = list;
|
||||||
|
const stillThere = devices.value.some((item) => item.serial === serial.value && item.state === "device");
|
||||||
|
if (!stillThere) {
|
||||||
|
const ready = devices.value.filter((item) => item.state === "device");
|
||||||
|
serial.value = ready.length === 1 ? ready[0].serial : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadDevices() {
|
async function loadDevices() {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
devicesBusy.value = true;
|
devicesBusy.value = true;
|
||||||
@@ -479,12 +488,7 @@ async function loadDevices() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(payload.detail || "读取设备失败");
|
throw new Error(payload.detail || "读取设备失败");
|
||||||
}
|
}
|
||||||
devices.value = payload.devices || [];
|
applyDeviceList(payload.devices || []);
|
||||||
const stillThere = devices.value.some((item) => item.serial === serial.value && item.state === "device");
|
|
||||||
if (!stillThere) {
|
|
||||||
const ready = devices.value.filter((item) => item.state === "device");
|
|
||||||
serial.value = ready.length === 1 ? ready[0].serial : null;
|
|
||||||
}
|
|
||||||
} catch (exc) {
|
} catch (exc) {
|
||||||
devices.value = [];
|
devices.value = [];
|
||||||
serial.value = null;
|
serial.value = null;
|
||||||
@@ -494,6 +498,29 @@ async function loadDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function probeMumu() {
|
||||||
|
error.value = "";
|
||||||
|
devicesBusy.value = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/adb/probe-mumu", { method: "POST" });
|
||||||
|
const payload = await response.json();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(payload.detail || "没有连上 MuMu");
|
||||||
|
}
|
||||||
|
applyDeviceList(payload.devices || []);
|
||||||
|
const ready = devices.value.filter((item) => item.state === "device");
|
||||||
|
if (!ready.length) {
|
||||||
|
const ports = Array.isArray(payload.ports) ? payload.ports.join("、") : "";
|
||||||
|
throw new Error(ports ? `已尝试连接 MuMu 端口 ${ports},但设备还不可用。请在模拟器里允许调试后再探测。` : "没有连上 MuMu");
|
||||||
|
}
|
||||||
|
} catch (exc) {
|
||||||
|
if (!devices.value.length) serial.value = null;
|
||||||
|
error.value = exc instanceof Error ? exc.message : "没有连上 MuMu";
|
||||||
|
} finally {
|
||||||
|
devicesBusy.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(method, (value) => {
|
watch(method, (value) => {
|
||||||
error.value = "";
|
error.value = "";
|
||||||
if (value === "adb") loadDevices();
|
if (value === "adb") loadDevices();
|
||||||
@@ -854,6 +881,7 @@ onUnmounted(stopLive);
|
|||||||
placeholder="请选择设备"
|
placeholder="请选择设备"
|
||||||
/>
|
/>
|
||||||
<n-button :loading="devicesBusy" @click="loadDevices">刷新</n-button>
|
<n-button :loading="devicesBusy" @click="loadDevices">刷新</n-button>
|
||||||
|
<n-button :loading="devicesBusy" @click="probeMumu">探测 MuMu</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-text v-if="method === 'guest'" class="hint" depth="3">将注册一个新的游客账号,并读取它的个人信息。</n-text>
|
<n-text v-if="method === 'guest'" class="hint" depth="3">将注册一个新的游客账号,并读取它的个人信息。</n-text>
|
||||||
@@ -943,6 +971,12 @@ html { color-scheme: dark; }
|
|||||||
.shell .main-pane .n-layout-content > .n-layout-scroll-container {
|
.shell .main-pane .n-layout-content > .n-layout-scroll-container {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.shell .main-pane .n-layout-content > .n-layout-scroll-container > * {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.sider { height: 100%; min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; padding: 12px 8px; }
|
.sider { height: 100%; min-height: 100%; display: flex; flex-direction: column; box-sizing: border-box; padding: 12px 8px; }
|
||||||
.user-nav {
|
.user-nav {
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ async function remove(id: string) {
|
|||||||
{{ resumeAll ? "继续" : "暂停" }}
|
{{ resumeAll ? "继续" : "暂停" }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<n-tab-pane name="active" :tab="`正在下载${props.active.length ? ` ${props.active.length}` : ''}`">
|
<n-tab-pane name="active" :tab="`正在下载${props.active.length ? ` ${props.active.length}` : ''}`" display-directive="show">
|
||||||
<div v-if="!props.active.length" class="progress-empty">没有正在下载的剧</div>
|
<div class="job-scroll">
|
||||||
<article v-for="job in props.active" :key="job.id" class="job">
|
<div v-if="!props.active.length" class="progress-empty">没有正在下载的剧</div>
|
||||||
|
<article v-for="job in props.active" :key="job.id" class="job">
|
||||||
<img v-if="coverOf(job.image)" class="job-cover" :src="coverOf(job.image)" alt="" />
|
<img v-if="coverOf(job.image)" class="job-cover" :src="coverOf(job.image)" alt="" />
|
||||||
<div v-else class="job-cover"></div>
|
<div v-else class="job-cover"></div>
|
||||||
<div class="job-main">
|
<div class="job-main">
|
||||||
@@ -115,10 +116,12 @@ async function remove(id: string) {
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="done" tab="已完成">
|
<n-tab-pane name="done" tab="已完成" display-directive="show">
|
||||||
<div v-if="!props.done.length" class="progress-empty">还没有完成的下载</div>
|
<div class="job-scroll">
|
||||||
<article v-for="job in props.done" :key="job.id" class="job">
|
<div v-if="!props.done.length" class="progress-empty">还没有完成的下载</div>
|
||||||
|
<article v-for="job in props.done" :key="job.id" class="job">
|
||||||
<img v-if="coverOf(job.image)" class="job-cover" :src="coverOf(job.image)" alt="" />
|
<img v-if="coverOf(job.image)" class="job-cover" :src="coverOf(job.image)" alt="" />
|
||||||
<div v-else class="job-cover"></div>
|
<div v-else class="job-cover"></div>
|
||||||
<div class="job-main">
|
<div class="job-main">
|
||||||
@@ -147,15 +150,34 @@ async function remove(id: string) {
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.progress-page { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; padding: 8px 20px 20px; }
|
.progress-page { height: 100%; min-height: 0; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; padding: 8px 20px 16px; }
|
||||||
.progress-page > .n-tabs { flex: 1; min-height: 0; display: flex; flex-direction: column; }
|
.progress-page > .n-tabs { flex: 1; min-height: 0; height: 100%; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
.progress-page .n-tabs-pane-wrapper { flex: 1; min-height: 0; overflow: auto; }
|
.progress-page .n-tabs-nav { flex: none; }
|
||||||
|
.progress-page .n-tabs-pane-wrapper {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.progress-page .n-tab-pane {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.job-scroll { flex: 1; min-height: 0; overflow: auto; }
|
||||||
.bulk-hold { margin-left: 12px; }
|
.bulk-hold { margin-left: 12px; }
|
||||||
.progress-empty { padding: 28px 0; color: rgba(255, 255, 255, 0.45); }
|
.progress-empty { padding: 28px 0; color: rgba(255, 255, 255, 0.45); }
|
||||||
.job {
|
.job {
|
||||||
|
|||||||
Reference in New Issue
Block a user