Compare commits

...

10 commits
V0.2 ... main

14 changed files with 425 additions and 92 deletions

10
.gitignore vendored
View file

@ -1,4 +1,4 @@
/launcher.json /conf.yml
/dist /dist
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
@ -162,4 +162,10 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ .idea/
launcher.iml
# Mower-ng
git/
python/
mower-ng/

View file

@ -9,5 +9,11 @@
前端运行 `npm run build` 生成 `ui/dist`,之后安装 PyInstaller,运行 前端运行 `npm run build` 生成 `ui/dist`,之后安装 PyInstaller,运行
```bash ```bash
pyinstaller -w -F --add-data ui/dist:ui/dist main.py pyinstaller -w --add-data ui/dist:ui/dist launcher.py
```
在dist文件夹生成launcher文件夹,切到dist文件夹下运行
```bash
tar -cf launcher.tar launcher
``` ```

View file

@ -2,28 +2,27 @@ import json
import mimetypes import mimetypes
from pathlib import Path from pathlib import Path
from shutil import rmtree from shutil import rmtree
from subprocess import PIPE, STDOUT, Popen from subprocess import CREATE_NO_WINDOW, PIPE, STDOUT, Popen
import requests
import webview import webview
from launcher import config
from log import logger
import shutil
import os
mimetypes.add_type("text/html", ".html") mimetypes.add_type("text/html", ".html")
mimetypes.add_type("text/css", ".css") mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/javascript", ".js") mimetypes.add_type("application/javascript", ".js")
version = "2024-11-28" version = "v0.2"
config = { get_new_version_url = (
"page": "init", "https://git.zhaozuohong.vip/api/v1/repos/mower-ng/launcher/releases/latest"
"branch": "slow", )
"mirror": "aliyun",
}
config_path = Path("launcher.json")
try: upgrade_script_name = "upgrade.bat"
with config_path.open("r") as f:
user_config = json.load(f)
config.update(user_config)
except Exception:
pass
def custom_event(data): def custom_event(data):
@ -39,42 +38,69 @@ mirror_list = {
"sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple", "sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple",
} }
command_list = { command_list = {
"lfs": "git\\bin\\git lfs install", "lfs": "git\\bin\\git lfs install",
"ensurepip": "python\\python -m ensurepip --default-pip", "ensurepip": "python\\python -m ensurepip --default-pip",
"clone": "git\\bin\\git clone https://git.zhaozuohong.vip/mower-ng/mower-ng.git --branch slow", "clone": "git\\bin\\git clone https://git.zhaozuohong.vip/mower-ng/mower-ng.git --branch slow",
"fetch": lambda: f"..\\git\\bin\\git fetch origin {config['branch']}", "fetch": lambda: f"..\\git\\bin\\git fetch origin {config.conf.branch}",
"switch": lambda: f"..\\git\\bin\\git switch -f {config['branch']}", "switch": lambda: f"..\\git\\bin\\git switch -f {config.conf.branch}",
"reset": lambda: f"..\\git\\bin\\git reset --hard origin/{config['branch']}", "reset": lambda: f"..\\git\\bin\\git reset --hard origin/{config.conf.branch}",
"pip_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config['mirror']]} -r requirements.txt --no-warn-script-location", "pip_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config.conf.mirror]} -r requirements.txt --no-warn-script-location",
"webview": "start ..\\python\\pythonw webview_ui.py", "webview": "start ..\\python\\pythonw webview_ui.py",
"manager": "start ..\\python\\pythonw manager.py", "manager": "start ..\\python\\pythonw manager.py",
} }
class Api: class Api:
def get_branch(self):
return config["branch"]
def set_branch(self, branch): def load_config(self):
config["branch"] = branch logger.info("读取配置文件")
return config.conf.model_dump()
def get_page(self): def save_config(self, conf):
return config["page"] logger.info(f"更新配置文件{conf}")
config.conf = config.Conf(**conf)
config.save_conf()
def set_page(self, page): def get_version(self):
config["page"] = page return version
def get_mirror(self): def get_new_version(self):
return config["mirror"] logger.info("获取最新版本号")
response = requests.get(get_new_version_url)
return response.json()
def set_mirror(self, mirror): # 更新启动器本身
config["mirror"] = mirror def update_self(self, download_url):
# 下载压缩包的全路径
download_path = os.path.join(os.getcwd(), os.path.basename(download_url))
logger.info(f"下载新版本: {download_url}{download_path}")
response = requests.get(download_url, stream=True)
if response.status_code == 200:
with open(download_path, "wb") as file:
shutil.copyfileobj(response.raw, file)
logger.info("下载完成")
else:
logger.error(f"下载新版本失败: {response.status_code}")
return f"下载新版本失败: {response.status_code}"
def update_self(self): script_path = os.path.join(os.getcwd(), upgrade_script_name)
# 更新启动器本身 folder_path = os.path.join(os.getcwd(), "_internal")
pass exe_path = os.path.join(os.getcwd(), "launcher.exe")
with open(script_path, "w") as b:
TempList = f"@echo off\n"
TempList += f"timeout /t 3 /nobreak\n" # 等待进程退出
TempList += f"rmdir {folder_path}\n" # 删除_internal
TempList += f"del {exe_path}\n" # 删除exe
TempList += f"tar -xf {download_path} -C ..\n" # 解压压缩包
TempList += f"timeout /t 1 /nobreak\n" # 等待解压
TempList += f"start {exe_path}\n" # 启动新程序
TempList += f"del {download_path}\n" # 删除压缩包
TempList += f"exit"
b.write(TempList)
# 不显示cmd窗口
Popen([script_path], creationflags=CREATE_NO_WINDOW)
os._exit(0)
def rm_site_packages(self): def rm_site_packages(self):
site_packages_path = Path("./python/Lib/site-packages") site_packages_path = Path("./python/Lib/site-packages")
@ -97,7 +123,7 @@ class Api:
custom_event(command + "\n") custom_event(command + "\n")
try: try:
with Popen( with Popen(
command, stdout=PIPE, stderr=STDOUT, shell=True, cwd=cwd, bufsize=0 command, stdout=PIPE, stderr=STDOUT, shell=True, cwd=cwd, bufsize=0
) as p: ) as p:
for data in p.stdout: for data in p.stdout:
try: try:
@ -112,8 +138,13 @@ class Api:
return "failed" return "failed"
window = webview.create_window("mower-ng launcher", "ui/dist/index.html", js_api=Api()) # 如果当前路径存在更新脚本,则删除
webview.start() if Path(upgrade_script_name).exists():
os.remove(upgrade_script_name)
with config_path.open("w") as f: # url = "ui/dist/index.html"
json.dump(config, f) url = "http://localhost:5173/"
window = webview.create_window(
f"mower-ng launcher {version}", url, js_api=Api()
)
webview.start()

0
launcher/__init__.py Normal file
View file

View file

@ -0,0 +1,41 @@
import json
import os
import yaml
from yamlcore import CoreDumper, CoreLoader
from launcher.config.conf import Conf
from pathlib import Path
conf_path = Path(os.path.join(os.getcwd(), "conf.json"))
def save_conf():
with conf_path.open("w", encoding="utf8") as f:
json.dump(conf.model_dump(), f, ensure_ascii=False, indent=4) # Use json.dump
# yaml.dump(
# conf.model_dump(),
# f,
# Dumper=CoreDumper,
# encoding="utf-8",
# default_flow_style=False,
# allow_unicode=True,
# )
def load_conf():
global conf
if not conf_path.is_file():
conf_path.parent.mkdir(exist_ok=True)
conf = Conf()
save_conf()
return
with conf_path.open("r", encoding="utf-8") as f:
data = yaml.load(f, Loader=CoreLoader)
if data is None:
data = {}
conf = Conf(**data)
conf: Conf
load_conf()

36
launcher/config/conf.py Normal file
View file

@ -0,0 +1,36 @@
from pydantic import BaseModel, model_validator
from pydantic_core import PydanticUndefined
class ConfModel(BaseModel):
@model_validator(mode="before")
@classmethod
def nested_defaults(cls, data):
for name, field in cls.model_fields.items():
if name not in data:
if field.default is PydanticUndefined:
data[name] = field.annotation()
else:
data[name] = field.default
return data
class Total(ConfModel):
"""整体"""
# 所在页面
page: str = "init"
class UpdatePart(ConfModel):
"""更新代码"""
# mower-ng 代码分支
branch: str = "slow"
# PyPI 仓库镜像
mirror: str = "aliyun"
class Conf(
Total,
UpdatePart,
):
pass

56
ui/package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "ui", "name": "ui",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"pinia": "^2.2.8",
"vue": "^3.5.11" "vue": "^3.5.11"
}, },
"devDependencies": { "devDependencies": {
@ -1131,6 +1132,11 @@
"@vue/shared": "3.5.11" "@vue/shared": "3.5.11"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"node_modules/@vue/eslint-config-prettier": { "node_modules/@vue/eslint-config-prettier": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.0.0.tgz", "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.0.0.tgz",
@ -2534,6 +2540,56 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pinia": {
"version": "2.2.8",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.2.8.tgz",
"integrity": "sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ==",
"dependencies": {
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"@vue/composition-api": "^1.4.0",
"typescript": ">=4.4.4",
"vue": "^2.6.14 || ^3.5.11"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/pinia/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/pkg-types": { "node_modules/pkg-types": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",

View file

@ -11,6 +11,7 @@
"format": "prettier --write src/" "format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"pinia": "^2.2.8",
"vue": "^3.5.11" "vue": "^3.5.11"
}, },
"devDependencies": { "devDependencies": {

View file

@ -4,15 +4,26 @@ import Launch from '@/pages/Launch.vue'
import Update from '@/pages/Update.vue' import Update from '@/pages/Update.vue'
import Fix from '@/pages/Fix.vue' import Fix from '@/pages/Fix.vue'
import { dateZhCN, zhCN } from 'naive-ui' import { dateZhCN, zhCN } from 'naive-ui'
import Settings from '@/pages/Settings.vue'
import { useConfigStore } from '@/stores/config.js'
const configStore = useConfigStore()
const loading = ref(true) const loading = ref(true)
const page = ref(null) const page = ref(null)
let conf
function load_config() { async function init_version() {
pywebview.api.get_page().then((value) => { version.value = await pywebview.api.get_version()
page.value = value new_version.value = await pywebview.api.get_new_version()
loading.value = false if (new_version.value.tag_name > version.value) {
}) update_able.value = true
}
}
async function initialize_config() {
await configStore.load_config()
conf = configStore.config
await init_version()
loading.value = false
} }
const log = ref('') const log = ref('')
@ -27,9 +38,11 @@ watch(log, () => {
onMounted(() => { onMounted(() => {
if (window.pywebview && pywebview.api) { if (window.pywebview && pywebview.api) {
load_config() initialize_config()
} else { } else {
window.addEventListener('pywebviewready', load_config) window.addEventListener('pywebviewready', () => {
initialize_config()
})
} }
window.addEventListener('log', (e) => { window.addEventListener('log', (e) => {
log.value += e.detail.log log.value += e.detail.log
@ -38,7 +51,6 @@ onMounted(() => {
function set_page(value) { function set_page(value) {
log.value = '' log.value = ''
pywebview.api.set_page(value)
} }
const running = ref(false) const running = ref(false)
@ -46,6 +58,15 @@ provide('running', running)
const steps = ref([]) const steps = ref([])
provide('steps', steps) provide('steps', steps)
const update_able = ref(false)
provide('update_able', update_able)
const version = ref('')
provide('version', version)
const new_version = ref({})
provide('new_version', new_version)
</script> </script>
<template> <template>
@ -58,13 +79,22 @@ provide('steps', steps)
type="card" type="card"
placement="left" placement="left"
class="container" class="container"
:default-value="page" v-model:value="conf.page"
@update:value="set_page" @update:value="set_page"
> >
<n-tab-pane :disabled="running" name="init" tab="初始化"><init /></n-tab-pane> <n-tab-pane :disabled="running" name="init" tab="初始化"><init /></n-tab-pane>
<n-tab-pane :disabled="running" name="update" tab="更新代码"><update /></n-tab-pane> <n-tab-pane :disabled="running" name="update" tab="更新代码"><update /></n-tab-pane>
<n-tab-pane :disabled="running" name="launch" tab="启动程序"><launch /></n-tab-pane> <n-tab-pane :disabled="running" name="launch" tab="启动程序"><launch /></n-tab-pane>
<n-tab-pane :disabled="running" name="fix" tab="依赖修复"><fix /></n-tab-pane> <n-tab-pane :disabled="running" name="fix" tab="依赖修复"><fix /></n-tab-pane>
<n-tab-pane :disabled="running" name="settings">
<template #tab>
<n-space :wrap="false">
设置
<n-tag v-if="update_able" round type="success"></n-tag>
</n-space>
</template>
<settings />
</n-tab-pane>
</n-tabs> </n-tabs>
</n-notification-provider> </n-notification-provider>
<n-global-style /> <n-global-style />

View file

@ -2,6 +2,10 @@ import 'vfonts/Lato.css'
import 'vfonts/FiraCode.css' import 'vfonts/FiraCode.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue' import App from './App.vue'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app') app.mount('#app')

View file

@ -3,39 +3,48 @@ const running = inject('running')
const notification = useNotification() const notification = useNotification()
async function rm_site_packages_and_python_scripts() { async function rm_site_packages_and_python_scripts() {
running.value = true running.value = true
notification['info']({ notification['info']({
content: '提示', content: '提示',
meta: '开始移除site-packages和python/Script目录', meta: '开始移除site-packages和python/Script目录',
duration: 3000 duration: 3000
}) })
const response = await pywebview.api.rm_site_packages() const response = await pywebview.api.rm_site_packages()
notification['info']({ notification['info']({
content: '提示', content: '提示',
meta: response, meta: response,
duration: 3000 duration: 3000
}) })
const response2 = await pywebview.api.rm_python_scripts() const response2 = await pywebview.api.rm_python_scripts()
notification['info']({ notification['info']({
content: '提示', content: '提示',
meta: response2, meta: response2,
duration: 3000 duration: 3000
}) })
running.value = false running.value = false
} }
</script> </script>
<template> <template>
<n-flex vertical style=" <n-flex
vertical
style="
gap: 16px; gap: 16px;
height: 100%; height: 100%;
padding: 16px; padding: 16px;
box-sizing: border-box; box-sizing: border-box;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
"> "
<n-button class="fix-btn" type="error" secondary size="large" @click="rm_site_packages_and_python_scripts"> >
移除 site-packages python/Scripts 目录 <n-button
class="fix-btn"
type="error"
secondary
size="large"
@click="rm_site_packages_and_python_scripts"
>
移除 site-packages python/Scripts 目录
</n-button> </n-button>
</n-flex> </n-flex>
</template> </template>

94
ui/src/pages/Settings.vue Normal file
View file

@ -0,0 +1,94 @@
<script setup>
import { SyncCircle } from '@vicons/ionicons5'
const notification = useNotification()
const update_able = inject('update_able')
const running = inject('running')
const version = inject('version')
const new_version = inject('new_version')
const check_running = ref(false)
const update_self_running = ref(false)
async function update_self() {
running.value = true
update_self_running.value = true
const response = await pywebview.api.update_self(
new_version.value['assets'][0]['browser_download_url']
)
notification['error']({
content: '错误',
meta: response,
duration: 3000
})
update_self_running.value = false
running.value = false
}
async function open_new_version_html() {
window.open(new_version.value['html_url'])
}
async function check_update() {
running.value = true
check_running.value = true
new_version.value = await pywebview.api.get_new_version()
if (new_version.value.tag_name > version.value) {
update_able.value = true
notification['info']({
content: '提示',
meta: '有新版本可更新',
duration: 3000
})
} else {
update_able.value = false
notification['info']({
content: '提示',
meta: '当前已是最新版本',
duration: 3000
})
}
check_running.value = false
running.value = false
}
</script>
<template>
<n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box">
<n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left">
<n-form-item label="版本">
<n-space align="center">
{{ version }}
<n-button
type="success"
:loading="check_running"
:disabled="running"
@click="check_update"
>
<template #icon>
<n-icon :component="SyncCircle"></n-icon>
</template>
检查更新
</n-button>
</n-space>
</n-form-item>
<n-alert style="margin: 8px 0" type="success" v-if="update_able">
<template #header>
最新版本{{ `${new_version.tag_name} ${new_version.name}` }}
<n-button style="float: right" @click="open_new_version_html">了解此版本</n-button>
</template>
<n-space>
<n-button
type="success"
:loading="update_self_running"
:disabled="running"
@click="update_self"
>
立即更新
</n-button>
</n-space>
</n-alert>
</n-form>
</n-flex>
</template>

View file

@ -1,23 +1,10 @@
<script setup> <script setup>
import { useConfigStore } from '@/stores/config.js'
const conf = useConfigStore().config
const branch = ref(null) const branch = ref(null)
const mirror = ref(null) const mirror = ref(null)
onMounted(() => {
pywebview.api.get_branch().then((value) => {
branch.value = value
})
pywebview.api.get_mirror().then((value) => {
mirror.value = value
})
})
watch(branch, () => {
pywebview.api.set_branch(branch.value)
})
watch(mirror, () => {
pywebview.api.set_mirror(mirror.value)
})
const steps = computed(() => [ const steps = computed(() => [
{ {
title: '更新源码', title: '更新源码',
@ -41,7 +28,7 @@ provide('current_state', current_state)
<n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box"> <n-flex vertical style="gap: 16px; height: 100%; padding: 16px; box-sizing: border-box">
<n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left"> <n-form label-placement="left" :show-feedback="false" label-width="auto" label-align="left">
<n-form-item label="mower-ng 代码分支"> <n-form-item label="mower-ng 代码分支">
<n-radio-group v-model:value="branch"> <n-radio-group v-model:value="conf.branch">
<n-flex> <n-flex>
<n-radio value="fast">测试版</n-radio> <n-radio value="fast">测试版</n-radio>
<n-radio value="slow">稳定版</n-radio> <n-radio value="slow">稳定版</n-radio>
@ -49,7 +36,7 @@ provide('current_state', current_state)
</n-radio-group> </n-radio-group>
</n-form-item> </n-form-item>
<n-form-item label="PyPI 仓库镜像"> <n-form-item label="PyPI 仓库镜像">
<n-radio-group v-model:value="mirror"> <n-radio-group v-model:value="conf.mirror">
<n-flex> <n-flex>
<n-radio value="pypi">PyPI</n-radio> <n-radio value="pypi">PyPI</n-radio>
<n-radio value="aliyun">阿里云镜像站</n-radio> <n-radio value="aliyun">阿里云镜像站</n-radio>

32
ui/src/stores/config.js Normal file
View file

@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
export const useConfigStore = defineStore('config', () => {
class Config {
constructor(conf) {
this.page = conf.page
this.branch = conf.branch
this.mirror = conf.mirror
}
}
const config = ref({})
async function load_config() {
const conf = await pywebview.api.load_config()
config.value = new Config(conf)
console.log('config.value', config.value)
}
watch(
config,
() => {
pywebview.api.save_config(config.value)
},
{ deep: true }
)
return {
load_config,
config
}
})