mower-ng/webview_ui.py

373 lines
11 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Copyright (c) 2023 zhbaor <zhbaor@zhaozuohong.vip>
This file is part of mower-ng (https://git.zhaozuohong.vip/mower-ng/mower-ng).
Mower-ng is free software: you may copy, redistribute and/or modify it
under the terms of the GNU General Public License as published by the
Free Software Foundation, version 3 or later.
This file is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
This file incorporates work covered by the following copyright and
permission notice:
Copyright (c) 2024 MuelNova <muel@nova.gal>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import multiprocessing as mp
def splash_screen(queue: mp.Queue):
import tkinter as tk
from tkinter.font import Font
from PIL import Image, ImageTk
from mower.utils.path import get_path
root = tk.Tk()
container = tk.Frame(root)
logo_path = get_path("@install/logo.png")
img = Image.open(logo_path)
img = ImageTk.PhotoImage(img)
canvas = tk.Canvas(container, width=256, height=256)
canvas.create_image(128, 128, image=img)
canvas.pack()
title_font = Font(size=24)
title_label = tk.Label(
container,
text="mower-ng",
font=title_font,
)
title_label.pack()
loading_label = tk.Label(container)
loading_label.pack()
container.pack(expand=1)
root.overrideredirect(True)
window_width = 500
window_height = 400
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
x = int(screen_width / 2 - window_width / 2)
y = int(screen_height / 2 - window_height / 2)
root.geometry(f"{window_width}x{window_height}+{x}+{y}")
def recv_msg():
try:
msg = queue.get(False)
if msg["type"] == "text":
loading_label.config(text=msg["data"] + "……")
root.after(100, recv_msg)
elif msg["type"] == "dialog":
from tkinter import messagebox
root.withdraw()
messagebox.showerror("mower-ng", msg["data"])
root.destroy()
except Exception:
pass
root.after(100, recv_msg)
root.mainloop()
def start_tray(queue: mp.Queue, global_space, port, url):
from PIL import Image
from pystray import Icon, Menu, MenuItem
from mower.utils.path import get_path
logo_path = get_path("@install/logo.png")
img = Image.open(logo_path)
title = "mower-ng@"
title += f"{port}({global_space})" if global_space else str(port)
def open_browser():
import webbrowser
webbrowser.open(url)
icon = Icon(
name="mower-ng",
icon=img,
menu=Menu(
MenuItem(
text=title,
action=None,
enabled=False,
),
Menu.SEPARATOR,
MenuItem(
text="打开/关闭窗口",
action=lambda: queue.put("toggle"),
default=True,
),
MenuItem(
text="在浏览器中打开网页面板",
action=open_browser,
),
Menu.SEPARATOR,
MenuItem(
text="退出",
action=lambda: queue.put("exit"),
),
),
title=title,
)
icon.run()
def webview_window(child_conn, window_size, version, host, port, url, confirm_close):
from threading import Thread
import webview
webview.settings["ALLOW_DOWNLOADS"] = True
class WindowSize:
def update(self, width, height):
self.width = width
self.height = height
webview_window_size = WindowSize()
webview_window_size.update(window_size["width"], window_size["height"])
window = webview.create_window(
f"mower-ng {version} (http://{host}:{port})",
url,
text_select=True,
confirm_close=confirm_close,
width=webview_window_size.width,
height=webview_window_size.height,
)
window.events.resized += webview_window_size.update
def recv_msg():
while True:
msg = child_conn.recv()
if msg == "exit":
window.confirm_close = False
window.destroy()
return
if msg == "file":
result = window.create_file_dialog(
dialog_type=webview.OPEN_DIALOG,
)
elif msg == "folder":
result = window.create_file_dialog(
dialog_type=webview.FOLDER_DIALOG,
)
if result is None:
result = ""
elif not isinstance(result, str):
if len(result) == 0:
result = ""
else:
result = result[0]
child_conn.send(result)
Thread(target=recv_msg, daemon=True).start()
import time
from datetime import datetime, timedelta
from mower.utils.network import is_port_in_use
start_time = datetime.now()
while datetime.now() - start_time < timedelta(seconds=10):
if is_port_in_use(port):
break
time.sleep(0.1)
try:
webview.start()
except Exception:
import webbrowser
webbrowser.open(url)
return
window_size["width"] = webview_window_size.width
window_size["height"] = webview_window_size.height
if __name__ == "__main__":
splash_queue = mp.Queue()
splash_process = mp.Process(target=splash_screen, args=(splash_queue,), daemon=True)
splash_process.start()
splash_queue.put({"type": "text", "data": "加载配置文件"})
import sys
from mower.utils import path
if len(sys.argv) == 2:
path.global_space = sys.argv[1]
from mower.utils import config
conf = config.conf
tray = conf.webview.tray
token = conf.webview.token
host = "0.0.0.0" if token else "127.0.0.1"
splash_queue.put({"type": "text", "data": "检测端口占用"})
import time
from mower.utils.network import get_new_port, is_port_in_use
if token:
port = conf.webview.port
if is_port_in_use(port):
splash_queue.put(
{"type": "dialog", "data": f"端口{port}已被占用,无法启动!"}
)
time.sleep(5)
sys.exit()
else:
port = get_new_port()
url = f"http://127.0.0.1:{port}"
if token:
url += f"?token={token}"
splash_queue.put({"type": "text", "data": "创建主窗口"})
from mower import __version__
manager = mp.Manager()
window_size = manager.dict()
window_size["width"] = config.conf.webview.width
window_size["height"] = config.conf.webview.height
config.parent_conn, child_conn = mp.Pipe()
config.webview_process = mp.Process(
target=webview_window,
args=(
child_conn,
window_size,
__version__,
host,
port,
url,
not tray,
),
daemon=True,
)
config.webview_process.start()
splash_queue.put({"type": "text", "data": "加载Flask依赖"})
from server import app
splash_queue.put({"type": "text", "data": "启动Flask网页服务器"})
from threading import Thread
if token:
app.token = token
Thread(
target=app.run,
kwargs={"host": "::" if token else "127.0.0.1", "port": port},
daemon=True,
).start()
from mower import __system__
if __system__ == "windows" and token:
Thread(
target=app.run,
kwargs={"host": "0.0.0.0", "port": port},
daemon=True,
).start()
if tray:
splash_queue.put({"type": "text", "data": "加载托盘图标"})
tray_queue = mp.Queue()
tray_process = mp.Process(
target=start_tray,
args=(tray_queue, path.global_space, port, url),
daemon=True,
)
tray_process.start()
splash_process.terminate()
splash_process.join()
if tray:
while True:
msg = tray_queue.get()
if msg == "toggle":
if config.webview_process.is_alive():
config.parent_conn.send("exit")
config.webview_process.join(3)
if config.webview_process.exitcode is None:
config.webview_process.terminate()
else:
config.parent_conn, child_conn = mp.Pipe()
config.webview_process = mp.Process(
target=webview_window,
args=(
child_conn,
window_size,
__version__,
host,
port,
url,
not tray,
),
daemon=True,
)
config.webview_process.start()
elif msg == "exit":
config.parent_conn.send("exit")
config.webview_process.join(3)
if config.webview_process.exitcode is None:
config.webview_process.terminate()
tray_process.terminate()
tray_process.join()
break
else:
config.webview_process.join()
config.conf.webview.width = window_size["width"]
config.conf.webview.height = window_size["height"]
config.save_conf()