Compare commits

..

No commits in common. "main" and "v0.3" have entirely different histories.
main ... v0.3

29 changed files with 170 additions and 910 deletions

11
.gitignore vendored
View file

@ -1,5 +1,4 @@
/conf.yml
/conf.json
/launcher.json
/dist
# Byte-compiled / optimized / DLL files
@ -163,10 +162,4 @@ cython_debug/
# 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
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
launcher.iml
# Mower-ng
git/
python/
mower-ng/
#.idea/

View file

@ -2,29 +2,12 @@
## 开发环境
安装 `pip-tools`
```bash
pip install pip-tools
```
安装依赖
```bash
pip-sync requirements.txt
```
目前只有 `pywebview` 一个依赖。
## 打包
前端运行 `npm run build` 生成 `ui/dist`,之后安装 PyInstaller,运行
```bash
pyinstaller -w --add-data "ui/dist:ui/dist" --add-data "launcher/sys_config/config_dist.json:launcher/sys_config" launcher.py
```
在dist文件夹生成launcher文件夹
```bash
cd dist
py7zr c launcher.7z launcher
pyinstaller -w -F --add-data ui/dist:ui/dist launcher.py
```

View file

@ -1,18 +1,119 @@
import json
import mimetypes
import shutil
from pathlib import Path
from launcher.constants import update_tmp_folder
from launcher.webview import start_webview
from shutil import rmtree
from subprocess import PIPE, STDOUT, Popen
import webview
mimetypes.add_type("text/html", ".html")
mimetypes.add_type("text/css", ".css")
mimetypes.add_type("application/javascript", ".js")
if __name__ == '__main__':
version = "2024-11-28"
# 如果当前路径存在临时文件夹,则删除
if Path(update_tmp_folder).exists():
shutil.rmtree(update_tmp_folder)
config = {
"page": "init",
"branch": "slow",
"mirror": "aliyun",
}
config_path = Path("launcher.json")
start_webview()
try:
with config_path.open("r") as f:
user_config = json.load(f)
config.update(user_config)
except Exception:
pass
def custom_event(data):
data = json.dumps({"log": data})
js = f"var event = new CustomEvent('log', {{detail: {data}}}); window.dispatchEvent(event);"
window.evaluate_js(js)
mirror_list = {
"pypi": "https://pypi.org/simple",
"aliyun": "https://mirrors.aliyun.com/pypi/simple/",
"tuna": "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",
"sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple",
}
command_list = {
"lfs": "git\\bin\\git lfs install",
"ensurepip": "python\\python -m ensurepip --default-pip",
"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']}",
"switch": lambda: f"..\\git\\bin\\git switch -f {config['branch']}",
"reset": lambda: f"..\\git\\bin\\git reset --hard origin/{config['branch']}",
"pip_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config['mirror']]} -r requirements.txt --no-warn-script-location",
"webview": "start ..\\python\\pythonw webview_ui.py",
"manager": "start ..\\python\\pythonw manager.py",
}
class Api:
def get_branch(self):
return config["branch"]
def set_branch(self, branch):
config["branch"] = branch
def get_page(self):
return config["page"]
def set_page(self, page):
config["page"] = page
def get_mirror(self):
return config["mirror"]
def set_mirror(self, mirror):
config["mirror"] = mirror
def update_self(self):
# 更新启动器本身
pass
def rm_site_packages(self):
site_packages_path = Path("./python/Lib/site-packages")
if site_packages_path.exists():
rmtree(site_packages_path)
return "site-packages目录移除成功"
return "python\\Lib\\site-packages目录不存在"
def rm_python_scripts(self):
python_scripts_path = Path("./python/Scripts")
if python_scripts_path.exists():
rmtree(python_scripts_path)
return "Scripts目录移除成功"
return "python\\Scripts目录不存在"
def run(self, command, cwd=None):
command = command_list[command]
if callable(command):
command = command()
custom_event(command + "\n")
try:
with Popen(
command, stdout=PIPE, stderr=STDOUT, shell=True, cwd=cwd, bufsize=0
) as p:
for data in p.stdout:
try:
text = data.decode("utf-8")
except Exception:
text = data.decode("gbk")
custom_event(text)
if p.returncode == 0:
return "success"
except Exception as e:
custom_event(str(e))
return "failed"
window = webview.create_window(f"mower-ng launcher {version}", "ui/dist/index.html", js_api=Api())
webview.start()
with config_path.open("w") as f:
json.dump(config, f)

View file

View file

@ -1,30 +0,0 @@
import json
import os
from pathlib import Path
from launcher.config.conf import Conf
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)
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 file:
data = json.load(file)
if data is None:
data = {}
conf = Conf(**data)
conf: Conf
load_conf()

View file

@ -1,36 +0,0 @@
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

View file

@ -1,25 +0,0 @@
"""
constants.py
该模块定义了应用程序中使用的各种常量这些常量包括API URL和其他配置参数
"""
# 更新临时文件夹名
update_tmp_folder = "download_tmp"
# 更新脚本名
upgrade_script_name = "upgrade.bat"
# 获取最新版本发布信息
get_new_version_url = "https://git.zhaozuohong.vip/api/v1/repos/mower-ng/launcher/releases/latest"
# 下载地址
download_git_url = "https://list.zhaozuohong.vip/mower-ng/git.7z"
download_python_url = "https://list.zhaozuohong.vip/mower-ng/python.7z"
# pip镜像地址
mirror_list = {
"pypi": "https://pypi.org/simple",
"aliyun": "https://mirrors.aliyun.com/pypi/simple/",
"tuna": "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple",
"sjtu": "https://mirror.sjtu.edu.cn/pypi/web/simple",
}

View file

@ -1,100 +0,0 @@
import os
import time
import requests
from launcher.file.extract import extract_7z_file
from launcher.file.utils import format_size
from launcher.webview.events import custom_event
def download_file(download_name, download_url, destination_folder):
"""
下载文件到指定文件夹
:param download_name: 下载内容名
:param download_url: 文件的下载 URL
:param destination_folder: 保存下载文件的目标文件夹
:return: 下载是否成功
"""
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
filename = os.path.basename(download_url)
download_path = os.path.join(destination_folder, filename)
custom_event(f"开始下载: {download_name}")
response = requests.get(download_url, stream=True)
if response.status_code == 200:
total_size = int(response.headers.get('content-length', 0))
downloaded_size = 0
start_time = time.time()
last_update_time = time.time() # 记录上次更新时间
block_size = 4096 # 每次读取的数据块大小
with open(download_path, "wb") as file:
for data in response.iter_content(chunk_size=block_size):
file.write(data)
downloaded_size += len(data)
current_time = time.time()
elapsed_time = current_time - start_time
if elapsed_time > 0:
download_speed = downloaded_size / elapsed_time # 字节/秒
else:
download_speed = 0
progress_percent = (downloaded_size / total_size) * 100 if total_size != 0 else 0
# 检查是否需要更新进度信息,每1秒更新一次
if current_time - last_update_time >= 1:
# 格式化输出
formatted_downloaded_size = format_size(downloaded_size)
formatted_total_size = format_size(total_size)
formatted_speed = format_size(download_speed) + "/s"
custom_event(
f"下载进度: {progress_percent:.2f}% ({formatted_downloaded_size}/{formatted_total_size}), 下载速度: {formatted_speed}")
last_update_time = current_time # 更新上次更新时间
end_time = time.time()
total_elapsed_time = end_time - start_time
average_download_speed = downloaded_size / total_elapsed_time if total_elapsed_time != 0 else 0
# 格式化输出
formatted_total_elapsed_time = f"{total_elapsed_time:.2f}"
formatted_average_download_speed = format_size(average_download_speed) + "/s"
custom_event(
f"下载完成: {filename}, 耗时: {formatted_total_elapsed_time}, 下载速度: {formatted_average_download_speed}")
else:
custom_event(f"下载失败: {response.status_code}")
return False
return True
def init_download(download_name, download_url, destination_folder):
"""
初始化中的下载任务返回一个函数用于执行下载任务
:param download_name: 下载任务的名称用于在日志中记录
:param download_url: 下载文件的 URL
:param destination_folder: 下载文件的目标文件夹
:return: 一个函数用于执行下载任务
"""
def download():
target_folder = os.path.join(download_name)
if os.path.exists(target_folder):
custom_event(f"{download_name} 文件夹已存在,跳过下载")
return True
filename = os.path.basename(download_url)
if not download_file(filename, download_url, destination_folder):
return False
download_path = os.path.join(destination_folder, filename)
if not extract_7z_file(filename, download_path, destination_folder, True):
return False
return True
return download

View file

@ -1,75 +0,0 @@
import os
import time
from py7zr import py7zr
from py7zr.callbacks import ExtractCallback
from launcher.file.utils import format_size
from launcher.webview.events import custom_event
class MyExtractCallback(ExtractCallback):
def __init__(self):
super().__init__()
self.total_size = 0
self.last_print_time = time.time()
def report_start(self, archive_name, archive_format):
pass
def report_start_preparation(self):
pass
def report_end(self, processing_file_path, wrote_bytes):
self.total_size += int(wrote_bytes)
current_time = time.time()
if current_time - self.last_print_time >= 1.0: # 至少每隔1秒输出一次
custom_event(f"已解压: {format_size(self.total_size)}")
self.last_print_time = current_time
def report_postprocess(self):
pass
def report_update(self, message):
pass
def report_warning(self, message):
pass
def extract_7z_file(file_name, file_path, destination_folder, delete_after_extract=True):
"""
解压7z文件到指定文件夹
:param file_name: 7z文件的名称
:param file_path: 7z文件的路径
:param destination_folder: 解压目标文件夹
:param delete_after_extract: 解压后删除压缩文件默认删除
:return: 解压是否成功
"""
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
custom_event(f"开始解压文件: {file_name}")
try:
start_time = time.time()
with py7zr.SevenZipFile(file_path, mode='r') as z:
callback = MyExtractCallback()
z.extractall(path=destination_folder, callback=callback)
end_time = time.time()
total_elapsed_time = end_time - start_time
formatted_total_elapsed_time = f"{total_elapsed_time:.2f}"
custom_event(
f"解压完成: {file_name}, 总大小: {format_size(callback.total_size)}, 耗时: {formatted_total_elapsed_time}")
except Exception as e:
e.print_exc()
custom_event(f"解压失败: {str(e)}")
return False
if delete_after_extract:
try:
os.remove(file_path)
custom_event(f"删除{file_path}成功")
except OSError as e:
custom_event(f"删除{file_path}失败: {str(e)}")
return True

View file

@ -1,16 +0,0 @@
import os
def format_size(size_bytes):
"""格式化文件大小为人类可读的形式"""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if size_bytes < 1024:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024
return f"{size_bytes:.2f} PB"
def ensure_directory_exists(directory):
"""确保目录存在,如果不存在则创建"""
if not os.path.exists(directory):
os.makedirs(directory)

View file

@ -1,55 +0,0 @@
import json
import os
import sys
class SysConfig:
"""
读取系统配置文件
"""
# 版本
version: str
# ui路径
url: str
# 日志输出级别
log_level: str
def __init__(self):
self.config = {}
self.config_path = self.get_config_path()
self.load_config()
def get_config_path(self):
if getattr(sys, 'frozen', False):
# logger.error("打包配置")
# 如果是打包后的可执行文件
base_path = sys._MEIPASS
config_subdir = 'launcher/sys_config' # 添加子目录
config_filename = 'config_dist.json'
else:
# logger.error("本地配置")
# 如果是本地开发环境
base_path = os.path.dirname(__file__)
config_subdir = '' # 本地开发环境不需要子目录
config_filename = 'config_local.json'
config_path = os.path.join(base_path, config_subdir, config_filename)
return config_path
def load_config(self):
try:
with open(self.config_path, 'r', encoding='utf-8') as file:
self.config = json.load(file)
except FileNotFoundError:
pass
# logger.error(f"配置文件未找到: {self.config_path}")
except Exception:
pass
# logger.error(f"加载配置文件时出错: {str(e)}")
def get(self, key):
return self.config.get(key)
# 创建一个全局配置实例
sys_config = SysConfig()

View file

@ -1,5 +0,0 @@
{
"version": "v0.4",
"url": "ui/dist/index.html",
"log_level": "ERROR"
}

View file

@ -1,5 +0,0 @@
{
"version": "dev",
"url": "http://localhost:5173/",
"log_level": "INFO"
}

View file

@ -1,13 +0,0 @@
import webview
from launcher.sys_config import sys_config
from launcher.webview.api import Api
window = None
def start_webview():
global window
window = webview.create_window(f"mower-ng launcher {sys_config.get('version')}", sys_config.get('url'),
js_api=Api())
webview.start()

View file

@ -1,152 +0,0 @@
import io
import os
import subprocess
import threading
from _winapi import CREATE_NO_WINDOW
from pathlib import Path
from shutil import rmtree
from subprocess import Popen
import requests
from launcher import config
from launcher.constants import download_git_url, download_python_url, get_new_version_url, upgrade_script_name, \
mirror_list
from launcher.file.download import init_download, download_file
from launcher.file.extract import extract_7z_file
from launcher.file.utils import ensure_directory_exists
from launcher.log import logger
from launcher.sys_config import sys_config
from launcher.webview.events import custom_event
command_list = {
"download_git": lambda: init_download("git", download_git_url, os.getcwd()),
"download_python": lambda: init_download("python", download_python_url, os.getcwd()),
"lfs": "git\\bin\\git lfs install",
"ensurepip": "python\\python -m ensurepip --default-pip",
"clone": "git\\bin\\git -c lfs.concurrenttransfers=100 clone https://git.zhaozuohong.vip/mower-ng/mower-ng.git --branch slow",
"fetch": lambda: f"..\\git\\bin\\git fetch origin {config.conf.branch} --progress",
"switch": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=100 switch -f {config.conf.branch} --progress",
"reset": lambda: f"..\\git\\bin\\git -c lfs.concurrenttransfers=200 reset --hard origin/{config.conf.branch}",
"pip_tools_install": lambda: f"..\\python\\Scripts\\pip install --no-cache-dir -i {mirror_list[config.conf.mirror]} pip-tools --no-warn-script-location",
"pip_sync": lambda: f"..\\python\\Scripts\\pip-sync -i {mirror_list[config.conf.mirror]} requirements.txt",
"webview": "start ..\\python\\pythonw webview_ui.py",
"manager": "start ..\\python\\pythonw manager.py",
}
def read_stream(stream, log_func):
def process_lines(text_io):
for line in iter(text_io.readline, ''):
text = line.rstrip('\n')
custom_event(f"{text.strip()}\n")
detected_encoding = 'utf-8'
text_io = io.TextIOWrapper(stream, encoding=detected_encoding, errors='replace')
try:
process_lines(text_io)
except UnicodeDecodeError:
stream.seek(0) # 重新将流指针重置到开头
text_io = io.TextIOWrapper(stream, encoding='gbk', errors='replace')
process_lines(text_io)
finally:
text_io.close()
class Api:
def load_config(self):
logger.info("读取配置文件")
return config.conf.model_dump()
def save_config(self, conf):
logger.info(f"更新配置文件{conf}")
config.conf = config.Conf(**conf)
config.save_conf()
def get_version(self):
return sys_config.get("version")
def get_new_version(self):
logger.info("获取最新版本号")
response = requests.get(get_new_version_url)
return response.json()
# 更新启动器本身
def update_self(self, download_url):
logger.info(f"开始更新启动器 {download_url}")
file_name = os.path.basename(download_url)
file_name = "launcher.7z"
current_path = os.getcwd()
download_tmp_folder = os.path.join(current_path, "download_tmp")
# 确保 download_tmp 文件夹存在
ensure_directory_exists(download_tmp_folder)
if not download_file("launcher", download_url, download_tmp_folder):
return "下载新版本失败"
download_path = os.path.join(download_tmp_folder, file_name)
if not extract_7z_file("launcher", download_path, download_tmp_folder, True):
return "解压新版本失败"
exe_path = os.path.join(current_path, "launcher.exe")
folder_path = os.path.join(current_path, "_internal")
new_exe_path = os.path.join(download_tmp_folder, "launcher", "launcher.exe")
new_folder_path = os.path.join(download_tmp_folder, "launcher", "_internal")
script_path = os.path.join(download_tmp_folder, upgrade_script_name)
with open(script_path, "w") as b:
temp_list = "@echo off\n"
temp_list += "timeout /t 3 /nobreak\n" # 等待进程退出
temp_list += f"rmdir {folder_path}\n" # 删除_internal
temp_list += f"del {exe_path}\n" # 删除exe
temp_list += f"xcopy /e /i /y {new_folder_path} {folder_path}\n"
temp_list += f"move {new_exe_path} {exe_path}\n"
temp_list += "timeout /t 1 /nobreak\n" # 等待操作完成
temp_list += f"start {exe_path}\n" # 启动新程序
temp_list += "exit"
b.write(temp_list)
# 不显示cmd窗口
Popen([script_path], creationflags=CREATE_NO_WINDOW)
os._exit(0)
def rm_site_packages(self):
site_packages_path = Path("./python/Lib/site-packages")
if site_packages_path.exists():
rmtree(site_packages_path)
return "site-packages目录移除成功"
return "python\\Lib\\site-packages目录不存在"
def rm_python_scripts(self):
python_scripts_path = Path("./python/Scripts")
if python_scripts_path.exists():
rmtree(python_scripts_path)
return "Scripts目录移除成功"
return "python\\Scripts目录不存在"
def run(self, command, cwd=None):
command = command_list[command]
if callable(command):
command = command()
if callable(command):
return "success" if command() else "failed"
custom_event(command + "\n")
try:
with subprocess.Popen(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=cwd, bufsize=0,
universal_newlines=False
) as p:
stdout_thread = threading.Thread(target=read_stream, args=(p.stdout, logger.info))
stderr_thread = threading.Thread(target=read_stream, args=(p.stderr, logger.error))
stdout_thread.start()
stderr_thread.start()
stdout_thread.join()
stderr_thread.join()
if p.returncode == 0:
return "success"
else:
logger.error(f"{command} 运行失败")
return "failed"
except Exception as e:
logger.exception(e)
custom_event(str(e))

View file

@ -1,11 +0,0 @@
import json
import launcher
from launcher.log import logger
def custom_event(data):
logger.info(data)
data = json.dumps({"log": data + "\n"})
js = f"var event = new CustomEvent('log', {{detail: {data}}}); window.dispatchEvent(event);"
launcher.webview.window.evaluate_js(js)

View file

@ -1,30 +1,19 @@
import io
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from launcher.sys_config import sys_config
# 配置日志
def setup_logger():
log_level = sys_config.get('log_level')
logger = logging.getLogger("launcher.log")
logger.setLevel(log_level)
# 设置标准输出编码为 UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
logger.setLevel(logging.DEBUG)
# 控制台输出
console_handler = logging.StreamHandler()
console_handler.setLevel(log_level)
console_handler.setLevel(logging.DEBUG)
# 文件输出
file_path = os.path.join(os.getcwd(), "launcher.log")
file_handler = RotatingFileHandler(
file_path, maxBytes=5 * 1024 * 1024, backupCount=3, encoding='utf-8'
"launcher.log", maxBytes=5 * 1024 * 1024, backupCount=3
)
file_handler.setLevel(logging.INFO)

View file

@ -1,4 +0,0 @@
pywebview==5.1
requests==2.32.3
py7zr==0.22.0
pydantic==2.10.3

View file

@ -1,63 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.in
#
--index-url https://pypi.tuna.tsinghua.edu.cn/simple
annotated-types==0.7.0
# via pydantic
bottle==0.13.2
# via pywebview
brotli==1.1.0
# via py7zr
certifi==2024.12.14
# via requests
cffi==1.17.1
# via clr-loader
charset-normalizer==3.4.0
# via requests
clr-loader==0.2.7.post0
# via pythonnet
idna==3.10
# via requests
inflate64==1.0.0
# via py7zr
multivolumefile==0.2.3
# via py7zr
proxy-tools==0.1.0
# via pywebview
psutil==6.1.0
# via py7zr
py7zr==0.22.0
# via -r requirements.in
pybcj==1.0.2
# via py7zr
pycparser==2.22
# via cffi
pycryptodomex==3.21.0
# via py7zr
pydantic==2.10.3
# via -r requirements.in
pydantic-core==2.27.1
# via pydantic
pyppmd==1.1.0
# via py7zr
pythonnet==3.0.5
# via pywebview
pywebview==5.1
# via -r requirements.in
pyzstd==0.16.2
# via py7zr
requests==2.32.3
# via -r requirements.in
texttable==1.7.0
# via py7zr
typing-extensions==4.12.2
# via
# pydantic
# pydantic-core
# pywebview
urllib3==2.2.3
# via requests

56
ui/package-lock.json generated
View file

@ -8,7 +8,6 @@
"name": "ui",
"version": "0.0.0",
"dependencies": {
"pinia": "^2.2.8",
"vue": "^3.5.11"
},
"devDependencies": {
@ -1132,11 +1131,6 @@
"@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": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.0.0.tgz",
@ -2540,56 +2534,6 @@
"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": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.0.tgz",

View file

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

View file

@ -4,26 +4,15 @@ import Launch from '@/pages/Launch.vue'
import Update from '@/pages/Update.vue'
import Fix from '@/pages/Fix.vue'
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 page = ref(null)
let conf
async function init_version() {
version.value = await pywebview.api.get_version()
new_version.value = await pywebview.api.get_new_version()
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
function load_config() {
pywebview.api.get_page().then((value) => {
page.value = value
loading.value = false
})
}
const log = ref('')
@ -38,11 +27,9 @@ watch(log, () => {
onMounted(() => {
if (window.pywebview && pywebview.api) {
initialize_config()
load_config()
} else {
window.addEventListener('pywebviewready', () => {
initialize_config()
})
window.addEventListener('pywebviewready', load_config)
}
window.addEventListener('log', (e) => {
log.value += e.detail.log
@ -51,6 +38,7 @@ onMounted(() => {
function set_page(value) {
log.value = ''
pywebview.api.set_page(value)
}
const running = ref(false)
@ -58,15 +46,6 @@ provide('running', running)
const steps = ref([])
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>
<template>
@ -79,22 +58,13 @@ provide('new_version', new_version)
type="card"
placement="left"
class="container"
v-model:value="conf.page"
:default-value="page"
@update:value="set_page"
>
<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="launch" tab="启动程序"><launch /></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-notification-provider>
<n-global-style />

View file

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

View file

@ -3,48 +3,39 @@ const running = inject('running')
const notification = useNotification()
async function rm_site_packages_and_python_scripts() {
running.value = true
notification['info']({
content: '提示',
meta: '开始移除site-packages和python/Script目录',
duration: 3000
})
const response = await pywebview.api.rm_site_packages()
notification['info']({
content: '提示',
meta: response,
duration: 3000
})
const response2 = await pywebview.api.rm_python_scripts()
notification['info']({
content: '提示',
meta: response2,
duration: 3000
})
running.value = false
running.value = true
notification['info']({
content: '提示',
meta: '开始移除site-packages和python/Script目录',
duration: 3000
})
const response = await pywebview.api.rm_site_packages()
notification['info']({
content: '提示',
meta: response,
duration: 3000
})
const response2 = await pywebview.api.rm_python_scripts()
notification['info']({
content: '提示',
meta: response2,
duration: 3000
})
running.value = false
}
</script>
<template>
<n-flex
vertical
style="
<n-flex vertical style="
gap: 16px;
height: 100%;
padding: 16px;
box-sizing: border-box;
justify-content: 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-flex>
</template>

View file

@ -1,16 +1,13 @@
<script setup>
const steps = ref([
{
title: '下载 git、python',
command: ['download_git', 'download_python']
},
{ title: '设置 Git LFS', command: ['lfs'] },
{
title: '安装 pip',
command: ['ensurepip']
},
{
title: '下载 mower-ng 代码',
command: ['lfs', 'clone']
command: ['clone']
}
])
provide('steps', steps)

View file

@ -1,94 +0,0 @@
<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,10 +1,23 @@
<script setup>
import { useConfigStore } from '@/stores/config.js'
const conf = useConfigStore().config
const branch = 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(() => [
{
title: '更新源码',
@ -13,7 +26,7 @@ const steps = computed(() => [
},
{
title: '安装依赖',
command: ['pip_tools_install', 'pip_sync'],
command: ['pip_install'],
cwd: 'mower-ng'
}
])
@ -28,7 +41,7 @@ provide('current_state', current_state)
<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="mower-ng 代码分支">
<n-radio-group v-model:value="conf.branch">
<n-radio-group v-model:value="branch">
<n-flex>
<n-radio value="fast">测试版</n-radio>
<n-radio value="slow">稳定版</n-radio>
@ -36,7 +49,7 @@ provide('current_state', current_state)
</n-radio-group>
</n-form-item>
<n-form-item label="PyPI 仓库镜像">
<n-radio-group v-model:value="conf.mirror">
<n-radio-group v-model:value="mirror">
<n-flex>
<n-radio value="pypi">PyPI</n-radio>
<n-radio value="aliyun">阿里云镜像站</n-radio>

View file

@ -1,32 +0,0 @@
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
}
})