From a087189e54c949eda97d481c3f85bf90ef4236e9 Mon Sep 17 00:00:00 2001
From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com>
Date: Tue, 29 Aug 2023 22:35:13 +0800
Subject: [PATCH 1/3] Add: Dashboard on GUI
---
assets/gui/css/alas-mobile.css | 9 ++-
assets/gui/css/alas-pc.css | 3 +-
assets/gui/css/alas.css | 52 +++++++++++-
assets/gui/css/dark-alas.css | 4 +
assets/gui/css/light-alas.css | 4 +
module/config/argument/args.json | 8 +-
module/config/argument/argument.yaml | 4 +
module/config/argument/gui.yaml | 10 +++
module/config/argument/stored.json | 84 ++++++++++++++++++++
module/config/config_updater.py | 28 +++++++
module/config/i18n/en-US.json | 9 +++
module/config/i18n/ja-JP.json | 9 +++
module/config/i18n/zh-CN.json | 9 +++
module/config/i18n/zh-TW.json | 9 +++
module/config/stored/classes.py | 35 +++------
module/webui/app.py | 113 ++++++++++++++++++---------
module/webui/base.py | 26 ++++++
module/webui/lang.py | 33 +++++++-
18 files changed, 380 insertions(+), 69 deletions(-)
create mode 100644 module/config/argument/stored.json
diff --git a/assets/gui/css/alas-mobile.css b/assets/gui/css/alas-mobile.css
index 9acad6eb7..3e3d6f75c 100644
--- a/assets/gui/css/alas-mobile.css
+++ b/assets/gui/css/alas-mobile.css
@@ -59,4 +59,11 @@
#pywebio-scope-waiting,
#pywebio-scope-log {
overflow-y: auto;
-}
\ No newline at end of file
+}
+
+[id^="pywebio-scope-dashboard-row-"] {
+ display: flex;
+ flex-grow: 1;
+ min-width: 4rem;
+ max-width: 50%;
+}
diff --git a/assets/gui/css/alas-pc.css b/assets/gui/css/alas-pc.css
index 57d95088e..0adfa2591 100644
--- a/assets/gui/css/alas-pc.css
+++ b/assets/gui/css/alas-pc.css
@@ -26,7 +26,6 @@
overflow-y: auto;
}
-#pywebio-scope-scheduler-bar,
#pywebio-scope-log-bar,
#pywebio-scope-log,
#pywebio-scope-daemon-overview #pywebio-scope-groups {
@@ -45,4 +44,4 @@
display: grid;
grid-auto-flow: column;
grid-template-columns: auto auto;
-}
\ No newline at end of file
+}
diff --git a/assets/gui/css/alas.css b/assets/gui/css/alas.css
index 735dc2012..5fc57f5d4 100644
--- a/assets/gui/css/alas.css
+++ b/assets/gui/css/alas.css
@@ -383,17 +383,65 @@ pre.rich-traceback-code {
}
#pywebio-scope-scheduler-bar,
-#pywebio-scope-log-bar {
+#pywebio-scope-log-title {
display: flex;
align-items: center;
justify-content: space-between;
}
-#pywebio-scope-log-bar-btns {
+#pywebio-scope-log-title-btns {
display: grid;
grid-auto-flow: column;
}
+#pywebio-scope-log-bar {
+ height: 11.5rem;
+}
+
+#pywebio-scope-dashboard {
+ display: flex;
+ align-content: flex-start;
+ flex-flow: row wrap;
+ overflow: auto;
+}
+
+.dashboard-icon {
+ margin: .6rem .8rem 0 .6rem;
+ width: .5rem;
+ height: .5rem;
+ border-radius: 50%;
+}
+
+[id^="pywebio-scope-dashboard-row-"] {
+ display: flex;
+ flex-grow: 1;
+ min-width: 4rem;
+ max-width: 10rem;
+}
+
+*[style*="--dashboard-value--"] {
+ font-size: 1rem;
+ font-weight: 500;
+ overflow-wrap: break-word;
+}
+
+*[style*="--dashboard-time--"] {
+ font-size: 0.8rem;
+ font-weight: 400;
+ overflow-wrap: break-word;
+}
+
+[id^="pywebio-scope-dashboard-row-"] p {
+ margin-bottom: 0;
+}
+
+[id^="pywebio-scope-dashboard-value-"] {
+ display: flex;
+ align-items: flex-end;
+ height: 1.5rem;
+}
+
+
#pywebio-scope-log {
line-height: 1.2;
font-size: 0.85rem;
diff --git a/assets/gui/css/dark-alas.css b/assets/gui/css/dark-alas.css
index c36fa31b0..c2d76e7bc 100644
--- a/assets/gui/css/dark-alas.css
+++ b/assets/gui/css/dark-alas.css
@@ -152,4 +152,8 @@ pre.rich-traceback-code {
*[style*="--arg-help--"],
[id^="pywebio-scope-group_"] > p + p {
color: #adb5bd;
+}
+
+*[style*="--dashboard-time--"] {
+ color: #adb5bd;
}
\ No newline at end of file
diff --git a/assets/gui/css/light-alas.css b/assets/gui/css/light-alas.css
index b65067f11..171d565af 100644
--- a/assets/gui/css/light-alas.css
+++ b/assets/gui/css/light-alas.css
@@ -151,4 +151,8 @@ pre.rich-traceback-code {
*[style*="--arg-help--"],
[id^="pywebio-scope-group_"] > p + p {
color: #777777;
+}
+
+*[style*="--dashboard-time--"] {
+ color: #777777;
}
\ No newline at end of file
diff --git a/module/config/argument/args.json b/module/config/argument/args.json
index 5afb63928..97694e31c 100644
--- a/module/config/argument/args.json
+++ b/module/config/argument/args.json
@@ -364,7 +364,9 @@
"type": "stored",
"value": {},
"display": "hide",
- "stored": "StoredTrailblazePower"
+ "stored": "StoredTrailblazePower",
+ "order": 1,
+ "color": "#eb8efe"
},
"DungeonDouble": {
"type": "stored",
@@ -782,7 +784,9 @@
"type": "stored",
"value": {},
"display": "hide",
- "stored": "StoredDailyActivity"
+ "stored": "StoredDailyActivity",
+ "order": 2,
+ "color": "#ffcf70"
},
"DailyQuest": {
"type": "stored",
diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml
index 2be96a989..7d0c54b74 100644
--- a/module/config/argument/argument.yaml
+++ b/module/config/argument/argument.yaml
@@ -111,6 +111,8 @@ DungeonSupport:
DungeonStorage:
TrailblazePower:
stored: StoredTrailblazePower
+ order: 1
+ color: "#eb8efe"
DungeonDouble:
stored: StoredDungeonDouble
@@ -124,6 +126,8 @@ AchievableQuest:
DailyStorage:
DailyActivity:
stored: StoredDailyActivity
+ order: 2
+ color: "#ffcf70"
DailyQuest:
stored: StoredDaily
diff --git a/module/config/argument/gui.yaml b/module/config/argument/gui.yaml
index 5866dd269..419167b0c 100644
--- a/module/config/argument/gui.yaml
+++ b/module/config/argument/gui.yaml
@@ -52,6 +52,16 @@ Overview:
Waiting:
NoTask:
+Dashboard:
+ # From lang.readable_time()
+ NoData:
+ TimeError:
+ JustNow:
+ MinutesAgo:
+ HoursAgo:
+ DaysAgo:
+ LongTimeAgo:
+
AddAlas:
PopupTitle:
NewName:
diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json
new file mode 100644
index 000000000..a42f73750
--- /dev/null
+++ b/module/config/argument/stored.json
@@ -0,0 +1,84 @@
+{
+ "TrailblazePower": {
+ "name": "TrailblazePower",
+ "path": "Dungeon.DungeonStorage.TrailblazePower",
+ "i18n": "DungeonStorage.TrailblazePower.name",
+ "stored": "StoredTrailblazePower",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "total": 180,
+ "value": 0
+ },
+ "order": 1,
+ "color": "#eb8efe"
+ },
+ "DailyActivity": {
+ "name": "DailyActivity",
+ "path": "DailyQuest.DailyStorage.DailyActivity",
+ "i18n": "DailyStorage.DailyActivity.name",
+ "stored": "StoredDailyActivity",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "total": 500,
+ "value": 0
+ },
+ "order": 2,
+ "color": "#ffcf70"
+ },
+ "Assignment": {
+ "name": "Assignment",
+ "path": "Assignment.Assignment.Assignment",
+ "i18n": "Assignment.Assignment.name",
+ "stored": "StoredAssignment",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "total": 0,
+ "value": 0
+ },
+ "order": 3,
+ "color": "#deba95"
+ },
+ "SimulatedUniverse": {
+ "name": "SimulatedUniverse",
+ "path": "Dungeon.DungeonStorage.SimulatedUniverse",
+ "i18n": "DungeonStorage.SimulatedUniverse.name",
+ "stored": "StoredSimulatedUniverse",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "total": 0,
+ "value": 0
+ },
+ "order": 5,
+ "color": "#8fb5fe"
+ },
+ "DungeonDouble": {
+ "name": "DungeonDouble",
+ "path": "Dungeon.DungeonStorage.DungeonDouble",
+ "i18n": "DungeonStorage.DungeonDouble.name",
+ "stored": "StoredDungeonDouble",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "calyx": 0,
+ "relic": 0
+ },
+ "order": 0,
+ "color": "#777777"
+ },
+ "DailyQuest": {
+ "name": "DailyQuest",
+ "path": "DailyQuest.DailyStorage.DailyQuest",
+ "i18n": "DailyStorage.DailyQuest.name",
+ "stored": "StoredDaily",
+ "attrs": {
+ "time": "2020-01-01 00:00:00",
+ "quest1": "",
+ "quest2": "",
+ "quest3": "",
+ "quest4": "",
+ "quest5": "",
+ "quest6": ""
+ },
+ "order": 0,
+ "color": "#777777"
+ }
+}
\ No newline at end of file
diff --git a/module/config/config_updater.py b/module/config/config_updater.py
index 3b0dbeea9..99d49603d 100644
--- a/module/config/config_updater.py
+++ b/module/config/config_updater.py
@@ -441,6 +441,32 @@ class ConfigGenerator:
return data
+ @cached_property
+ def stored(self):
+ import module.config.stored.classes as classes
+ data = {}
+ for path, value in deep_iter(self.args, depth=3):
+ if value.get('type') != 'stored':
+ continue
+ name = path[-1]
+ stored = value.get('stored')
+ stored_class = getattr(classes, stored)
+ row = {
+ 'name': name,
+ 'path': '.'.join(path),
+ 'i18n': f'{path[1]}.{path[2]}.name',
+ 'stored': stored,
+ 'attrs': stored_class('')._attrs,
+ 'order': value.get('order', 0),
+ 'color': value.get('color', '#777777')
+ }
+ data[name] = row
+
+ # sort by `order` ascending, but `order`==0 at last
+ data = sorted(data.items(), key=lambda kv: (kv[1]['order'] == 0, kv[1]['order']))
+ data = {k: v for k, v in data}
+ return data
+
@staticmethod
def generate_deploy_template():
template = poor_yaml_read(DEPLOY_TEMPLATE)
@@ -495,12 +521,14 @@ class ConfigGenerator:
def generate(self):
_ = self.args
_ = self.menu
+ _ = self.stored
# _ = self.event
self.insert_assignment()
self.insert_package()
# self.insert_server()
write_file(filepath_args(), self.args)
write_file(filepath_args('menu'), self.menu)
+ write_file(filepath_args('stored'), self.stored)
self.generate_code()
self.generate_stored()
for lang in LANGUAGES:
diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json
index b4fad33df..c3eba38d1 100644
--- a/module/config/i18n/en-US.json
+++ b/module/config/i18n/en-US.json
@@ -695,6 +695,15 @@
"Waiting": "Waiting",
"NoTask": "No Task"
},
+ "Dashboard": {
+ "NoData": "no data",
+ "TimeError": "time error",
+ "JustNow": "just now",
+ "MinutesAgo": "{time}min ago",
+ "HoursAgo": "{time}h ago",
+ "DaysAgo": "{time}d ago",
+ "LongTimeAgo": "long time ago"
+ },
"AddAlas": {
"PopupTitle": "Add new config",
"NewName": "New name",
diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json
index 01c0bb4ba..7e6199cc9 100644
--- a/module/config/i18n/ja-JP.json
+++ b/module/config/i18n/ja-JP.json
@@ -695,6 +695,15 @@
"Waiting": "Waiting",
"NoTask": "No Task"
},
+ "Dashboard": {
+ "NoData": "Gui.Dashboard.NoData",
+ "TimeError": "Gui.Dashboard.TimeError",
+ "JustNow": "Gui.Dashboard.JustNow",
+ "MinutesAgo": "Gui.Dashboard.MinutesAgo",
+ "HoursAgo": "Gui.Dashboard.HoursAgo",
+ "DaysAgo": "Gui.Dashboard.DaysAgo",
+ "LongTimeAgo": "Gui.Dashboard.LongTimeAgo"
+ },
"AddAlas": {
"PopupTitle": "新しいコンフィグを追加",
"NewName": "コンフィグ名",
diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json
index 2d352e35f..c7ecf1396 100644
--- a/module/config/i18n/zh-CN.json
+++ b/module/config/i18n/zh-CN.json
@@ -695,6 +695,15 @@
"Waiting": "等待中",
"NoTask": "无任务"
},
+ "Dashboard": {
+ "NoData": "无数据",
+ "TimeError": "时间错误",
+ "JustNow": "刚刚",
+ "MinutesAgo": "{time}分钟前",
+ "HoursAgo": "{time}小时前",
+ "DaysAgo": "{time}天前",
+ "LongTimeAgo": "很久以前"
+ },
"AddAlas": {
"PopupTitle": "添加新配置",
"NewName": "新的配置文件名",
diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json
index 300bf11ca..aa2a45450 100644
--- a/module/config/i18n/zh-TW.json
+++ b/module/config/i18n/zh-TW.json
@@ -695,6 +695,15 @@
"Waiting": "等待中",
"NoTask": "無任務"
},
+ "Dashboard": {
+ "NoData": "無數據",
+ "TimeError": "時間錯誤",
+ "JustNow": "剛剛",
+ "MinutesAgo": "{time}分鐘前",
+ "HoursAgo": "{time}小時前",
+ "DaysAgo": "{time}天前",
+ "LongTimeAgo": "很久以前"
+ },
"AddAlas": {
"PopupTitle": "添加新的設定",
"NewName": "新設定的檔案名",
diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py
index 227fb3a00..c1df02fb9 100644
--- a/module/config/stored/classes.py
+++ b/module/config/stored/classes.py
@@ -1,4 +1,3 @@
-import time
from datetime import datetime
from functools import cached_property as functools_cached_property
@@ -111,29 +110,6 @@ class StoredBase:
from module.logger import logger
logger.attr(self._name, self._stored)
- def dashboard(self) -> str:
- """
- Return a string to show on GUI
- """
- return 'None'
-
- def readable_time(self):
- diff = self.time.timestamp() - time.time()
- if diff < -1:
- return '', 'TimeError'
- elif diff < 60:
- # < 1 min
- return '', 'JustNow'
- elif diff < 3600:
- return str(int(diff // 60)), 'MinutesAgo'
- elif diff < 86400:
- return str(int(diff // 86400)), 'HoursAgo'
- elif diff < 129600:
- return str(int(diff // 129600)), 'DaysAgo'
- else:
- # > 15 days
- return '', 'LongTimeAgo'
-
class StoredExpiredAt0400(StoredBase):
def is_expired(self):
@@ -154,11 +130,11 @@ class StoredCounter(StoredBase):
FIXED_TOTAL = 0
- def set(self, current, total=0):
+ def set(self, value, total=0):
if self.FIXED_TOTAL:
total = self.FIXED_TOTAL
with self._config.multi_set():
- self.value = current
+ self.value = value
self.total = total
def to_counter(self) -> str:
@@ -170,6 +146,13 @@ class StoredCounter(StoredBase):
def get_remain(self) -> int:
return self.total - self.value
+ @cached_property
+ def _attrs(self) -> dict:
+ attrs = super()._attrs
+ if self.FIXED_TOTAL:
+ attrs['total'] = self.FIXED_TOTAL
+ return attrs
+
@functools_cached_property
def _stored(self):
stored = super()._stored
diff --git a/module/webui/app.py b/module/webui/app.py
index 181d966f0..5c25cdd05 100644
--- a/module/webui/app.py
+++ b/module/webui/app.py
@@ -88,11 +88,13 @@ task_handler = TaskHandler()
class AlasGUI(Frame):
ALAS_MENU: Dict[str, Dict[str, List[str]]]
ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]]
+ ALAS_STORED: Dict[str, Dict[str, Dict[str, str]]]
theme = "default"
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.ALAS_STORED = read_file(filepath_args("stored", self.alas_mod))
self._init_alas_config_watcher()
def __init__(self) -> None:
@@ -318,6 +320,35 @@ class AlasGUI(Frame):
color="navigator",
)
+ def set_dashboard(self, arg, arg_dict, config):
+ i18n = arg_dict.get('i18n')
+ if i18n:
+ name = t(i18n)
+ else:
+ name = arg
+ color = arg_dict.get("color", "#777777")
+ nodata = t("Gui.Dashboard.NoData")
+
+ def set_value(dic):
+ if "total" in dic.get("attrs", []) and config.get("total") is not None:
+ return [
+ put_text(config.get("value", nodata)).style("--dashboard-value--"),
+ put_text(f' / {config.get("total", "")}').style("--dashboard-time--"),
+ ]
+ else:
+ return [
+ put_text(config.get("value", nodata)).style("--dashboard-value--"),
+ ]
+
+ with use_scope(f"dashboard-row-{arg}", clear=True):
+ put_html(f'
'),
+ put_scope(f"dashboard-content-{arg}", [
+ put_scope(f"dashboard-value-{arg}", set_value(arg_dict)),
+ put_scope(f"dashboard-time-{arg}", [
+ put_text(f"{name} - {lang.readable_time(config.get('time', ''))}").style("--dashboard-time--"),
+ ])
+ ])
+
@use_scope("content", clear=True)
def alas_overview(self) -> None:
self.init_menu(name="Overview")
@@ -374,20 +405,19 @@ class AlasGUI(Frame):
log = RichLog("log")
with use_scope("logs"):
- put_scope(
- "log-bar",
- [
- put_text(t("Gui.Overview.Log")).style(
- "font-size: 1.25rem; margin: auto .5rem auto;"
- ),
- put_scope(
- "log-bar-btns",
- [
- put_scope("log_scroll_btn"),
- ],
- ),
- ],
- ),
+ put_scope("log-bar", [
+ put_scope("log-title", [
+ put_text(t("Gui.Overview.Log")).style("font-size: 1.25rem; margin: auto .5rem auto;"),
+ put_scope("log-title-btns", [
+ put_scope("log_scroll_btn"),
+ ]),
+ ]),
+ put_html('
'),
+ put_scope("dashboard", [
+ # Empty dashboard, values will be updated in alas_update_overview_task()
+ put_scope(f"dashboard-row-{arg}", []) for arg in self.ALAS_STORED.keys()
+ ])
+ ])
put_scope("log", [put_html("")])
log.console.width = log.get_width()
@@ -501,6 +531,7 @@ class AlasGUI(Frame):
self.alas_config.load()
self.alas_config.get_next_task()
+ alive = self.alas.alive
if len(self.alas_config.pending_task) >= 1:
if self.alas.alive:
running = self.alas_config.pending_task[:1]
@@ -528,27 +559,39 @@ class AlasGUI(Frame):
color="off",
)
- clear("running_tasks")
- clear("pending_tasks")
- clear("waiting_tasks")
- with use_scope("running_tasks"):
- if running:
- for task in running:
- put_task(task)
- else:
- put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
- with use_scope("pending_tasks"):
- if pending:
- for task in pending:
- put_task(task)
- else:
- put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
- with use_scope("waiting_tasks"):
- if waiting:
- for task in waiting:
- put_task(task)
- else:
- put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
+ if self.scope_expired("scheduler_alive", alive) \
+ or self.scope_expired("pending_task", self.alas_config.pending_task):
+ clear("running_tasks")
+ clear("pending_tasks")
+ clear("waiting_tasks")
+ with use_scope("running_tasks"):
+ if running:
+ for task in running:
+ put_task(task)
+ else:
+ put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
+ with use_scope("pending_tasks"):
+ if pending:
+ for task in pending:
+ put_task(task)
+ else:
+ put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
+ with use_scope("waiting_tasks"):
+ if waiting:
+ for task in waiting:
+ put_task(task)
+ else:
+ put_text(t("Gui.Overview.NoTask")).style("--overview-notask-text--")
+ self.scope_add("scheduler_alive", alive)
+ self.scope_add("pending_task", self.alas_config.pending_task)
+
+ for arg, arg_dict in self.ALAS_STORED.items():
+ path = arg_dict["path"]
+ if self.scope_expired_then_add(
+ key=f"dashboard-time-value-{arg}",
+ value=lang.readable_time(deep_get(self.alas_config.data, keys=f"{path}.time"))
+ ):
+ self.set_dashboard(arg, arg_dict, deep_get(self.alas_config.data, keys=path, default={}))
@use_scope("content", clear=True)
def alas_daemon_overview(self, task: str) -> None:
diff --git a/module/webui/base.py b/module/webui/base.py
index 0f43cbeba..29619551b 100644
--- a/module/webui/base.py
+++ b/module/webui/base.py
@@ -1,3 +1,5 @@
+from typing import Any, Dict
+
from pywebio.output import clear, put_html, put_scope, put_text, use_scope
from pywebio.session import defer_call, info, run_js
@@ -13,12 +15,34 @@ class Base:
self.is_mobile = info.user_agent.is_mobile
# Task handler
self.task_handler = WebIOTaskHandler()
+ # Record scopes to reduce data transfer to frontend
+ # Key: scope name, value: last update time
+ self.scope: Dict[str, Any] = {}
defer_call(self.stop)
def stop(self) -> None:
self.alive = False
self.task_handler.stop()
+ def scope_clear(self):
+ self.scope = {}
+
+ def scope_add(self, key, value):
+ self.scope[key] = value
+
+ def scope_expired(self, key, value) -> bool:
+ try:
+ return self.scope[key] != value
+ except KeyError:
+ return True
+
+ def scope_expired_then_add(self, key, value) -> bool:
+ if self.scope_expired(key, value):
+ self.scope_add(key, value)
+ return True
+ else:
+ return False
+
class Frame(Base):
def __init__(self) -> None:
@@ -33,6 +57,7 @@ class Frame(Base):
name: button name(label) to be highlight
"""
self.visible = True
+ self.scope_clear()
self.task_handler.remove_pending_task()
clear("menu")
if expand_menu:
@@ -50,6 +75,7 @@ class Frame(Base):
"""
self.visible = True
self.page = name
+ self.scope_clear()
self.task_handler.remove_pending_task()
clear("content")
if collapse_menu:
diff --git a/module/webui/lang.py b/module/webui/lang.py
index 06c5c5dd5..c1134f034 100644
--- a/module/webui/lang.py
+++ b/module/webui/lang.py
@@ -1,8 +1,9 @@
+import time
from typing import Dict
from module.config.utils import *
-from module.webui.setting import State
from module.webui.fake import list_mod
+from module.webui.setting import State
LANG = "zh-CN"
TRANSLATE_MODE = False
@@ -67,3 +68,33 @@ def reload():
for key in dic_lang["ja-JP"].keys():
if dic_lang["ja-JP"][key] == key:
dic_lang["ja-JP"][key] = dic_lang["en-US"][key]
+
+
+def readable_time(before: str) -> str:
+ """
+ Convert "2023-08-29 12:30:53" to "3 Minutes Ago"
+ """
+ if not before:
+ return t("Gui.Dashboard.NoData")
+ try:
+ ti = datetime.fromisoformat(before)
+ except ValueError:
+ return t("Gui.Dashboard.TimeError")
+ if ti == DEFAULT_TIME:
+ return t("Gui.Dashboard.NoData")
+
+ diff = time.time() - ti.timestamp()
+ if diff < -1:
+ return t("Gui.Dashboard.TimeError")
+ elif diff < 60:
+ # < 1 min
+ return t("Gui.Dashboard.JustNow")
+ elif diff < 3600:
+ return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60))
+ elif diff < 86400:
+ return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600))
+ elif diff < 129600:
+ return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400))
+ else:
+ # > 15 days
+ return t("Gui.Dashboard.LongTimeAgo")
From 246c039dc8bfe9cb894ab48108b49aa22e176dbc Mon Sep 17 00:00:00 2001
From: LmeSzinc <37934724+LmeSzinc@users.noreply.github.com>
Date: Wed, 30 Aug 2023 00:14:56 +0800
Subject: [PATCH 2/3] Add: Assigment and Sim.Uni. in dashboard
---
assets/gui/css/alas-pc.css | 10 +++++++
assets/gui/css/alas.css | 27 ++++++++++---------
assets/share/dungeon/ui/OCR_SIMUNI_POINT.png | Bin 0 -> 21261 bytes
config/template.json | 6 +++--
module/config/argument/args.json | 16 +++++++++++
module/config/argument/argument.yaml | 10 ++++++-
module/config/config_generated.py | 2 ++
module/config/i18n/en-US.json | 8 ++++++
module/config/i18n/ja-JP.json | 8 ++++++
module/config/i18n/zh-CN.json | 8 ++++++
module/config/i18n/zh-TW.json | 8 ++++++
module/config/stored/classes.py | 8 ++++++
module/config/stored/stored_generated.py | 4 +++
module/webui/app.py | 3 ++-
tasks/assignment/ui.py | 9 ++++++-
tasks/dungeon/assets/assets_dungeon_ui.py | 10 +++++++
tasks/dungeon/dungeon.py | 3 +++
tasks/dungeon/ui.py | 22 ++++++++++++++-
18 files changed, 144 insertions(+), 18 deletions(-)
create mode 100644 assets/share/dungeon/ui/OCR_SIMUNI_POINT.png
diff --git a/assets/gui/css/alas-pc.css b/assets/gui/css/alas-pc.css
index 0adfa2591..a3873f49e 100644
--- a/assets/gui/css/alas-pc.css
+++ b/assets/gui/css/alas-pc.css
@@ -26,6 +26,16 @@
overflow-y: auto;
}
+#pywebio-scope-log-bar {
+ display: flex;
+ flex-direction: column;
+ height: 11.5rem;
+}
+
+#pywebio-scope-dashboard {
+ flex: 1;
+}
+
#pywebio-scope-log-bar,
#pywebio-scope-log,
#pywebio-scope-daemon-overview #pywebio-scope-groups {
diff --git a/assets/gui/css/alas.css b/assets/gui/css/alas.css
index 5fc57f5d4..b7128f4ae 100644
--- a/assets/gui/css/alas.css
+++ b/assets/gui/css/alas.css
@@ -394,15 +394,25 @@ pre.rich-traceback-code {
grid-auto-flow: column;
}
-#pywebio-scope-log-bar {
- height: 11.5rem;
-}
-
#pywebio-scope-dashboard {
display: flex;
- align-content: flex-start;
+ align-content: space-between;
+ justify-content: flex-start;
flex-flow: row wrap;
overflow: auto;
+ margin-top: .5rem;
+}
+
+#pywebio-scope-dashboard > i {
+ flex-grow: 1;
+ align-self: flex-end;
+ width: 10rem;
+}
+
+[id^="pywebio-scope-dashboard-row-"] {
+ display: flex;
+ flex-grow: 1;
+ width: 10rem;
}
.dashboard-icon {
@@ -412,13 +422,6 @@ pre.rich-traceback-code {
border-radius: 50%;
}
-[id^="pywebio-scope-dashboard-row-"] {
- display: flex;
- flex-grow: 1;
- min-width: 4rem;
- max-width: 10rem;
-}
-
*[style*="--dashboard-value--"] {
font-size: 1rem;
font-weight: 500;
diff --git a/assets/share/dungeon/ui/OCR_SIMUNI_POINT.png b/assets/share/dungeon/ui/OCR_SIMUNI_POINT.png
new file mode 100644
index 0000000000000000000000000000000000000000..f67fa87cb941e7072972339390e1000ce0e9c923
GIT binary patch
literal 21261
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
literal 0
HcmV?d00001
diff --git a/config/template.json b/config/template.json
index 6ec1415f7..c05b72df5 100644
--- a/config/template.json
+++ b/config/template.json
@@ -57,7 +57,8 @@
},
"DungeonStorage": {
"TrailblazePower": {},
- "DungeonDouble": {}
+ "DungeonDouble": {},
+ "SimulatedUniverse": {}
}
},
"DailyQuest": {
@@ -119,7 +120,8 @@
"Name_2": "Akashic_Records",
"Name_3": "The_Invisible_Hand",
"Name_4": "Nine_Billion_Names",
- "Duration": 20
+ "Duration": 20,
+ "Assignment": {}
}
}
}
\ No newline at end of file
diff --git a/module/config/argument/args.json b/module/config/argument/args.json
index 97694e31c..4d3f4208e 100644
--- a/module/config/argument/args.json
+++ b/module/config/argument/args.json
@@ -373,6 +373,14 @@
"value": {},
"display": "hide",
"stored": "StoredDungeonDouble"
+ },
+ "SimulatedUniverse": {
+ "type": "stored",
+ "value": {},
+ "display": "hide",
+ "stored": "StoredSimulatedUniverse",
+ "order": 5,
+ "color": "#8fb5fe"
}
}
},
@@ -935,6 +943,14 @@
12,
20
]
+ },
+ "Assignment": {
+ "type": "stored",
+ "value": {},
+ "display": "hide",
+ "stored": "StoredAssignment",
+ "order": 3,
+ "color": "#deba95"
}
}
}
diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml
index 7d0c54b74..383436c54 100644
--- a/module/config/argument/argument.yaml
+++ b/module/config/argument/argument.yaml
@@ -115,6 +115,10 @@ DungeonStorage:
color: "#eb8efe"
DungeonDouble:
stored: StoredDungeonDouble
+ SimulatedUniverse:
+ stored: StoredSimulatedUniverse
+ order: 5
+ color: "#8fb5fe"
AchievableQuest:
# Quests will be injected in config updater
@@ -147,4 +151,8 @@ Assignment:
option: [ Nameless_Land_Nameless_People, ]
Duration:
value: 20
- option: [ 4, 8, 12, 20 ]
\ No newline at end of file
+ option: [ 4, 8, 12, 20 ]
+ Assignment:
+ stored: StoredAssignment
+ order: 3
+ color: "#deba95"
diff --git a/module/config/config_generated.py b/module/config/config_generated.py
index 34647ddbe..bdba746c7 100644
--- a/module/config/config_generated.py
+++ b/module/config/config_generated.py
@@ -57,6 +57,7 @@ class GeneratedConfig:
# Group `DungeonStorage`
DungeonStorage_TrailblazePower = {}
DungeonStorage_DungeonDouble = {}
+ DungeonStorage_SimulatedUniverse = {}
# Group `AchievableQuest`
AchievableQuest_Complete_1_Daily_Mission = 'achievable' # achievable, not_set, not_supported
@@ -95,3 +96,4 @@ class GeneratedConfig:
Assignment_Name_3 = 'The_Invisible_Hand' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
Assignment_Name_4 = 'Nine_Billion_Names' # Nine_Billion_Names, Destruction_of_the_Destroyer, Winter_Soldiers, Born_to_Obey, Root_Out_the_Turpitude, Fire_Lord_Inflames_Blades_of_War, Nameless_Land_Nameless_People, Akashic_Records, The_Invisible_Hand, Abandoned_and_Insulted, Spring_of_Life, The_Land_of_Gold, The_Blossom_in_the_Storm
Assignment_Duration = 20 # 4, 8, 12, 20
+ Assignment_Assignment = {}
diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json
index c3eba38d1..31ba90310 100644
--- a/module/config/i18n/en-US.json
+++ b/module/config/i18n/en-US.json
@@ -364,6 +364,10 @@
"DungeonDouble": {
"name": "Dungeon Double",
"help": ""
+ },
+ "SimulatedUniverse": {
+ "name": "Sim.Uni.",
+ "help": ""
}
},
"AchievableQuest": {
@@ -641,6 +645,10 @@
"8": "8 Hours",
"12": "12 Hours",
"20": "20 Hours"
+ },
+ "Assignment": {
+ "name": "Assignment",
+ "help": ""
}
},
"Gui": {
diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json
index 7e6199cc9..e5d789860 100644
--- a/module/config/i18n/ja-JP.json
+++ b/module/config/i18n/ja-JP.json
@@ -364,6 +364,10 @@
"DungeonDouble": {
"name": "DungeonStorage.DungeonDouble.name",
"help": "DungeonStorage.DungeonDouble.help"
+ },
+ "SimulatedUniverse": {
+ "name": "DungeonStorage.SimulatedUniverse.name",
+ "help": "DungeonStorage.SimulatedUniverse.help"
}
},
"AchievableQuest": {
@@ -641,6 +645,10 @@
"8": "8",
"12": "12",
"20": "20"
+ },
+ "Assignment": {
+ "name": "Assignment.Assignment.name",
+ "help": "Assignment.Assignment.help"
}
},
"Gui": {
diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json
index c7ecf1396..ddcf77838 100644
--- a/module/config/i18n/zh-CN.json
+++ b/module/config/i18n/zh-CN.json
@@ -364,6 +364,10 @@
"DungeonDouble": {
"name": "副本双倍",
"help": ""
+ },
+ "SimulatedUniverse": {
+ "name": "模拟宇宙",
+ "help": ""
}
},
"AchievableQuest": {
@@ -641,6 +645,10 @@
"8": "8小时",
"12": "12小时",
"20": "20小时"
+ },
+ "Assignment": {
+ "name": "委托",
+ "help": ""
}
},
"Gui": {
diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json
index aa2a45450..49272650a 100644
--- a/module/config/i18n/zh-TW.json
+++ b/module/config/i18n/zh-TW.json
@@ -364,6 +364,10 @@
"DungeonDouble": {
"name": "副本雙倍",
"help": ""
+ },
+ "SimulatedUniverse": {
+ "name": "模擬宇宙",
+ "help": ""
}
},
"AchievableQuest": {
@@ -641,6 +645,10 @@
"8": "8小時",
"12": "12小時",
"20": "20小時"
+ },
+ "Assignment": {
+ "name": "委託",
+ "help": ""
}
},
"Gui": {
diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py
index c1df02fb9..a56e86a49 100644
--- a/module/config/stored/classes.py
+++ b/module/config/stored/classes.py
@@ -169,6 +169,14 @@ class StoredTrailblazePower(StoredCounter):
FIXED_TOTAL = 180
+class StoredSimulatedUniverse(StoredCounter, StoredExpiredAt0400):
+ pass
+
+
+class StoredAssignment(StoredCounter):
+ pass
+
+
class StoredDaily(StoredExpiredAt0400):
quest1 = ''
quest2 = ''
diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py
index 95462b4aa..18f8fe087 100644
--- a/module/config/stored/stored_generated.py
+++ b/module/config/stored/stored_generated.py
@@ -1,4 +1,5 @@
from module.config.stored.classes import (
+ StoredAssignment,
StoredBase,
StoredCounter,
StoredDaily,
@@ -6,6 +7,7 @@ from module.config.stored.classes import (
StoredDungeonDouble,
StoredExpiredAt0400,
StoredInt,
+ StoredSimulatedUniverse,
StoredTrailblazePower,
)
@@ -16,5 +18,7 @@ from module.config.stored.classes import (
class StoredGenerated:
TrailblazePower = StoredTrailblazePower("Dungeon.DungeonStorage.TrailblazePower")
DungeonDouble = StoredDungeonDouble("Dungeon.DungeonStorage.DungeonDouble")
+ SimulatedUniverse = StoredSimulatedUniverse("Dungeon.DungeonStorage.SimulatedUniverse")
DailyActivity = StoredDailyActivity("DailyQuest.DailyStorage.DailyActivity")
DailyQuest = StoredDaily("DailyQuest.DailyStorage.DailyQuest")
+ Assignment = StoredAssignment("Assignment.Assignment.Assignment")
diff --git a/module/webui/app.py b/module/webui/app.py
index 5c25cdd05..d3d07d68f 100644
--- a/module/webui/app.py
+++ b/module/webui/app.py
@@ -416,7 +416,8 @@ class AlasGUI(Frame):
put_scope("dashboard", [
# Empty dashboard, values will be updated in alas_update_overview_task()
put_scope(f"dashboard-row-{arg}", []) for arg in self.ALAS_STORED.keys()
- ])
+ # Empty content to left-align last row
+ ] + [put_html("")] * len(self.ALAS_STORED))
])
put_scope("log", [put_html("")])
diff --git a/tasks/assignment/ui.py b/tasks/assignment/ui.py
index ae57bc05e..cadd5d19a 100644
--- a/tasks/assignment/ui.py
+++ b/tasks/assignment/ui.py
@@ -145,7 +145,14 @@ class AssignmentUI(UI):
@property
def _limit_status(self) -> tuple[int, int, int]:
self.device.screenshot()
- return DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
+ current, remain, total = DigitCounter(OCR_ASSIGNMENT_LIMIT).ocr_single_line(self.device.image)
+ if total and current <= total:
+ logger.attr('Assignment', f'{current}/{total}')
+ self.config.stored.Assignment.set(current, total)
+ else:
+ logger.warning(f'Invalid assignment limit: {current}/{total}')
+ self.config.stored.Assignment.set(0, 0)
+ return current, remain, total
def _iter_groups(self) -> Iterator[AssignmentGroup]:
for state in ASSIGNMENT_TOP_SWITCH.state_list:
diff --git a/tasks/dungeon/assets/assets_dungeon_ui.py b/tasks/dungeon/assets/assets_dungeon_ui.py
index 1a1d5adfe..5cf5b0695 100644
--- a/tasks/dungeon/assets/assets_dungeon_ui.py
+++ b/tasks/dungeon/assets/assets_dungeon_ui.py
@@ -53,6 +53,16 @@ OCR_DUNGEON_NAV = ButtonWrapper(
button=(108, 132, 428, 613),
),
)
+OCR_SIMUNI_POINT = ButtonWrapper(
+ name='OCR_SIMUNI_POINT',
+ share=Button(
+ file='./assets/share/dungeon/ui/OCR_SIMUNI_POINT.png',
+ area=(580, 190, 880, 235),
+ search=(560, 170, 900, 255),
+ color=(158, 164, 252),
+ button=(580, 190, 880, 235),
+ ),
+)
OPERATION_BRIEFING_CHECK = ButtonWrapper(
name='OPERATION_BRIEFING_CHECK',
share=Button(
diff --git a/tasks/dungeon/dungeon.py b/tasks/dungeon/dungeon.py
index 0463f464e..7185ba774 100644
--- a/tasks/dungeon/dungeon.py
+++ b/tasks/dungeon/dungeon.py
@@ -143,6 +143,9 @@ class Dungeon(DungeonUI, DungeonEvent, Combat):
with self.config.multi_set():
self.config.stored.DungeonDouble.calyx = calyx
self.config.stored.DungeonDouble.relic = relic
+ # Update SimulatedUniverse points
+ logger.info('Get simulated universe points')
+ self.dungeon_get_simuni_point()
# Run double events
ran_calyx_golden = False
diff --git a/tasks/dungeon/ui.py b/tasks/dungeon/ui.py
index ab4c8e388..f06481bd7 100644
--- a/tasks/dungeon/ui.py
+++ b/tasks/dungeon/ui.py
@@ -5,7 +5,7 @@ from module.base.button import ClickButton
from module.base.timer import Timer
from module.base.utils import get_color
from module.logger import logger
-from module.ocr.ocr import Ocr, OcrResultButton
+from module.ocr.ocr import DigitCounter, Ocr, OcrResultButton
from module.ocr.utils import split_and_pair_button_attr
from module.ui.draggable_list import DraggableList
from module.ui.switch import Switch
@@ -74,6 +74,10 @@ class OcrDungeonList(Ocr):
return result
+class OcrSimUniPoint(DigitCounter):
+ merge_thres_x = 50
+
+
class OcrDungeonListLimitEntrance(OcrDungeonList):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -219,6 +223,22 @@ class DungeonUI(UI):
logger.info('DungeonNav row Forgotten_Hall stabled')
return True
+ def dungeon_get_simuni_point(self) -> int:
+ """
+ Page:
+ in: page_guide, Survival_Index, Simulated_Universe
+ """
+ ocr = OcrSimUniPoint(OCR_SIMUNI_POINT)
+ value, _, total = ocr.ocr_single_line(self.device.image)
+ if total and value <= total:
+ logger.attr('SimulatedUniverse', f'{value}/{total}')
+ self.config.stored.SimulatedUniverse.set(value, total)
+ return value
+ else:
+ logger.warning(f'Invalid SimulatedUniverse points: {value}/{total}')
+ self.config.stored.SimulatedUniverse.set(0, 0)
+ return 0
+
def _dungeon_nav_goto(self, dungeon: DungeonList, skip_first_screenshot=True):
"""
Equivalent to `DUNGEON_NAV_LIST.select_row(dungeon.dungeon_nav, main=self)`
From eb28485d5df22c20c8bfc9c7df5c4e6ef193152a Mon Sep 17 00:00:00 2001
From: X-Zero-L <98764734+X-Zero-L@users.noreply.github.com>
Date: Wed, 30 Aug 2023 00:58:32 +0800
Subject: [PATCH 3/3] Fix: support (#64)
* Fix: support
* Upd: support
* Upd: support
* Upd: support
* Upd: support
* Upd: support
* Upd: support
* Upd: support
---------
Co-authored-by: LmeSzinc
---
assets/support/selected_character_arrow.png | Bin 0 -> 668 bytes
tasks/combat/support.py | 170 ++++++++++++++++++--
2 files changed, 161 insertions(+), 9 deletions(-)
create mode 100644 assets/support/selected_character_arrow.png
diff --git a/assets/support/selected_character_arrow.png b/assets/support/selected_character_arrow.png
new file mode 100644
index 0000000000000000000000000000000000000000..8e079c820c33a370a4cffc69aef0c4dde00c0cc2
GIT binary patch
literal 668
zcmV;N0%QG&P)}>`&0007GNklzu9h;NR^W0iTo-|HSA=0fh_aHwz97ky$p_KNY_
z?N-Om=X1GUuhV$EM-<8o)=eQNjE{LXK~Mk!6ex(Sr35IUir6p4Pp8wzKI{?mYPjmC
z@<(UR3E+(pMcB`j^)X0_$LCuJ`9ze;LXH~ldZ9-+3NNcX)>IqHLT?bZt?&(D5fvIi
zN>xA)o{3GeOkGw)l7nz>k*?83ULZ<{M#qO0E#rw0_hJ^_cv}sf
z^V%Hn5~Q}|gk#DNNq$sHl6Y(K%!KKfX4YU7?*l&`MIu1U%T~iPuebDGy5C5M7V#oy
z!v$+s)dKT08cRF7mco$UGsx#%Pi)jW-}U?w#(T=FX=}4F&U=plqHSrEyq$UNi?@O^
zsx^B}w$_YIx3)yD%y5pMMstokzLp9!<*_13OD`yY
zg}F8~yFc{c6bxP&i*%A?W~s0Ek`lCm(-}z3j 0.75
+ else None
+ )
+
+
+class NextSupportCharacter:
+ _arrow = ArrowWrapper(
+ name="NextSupportCharacterArrow",
+ share=Button(
+ file='./assets/support/selected_character_arrow.png',
+ area=None,
+ search=None,
+ color=None,
+ button=None,
+ )
+ )
+ _crop_area = (290, 115, 435, 634)
+
+ def __init__(self, screenshot):
+ self.name = "SupportCharacterArrow"
+ self.screenshot = crop(screenshot, NextSupportCharacter._crop_area)
+ self.arrow_center = self._find_center()
+ self.button = self._get_next_support_character_button()
+
+ def __bool__(self):
+ return self.button is not None
+
+ def _find_center(self):
+ center = NextSupportCharacter._arrow.find_center(self.screenshot)
+ center = get_position_in_original_image(
+ center, NextSupportCharacter._crop_area) if center else None
+ return center
+
+ def _get_next_support_character_button(self):
+ area = (self.arrow_center[0] - 200, min(self.arrow_center[1] + 65, 615), self.arrow_center[0] + 10, min(
+ self.arrow_center[1] + 80, 620)) if self.arrow_center and self.arrow_center[1] < 510 else None
+ return ButtonWrapper(
+ name="NextSupportCharacterButton",
+ share=Button(
+ file='./assets/support/selected_character_arrow.png',
+ area=area,
+ search=area,
+ # if next support was selected, the average color of the button will larger than 220
+ color=(220, 220, 220),
+ button=area,
+ )
+ ) if self.arrow_center and self.arrow_center[1] < 510 else None
+
+
class SupportListScroll(Scroll):
def cal_position(self, main):
"""
@@ -130,11 +205,18 @@ class CombatSupport(UI):
return True
# Click
- if self.appear(COMBAT_TEAM_SUPPORT, interval=2):
+ if self.appear(COMBAT_TEAM_SUPPORT, interval=1):
self.device.click(COMBAT_TEAM_SUPPORT)
self.interval_reset(COMBAT_TEAM_SUPPORT)
continue
- if self.appear(COMBAT_SUPPORT_LIST, interval=2):
+ if self.appear(CANCEL_POPUP, interval=1):
+ logger.warning(
+ "selected identical character, trying select another")
+ self._cancel_popup()
+ self._select_next_support()
+ self.interval_reset(CANCEL_POPUP)
+ continue
+ if self.appear(COMBAT_SUPPORT_LIST, interval=1):
if support_character_name != "FirstCharacter":
self._search_support(
support_character_name) # Search support
@@ -167,7 +249,6 @@ class CombatSupport(UI):
logger.info("Searching support")
skip_first_screenshot = False
- character = None
while 1:
if skip_first_screenshot:
skip_first_screenshot = False
@@ -175,7 +256,8 @@ class CombatSupport(UI):
self.device.screenshot()
if not support_character_name.startswith("Trailblazer"):
- character = SupportCharacter(support_character_name, self.device.image)
+ character = SupportCharacter(
+ support_character_name, self.device.image)
else:
character = SupportCharacter(f"Stelle{support_character_name[11:]}",
self.device.image) or SupportCharacter(
@@ -223,3 +305,73 @@ class CombatSupport(UI):
self.device.click(character)
interval.reset()
continue
+
+ def _cancel_popup(self):
+ """
+ Pages:
+ in: CANCEL_POPUP
+ out: COMBAT_SUPPORT_LIST
+ """
+ logger.hr("Combat support cancel popup")
+ skip_first_screenshot = True
+
+ while 1:
+ if skip_first_screenshot:
+ skip_first_screenshot = False
+ else:
+ self.device.screenshot()
+
+ # End
+ if self.appear(COMBAT_SUPPORT_LIST):
+ logger.info("Popup canceled")
+ return
+
+ if self.appear(CANCEL_POPUP):
+ self.device.click(CANCEL_POPUP)
+ continue
+
+ def _select_next_support(self):
+ """
+ Pages:
+ in: COMBAT_SUPPORT_LIST
+ out: COMBAT_SUPPORT_LIST
+ """
+ skip_first_screenshot = True
+ scroll = SupportListScroll(area=COMBAT_SUPPORT_LIST_SCROLL.area, color=(194, 196, 205),
+ name=COMBAT_SUPPORT_LIST_SCROLL.name)
+ interval = Timer(1)
+ next_support = None
+ if scroll.appear(main=self):
+ while 1:
+ if skip_first_screenshot:
+ skip_first_screenshot = False
+ else:
+ self.device.screenshot()
+
+ # End
+ if next_support and self._next_support_selected(next_support):
+ return
+
+ if interval.reached():
+ next_support = NextSupportCharacter(self.device.image)
+ if next_support:
+ logger.info("Next support found, clicking")
+ self.device.click(next_support.button)
+ elif not scroll.at_bottom(main=self):
+ scroll.next_page(main=self, page=0.4)
+ else:
+ logger.warning("No more support")
+ return
+
+ interval.reset()
+ continue
+
+ def _next_support_selected(self, next_support: NextSupportCharacter):
+ """
+ Returns:
+ bool: True if selected else False
+ """
+ if self.match_color(next_support.button, threshold=20):
+ logger.info("Next support selected")
+ return True
+ return False