diff --git a/assets/cn/campaign/OCR_COIN_LIMIT.png b/assets/cn/campaign/OCR_COIN_LIMIT.png new file mode 100644 index 0000000000..7c9e29b788 Binary files /dev/null and b/assets/cn/campaign/OCR_COIN_LIMIT.png differ diff --git a/assets/cn/campaign/OCR_OIL_LIMIT.png b/assets/cn/campaign/OCR_OIL_LIMIT.png new file mode 100644 index 0000000000..9392769fbb Binary files /dev/null and b/assets/cn/campaign/OCR_OIL_LIMIT.png differ diff --git a/assets/en/campaign/OCR_COIN_LIMIT.png b/assets/en/campaign/OCR_COIN_LIMIT.png new file mode 100644 index 0000000000..7c9e29b788 Binary files /dev/null and b/assets/en/campaign/OCR_COIN_LIMIT.png differ diff --git a/assets/en/campaign/OCR_OIL_LIMIT.png b/assets/en/campaign/OCR_OIL_LIMIT.png new file mode 100644 index 0000000000..9392769fbb Binary files /dev/null and b/assets/en/campaign/OCR_OIL_LIMIT.png differ diff --git a/assets/gui/css/alas-mobile.css b/assets/gui/css/alas-mobile.css index 9acad6eb77..9b144a3f68 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; +} + +[id^="pywebio-scope-dashboard-row-"] { + display: flex; + flex-grow: 1; + min-width: 4rem; + max-width: 50%; } \ No newline at end of file diff --git a/assets/gui/css/alas-pc.css b/assets/gui/css/alas-pc.css index 9240171a0c..5f968cc269 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.4rem; +} + +#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 735dc20121..4098fcf8b9 100644 --- a/assets/gui/css/alas.css +++ b/assets/gui/css/alas.css @@ -376,6 +376,7 @@ pre.rich-traceback-code { #pywebio-scope-scheduler-bar, #pywebio-scope-log-bar, #pywebio-scope-log, +#pywebio-scope-daemon-log-bar, #pywebio-scope-daemon-overview #pywebio-scope-groups { font-weight: 500; margin: 0.3125rem; @@ -383,17 +384,67 @@ 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-dashboard { + display: flex; + 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 { + margin: .6rem .8rem 0 .6rem; + width: .5rem; + height: .5rem; + border-radius: 50%; +} + +*[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 c36fa31b06..1237ec2502 100644 --- a/assets/gui/css/dark-alas.css +++ b/assets/gui/css/dark-alas.css @@ -139,6 +139,7 @@ pre.rich-traceback-code { #pywebio-scope-running, #pywebio-scope-pending, #pywebio-scope-waiting, +#pywebio-scope-daemon-log-bar, #pywebio-scope-daemon-overview #pywebio-scope-groups { background-color: #2f3136; border: 1px solid #21262d; @@ -152,4 +153,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 b65067f113..250ab3f8d3 100644 --- a/assets/gui/css/light-alas.css +++ b/assets/gui/css/light-alas.css @@ -139,6 +139,7 @@ pre.rich-traceback-code { #pywebio-scope-running, #pywebio-scope-pending, #pywebio-scope-waiting, +#pywebio-scope-daemon-log-bar, #pywebio-scope-daemon-overview #pywebio-scope-groups { background-color: white; border: 1px solid lightgrey; @@ -151,4 +152,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/assets/jp/campaign/OCR_COIN_LIMIT.png b/assets/jp/campaign/OCR_COIN_LIMIT.png new file mode 100644 index 0000000000..7c9e29b788 Binary files /dev/null and b/assets/jp/campaign/OCR_COIN_LIMIT.png differ diff --git a/assets/jp/campaign/OCR_OIL_LIMIT.png b/assets/jp/campaign/OCR_OIL_LIMIT.png new file mode 100644 index 0000000000..9392769fbb Binary files /dev/null and b/assets/jp/campaign/OCR_OIL_LIMIT.png differ diff --git a/assets/tw/campaign/OCR_COIN_LIMIT.png b/assets/tw/campaign/OCR_COIN_LIMIT.png new file mode 100644 index 0000000000..7c9e29b788 Binary files /dev/null and b/assets/tw/campaign/OCR_COIN_LIMIT.png differ diff --git a/assets/tw/campaign/OCR_OIL_LIMIT.png b/assets/tw/campaign/OCR_OIL_LIMIT.png new file mode 100644 index 0000000000..9392769fbb Binary files /dev/null and b/assets/tw/campaign/OCR_OIL_LIMIT.png differ diff --git a/config/template.json b/config/template.json index b9cf7f3f0e..6a0ccde43b 100644 --- a/config/template.json +++ b/config/template.json @@ -358,6 +358,12 @@ "CoinLimit": 10000, "TaskCall": "Main" }, + "CampaignStorage": { + "Oil": {}, + "Coin": {}, + "Gem": {}, + "Pt": {} + }, "Storage": { "Storage": {} } @@ -1510,6 +1516,9 @@ "UseTicket": true, "UseDrill": false }, + "GachaStorage": { + "Cube": {} + }, "Storage": { "Storage": {} } @@ -1565,6 +1574,11 @@ "DoRandomMapEvent": true, "AkashiShopFilter": "ActionPoint > PurpleCoins" }, + "OpsiStorage": { + "YellowCoin": {}, + "PurpleCoin": {}, + "ActionPoint": {} + }, "Storage": { "Storage": {} } diff --git a/module/base/code_generator.py b/module/base/code_generator.py new file mode 100644 index 0000000000..023c6a1ea7 --- /dev/null +++ b/module/base/code_generator.py @@ -0,0 +1,227 @@ +import typing as t + + +class TabWrapper: + def __init__(self, generator, prefix='', suffix='', newline=True): + """ + Args: + generator (CodeGenerator): + """ + self.generator = generator + self.prefix = prefix + self.suffix = suffix + self.newline = newline + + self.nested = False + + def __enter__(self): + if not self.nested and self.prefix: + self.generator.add(self.prefix, newline=self.newline) + self.generator.tab_count += 1 + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.generator.tab_count -= 1 + if self.suffix: + self.generator.add(self.suffix) + + def __repr__(self): + return self.prefix + + def set_nested(self, suffix=''): + self.nested = True + self.suffix += suffix + + +class VariableWrapper: + def __init__(self, name): + self.name = name + + def __repr__(self): + return str(self.name) + + __str__ = __repr__ + + +class CodeGenerator: + def __init__(self): + self.tab_count = 0 + self.lines = [] + + def add(self, line, comment=False, newline=True): + self.lines.append(self._line_with_tabs(line, comment=comment, newline=newline)) + + def generate(self): + return ''.join(self.lines) + + def print(self): + lines = self.generate() + print(lines) + + def write(self, file: str = None): + lines = self.generate() + with open(file, 'w', encoding='utf-8', newline='') as f: + f.write(lines) + + def _line_with_tabs(self, line, comment=False, newline=True): + if comment: + line = '# ' + line + out = ' ' * self.tab_count + line + if newline: + out += '\n' + return out + + def _repr(self, obj): + if isinstance(obj, str): + if '\n' in obj: + out = '"""\n' + with self.tab(): + for line in obj.strip().split('\n'): + line = line.strip() + out += self._line_with_tabs(line) + out += self._line_with_tabs('"""', newline=False) + return out + return repr(obj) + + def tab(self): + return TabWrapper(self) + + def Empty(self): + self.lines.append('\n') + + def Pass(self): + self.add('pass') + + def Import(self, text, empty=2): + for line in text.strip().split('\n'): + line = line.strip() + self.add(line) + for _ in range(empty): + self.Empty() + + def Variable(self, name): + return VariableWrapper(name) + + def Value(self, key=None, value=None, type_=None, **kwargs): + if key is not None: + if type_ is not None: + self.add(f'{key}: {type_} = {self._repr(value)}') + else: + self.add(f'{key} = {self._repr(value)}') + for key, value in kwargs.items(): + self.Value(key, value) + + def Comment(self, text): + for line in text.strip().split('\n'): + line = line.strip() + self.add(line, comment=True) + + def CommentAutoGenerage(self, file): + """ + Args: + file: dev_tools.button_extract + """ + # Only leave one blank line at above + if len(self.lines) >= 2: + if self.lines[-2:] == ['\n', '\n']: + self.lines.pop(-1) + self.Comment('This file was auto-generated, do not modify it manually. To generate:') + self.Comment(f'``` python -m {file} ```') + self.Empty() + + def List(self, key=None): + if key is not None: + return TabWrapper(self, prefix=str(key) + ' = [', suffix=']') + else: + return TabWrapper(self, prefix='[', suffix=']') + + def ListItem(self, value): + if isinstance(value, TabWrapper): + value.set_nested(suffix=',') + self.add(f'{self._repr(value)}') + return value + else: + self.add(f'{self._repr(value)},') + + def Dict(self, key=None): + if key is not None: + return TabWrapper(self, prefix=str(key) + ' = {', suffix='}') + else: + return TabWrapper(self, prefix='{', suffix='}') + + def DictItem(self, key=None, value=None): + if isinstance(value, TabWrapper): + value.set_nested(suffix=',') + if key is not None: + self.add(f'{self._repr(key)}: {self._repr(value)}') + return value + else: + if key is not None: + self.add(f'{self._repr(key)}: {self._repr(value)},') + + def Object(self, object_class, key=None): + if key is not None: + return TabWrapper(self, prefix=f'{key} = {object_class}(', suffix=')') + else: + return TabWrapper(self, prefix=f'{object_class}(', suffix=')') + + def ObjectAttr(self, key=None, value=None): + if isinstance(value, TabWrapper): + value.set_nested(suffix=',') + if key is None: + self.add(f'{self._repr(value)}') + else: + self.add(f'{key}={self._repr(value)}') + return value + else: + if key is None: + self.add(f'{self._repr(value)},') + else: + self.add(f'{key}={self._repr(value)},') + + def Class(self, name, inherit=None): + if inherit is not None: + return TabWrapper(self, prefix=f'class {name}({inherit}):') + else: + return TabWrapper(self, prefix=f'class {name}:') + + def Def(self, name, args=''): + return TabWrapper(self, prefix=f'def {name}({args}):') + + +generator = CodeGenerator() +Import = generator.Import +Value = generator.Value +Comment = generator.Comment +Dict = generator.Dict +DictItem = generator.DictItem + + +class MarkdownGenerator: + def __init__(self, column: t.List[str]): + self.rows = [column] + + def add_row(self, row): + self.rows.append([str(ele) for ele in row]) + + def product_line(self, row, max_width): + row = [ele.ljust(width) for ele, width in zip(row, max_width)] + row = ' | '.join(row) + row = '| ' + row + ' |' + return row + + def generate(self) -> t.List[str]: + import numpy as np + width = np.array([ + [len(ele) for ele in row] for row in self.rows + ]) + max_width = np.max(width, axis=0) + dash = ['-' * width for width in max_width] + + rows = [ + self.product_line(self.rows[0], max_width), + self.product_line(dash, max_width), + ] + [ + self.product_line(row, max_width) for row in self.rows[1:] + ] + return rows diff --git a/module/campaign/assets.py b/module/campaign/assets.py index 218b7ae1c5..2f1e3aae16 100644 --- a/module/campaign/assets.py +++ b/module/campaign/assets.py @@ -9,9 +9,11 @@ COMMISSION_NOTICE_AT_CAMPAIGN = Button(area={'cn': (1077, 637, 1083, 643), 'en': (1077, 637, 1083, 643), 'jp': (1077, 637, 1083, 643), 'tw': (1077, 637, 1083, 643)}, color={'cn': (172, 72, 49), 'en': (172, 72, 49), 'jp': (172, 72, 49), 'tw': (172, 72, 49)}, button={'cn': (1077, 637, 1083, 643), 'en': (1077, 637, 1083, 643), 'jp': (1077, 637, 1083, 643), 'tw': (1077, 637, 1083, 643)}, file={'cn': './assets/cn/campaign/COMMISSION_NOTICE_AT_CAMPAIGN.png', 'en': './assets/en/campaign/COMMISSION_NOTICE_AT_CAMPAIGN.png', 'jp': './assets/jp/campaign/COMMISSION_NOTICE_AT_CAMPAIGN.png', 'tw': './assets/tw/campaign/COMMISSION_NOTICE_AT_CAMPAIGN.png'}) EVENT_20230817_STORY = Button(area={'cn': (610, 320, 670, 380), 'en': (610, 320, 670, 380), 'jp': (610, 320, 670, 380), 'tw': (610, 320, 670, 380)}, color={'cn': (183, 180, 190), 'en': (183, 180, 190), 'jp': (183, 180, 190), 'tw': (183, 180, 190)}, button={'cn': (610, 320, 670, 380), 'en': (610, 320, 670, 380), 'jp': (610, 320, 670, 380), 'tw': (610, 320, 670, 380)}, file={'cn': './assets/cn/campaign/EVENT_20230817_STORY.png', 'en': './assets/en/campaign/EVENT_20230817_STORY.png', 'jp': './assets/jp/campaign/EVENT_20230817_STORY.png', 'tw': './assets/tw/campaign/EVENT_20230817_STORY.png'}) OCR_COIN = Button(area={'cn': (815, 23, 922, 51), 'en': (815, 23, 922, 51), 'jp': (815, 23, 922, 51), 'tw': (815, 23, 922, 51)}, color={'cn': (61, 61, 73), 'en': (61, 61, 73), 'jp': (61, 61, 73), 'tw': (61, 61, 73)}, button={'cn': (815, 23, 922, 51), 'en': (815, 23, 922, 51), 'jp': (815, 23, 922, 51), 'tw': (815, 23, 922, 51)}, file={'cn': './assets/cn/campaign/OCR_COIN.png', 'en': './assets/en/campaign/OCR_COIN.png', 'jp': './assets/jp/campaign/OCR_COIN.png', 'tw': './assets/tw/campaign/OCR_COIN.png'}) +OCR_COIN_LIMIT = Button(area={'cn': (807, 0, 944, 19), 'en': (807, 0, 944, 19), 'jp': (807, 0, 944, 19), 'tw': (807, 0, 944, 19)}, color={'cn': (206, 206, 206), 'en': (206, 206, 206), 'jp': (206, 206, 206), 'tw': (206, 206, 206)}, button={'cn': (807, 0, 944, 19), 'en': (807, 0, 944, 19), 'jp': (807, 0, 944, 19), 'tw': (807, 0, 944, 19)}, file={'cn': './assets/cn/campaign/OCR_COIN_LIMIT.png', 'en': './assets/en/campaign/OCR_COIN_LIMIT.png', 'jp': './assets/jp/campaign/OCR_COIN_LIMIT.png', 'tw': './assets/tw/campaign/OCR_COIN_LIMIT.png'}) OCR_EVENT_PT = Button(area={'cn': (1196, 109, 1280, 131), 'en': (1190, 109, 1280, 129), 'jp': (1196, 109, 1280, 131), 'tw': (1196, 109, 1280, 131)}, color={'cn': (121, 110, 59), 'en': (88, 78, 51), 'jp': (121, 110, 59), 'tw': (121, 110, 59)}, button={'cn': (1196, 109, 1280, 131), 'en': (1190, 109, 1280, 129), 'jp': (1196, 109, 1280, 131), 'tw': (1196, 109, 1280, 131)}, file={'cn': './assets/cn/campaign/OCR_EVENT_PT.png', 'en': './assets/en/campaign/OCR_EVENT_PT.png', 'jp': './assets/jp/campaign/OCR_EVENT_PT.png', 'tw': './assets/tw/campaign/OCR_EVENT_PT.png'}) OCR_OIL = Button(area={'cn': (614, 23, 714, 51), 'en': (614, 23, 714, 51), 'jp': (614, 23, 714, 51), 'tw': (614, 23, 714, 51)}, color={'cn': (64, 65, 79), 'en': (64, 65, 79), 'jp': (64, 65, 79), 'tw': (64, 65, 79)}, button={'cn': (614, 23, 714, 51), 'en': (614, 23, 714, 51), 'jp': (614, 23, 714, 51), 'tw': (614, 23, 714, 51)}, file={'cn': './assets/cn/campaign/OCR_OIL.png', 'en': './assets/en/campaign/OCR_OIL.png', 'jp': './assets/jp/campaign/OCR_OIL.png', 'tw': './assets/tw/campaign/OCR_OIL.png'}) OCR_OIL_CHECK = Button(area={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, color={'cn': (82, 82, 82), 'en': (82, 82, 82), 'jp': (82, 82, 82), 'tw': (82, 82, 82)}, button={'cn': (573, 30, 592, 49), 'en': (573, 30, 592, 49), 'jp': (573, 30, 592, 49), 'tw': (573, 30, 592, 49)}, file={'cn': './assets/cn/campaign/OCR_OIL_CHECK.png', 'en': './assets/en/campaign/OCR_OIL_CHECK.png', 'jp': './assets/jp/campaign/OCR_OIL_CHECK.png', 'tw': './assets/tw/campaign/OCR_OIL_CHECK.png'}) +OCR_OIL_LIMIT = Button(area={'cn': (608, 0, 736, 19), 'en': (608, 0, 736, 19), 'jp': (608, 0, 736, 19), 'tw': (608, 0, 736, 19)}, color={'cn': (202, 202, 202), 'en': (202, 202, 202), 'jp': (202, 202, 202), 'tw': (202, 202, 202)}, button={'cn': (608, 0, 736, 19), 'en': (608, 0, 736, 19), 'jp': (608, 0, 736, 19), 'tw': (608, 0, 736, 19)}, file={'cn': './assets/cn/campaign/OCR_OIL_LIMIT.png', 'en': './assets/en/campaign/OCR_OIL_LIMIT.png', 'jp': './assets/jp/campaign/OCR_OIL_LIMIT.png', 'tw': './assets/tw/campaign/OCR_OIL_LIMIT.png'}) SWITCH_1_HARD = Button(area={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, color={'cn': (233, 141, 128), 'en': (234, 139, 124), 'jp': (219, 116, 106), 'tw': (236, 159, 148)}, button={'cn': (82, 641, 148, 675), 'en': (87, 642, 148, 676), 'jp': (24, 645, 150, 697), 'tw': (82, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_HARD.png', 'en': './assets/en/campaign/SWITCH_1_HARD.png', 'jp': './assets/jp/campaign/SWITCH_1_HARD.png', 'tw': './assets/tw/campaign/SWITCH_1_HARD.png'}) SWITCH_1_NORMAL = Button(area={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, color={'cn': (157, 180, 227), 'en': (157, 180, 227), 'jp': (143, 169, 222), 'tw': (156, 179, 227)}, button={'cn': (80, 641, 148, 675), 'en': (79, 638, 147, 675), 'jp': (24, 644, 150, 697), 'tw': (79, 641, 148, 675)}, file={'cn': './assets/cn/campaign/SWITCH_1_NORMAL.png', 'en': './assets/en/campaign/SWITCH_1_NORMAL.png', 'jp': './assets/jp/campaign/SWITCH_1_NORMAL.png', 'tw': './assets/tw/campaign/SWITCH_1_NORMAL.png'}) SWITCH_2_EX = Button(area={'cn': (272, 658, 310, 676), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, color={'cn': (253, 168, 98), 'en': (254, 163, 80), 'jp': (205, 136, 64), 'tw': (254, 161, 72)}, button={'cn': (272, 658, 310, 676), 'en': (251, 644, 313, 697), 'jp': (186, 638, 314, 692), 'tw': (241, 640, 312, 692)}, file={'cn': './assets/cn/campaign/SWITCH_2_EX.png', 'en': './assets/en/campaign/SWITCH_2_EX.png', 'jp': './assets/jp/campaign/SWITCH_2_EX.png', 'tw': './assets/tw/campaign/SWITCH_2_EX.png'}) diff --git a/module/campaign/campaign_event.py b/module/campaign/campaign_event.py index 6929b9bee2..59d4e3c48a 100644 --- a/module/campaign/campaign_event.py +++ b/module/campaign/campaign_event.py @@ -47,12 +47,18 @@ def event_pt_limit_triggered(self): ) tasks = EVENTS + RAIDS + COALITIONS + GEMS_FARMINGS command = self.config.Scheduler_Command - if limit <= 0 or command not in tasks: + if command not in tasks: return False if command == 'GemsFarming' and self.stage_is_main(self.config.Campaign_Name): return False pt = self.get_event_pt() + if pt > 0: + self.config.stored.Pt.value = pt + + if limit <= 0: + return False + logger.attr('Event_PT_limit', f'{pt}/{limit}') if pt >= limit: logger.hr(f'Reach event PT limit: {limit}') diff --git a/module/campaign/campaign_status.py b/module/campaign/campaign_status.py index c98ce3e75b..b733bc606b 100644 --- a/module/campaign/campaign_status.py +++ b/module/campaign/campaign_status.py @@ -5,12 +5,14 @@ from module.base.timer import Timer from module.base.utils import color_similar, get_color -from module.campaign.assets import OCR_COIN, OCR_EVENT_PT, OCR_OIL, OCR_OIL_CHECK +from module.campaign.assets import OCR_COIN, OCR_EVENT_PT, OCR_OIL, OCR_OIL_CHECK, OCR_COIN_LIMIT, OCR_OIL_LIMIT from module.logger import logger from module.ocr.ocr import Digit, Ocr +from module.shop.shop_status import OCR_SHOP_GEMS from module.ui.ui import UI OCR_COIN = Digit(OCR_COIN, name='OCR_COIN', letter=(239, 239, 239), threshold=128) +OCR_COIN_LIMIT = Digit(OCR_COIN_LIMIT, name='OCR_COIN_LIMIT', letter=(239, 239, 239), threshold=128) class PtOcr(Ocr): @@ -60,6 +62,7 @@ def get_coin(self, skip_first_screenshot=True): int: Coin amount """ amount = 0 + limit = 0 timeout = Timer(1, count=2).start() while 1: if skip_first_screenshot: @@ -72,9 +75,11 @@ def get_coin(self, skip_first_screenshot=True): break amount = OCR_COIN.ocr(self.device.image) + limit = OCR_COIN_LIMIT.ocr(self.device.image) if amount >= 100: break + self.config.stored.Coin.set(amount, limit) return amount def _get_oil(self): @@ -84,15 +89,18 @@ def _get_oil(self): color = get_color(self.device.image, OCR_OIL_CHECK.button) if color_similar(color, OCR_OIL_CHECK.color): # Original color - ocr = Digit(OCR_OIL, name='OCR_OIL', letter=(247, 247, 247), threshold=128) + ocr_oil = Digit(OCR_OIL, name='OCR_OIL', letter=(247, 247, 247), threshold=128) + ocr_oil_limit = Digit(OCR_OIL_LIMIT, name='OCR_OIL_LIMIT', letter=(247, 247, 247), threshold=128) elif color_similar(color, (59, 59, 64)): # With black overlay - ocr = Digit(OCR_OIL, name='OCR_OIL', letter=(165, 165, 165), threshold=128) + ocr_oil = Digit(OCR_OIL, name='OCR_OIL', letter=(165, 165, 165), threshold=128) + ocr_oil_limit = Digit(OCR_OIL_LIMIT, name='OCR_OIL_LIMIT', letter=(165, 165, 165), threshold=128) else: logger.warning(f'Unexpected OCR_OIL_CHECK color') - ocr = Digit(OCR_OIL, name='OCR_OIL', letter=(247, 247, 247), threshold=128) + ocr_oil = Digit(OCR_OIL, name='OCR_OIL', letter=(247, 247, 247), threshold=128) + ocr_oil_limit = Digit(OCR_OIL_LIMIT, name='OCR_OIL_LIMIT', letter=(247, 247, 247), threshold=128) - return ocr.ocr(self.device.image) + return ocr_oil.ocr(self.device.image), ocr_oil_limit.ocr(self.device.image) def get_oil(self, skip_first_screenshot=True): """ @@ -100,6 +108,7 @@ def get_oil(self, skip_first_screenshot=True): int: Oil amount """ amount = 0 + limit = 0 timeout = Timer(1, count=2).start() while 1: if skip_first_screenshot: @@ -115,10 +124,16 @@ def get_oil(self, skip_first_screenshot=True): logger.info('No oil icon') continue - amount = self._get_oil() + amount, limit = self._get_oil() if amount >= 100: break + self.config.stored.Oil.set(amount, limit) + return amount + + def status_get_gems(self): + amount = OCR_SHOP_GEMS.ocr(self.device.image) + self.config.stored.Gem.value = amount return amount def is_balancer_task(self): diff --git a/module/campaign/run.py b/module/campaign/run.py index edd40635c6..35d1424953 100644 --- a/module/campaign/run.py +++ b/module/campaign/run.py @@ -72,65 +72,68 @@ def triggered_stop_condition(self, oil_check=True): Returns: bool: If triggered a stop condition. """ - # Run count limit - if self.run_limit and self.config.StopCondition_RunCount <= 0: - logger.hr('Triggered stop condition: Run count') - self.config.StopCondition_RunCount = 0 - self.config.Scheduler_Enable = False - handle_notify( - self.config.Error_OnePushConfig, - title=f"Alas <{self.config.config_name}> campaign finished", - content=f"<{self.config.config_name}> {self.name} reached run count limit" - ) - return True - # Lv120 limit - if self.config.StopCondition_ReachLevel and self.campaign.config.LV_TRIGGERED: - logger.hr(f'Triggered stop condition: Reach level {self.config.StopCondition_ReachLevel}') - self.config.Scheduler_Enable = False - handle_notify( - self.config.Error_OnePushConfig, - title=f"Alas <{self.config.config_name}> campaign finished", - content=f"<{self.config.config_name}> {self.name} reached level limit" - ) - return True - # Oil limit - if oil_check: - if self.get_oil() < max(500, self.config.StopCondition_OilLimit): - logger.hr('Triggered stop condition: Oil limit') + with self.config.multi_set(): + # Run count limit + if self.run_limit and self.config.StopCondition_RunCount <= 0: + logger.hr('Triggered stop condition: Run count') + self.config.StopCondition_RunCount = 0 + self.config.Scheduler_Enable = False + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config.config_name}> campaign finished", + content=f"<{self.config.config_name}> {self.name} reached run count limit" + ) + return True + # Lv120 limit + if self.config.StopCondition_ReachLevel and self.campaign.config.LV_TRIGGERED: + logger.hr(f'Triggered stop condition: Reach level {self.config.StopCondition_ReachLevel}') + self.config.Scheduler_Enable = False + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config.config_name}> campaign finished", + content=f"<{self.config.config_name}> {self.name} reached level limit" + ) + return True + # Oil limit + if oil_check: + self.status_get_gems() + self.get_coin() + if self.get_oil() < max(500, self.config.StopCondition_OilLimit): + logger.hr('Triggered stop condition: Oil limit') + self.config.task_delay(minute=(120, 240)) + return True + # Auto search oil limit + if self.campaign.auto_search_oil_limit_triggered: + logger.hr('Triggered stop condition: Auto search oil limit') self.config.task_delay(minute=(120, 240)) return True - # Auto search oil limit - if self.campaign.auto_search_oil_limit_triggered: - logger.hr('Triggered stop condition: Auto search oil limit') - self.config.task_delay(minute=(120, 240)) - return True - # If Get a New Ship - if self.config.StopCondition_GetNewShip and self.campaign.config.GET_SHIP_TRIGGERED: - logger.hr('Triggered stop condition: Get new ship') - self.config.Scheduler_Enable = False - handle_notify( - self.config.Error_OnePushConfig, - title=f"Alas <{self.config.config_name}> campaign finished", - content=f"<{self.config.config_name}> {self.name} got new ship" - ) - return True - # Event limit - if oil_check and self.campaign.event_pt_limit_triggered(): - logger.hr('Triggered stop condition: Event PT limit') - return True - # Auto search TaskBalancer - if self.config.TaskBalancer_Enable and self.campaign.auto_search_coin_limit_triggered: - logger.hr('Triggered stop condition: Auto search coin limit') - self.handle_task_balancer() - return True - # TaskBalancer - if oil_check and self.run_count >= 1: - if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): - logger.hr('Triggered stop condition: Coin limit') + # If Get a New Ship + if self.config.StopCondition_GetNewShip and self.campaign.config.GET_SHIP_TRIGGERED: + logger.hr('Triggered stop condition: Get new ship') + self.config.Scheduler_Enable = False + handle_notify( + self.config.Error_OnePushConfig, + title=f"Alas <{self.config.config_name}> campaign finished", + content=f"<{self.config.config_name}> {self.name} got new ship" + ) + return True + # Event limit + if oil_check and self.campaign.event_pt_limit_triggered(): + logger.hr('Triggered stop condition: Event PT limit') + return True + # Auto search TaskBalancer + if self.config.TaskBalancer_Enable and self.campaign.auto_search_coin_limit_triggered: + logger.hr('Triggered stop condition: Auto search coin limit') self.handle_task_balancer() return True + # TaskBalancer + if oil_check and self.run_count >= 1: + if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): + logger.hr('Triggered stop condition: Coin limit') + self.handle_task_balancer() + return True - return False + return False def _triggered_app_restart(self): """ diff --git a/module/coalition/coalition.py b/module/coalition/coalition.py index f36c537dd5..32d5915456 100644 --- a/module/coalition/coalition.py +++ b/module/coalition/coalition.py @@ -49,31 +49,32 @@ def triggered_stop_condition(self, oil_check=False, pt_check=False): Returns: bool: If triggered a stop condition. """ - # Run count limit - if self.run_limit and self.config.StopCondition_RunCount <= 0: - logger.hr('Triggered stop condition: Run count') - self.config.StopCondition_RunCount = 0 - self.config.Scheduler_Enable = False - return True - # Oil limit - if oil_check: - if self.get_oil() < max(500, self.config.StopCondition_OilLimit): - logger.hr('Triggered stop condition: Oil limit') - self.config.task_delay(minute=(120, 240)) + with self.config.multi_set(): + # Run count limit + if self.run_limit and self.config.StopCondition_RunCount <= 0: + logger.hr('Triggered stop condition: Run count') + self.config.StopCondition_RunCount = 0 + self.config.Scheduler_Enable = False return True - # Event limit - if pt_check: - if self.event_pt_limit_triggered(): - logger.hr('Triggered stop condition: Event PT limit') - return True - # TaskBalancer - if self.run_count >= 1: - if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): - logger.hr('Triggered stop condition: Coin limit') - self.handle_task_balancer() - return True - - return False + # Oil limit + if oil_check: + if self.get_oil() < max(500, self.config.StopCondition_OilLimit): + logger.hr('Triggered stop condition: Oil limit') + self.config.task_delay(minute=(120, 240)) + return True + # Event limit + if pt_check: + if self.event_pt_limit_triggered(): + logger.hr('Triggered stop condition: Event PT limit') + return True + # TaskBalancer + if self.run_count >= 1: + if self.config.TaskBalancer_Enable and self.triggered_task_balancer(): + logger.hr('Triggered stop condition: Coin limit') + self.handle_task_balancer() + return True + + return False def coalition_execute_once(self, event, stage, fleet): """ diff --git a/module/combat/auto_search_combat.py b/module/combat/auto_search_combat.py index 7433f15763..5b221c2c44 100644 --- a/module/combat/auto_search_combat.py +++ b/module/combat/auto_search_combat.py @@ -98,10 +98,11 @@ def auto_search_watch_oil(self, checked=False): This will set auto_search_oil_limit_triggered. """ if not checked: - oil = self._get_oil() + oil, limit = self._get_oil() if oil == 0: logger.warning('Oil not found') else: + self.config.stored.Oil.set(oil, limit) if oil < max(500, self.config.StopCondition_OilLimit): logger.info('Reach oil limit') self.auto_search_oil_limit_triggered = True @@ -184,8 +185,9 @@ def auto_search_moving(self, skip_first_screenshot=True): if self.is_auto_search_running(): checked_fleet = self.auto_search_watch_fleet(checked_fleet) if not checked_oil or not checked_coin: - checked_oil = self.auto_search_watch_oil(checked_oil) - checked_coin = self.auto_search_watch_coin(checked_coin) + with self.config.multi_set(): + checked_oil = self.auto_search_watch_oil(checked_oil) + checked_coin = self.auto_search_watch_coin(checked_coin) if self.handle_retirement(): self.map_offensive_auto_search() continue diff --git a/module/config/argument/args.json b/module/config/argument/args.json index 8667d0587e..4e959531d3 100644 --- a/module/config/argument/args.json +++ b/module/config/argument/args.json @@ -1920,6 +1920,40 @@ ] } }, + "CampaignStorage": { + "Oil": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredOil", + "order": 1, + "color": "#000000" + }, + "Coin": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredCoin", + "order": 2, + "color": "#FFAA33" + }, + "Gem": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredInt", + "order": 3, + "color": "#FF3333" + }, + "Pt": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredInt", + "order": 4, + "color": "#00BFFF" + } + }, "Storage": { "Storage": { "type": "storage", @@ -8020,6 +8054,16 @@ "value": false } }, + "GachaStorage": { + "Cube": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredInt", + "order": 5, + "color": "#33FFFF" + } + }, "Storage": { "Storage": { "type": "storage", @@ -8203,6 +8247,32 @@ "value": "ActionPoint > PurpleCoins" } }, + "OpsiStorage": { + "YellowCoin": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredInt", + "order": 7, + "color": "#FF8800" + }, + "PurpleCoin": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredInt", + "order": 8, + "color": "#7700BB" + }, + "ActionPoint": { + "type": "stored", + "value": {}, + "display": "hide", + "stored": "StoredActionPoint", + "order": 6, + "color": "#008000" + } + }, "Storage": { "Storage": { "type": "storage", diff --git a/module/config/argument/argument.yaml b/module/config/argument/argument.yaml index 59cab0349e..b4b7b4038a 100644 --- a/module/config/argument/argument.yaml +++ b/module/config/argument/argument.yaml @@ -278,6 +278,23 @@ GemsFarming: value: any option: [ any, favourite, aulick_or_foote , cassin_or_downes, z20_or_z21 ] CommissionLimit: true +CampaignStorage: + Oil: + stored: StoredOil + order: 1 + color: "#000000" + Coin: + stored: StoredCoin + order: 2 + color: "#FFAA33" + Gem: + stored: StoredInt + order: 3 + color: "#FF3333" + Pt: + stored: StoredInt + order: 4 + color: "#00BFFF" # ==================== Event ==================== @@ -312,7 +329,6 @@ Coalition: value: single option: [ single, multi ] - # ==================== Reward ==================== Commission: @@ -569,6 +585,12 @@ SupplyPack: option: [ 0, 1, 2, 3, 4, 5, 6 ] Minigame: Collect: false +GachaStorage: + Cube: + stored: StoredInt + order: 5 + color: "#33FFFF" + # ==================== Daily ==================== Daily: @@ -711,6 +733,19 @@ OpsiHazard1Leveling: TargetZone: value: 0 option: [ 0, 44, 22 ] +OpsiStorage: + YellowCoin: + stored: StoredInt + order: 7 + color: "#FF8800" + PurpleCoin: + stored: StoredInt + order: 8 + color: "#7700BB" + ActionPoint: + stored: StoredActionPoint + order: 6 + color: "#008000" # ==================== Tools ==================== diff --git a/module/config/argument/gui.yaml b/module/config/argument/gui.yaml index 82302d3ea2..7ebd976578 100644 --- a/module/config/argument/gui.yaml +++ b/module/config/argument/gui.yaml @@ -113,4 +113,13 @@ Text: InvalidFeedBack: Clear: EnterPassword: - ChooseFile: \ No newline at end of file + ChooseFile: + +Dashboard: + NoData: + TimeError: + JustNow: + MinutesAgo: + HoursAgo: + DaysAgo: + LongTimeAgo: \ No newline at end of file diff --git a/module/config/argument/stored.json b/module/config/argument/stored.json new file mode 100644 index 0000000000..d9a732008b --- /dev/null +++ b/module/config/argument/stored.json @@ -0,0 +1,100 @@ +{ + "Oil": { + "name": "Oil", + "path": "EventGeneral.CampaignStorage.Oil", + "i18n": "CampaignStorage.Oil.name", + "stored": "StoredOil", + "attrs": { + "time": "2020-01-01 00:00:00", + "total": 0, + "value": 0 + }, + "order": 1, + "color": "#000000" + }, + "Coin": { + "name": "Coin", + "path": "EventGeneral.CampaignStorage.Coin", + "i18n": "CampaignStorage.Coin.name", + "stored": "StoredCoin", + "attrs": { + "time": "2020-01-01 00:00:00", + "total": 0, + "value": 0 + }, + "order": 2, + "color": "#FFAA33" + }, + "Gem": { + "name": "Gem", + "path": "EventGeneral.CampaignStorage.Gem", + "i18n": "CampaignStorage.Gem.name", + "stored": "StoredInt", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": 0 + }, + "order": 3, + "color": "#FF3333" + }, + "Pt": { + "name": "Pt", + "path": "EventGeneral.CampaignStorage.Pt", + "i18n": "CampaignStorage.Pt.name", + "stored": "StoredInt", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": 0 + }, + "order": 4, + "color": "#00BFFF" + }, + "Cube": { + "name": "Cube", + "path": "Gacha.GachaStorage.Cube", + "i18n": "GachaStorage.Cube.name", + "stored": "StoredInt", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": 0 + }, + "order": 5, + "color": "#33FFFF" + }, + "ActionPoint": { + "name": "ActionPoint", + "path": "OpsiGeneral.OpsiStorage.ActionPoint", + "i18n": "OpsiStorage.ActionPoint.name", + "stored": "StoredActionPoint", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": "" + }, + "order": 6, + "color": "#008000" + }, + "YellowCoin": { + "name": "YellowCoin", + "path": "OpsiGeneral.OpsiStorage.YellowCoin", + "i18n": "OpsiStorage.YellowCoin.name", + "stored": "StoredInt", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": 0 + }, + "order": 7, + "color": "#FF8800" + }, + "PurpleCoin": { + "name": "PurpleCoin", + "path": "OpsiGeneral.OpsiStorage.PurpleCoin", + "i18n": "OpsiStorage.PurpleCoin.name", + "stored": "StoredInt", + "attrs": { + "time": "2020-01-01 00:00:00", + "value": 0 + }, + "order": 8, + "color": "#7700BB" + } +} \ No newline at end of file diff --git a/module/config/argument/task.yaml b/module/config/argument/task.yaml index 74e40c0883..bdec3563f0 100644 --- a/module/config/argument/task.yaml +++ b/module/config/argument/task.yaml @@ -71,6 +71,7 @@ Event: EventGeneral: - EventGeneral - TaskBalancer + - CampaignStorage Event: - Scheduler - Campaign @@ -247,6 +248,7 @@ DailyMission: Gacha: - Scheduler - Gacha + - GachaStorage Freebies: - Scheduler - BattlePass @@ -264,6 +266,7 @@ Opsi: tasks: OpsiGeneral: - OpsiGeneral + - OpsiStorage OpsiAshBeacon: - Scheduler - OpsiAshBeacon diff --git a/module/config/config.py b/module/config/config.py index f57aa0be02..9745e47849 100644 --- a/module/config/config.py +++ b/module/config/config.py @@ -4,11 +4,14 @@ import threading import pywebio +from module.base.decorator import cached_property, del_cached_property from module.base.filter import Filter from module.config.config_generated import GeneratedConfig from module.config.config_manual import ManualConfig, OutputConfig from module.config.config_updater import ConfigUpdater +from module.config.stored.classes import iter_attribute +from module.config.stored.stored_generated import StoredGenerated from module.config.watcher import ConfigWatcher from module.config.utils import * from module.exception import RequestHumanTakeover, ScriptError @@ -198,6 +201,15 @@ def close_game(self): def is_actual_task(self): return self.task.command.lower() not in ['alas', 'template'] + @cached_property + def stored(self) -> StoredGenerated: + stored = StoredGenerated() + # Bind config + for _, value in iter_attribute(stored): + value._bind(self) + del_cached_property(value, '_stored') + return stored + def get_next_task(self): """ Calculate tasks, set pending_task and waiting_task diff --git a/module/config/config_generated.py b/module/config/config_generated.py index c790347cf7..2b587bf3b1 100644 --- a/module/config/config_generated.py +++ b/module/config/config_generated.py @@ -154,6 +154,12 @@ class GeneratedConfig: GemsFarming_CommonDD = 'any' # any, favourite, aulick_or_foote, cassin_or_downes, z20_or_z21 GemsFarming_CommissionLimit = True + # Group `CampaignStorage` + CampaignStorage_Oil = {} + CampaignStorage_Coin = {} + CampaignStorage_Gem = {} + CampaignStorage_Pt = {} + # Group `EventGeneral` EventGeneral_PtLimit = 0 EventGeneral_TimeLimit = datetime.datetime(2020, 1, 1, 0, 0) @@ -326,6 +332,9 @@ class GeneratedConfig: # Group `Minigame` Minigame_Collect = False + # Group `GachaStorage` + GachaStorage_Cube = {} + # Group `Daily` Daily_UseDailySkip = True Daily_EscortMission = 'first' # skip, first, second, third @@ -421,6 +430,11 @@ class GeneratedConfig: # Group `OpsiHazard1Leveling` OpsiHazard1Leveling_TargetZone = 0 # 0, 44, 22 + # Group `OpsiStorage` + OpsiStorage_YellowCoin = {} + OpsiStorage_PurpleCoin = {} + OpsiStorage_ActionPoint = {} + # Group `Daemon` Daemon_EnterMap = True diff --git a/module/config/config_updater.py b/module/config/config_updater.py index 74d69eb567..775e1109c3 100644 --- a/module/config/config_updater.py +++ b/module/config/config_updater.py @@ -38,6 +38,11 @@ class GeneratedConfig: MARITIME_ESCORTS = ['MaritimeEscort'] +def get_generator(): + from module.base.code_generator import CodeGenerator + return CodeGenerator() + + class Event: def __init__(self, text): self.date, self.directory, self.name, self.cn, self.en, self.jp, self.tw \ @@ -95,6 +100,9 @@ def argument(self): if not isinstance(value, dict): value = {'value': value} arg['type'] = data_to_type(value, arg=path[1]) + if arg['type'] == 'stored': + value['value'] = {} + arg['display'] = 'hide' # Hide `stored` by default if isinstance(value['value'], datetime): arg['type'] = 'datetime' arg['validate'] = 'datetime' @@ -258,6 +266,28 @@ def generate_code(self): for text in lines: f.write(text + '\n') + @timer + def generate_stored(self): + import module.config.stored.classes as classes + gen = get_generator() + gen.add('from module.config.stored.classes import (') + with gen.tab(): + for cls in sorted([name for name in dir(classes) if name.startswith('Stored')]): + gen.add(cls + ',') + gen.add(')') + gen.Empty() + gen.Empty() + gen.Empty() + gen.CommentAutoGenerage('module/config/config_updater.py') + + with gen.Class('StoredGenerated'): + for path, data in deep_iter(self.args, depth=3): + cls = data.get('stored') + if cls: + gen.add(f'{path[-1]} = {cls}("{".".join(path)}")') + + gen.write('module/config/stored/stored_generated.py') + @timer def generate_i18n(self, lang): """ @@ -375,6 +405,32 @@ def menu(self): 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 + @cached_property @timer def event(self): @@ -514,7 +570,9 @@ def generate(self): 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: self.generate_i18n(lang) self.generate_deploy_template() diff --git a/module/config/i18n/en-US.json b/module/config/i18n/en-US.json index 402d9d587a..6ec2484881 100644 --- a/module/config/i18n/en-US.json +++ b/module/config/i18n/en-US.json @@ -1166,6 +1166,28 @@ "help": "When running 7x24, prevent having a lot of urgent commissions and not being able to complete daily commissions. It is recommended to select only short-terms and high-yields in the commission filter" } }, + "CampaignStorage": { + "_info": { + "name": "CampaignStorage._info.name", + "help": "CampaignStorage._info.help" + }, + "Oil": { + "name": "Oil", + "help": "" + }, + "Coin": { + "name": "Coin", + "help": "" + }, + "Gem": { + "name": "Gem", + "help": "" + }, + "Pt": { + "name": "Event Pt", + "help": "" + } + }, "EventGeneral": { "_info": { "name": "Event General Settings", @@ -1967,6 +1989,16 @@ "help": "" } }, + "GachaStorage": { + "_info": { + "name": "GachaStorage._info.name", + "help": "GachaStorage._info.help" + }, + "Cube": { + "name": "Cube", + "help": "" + } + }, "Daily": { "_info": { "name": "Daily Settings", @@ -2420,6 +2452,24 @@ "22": "22 | NA Ocean SW Sector B" } }, + "OpsiStorage": { + "_info": { + "name": "OpsiStorage._info.name", + "help": "OpsiStorage._info.help" + }, + "YellowCoin": { + "name": "Operation Supply Coin", + "help": "" + }, + "PurpleCoin": { + "name": "Special Item Token", + "help": "" + }, + "ActionPoint": { + "name": "Action Point", + "help": "" + } + }, "Daemon": { "_info": { "name": "Semi-auto Clicking", @@ -2610,6 +2660,15 @@ "Clear": "Clear", "EnterPassword": "Enter password", "ChooseFile": "Choose file" + }, + "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" } } } \ No newline at end of file diff --git a/module/config/i18n/ja-JP.json b/module/config/i18n/ja-JP.json index 74fc85ecad..a4c359700c 100644 --- a/module/config/i18n/ja-JP.json +++ b/module/config/i18n/ja-JP.json @@ -1166,6 +1166,28 @@ "help": "GemsFarming.CommissionLimit.help" } }, + "CampaignStorage": { + "_info": { + "name": "CampaignStorage._info.name", + "help": "CampaignStorage._info.help" + }, + "Oil": { + "name": "CampaignStorage.Oil.name", + "help": "CampaignStorage.Oil.help" + }, + "Coin": { + "name": "CampaignStorage.Coin.name", + "help": "CampaignStorage.Coin.help" + }, + "Gem": { + "name": "CampaignStorage.Gem.name", + "help": "CampaignStorage.Gem.help" + }, + "Pt": { + "name": "CampaignStorage.Pt.name", + "help": "CampaignStorage.Pt.help" + } + }, "EventGeneral": { "_info": { "name": "EventGeneral._info.name", @@ -1967,6 +1989,16 @@ "help": "Minigame.Collect.help" } }, + "GachaStorage": { + "_info": { + "name": "GachaStorage._info.name", + "help": "GachaStorage._info.help" + }, + "Cube": { + "name": "GachaStorage.Cube.name", + "help": "GachaStorage.Cube.help" + } + }, "Daily": { "_info": { "name": "Daily._info.name", @@ -2420,6 +2452,24 @@ "22": "22" } }, + "OpsiStorage": { + "_info": { + "name": "OpsiStorage._info.name", + "help": "OpsiStorage._info.help" + }, + "YellowCoin": { + "name": "OpsiStorage.YellowCoin.name", + "help": "OpsiStorage.YellowCoin.help" + }, + "PurpleCoin": { + "name": "OpsiStorage.PurpleCoin.name", + "help": "OpsiStorage.PurpleCoin.help" + }, + "ActionPoint": { + "name": "OpsiStorage.ActionPoint.name", + "help": "OpsiStorage.ActionPoint.help" + } + }, "Daemon": { "_info": { "name": "Daemon._info.name", @@ -2610,6 +2660,15 @@ "Clear": "消除", "EnterPassword": "パスワードを入力してください", "ChooseFile": "ファイルを選択してください" + }, + "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" } } } \ No newline at end of file diff --git a/module/config/i18n/zh-CN.json b/module/config/i18n/zh-CN.json index 89eb3ad009..1f0481cdb2 100644 --- a/module/config/i18n/zh-CN.json +++ b/module/config/i18n/zh-CN.json @@ -1166,6 +1166,28 @@ "help": "在7x24运行时防止紧急委托数量过多做不完每日委托,建议在委托过滤器仅选择短时长高收益委托" } }, + "CampaignStorage": { + "_info": { + "name": "CampaignStorage._info.name", + "help": "" + }, + "Oil": { + "name": "石油", + "help": "" + }, + "Coin": { + "name": "物资", + "help": "" + }, + "Gem": { + "name": "钻石", + "help": "" + }, + "Pt": { + "name": "活动PT", + "help": "" + } + }, "EventGeneral": { "_info": { "name": "活动通用设置", @@ -1967,6 +1989,16 @@ "help": "" } }, + "GachaStorage": { + "_info": { + "name": "GachaStorage._info.name", + "help": "GachaStorage._info.help" + }, + "Cube": { + "name": "魔方", + "help": "" + } + }, "Daily": { "_info": { "name": "每日任务", @@ -2420,6 +2452,24 @@ "22": "22 | NA海域西南B" } }, + "OpsiStorage": { + "_info": { + "name": "OpsiStorage._info.name", + "help": "OpsiStorage._info.help" + }, + "YellowCoin": { + "name": "大世界黄币", + "help": "" + }, + "PurpleCoin": { + "name": "大世界紫币", + "help": "" + }, + "ActionPoint": { + "name": "行动力", + "help": "" + } + }, "Daemon": { "_info": { "name": "半自动点击", @@ -2610,6 +2660,15 @@ "Clear": "清除", "EnterPassword": "输入密码", "ChooseFile": "选择文件" + }, + "Dashboard": { + "NoData": "无数据", + "TimeError": "时间错误", + "JustNow": "刚刚", + "MinutesAgo": "{time}分钟前", + "HoursAgo": "{time}小时前", + "DaysAgo": "{time}天前", + "LongTimeAgo": "很久以前" } } } \ No newline at end of file diff --git a/module/config/i18n/zh-TW.json b/module/config/i18n/zh-TW.json index 58de47dfc9..f6cd2eb33d 100644 --- a/module/config/i18n/zh-TW.json +++ b/module/config/i18n/zh-TW.json @@ -1166,6 +1166,28 @@ "help": "在7x24運行時防止緊急委託數量過多做不完每日委託,建議在委託過濾器僅選擇短時長高收益委託" } }, + "CampaignStorage": { + "_info": { + "name": "CampaignStorage._info.name", + "help": "CampaignStorage._info.help" + }, + "Oil": { + "name": "石油", + "help": "" + }, + "Coin": { + "name": "物資", + "help": "" + }, + "Gem": { + "name": "鑽石", + "help": "" + }, + "Pt": { + "name": "活動PT", + "help": "" + } + }, "EventGeneral": { "_info": { "name": "活動通用設定", @@ -1967,6 +1989,16 @@ "help": "" } }, + "GachaStorage": { + "_info": { + "name": "GachaStorage._info.name", + "help": "GachaStorage._info.help" + }, + "Cube": { + "name": "魔方", + "help": "" + } + }, "Daily": { "_info": { "name": "每日任務", @@ -2420,6 +2452,24 @@ "22": "22 | NA海域西南B" } }, + "OpsiStorage": { + "_info": { + "name": "OpsiStorage._info.name", + "help": "OpsiStorage._info.help" + }, + "YellowCoin": { + "name": "大世界黃幣", + "help": "" + }, + "PurpleCoin": { + "name": "大世界紫幣", + "help": "" + }, + "ActionPoint": { + "name": "行動力", + "help": "" + } + }, "Daemon": { "_info": { "name": "半自動點擊", @@ -2610,6 +2660,15 @@ "Clear": "清除", "EnterPassword": "輸入密碼", "ChooseFile": "選擇檔案" + }, + "Dashboard": { + "NoData": "無數據", + "TimeError": "時間錯誤", + "JustNow": "剛剛", + "MinutesAgo": "{time}分鐘前", + "HoursAgo": "{time}小時前", + "DaysAgo": "{time}天前", + "LongTimeAgo": "很久以前" } } } \ No newline at end of file diff --git a/module/config/stored/classes.py b/module/config/stored/classes.py new file mode 100644 index 0000000000..5c942660c5 --- /dev/null +++ b/module/config/stored/classes.py @@ -0,0 +1,179 @@ +import re +from datetime import datetime + +from module.base.decorator import cached_property +from module.config.utils import DEFAULT_TIME, deep_get + + +def now(): + return datetime.now().replace(microsecond=0) + + +def iter_attribute(cls): + """ + Args: + cls: Class or object + + Yields: + str, obj: Attribute name, attribute value + """ + for attr in dir(cls): + if attr.startswith('_'): + continue + value = getattr(cls, attr) + if type(value).__name__ in ['function', 'property']: + continue + yield attr, value + + +class StoredBase: + time = DEFAULT_TIME + + def __init__(self, key): + self._key = key + self._config = None + + @cached_property + def _name(self): + return self._key.split('.')[-1] + + def _bind(self, config): + """ + Args: + config (AzurLaneConfig): + """ + self._config = config + + @cached_property + def _stored(self): + assert self._config is not None, 'StoredBase._bind() must be called before getting stored data' + from module.logger import logger + + out = {} + stored = deep_get(self._config.data, keys=self._key, default={}) + for attr, default in self._attrs.items(): + value = stored.get(attr, default) + if attr == 'time': + if not isinstance(value, datetime): + try: + value = datetime.fromisoformat(value) + except ValueError: + logger.warning(f'{self._name} has invalid attr: {attr}={value}, use default={default}') + value = default + else: + if not isinstance(value, type(default)): + logger.warning(f'{self._name} has invalid attr: {attr}={value}, use default={default}') + value = default + + out[attr] = value + return out + + @cached_property + def _attrs(self) -> dict: + """ + All attributes defined + """ + attrs = { + # time is the first one + 'time': DEFAULT_TIME + } + for attr, value in iter_attribute(self.__class__): + if attr.islower(): + attrs[attr] = value + return attrs + + def __setattr__(self, key, value): + if key in self._attrs: + stored = self._stored + stored['time'] = now() + stored[key] = value + self._config.modified[self._key] = stored + if self._config.auto_update: + self._config.update() + else: + super().__setattr__(key, value) + + def __getattribute__(self, item): + if not item.startswith('_') and item in self._attrs: + return self._stored[item] + else: + return super().__getattribute__(item) + + def show(self): + """ + Log self + """ + from module.logger import logger + logger.attr(self._name, self._stored) + + +class StoredInt(StoredBase): + value = 0 + + +class StoredStr(StoredBase): + value = '' + + +class StoredCounter(StoredBase): + value = 0 + total = 0 + + FIXED_TOTAL = 0 + + def set(self, value, total=0): + if self.FIXED_TOTAL: + total = self.FIXED_TOTAL + with self._config.multi_set(): + self.value = value + self.total = total + + def to_counter(self) -> str: + return f'{self.value}/{self.total}' + + def is_full(self) -> bool: + return self.value >= self.total + + def get_remain(self) -> int: + return self.total - self.value + + def add(self, value=1): + self.value += value + + @cached_property + def _attrs(self) -> dict: + attrs = super()._attrs + if self.FIXED_TOTAL: + attrs['total'] = self.FIXED_TOTAL + return attrs + + @cached_property + def _stored(self): + stored = super()._stored + if self.FIXED_TOTAL: + stored['total'] = self.FIXED_TOTAL + return stored + + +class StoredOil(StoredCounter): + pass + + +class StoredCoin(StoredCounter): + pass + + +class StoredActionPoint(StoredStr): + _current = 0 + _total = 0 + + def __setattr__(self, key, value): + if key == 'value' and value: + res = re.search(r'(\d+) \((\d+)\)', value) + if res: + self._current = int(res.group(1)) + self._total = int(res.group(2)) + super().__setattr__(key, value) + + def set(self, current, total): + self.value = f'{current} ({total})' diff --git a/module/config/stored/stored_generated.py b/module/config/stored/stored_generated.py new file mode 100644 index 0000000000..fecde6d5ce --- /dev/null +++ b/module/config/stored/stored_generated.py @@ -0,0 +1,23 @@ +from module.config.stored.classes import ( + StoredActionPoint, + StoredBase, + StoredCoin, + StoredCounter, + StoredInt, + StoredOil, + StoredStr, +) + + +# This file was auto-generated, do not modify it manually. To generate: +# ``` python -m module/config/config_updater.py ``` + +class StoredGenerated: + Oil = StoredOil("EventGeneral.CampaignStorage.Oil") + Coin = StoredCoin("EventGeneral.CampaignStorage.Coin") + Gem = StoredInt("EventGeneral.CampaignStorage.Gem") + Pt = StoredInt("EventGeneral.CampaignStorage.Pt") + Cube = StoredInt("Gacha.GachaStorage.Cube") + YellowCoin = StoredInt("OpsiGeneral.OpsiStorage.YellowCoin") + PurpleCoin = StoredInt("OpsiGeneral.OpsiStorage.PurpleCoin") + ActionPoint = StoredActionPoint("OpsiGeneral.OpsiStorage.ActionPoint") diff --git a/module/config/utils.py b/module/config/utils.py index 60e07c4cc1..916fdf2d81 100644 --- a/module/config/utils.py +++ b/module/config/utils.py @@ -352,10 +352,12 @@ def data_to_type(data, **kwargs): str: """ kwargs.update(data) - if isinstance(kwargs['value'], bool): + if isinstance(kwargs.get('value'), bool): return 'checkbox' elif 'option' in kwargs and kwargs['option']: return 'select' + elif 'stored' in kwargs and kwargs['stored']: + return 'stored' elif 'Filter' in kwargs['arg']: return 'textarea' else: diff --git a/module/gacha/gacha_reward.py b/module/gacha/gacha_reward.py index 0460cc9514..d14ca07ddb 100644 --- a/module/gacha/gacha_reward.py +++ b/module/gacha/gacha_reward.py @@ -124,6 +124,7 @@ def gacha_calculate(self, target_count, gold_cost, cube_cost): logger.info(f'Able to submit up to {target_count} build orders') self._currency -= gold_total self.build_cube_count -= cube_total + self.config.stored.Cube.value = self.build_cube_count return target_count def gacha_goto_pool(self, target_pool): diff --git a/module/os_handler/action_point.py b/module/os_handler/action_point.py index 87d0e9c217..f3ff5d78f1 100644 --- a/module/os_handler/action_point.py +++ b/module/os_handler/action_point.py @@ -139,6 +139,9 @@ def action_point_update(self): oil = box[0] logger.info(f'Action points: {current}({total}), oil: {oil}') + with self.config.multi_set(): + self.config.stored.Oil.value = oil + self.config.stored.ActionPoint.set(current, total) self._action_point_current = current self._action_point_box = box self._action_point_total = total diff --git a/module/os_handler/os_status.py b/module/os_handler/os_status.py index c472733ee8..55acef60f9 100644 --- a/module/os_handler/os_status.py +++ b/module/os_handler/os_status.py @@ -77,13 +77,16 @@ def get_yellow_coins(self, skip_first_screenshot=True) -> int: else: break + self.config.stored.YellowCoin.value = yellow_coins return yellow_coins def get_purple_coins(self) -> int: if self.appear(OS_SHOP_CHECK): - return OCR_OS_SHOP_PURPLE_COINS.ocr(self.device.image) + purple_coins = OCR_OS_SHOP_PURPLE_COINS.ocr(self.device.image) else: - return OCR_SHOP_PURPLE_COINS.ocr(self.device.image) + purple_coins = OCR_SHOP_PURPLE_COINS.ocr(self.device.image) + self.config.stored.PurpleCoin.value = purple_coins + return purple_coins def os_shop_get_coins(self): self._shop_yellow_coins = self.get_yellow_coins() diff --git a/module/raid/raid.py b/module/raid/raid.py index bf9c18944d..c388a32104 100644 --- a/module/raid/raid.py +++ b/module/raid/raid.py @@ -222,8 +222,9 @@ def check_coin(): if self.appear(BATTLE_PREPARATION, offset=(30, 20)): if self.handle_combat_automation_set(auto=auto == 'combat_auto'): continue - check_oil() - check_coin() + with self.config.multi_set(): + check_oil() + check_coin() if self.handle_raid_ticket_use(): continue if self.handle_retirement(): diff --git a/module/shop/shop_general.py b/module/shop/shop_general.py index 04375290f2..86a5b11d36 100644 --- a/module/shop/shop_general.py +++ b/module/shop/shop_general.py @@ -7,9 +7,6 @@ from module.shop.shop_status import ShopStatus from module.shop.ui import ShopUI -OCR_SHOP_GOLD_COINS = Digit(SHOP_GOLD_COINS, letter=(239, 239, 239), name='OCR_SHOP_GOLD_COINS') -OCR_SHOP_GEMS = Digit(SHOP_GEMS, letter=(255, 243, 82), name='OCR_SHOP_GEMS') - class GeneralShop(ShopClerk, ShopUI, ShopStatus): gems = 0 diff --git a/module/shop/shop_status.py b/module/shop/shop_status.py index 026c54b741..d1df2769de 100644 --- a/module/shop/shop_status.py +++ b/module/shop/shop_status.py @@ -32,6 +32,7 @@ def status_get_gems(self): in: page_shop, medal shop """ amount = OCR_SHOP_GEMS.ocr(self.device.image) + self.config.stored.Gem.value = amount return amount def status_get_medal(self): diff --git a/module/webui/app.py b/module/webui/app.py index abb82a610e..6ffa8b84c6 100644 --- a/module/webui/app.py +++ b/module/webui/app.py @@ -100,11 +100,13 @@ 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: @@ -339,6 +341,35 @@ def set_navigator(self, group): 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") @@ -395,20 +426,31 @@ def alas_overview(self) -> None: 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", - [ + if self.alas_mod == 'alas': + 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() if deep_get(self.ALAS_STORED, keys=[arg, "order"], default=0) + # Empty content to left-align last row + ] + [put_html("")] * min(len(self.ALAS_STORED), 4)) + ]) + else: + 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"), + ]), + ]) + ]).style("height: auto;") put_scope("log", [put_html("")]) log.console.width = log.get_width() @@ -519,6 +561,7 @@ def alas_update_overview_task(self) -> None: 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] @@ -546,27 +589,42 @@ def put_task(func: Function): 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_then_add("pending_task", [ + alive, + 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--") + + for arg, arg_dict in self.ALAS_STORED.items(): + # Skip order=0 + if not arg_dict.get("order", 0): + continue + path = arg_dict["path"] + if self.scope_expired_then_add(f"dashboard-time-value-{arg}", [ + deep_get(self.alas_config.data, keys=f"{path}.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: @@ -581,7 +639,7 @@ def alas_daemon_overview(self, task: str) -> None: [ put_scope("scheduler-bar"), put_scope("groups"), - put_scope("log-bar"), + put_scope("daemon-log-bar"), put_scope("log", [put_html("")]), ], ) @@ -595,7 +653,7 @@ def alas_daemon_overview(self, task: str) -> None: [ put_scope( "_daemon_upper", - [put_scope("scheduler-bar"), put_scope("log-bar")], + [put_scope("scheduler-bar"), put_scope("daemon-log-bar")], ), put_scope("groups"), put_scope("log", [put_html("")]), @@ -624,16 +682,17 @@ def alas_daemon_overview(self, task: str) -> None: scope="scheduler_btn", ) - with use_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"), - ], - ) + with use_scope("daemon-log-bar"): + with use_scope("log-title"): + 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"), + ], + ) switch_log_scroll = BinarySwitchButton( label_on=t("Gui.Button.ScrollON"), diff --git a/module/webui/base.py b/module/webui/base.py index 0f43cbebab..29619551ba 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 @@ def __init__(self) -> None: 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 @@ def init_aside(self, expand_menu: bool = True, name: str = None) -> None: 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 @@ def init_menu(self, collapse_menu: bool = True, name: str = None) -> None: """ 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 0a5cd39991..4eda22dcd0 100644 --- a/module/webui/lang.py +++ b/module/webui/lang.py @@ -1,3 +1,5 @@ +import time + from typing import Dict from module.config.utils import * @@ -67,3 +69,36 @@ 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 < 5400: + # < 90 min + return t("Gui.Dashboard.MinutesAgo", time=int(diff // 60)) + elif diff < 129600: + # < 36 hours + return t("Gui.Dashboard.HoursAgo", time=int(diff // 3600)) + elif diff < 1296000: + # < 15 days + return t("Gui.Dashboard.DaysAgo", time=int(diff // 86400)) + else: + # >= 15 days + return t("Gui.Dashboard.LongTimeAgo")