Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/gui/css/alas.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
src: url('/static/assets/spa/MiSans-Demibold.ttf') format('truetype');
font-weight: 450;
font-style: normal;
font-display: swap;
font-display: optional;
}

@font-face {
Expand Down
2,538 changes: 2,538 additions & 0 deletions module/config/zh-MIAO.json

Large diffs are not rendered by default.

24 changes: 21 additions & 3 deletions module/webui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
argparse,
asgi_app,
get_device_id,
get_localstorage_values,
info,
lang,
load_webui_styles,
Expand All @@ -35,6 +36,7 @@
task_handler,
time,
updater,
webconfig,
)
from module.webui.app_developer_menu import DeveloperMenuMixin
from module.webui.app_developer_settings import DeveloperSettingsMixin
Expand Down Expand Up @@ -74,6 +76,9 @@
from module.webui.app_task_config import TaskConfigMixin


INITIAL_WEBUI_CSS = "/static/assets/gui/css/alas.css"


class AlasGUI(
AppShellMixin,
StatisticsPageMixin,
Expand Down Expand Up @@ -190,21 +195,34 @@ def _block_public_webui_password_error() -> bool:

def _run_gui(initial_page: str = "home") -> None:
set_env(title="AzurPilot", output_animation=False)
load_webui_styles(theme=AlasGUI.theme, is_mobile=info.user_agent.is_mobile)
load_webui_styles(
theme=AlasGUI.theme,
is_mobile=info.user_agent.is_mobile,
preloaded_styles=("alas",),
)
if _block_restricted_device() or _block_public_webui_password_error():
return
if is_webui_password_set(key) and not login(key):
localstorage = None
if is_webui_password_set(key):
localstorage = get_localstorage_values(
("password", "clarity_notice_shown", "aside")
)
if is_webui_password_set(key) and not login(
key, stored_password=localstorage.get("password")
):
logger.warning(f"[WebUI] {info.user_ip} 登录失败")
time.sleep(1.5)
run_js("location.reload();")
return
gui = AlasGUI()
local.gui = gui
gui.run(initial_page=initial_page)
gui.run(initial_page=initial_page, localstorage=localstorage)

@webconfig(css_file=INITIAL_WEBUI_CSS)
def index() -> None:
_run_gui()

@webconfig(css_file=INITIAL_WEBUI_CSS)
def manage() -> None:
_run_gui(initial_page="manage")

Expand Down
2 changes: 1 addition & 1 deletion module/webui/app_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from module.webui.fake_pil_module import import_fake_pil_module
from module.config.time_source import now as current_time, status as time_source_status
from module.statistics.azurstats import AzurStats
from module.os_simulator.simulator import OSSimulator

import_fake_pil_module()

Expand Down Expand Up @@ -104,6 +103,7 @@
TaskHandler,
get_alas_config_listen_path,
get_localstorage,
get_localstorage_values,
load_webui_styles,
set_localstorage,
get_window_visibility_state,
Expand Down
79 changes: 45 additions & 34 deletions module/webui/app_home.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
_t,
alas_instance,
eval_js,
get_localstorage,
get_localstorage_values,
get_window_visibility_state,
go_app,
is_oobe_needed,
Expand Down Expand Up @@ -236,18 +236,11 @@ def ui_check_announcement(self, force=False) -> None:
if force:
toast("正在获取公告... / Fetching announcement...", color="info")

def run(self, initial_page="home") -> None:
# setup gui
set_env(title="AzurPilot", output_animation=False)
if get_localstorage("clarity_notice_shown") != "1":
set_localstorage("clarity_notice_shown", "1")
toast(
"本 WebUI 使用 Microsoft Clarity 收集页面访问、点击交互和性能数据,用于分析并改进使用体验。",
color="info",
duration=12,
)
# 加载 Microsoft Clarity 行为分析脚本,同一会话仅注入一次。
def _load_deferred_client_assets(self) -> None:
"""在首次绘制后再加载非关键的分析和交互脚本。"""
run_js(
"(function() {"
"function load() {"
"if (!document.getElementById('microsoft-clarity-script')) {"
"(function(c,l,a,r,i,t,y){"
"c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};"
Expand All @@ -256,38 +249,49 @@ def run(self, initial_page="home") -> None:
"y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);"
"})(window,document,'clarity','script','xszl2nrp3q');"
"}"
)
run_js(
"document.head.append(Object.assign(document.createElement('link'), { rel: 'manifest', href: '/static/assets/spa/manifest.json' }))"
)
load_webui_styles(theme=self.theme, is_mobile=self.is_mobile)

# 加载静态 JS 工具文件(公告弹窗、截图查看器、自动刷新等)
# 替代原来的多个 run_js() 运行时注入
run_js(
"var s=document.createElement('script');"
"s.src='/static/assets/gui/js/alas-utils.js';"
"document.head.appendChild(s);"
"if (!document.querySelector('link[rel=\"manifest\"]')) {"
"var manifest=document.createElement('link');"
"manifest.rel='manifest';manifest.href='/static/assets/spa/manifest.json';"
"document.head.appendChild(manifest);"
"}"
"if (!document.getElementById('alas-utils-script')) {"
"var script=document.createElement('script');"
"script.id='alas-utils-script';script.async=true;"
"script.src='/static/assets/gui/js/alas-utils.js';"
"document.head.appendChild(script);"
"}"
"}"
"if (window.requestIdleCallback) {"
"window.requestIdleCallback(load, {timeout: 3000});"
"} else { window.setTimeout(load, 0); }"
"})();"
)

aside = get_localstorage("aside")
def run(self, initial_page="home", localstorage=None) -> None:
# setup gui
set_env(title="AzurPilot", output_animation=False)
load_webui_styles(theme=self.theme, is_mobile=self.is_mobile)
if localstorage is None:
localstorage = get_localstorage_values(("clarity_notice_shown", "aside"))
aside = localstorage.get("aside")
self._stored_aside = aside
show_clarity_notice = localstorage.get("clarity_notice_shown") != "1"

# OOBE 初次设置向导:无用户配置时引导完成基本设置
if is_oobe_needed():
from module.webui.oobe import OOBEWizard

OOBEWizard(self).start()
self._load_deferred_client_assets()
return

self.mount_shell()
restore_instance = initial_page == "home" and aside in alas_instance()
if initial_page == "manage":
self.ui_manage()
else:
elif not restore_instance:
self.show_home()

# init config watcher
self._init_alas_config_watcher()

# save config
_thread_save_config = threading.Thread(target=self._alas_thread_update_config)
register_thread(_thread_save_config)
Expand Down Expand Up @@ -379,10 +383,17 @@ def announcement_checker():
# 添加公告检查任务(初始延迟5秒)
self.task_handler.add(announcement_checker(), delay=5)

# 启动任务处理器
self.task_handler.start()
if restore_instance:
self.ui_alas(aside)

# Return to previous page
if show_clarity_notice:
set_localstorage("clarity_notice_shown", "1")
toast(
"本 WebUI 使用 Microsoft Clarity 收集页面访问、点击交互和性能数据,用于分析并改进使用体验。",
color="info",
duration=12,
)
self._load_deferred_client_assets()

if initial_page == "home" and aside in alas_instance():
self.ui_alas(aside)
# 启动任务处理器
self.task_handler.start()
27 changes: 21 additions & 6 deletions module/webui/app_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@
from module.webui.app_dependencies import (
AzurLaneConfig,
Icon,
OSSimulator,
ProcessManager,
State,
alas_instance,
clear,
current_time,
filepath_args,
get_localstorage,
put_buttons,
put_html,
put_icon_buttons,
Expand Down Expand Up @@ -38,7 +36,6 @@ class AppShellMixin(WebUIMixinBase):
def initial(self) -> None:
self.ALAS_MENU = read_file(filepath_args("menu", self.alas_mod))
self.ALAS_ARGS = read_file(filepath_args("args", self.alas_mod))
self._init_alas_config_watcher()

def __init__(self) -> None:
super().__init__()
Expand All @@ -54,18 +51,36 @@ def __init__(self) -> None:
self.inst_cache = []
self._shell_mounted = False
self._active_aside = None
self._stored_aside = None
self._overview_snapshot = None
self.af_flag = False
self._last_announcement_id = None
self._announcement_result = None
self._announcement_fetching = False
self._announcement_force = False
self._update_notified = False
self.simulator = OSSimulator()
self._simulator = None
self._simulator_logger_pm = None
self._overview_log = None
self._overview_log_config_name = None

@property
def simulator(self):
"""在首次进入大世界模拟器时再加载其运行时依赖。"""
if self._simulator is None:
import sys

from module.webui.fake_pil_module import remove_fake_pil_module

# matplotlib 需要真实 PIL;仅移除 WebUI 启动阶段安装的替身,
# 避免其他会话已加载真实 PIL 时再次从模块缓存中删除它。
if not hasattr(sys.modules.get("PIL"), "__path__"):
remove_fake_pil_module()
from module.os_simulator.simulator import OSSimulator

self._simulator = OSSimulator()
return self._simulator

def _close_update_notice(self) -> None:
run_js(
r"""
Expand Down Expand Up @@ -185,7 +200,7 @@ def refresh_aside_labels(self) -> None:
],
onclick=[self.ui_manage],
)
aside_name = self._active_aside or get_localstorage("aside")
aside_name = self._active_aside or self._stored_aside or "Home"
self.active_button("aside", aside_name)

@use_scope("aside_instance")
Expand Down Expand Up @@ -237,7 +252,7 @@ def update(name, seq):
changed = True

if changed:
aside_name = self._active_aside or get_localstorage("aside")
aside_name = self._active_aside or self._stored_aside or "Home"
self.active_button("aside", aside_name)

def set_aside_status(self) -> None:
Expand Down
33 changes: 23 additions & 10 deletions module/webui/app_task_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
deep_set,
dict_to_kv,
filepath_config,
get_alas_config_listen_path,
get_device_id,
logger,
os,
parse_pin_value,
partial,
pin,
pin_on_change,
popup,
Expand Down Expand Up @@ -158,6 +156,7 @@ def set_group(self, group, arg_dict, config: Dict[str, Any], task: str) -> int:
group_name = group[0]

output_list: List[Output] = []
watcher_paths: List[List[str]] = []
for arg, arg_dict in deep_iter(arg_dict, depth=1):
output_kwargs: T_Output_Kwargs = arg_dict.copy()

Expand All @@ -170,6 +169,7 @@ def set_group(self, group, arg_dict, config: Dict[str, Any], task: str) -> int:
output_kwargs["disabled"] = True
# Output type
output_kwargs["widget_type"] = output_kwargs.pop("type")
widget_type = output_kwargs["widget_type"]

arg_name = arg[0] # [arg_name,]
# Internal pin widget name
Expand Down Expand Up @@ -237,6 +237,8 @@ def set_group(self, group, arg_dict, config: Dict[str, Any], task: str) -> int:
# output will inherit current scope when created, override here
o.spec["scope"] = f"#pywebio-scope-group_{group_name}"
output_list.append(o)
if display != "readonly" and widget_type != "stored":
watcher_paths.append([task, group_name, arg_name])

if not output_list:
return 0
Expand All @@ -250,6 +252,9 @@ def set_group(self, group, arg_dict, config: Dict[str, Any], task: str) -> int:
for output in output_list:
output.show()

for path in watcher_paths:
self._bind_config_watcher(path)

# 在掉落记录组中显示可复制的设备ID
if group_name == "DropRecord":
device_id = DEMO_DEVICE_ID_TEXT if is_demo_mode() else get_device_id()
Expand Down Expand Up @@ -280,15 +285,23 @@ def _simulator_start(self):
return
self.simulator.start()

def _init_alas_config_watcher(self) -> None:
def put_queue(path, value):
self.modified_config_queue.put({"name": path, "value": value})
def _bind_config_watcher(self, path: List[str]) -> None:
"""为已渲染的配置控件注册一次变更监听。"""
pin_name = "_".join(path)
watcher_pins = getattr(self, "_config_watcher_pins", None)
if watcher_pins is None:
watcher_pins = set()
self._config_watcher_pins = watcher_pins
if pin_name in watcher_pins:
return

for path in get_alas_config_listen_path(self.ALAS_ARGS):
pin_on_change(
name="_".join(path), onchange=partial(put_queue, ".".join(path))
)
logger.info("[WebUI-任务配置] 配置监听器初始化完成")
path_text = ".".join(path)

def put_queue(value: Any) -> None:
self.modified_config_queue.put({"name": path_text, "value": value})

pin_on_change(name=pin_name, onchange=put_queue)
watcher_pins.add(pin_name)

def _alas_thread_update_config(self) -> None:
modified = {}
Expand Down
Loading
Loading