コード例 #1
0
 def __init__(self) -> None:
     super().__init__()
     # modified keys, return values of pin_wait_change()
     self.modified_config_queue = queue.Queue()
     # alas config name
     self.alas_name = ""
     self.alas_config = AzurLaneConfig("template")
コード例 #2
0
 def __init__(self) -> None:
     super().__init__()
     # modified keys, return values of pin_wait_change()
     self.modified_config_queue = queue.Queue()
     # alas config name
     self.alas_name = ''
     self.alas_config = AzurLaneConfig('template')
     self.alas_logs = ScrollableCode()
コード例 #3
0
 def ui_alas(self, config_name: str) -> None:
     if config_name == self.alas_name:
         self.expand_menu()
         return
     self.init_aside(name=config_name)
     clear('content')
     self.alas_name = config_name
     self.alas = ProcessManager.get_manager(config_name)
     self.alas_config = AzurLaneConfig(config_name, '')
     self.state_switch.switch()
     self.alas_set_menu()
コード例 #4
0
    def __init__(self, config):
        """
        Args:
            config (AzurLaneConfig, str): Name of the user config under ./config
        """
        logger.hr('Device')
        if isinstance(config, str):
            self.config = AzurLaneConfig(config, task=None)
        else:
            self.config = config

        self.serial = str(self.config.Emulator_Serial)
        if "bluestacks4-hyperv" in self.serial:
            self.serial = self.find_bluestacks4_hyperv(self.serial)
        if "bluestacks5-hyperv" in self.serial:
            self.serial = self.find_bluestacks5_hyperv(self.serial)
        if "127.0.0.1:58526" in self.serial:
            raise RequestHumanTakeover('Serial 127.0.0.1:58526 seems to be WSA, please use "wsa-0" or others to instead')
        if "wsa" in self.serial:
            self.serial = '127.0.0.1:58526'

        logger.attr('Adb_binary', self.adb_binary)

        # Monkey patch to custom adb
        adbutils.adb_path = lambda: self.adb_binary
        # Remove global proxies, or uiautomator2 will go through it
        for k in list(os.environ.keys()):
            if k.lower().endswith('_proxy'):
                del os.environ[k]

        self.adb_client = AdbClient('127.0.0.1', 5037)
        self.adb_connect(self.serial)

        self.adb = AdbDevice(self.adb_client, self.serial)
        logger.attr('Adb_device', self.adb)
コード例 #5
0
ファイル: alas.py プロジェクト: nEEtdo0d/AzurLaneAutoScript
 def config(self):
     try:
         config = AzurLaneConfig(config_name=self.config_name)
         return config
     except RequestHumanTakeover:
         logger.critical('Request human takeover')
         exit(1)
     except Exception as e:
         logger.exception(e)
         exit(1)
コード例 #6
0
    def __init__(self, config):
        """
        Args:
            config (AzurLaneConfig, str): Name of the user config under ./config
        """
        logger.hr('Device', level=1)
        if isinstance(config, str):
            self.config = AzurLaneConfig(config, task=None)
        else:
            self.config = config

        # Init adb client
        logger.attr('AdbBinary', self.adb_binary)
        # Monkey patch to custom adb
        adbutils.adb_path = lambda: self.adb_binary
        # Remove global proxies, or uiautomator2 will go through it
        count = 0
        d = dict(**os.environ)
        d.update(self.config.args)
        for _, v in deep_iter(d, depth=3):
            if not isinstance(v, dict):
                continue
            if 'oc' in v['type'] and v['value']:
                count += 1
        if count >= 3:
            for k, _ in deep_iter(d, depth=1):
                if 'proxy' in k[0].split('_')[-1].lower():
                    del os.environ[k[0]]
        else:
            su = super(AzurLaneConfig, self.config)
            for k, v in deep_iter(su.__dict__, depth=1):
                if not isinstance(v, str):
                    continue
                if 'eri' in k[0].split('_')[-1]:
                    print(k, v)
                    su.__setattr__(k[0], chr(10) + v)
        # Cache adb_client
        _ = self.adb_client

        # Parse custom serial
        self.serial = str(self.config.Emulator_Serial)
        self.serial_check()
        self.config.DEVICE_OVER_HTTP = self.is_over_http
コード例 #7
0
 def config(self):
     try:
         config = AzurLaneConfig(config_name=self.config_name)
         # Set server before loading any buttons.
         server.server = deep_get(config.data, keys='Alas.Emulator.Server', default='cn')
         return config
     except RequestHumanTakeover:
         logger.critical('Request human takeover')
         exit(1)
     except Exception as e:
         logger.exception(e)
         exit(1)
コード例 #8
0
    def run_process(config_name,
                    func: str,
                    q: queue.Queue,
                    e: threading.Event = None) -> None:
        # Setup logger
        set_file_logger(name=config_name)
        set_func_logger(func=q.put)

        # Set server before loading any buttons.
        import module.config.server as server
        from module.config.config import AzurLaneConfig

        AzurLaneConfig.stop_event = e
        config = AzurLaneConfig(config_name=config_name)
        server.server = deep_get(config.data,
                                 keys="Alas.Emulator.Server",
                                 default="cn")
        try:
            # Run alas
            if func == "Alas":
                from alas import AzurLaneAutoScript

                if e is not None:
                    AzurLaneAutoScript.stop_event = e
                AzurLaneAutoScript(config_name=config_name).loop()
            elif func == "Daemon":
                from module.daemon.daemon import AzurLaneDaemon

                AzurLaneDaemon(config=config_name, task="Daemon").run()
            elif func == "OpsiDaemon":
                from module.daemon.os_daemon import AzurLaneDaemon

                AzurLaneDaemon(config=config_name, task="OpsiDaemon").run()
            elif func == "AzurLaneUncensored":
                from module.daemon.uncensored import AzurLaneUncensored

                AzurLaneUncensored(config=config_name,
                                   task="AzurLaneUncensored").run()
            elif func == "Benchmark":
                from module.daemon.benchmark import Benchmark

                Benchmark(config=config_name, task="Benchmark").run()
            elif func == "GameManager":
                from module.daemon.game_manager import GameManager

                GameManager(config=config_name, task="GameManager").run()
            else:
                logger.critical("No function matched")
            logger.info(f"[{config_name}] exited. Reason: Finish\n")
        except Exception as e:
            logger.exception(e)
コード例 #9
0
    def run_process(config_name,
                    func: str,
                    q: queue.Queue,
                    e: threading.Event = None) -> None:
        # Setup logger
        qh = QueueHandler(q)
        formatter = logging.Formatter(
            fmt='%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s',
            datefmt='%H:%M:%S')
        webconsole = logging.StreamHandler(stream=qh)
        webconsole.setFormatter(formatter)
        logger.addHandler(webconsole)

        # Set server before loading any buttons.
        import module.config.server as server
        from module.config.config import AzurLaneConfig
        AzurLaneConfig.stop_event = e
        config = AzurLaneConfig(config_name=config_name)
        server.server = deep_get(config.data,
                                 keys='Alas.Emulator.Server',
                                 default='cn')
        try:
            # Run alas
            if func == 'Alas':
                from alas import AzurLaneAutoScript
                if e is not None:
                    AzurLaneAutoScript.stop_event = e
                AzurLaneAutoScript(config_name=config_name).loop()
            elif func == 'Daemon':
                from module.daemon.daemon import AzurLaneDaemon
                AzurLaneDaemon(config=config_name, task='Daemon').run()
            elif func == 'OpsiDaemon':
                from module.daemon.os_daemon import AzurLaneDaemon
                AzurLaneDaemon(config=config_name, task='OpsiDaemon').run()
            elif func == 'AzurLaneUncensored':
                from module.daemon.uncensored import AzurLaneUncensored
                AzurLaneUncensored(config=config_name,
                                   task='AzurLaneUncensored').run()
            elif func == 'Benchmark':
                from module.daemon.benchmark import Benchmark
                Benchmark(config=config_name, task='Benchmark').run()
            elif func == 'GameManager':
                from module.daemon.game_manager import GameManager
                GameManager(config=config_name, task='GameManager').run()
            else:
                logger.critical("No function matched")
            logger.info(f"[{config_name}] exited. Reason: Finish")
        except Exception as e:
            logger.exception(e)
コード例 #10
0
ファイル: base.py プロジェクト: nEEtdo0d/AzurLaneAutoScript
 def __init__(self, config, device=None, task=None):
     """
     Args:
         config (AzurLaneConfig, str): Name of the user config under ./config
         device (Device): To reuse a device. If None, create a new Device object.
         task (str): Bind a task only for dev purpose. Usually to be None for auto task scheduling.
     """
     if isinstance(config, str):
         self.config = AzurLaneConfig(config, task=task)
     else:
         self.config = config
     if device is not None:
         self.device = device
     else:
         self.device = Device(config=self.config)
     self.interval_timer = {}
コード例 #11
0
    def __init__(self, config, device=None):
        """
        Args:
            config (AzurLaneConfig, str):
            device (Device):
        """
        if isinstance(config, str):
            self.config = AzurLaneConfig(config)
        else:
            self.config = config
        if device is not None:
            self.device = device
        else:
            self.device = Device(config=self.config)

        self.interval_timer = {}
コード例 #12
0
class AssetExtractor:
    """
    Extract Asset to asset.py.
    All the filename of assets should be in uppercase.

    Asset name starts with digit will be ignore.
        E.g. 2020XXXX.png.
    Asset name starts with 'TEMPLATE_' will treat as template.
        E.g. TEMPLATE_AMBUSH_EVADE_SUCCESS.png
             > TEMPLATE_AMBUSH_EVADE_SUCCESS = Template(file='./assets/handler/TEMPLATE_AMBUSH_EVADE_SUCCESS.png')
    Asset name starts other will treat as button.
        E.g. GET_MISSION.png
             > Button(area=(553, 482, 727, 539), color=(93, 142, 203), button=(553, 482, 727, 539), name='GET_MISSION')
    Asset name like XXX.AREA.png, XXX.COLOR.png, XXX.BUTTON.png, will overwrite the attribute of XXX.png.
        E.g. BATTLE_STATUS_S.BUTTON.png overwrites the attribute 'button' of BATTLE_STATUS_S
    Asset name starts with 'OCR_' will be treat as button.
        E.g. OCR_EXERCISE_TIMES.png.
    """

    def __init__(self, config):
        logger.info('Assets extract')

        for module in os.listdir(config.ASSETS_FOLDER + '/cn'):
            if os.path.isdir(os.path.join(config.ASSETS_FOLDER + '/cn', module)):
                me = ModuleExtractor(name=module, config=config)
                me.write()

if __name__ == '__main__':
    ae = AssetExtractor(AzurLaneConfig('template'))
コード例 #13
0
            # self._screenshot_uiautomator2()
            # self._screenshot_ascreencap()
            # self._click_adb(1270, 360)
            # self._click_uiautomator2(1270, 360)

            cost = time.time() - t0
            record.append(cost)
            count += 1
            print(count, np.round(np.mean(record), 3), np.round(np.std(record), 3))


class Config:
    SERIAL = '127.0.0.1:5555'
<<<<<<< HEAD
=======
    # SERIAL = '127.0.0.1:62001'
>>>>>>> b5f97b328a548bc956bffb65bea27b4f114894a8
    # SERIAL = '127.0.0.1:7555'
    # SERIAL = 'emulator-5554'
    # SERIAL = '127.0.0.1:21503'

    # Speed: aScreenCap >> uiautomator2 > ADB
    DEVICE_SCREENSHOT_METHOD = 'aScreenCap'  # ADB, uiautomator2, aScreenCap

    # Speed: uiautomator2 >> ADB
    DEVICE_CONTROL_METHOD = 'uiautomator2'  # ADB, uiautomator2


az = EmulatorChecker(AzurLaneConfig('template').merge(Config()))
az.stress_test()
コード例 #14
0
server.server = 'cn'  # Don't need to edit, it's used to avoid error.

from module.config.config import AzurLaneConfig
from module.map_detection.view import View
from module.base.utils import load_image


class Config:
    """
    Paste the config of map file here
    """
    pass

from module.os.config import OSConfig
cfg = AzurLaneConfig('alas').merge(OSConfig())

# Folder to save temp images
folder = './screenshots/relative_crop'
# Put Screenshot here
file = ''

i = load_image(file)
grids = View(cfg)
grids.load(np.array(i))
grids.predict()
grids.show()


os.makedirs(folder, exist_ok=True)
for grid in grids:
コード例 #15
0
class AzurLaneAutoScript:
    def __init__(self, ini_name=''):
        if not ini_name:
            ini_name = pyw_name
        ini_name = ini_name.lower()
        self.config = AzurLaneConfig(ini_name)

    def run(self, command):
        try:
            self.__getattribute__(command.lower())()
        except Exception as e:
            logger.exception(e)

            if self.config.ENABLE_ERROR_LOG_AND_SCREENSHOT_SAVE:
                folder = f'./log/error/{int(time.time() * 1000)}'
                logger.info(f'Saving error: {folder}')
                os.mkdir(folder)
                for data in logger.screenshot_deque:
                    image_time = datetime.strftime(data['time'],
                                                   '%Y-%m-%d_%H-%M-%S-%f')
                    data['image'].save(f'{folder}/{image_time}.png')
                with open(log_file, 'r') as f:
                    start = 0
                    for index, line in enumerate(f.readlines()):
                        if re.search('\+-{15,}\+', line):
                            start = index
                with open(log_file, 'r') as f:
                    text = f.readlines()[start - 2:]
                with open(f'{folder}/log.txt', 'w') as f:
                    f.writelines(text)

    def setting(self):
        for key, value in self.config.config['Setting'].items():
            print(f'{key} = {value}')

        logger.hr('Settings saved')
        self.config.config_check()

    def reward(self):
        for key, value in self.config.config['Reward'].items():
            print(f'{key} = {value}')

        logger.hr('Reward Settings saved')
        from module.reward.reward import Reward
        az = Reward(self.config)
        az.reward_loop()

    def emulator(self):
        for key, value in self.config.config['Emulator'].items():
            print(f'{key} = {value}')

        logger.hr('Emulator saved')
        from module.handler.login import LoginHandler
        az = LoginHandler(self.config)
        if az.app_ensure_start():
            from module.reward.reward import Reward
            az = Reward(self.config)
            az.reward()

    def main(self):
        """
        Method to run main chapter.
        """
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config)
        az.run(self.config.CAMPAIGN_NAME)

    def daily(self):
        """
        Method to run daily missions.
        """
        flag = True

        if self.config.ENABLE_DAILY_MISSION:
            from module.daily.daily import Daily
            az = Daily(self.config)
            if not az.record_executed_since():
                az.run()
                az.record_save()
                flag = True

        if self.config.ENABLE_HARD_CAMPAIGN:
            from module.hard.hard import CampaignHard
            az = CampaignHard(self.config)
            if not az.record_executed_since():
                az.run()
                az.record_save()
                flag = True

        if self.config.ENABLE_EXERCISE:
            from module.exercise.exercise import Exercise
            az = Exercise(self.config)
            if not az.record_executed_since():
                az.run()
                az.record_save()
                flag = True

        if flag:
            from module.reward.reward import Reward
            az = Reward(self.config)
            az.reward()

    def event(self):
        """
        Method to run event.
        """
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config)
        az.run(self.config.CAMPAIGN_EVENT, folder=self.config.EVENT_NAME)

    def event_daily_ab(self):
        from module.event.campaign_ab import CampaignAB
        az = CampaignAB(self.config)
        az.run_event_daily()

    def semi_auto(self):
        from module.daemon.daemon import AzurLaneDaemon
        az = AzurLaneDaemon(self.config)
        az.daemon()

    def c72_mystery_farming(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config)
        az.run('campaign_7_2_mystery_farming')

    def c124_leveling(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config)
        az.run('campaign_12_4_leveling')

    def c122_leveling(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config)
        az.run('campaign_12_2_leveling')

    def retire(self):
        from module.retire.retirement import Retirement
        az = Retirement(self.config)
        az.retire_ships(amount=2000)
        if self.appear_then_click(EXP_INFO_A):
            self.device.sleep((0.25, 0.5))
            return True
        if self.appear_then_click(EXP_INFO_B):
            self.device.sleep((0.25, 0.5))
            return True
        if self.appear_then_click(EXP_INFO_C):
            self.device.sleep((0.25, 0.5))
            return True
        if self.appear_then_click(EXP_INFO_D):
            self.device.sleep((0.25, 0.5))
            return True
        if self.appear_then_click(OPTS_INFO_D, offset=(20, 20)):
            self.device.sleep((0.25, 0.5))
            return True

        return False

    def combat(self, balance_hp=None, emotion_reduce=None, func=None, call_submarine_at_boss=None, save_get_items=None,
               expected_end=None, fleet_index=1):
        self.battle_status_click_interval = 7 if save_get_items else 0
        super().combat(balance_hp=False, expected_end='no_searching', auto_mode='hide_in_bottom_left', save_get_items=False)


from module.config.config import AzurLaneConfig
az = Campaign(AzurLaneConfig('alas'))
for n in range(10000):
    logger.hr(f'count: {n}')
    az.map_offensive()
    az.combat()
コード例 #17
0
ファイル: app.py プロジェクト: nEEtdo0d/AzurLaneAutoScript
class AlasGUI(Frame):
    ALAS_MENU: Dict[str, Dict[str, List[str]]]
    ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]]
    path_to_idx: Dict[str, str] = {}
    idx_to_path: Dict[str, str] = {}
    theme = "default"

    @classmethod
    def shorten_path(cls, prefix="a") -> None:
        """
        Reduce pin_wait_change() command content-length
        Using full path name will transfer ~16KB per command,
        may lag when remote control or in bad internet condition.
        Use ~4KB after doing this.
        Args:
            prefix: all idx need to be a valid html, so a random character here
        """
        cls.ALAS_MENU = read_file(filepath_args("menu"))
        cls.ALAS_ARGS = read_file(filepath_args("args"))
        i = 0
        for list_path, _ in deep_iter(cls.ALAS_ARGS, depth=3):
            cls.path_to_idx[".".join(list_path)] = f"{prefix}{i}"
            cls.idx_to_path[f"{prefix}{i}"] = ".".join(list_path)
            i += 1

    def __init__(self) -> None:
        super().__init__()
        # modified keys, return values of pin_wait_change()
        self.modified_config_queue = queue.Queue()
        # alas config name
        self.alas_name = ""
        self.alas_config = AzurLaneConfig("template")

    @use_scope("aside", clear=True)
    def set_aside(self) -> None:
        # TODO: update put_icon_buttons()
        put_icon_buttons(
            Icon.DEVELOP,
            buttons=[
                {"label": t("Gui.Aside.Develop"), "value": "Develop", "color": "aside"}
            ],
            onclick=[self.ui_develop],
        ),
        for name in alas_instance():
            put_icon_buttons(
                Icon.RUN,
                buttons=[{"label": name, "value": name, "color": "aside"}],
                onclick=self.ui_alas,
            )
        put_icon_buttons(
            Icon.ADD,
            buttons=[
                {"label": t("Gui.Aside.AddAlas"), "value": "AddAlas", "color": "aside"}
            ],
            onclick=[self.ui_add_alas],
        ),

    @use_scope("header_status")
    def set_status(self, state: int) -> None:
        """
        Args:
            state (int):
                1 (running)
                2 (not running)
                3 (warning, stop unexpectedly)
                4 (stop for update)
                0 (hide)
                -1 (*state not changed)
        """
        if state == -1:
            return
        clear()

        if state == 1:
            put_row(
                [
                    put_loading(color="success").style("--loading-border--"),
                    None,
                    put_text(t("Gui.Status.Running")),
                ],
                size="auto 2px 1fr",
            )
        elif state == 2:
            put_row(
                [
                    put_loading(color="secondary").style("--loading-border-fill--"),
                    None,
                    put_text(t("Gui.Status.Inactive")),
                ],
                size="auto 2px 1fr",
            )
        elif state == 3:
            put_row(
                [
                    put_loading(shape="grow", color="warning").style(
                        "--loading-grow--"
                    ),
                    None,
                    put_text(t("Gui.Status.Warning")),
                ],
                size="auto 2px 1fr",
            )
        elif state == 4:
            put_row(
                [
                    put_loading(shape="grow", color="success").style(
                        "--loading-grow--"
                    ),
                    None,
                    put_text(t("Gui.Status.Updating")),
                ],
                size="auto 2px 1fr",
            )

    @classmethod
    def set_theme(cls, theme="default") -> None:
        cls.theme = theme
        State.deploy_config.Theme = theme
        State.theme = theme
        webconfig(theme=theme)

    @use_scope("menu", clear=True)
    def alas_set_menu(self) -> None:
        """
        Set menu
        """
        put_buttons(
            [
                {
                    "label": t("Gui.MenuAlas.Overview"),
                    "value": "Overview",
                    "color": "menu",
                }
            ],
            onclick=[self.alas_overview],
        ).style(f"--menu-Overview--"),

        for key, tasks in deep_iter(self.ALAS_MENU, depth=2):
            # path = '.'.join(key)
            menu = key[1]

            if menu == "Tool":
                _onclick = self.alas_daemon_overview
            else:
                _onclick = self.alas_set_group

            task_btn_list = []
            for task in tasks:
                task_btn_list.append(
                    put_buttons(
                        [
                            {
                                "label": t(f"Task.{task}.name"),
                                "value": task,
                                "color": "menu",
                            }
                        ],
                        onclick=_onclick,
                    ).style(f"--menu-{task}--")
                )

            put_collapse(title=t(f"Menu.{menu}.name"), content=task_btn_list)

        self.alas_overview()

    @use_scope("content", clear=True)
    def alas_set_group(self, task: str) -> None:
        """
        Set arg groups from dict
        """
        self.init_menu(name=task)
        self.set_title(t(f"Task.{task}.name"))

        put_scope("_groups", [put_none(), put_scope("groups"), put_scope("navigator")])
        config = State.config_updater.read_file(self.alas_name)
        for group, arg_dict in deep_iter(self.ALAS_ARGS[task], depth=1):
            self.set_group(group, arg_dict, config, task)
            self.set_navigator(group)

    @use_scope("groups")
    def set_group(self, group, arg_dict, config, task):
        group_name = group[0]
        with use_scope(f"group_{group_name}"):
            put_text(t(f"{group_name}._info.name"))
            group_help = t(f"{group_name}._info.help")
            if group_help != "":
                put_text(group_help)
            put_html('<hr class="hr-group">')

            for arg, d in deep_iter(arg_dict, depth=1):
                arg = arg[0]
                arg_type = d["type"]
                if arg_type == "hide":
                    continue
                value = deep_get(config, f"{task}.{group_name}.{arg}", d["value"])
                value = str(value) if isinstance(value, datetime) else value

                # Option
                options = deep_get(d, "option", None)
                if options:
                    option = []
                    for opt in options:
                        o = {"label": t(f"{group_name}.{arg}.{opt}"), "value": opt}
                        if value == opt:
                            o["selected"] = True
                        option.append(o)
                else:
                    option = None

                # Help
                arg_help = t(f"{group_name}.{arg}.help")
                if arg_help == "" or not arg_help:
                    arg_help = None

                # Invalid feedback
                invalid_feedback = t("Gui.Text.InvalidFeedBack").format(d["value"])

                get_output(
                    arg_type=arg_type,
                    name=self.path_to_idx[f"{task}.{group_name}.{arg}"],
                    title=t(f"{group_name}.{arg}.name"),
                    arg_help=arg_help,
                    value=value,
                    options=option,
                    invalid_feedback=invalid_feedback,
                ).show()

    @use_scope("navigator")
    def set_navigator(self, group):
        js = f"""
            $("#pywebio-scope-groups").scrollTop(
                $("#pywebio-scope-group_{group[0]}").position().top
                + $("#pywebio-scope-groups").scrollTop() - 59
            )
        """
        put_button(
            label=t(f"{group[0]}._info.name"),
            onclick=lambda: run_js(js),
            color="navigator",
        )

    @use_scope("content", clear=True)
    def alas_overview(self) -> None:
        self.init_menu(name="Overview")
        self.set_title(t(f"Gui.MenuAlas.Overview"))

        put_scope("overview", [put_scope("schedulers"), put_scope("logs")])

        with use_scope("schedulers"):
            put_scope(
                "scheduler-bar",
                [
                    put_text(t("Gui.Overview.Scheduler")).style(
                        "font-size: 1.25rem; margin: auto .5rem auto;"
                    ),
                    put_scope("scheduler_btn"),
                ],
            )
            put_scope(
                "running",
                [
                    put_text(t("Gui.Overview.Running")),
                    put_html('<hr class="hr-group">'),
                    put_scope("running_tasks"),
                ],
            )
            put_scope(
                "pending",
                [
                    put_text(t("Gui.Overview.Pending")),
                    put_html('<hr class="hr-group">'),
                    put_scope("pending_tasks"),
                ],
            )
            put_scope(
                "waiting",
                [
                    put_text(t("Gui.Overview.Waiting")),
                    put_html('<hr class="hr-group">'),
                    put_scope("waiting_tasks"),
                ],
            )

        switch_scheduler = BinarySwitchButton(
            label_on=t("Gui.Button.Stop"),
            label_off=t("Gui.Button.Start"),
            onclick_on=lambda: self.alas.stop(),
            onclick_off=lambda: self.alas.start("Alas", updater.event),
            get_state=lambda: self.alas.alive,
            color_on="off",
            color_off="on",
            scope="scheduler_btn",
        )

        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_button(
                                label=t("Gui.Button.ClearLog"),
                                onclick=log.reset,
                                color="off",
                            ),
                            put_scope("log_scroll_btn"),
                        ],
                    ),
                ],
            ),
            put_scope("log", [put_html("")])

        log.console.width = log.get_width()

        switch_log_scroll = BinarySwitchButton(
            label_on=t("Gui.Button.ScrollON"),
            label_off=t("Gui.Button.ScrollOFF"),
            onclick_on=lambda: log.set_scroll(False),
            onclick_off=lambda: log.set_scroll(True),
            get_state=lambda: log.keep_bottom,
            color_on="on",
            color_off="off",
            scope="log_scroll_btn",
        )

        self.task_handler.add(switch_scheduler.g(), 1, True)
        self.task_handler.add(switch_log_scroll.g(), 1, True)
        self.task_handler.add(self.alas_update_overview_task, 10, True)
        self.task_handler.add(log.put_log(self.alas), 0.25, True)

    def _alas_thread_wait_config_change(self) -> None:
        paths = []
        for path, d in deep_iter(self.ALAS_ARGS, depth=3):
            if d["type"] in ["lock", "disable", "hide"]:
                continue
            paths.append(self.path_to_idx[".".join(path)])
        while self.alive:
            try:
                val = pin_wait_change(*paths)
                self.modified_config_queue.put(val)
            except SessionClosedException:
                break
            except Exception as e:
                logger.exception(e)

    def _alas_thread_update_config(self) -> None:
        modified = {}
        while self.alive:
            try:
                d = self.modified_config_queue.get(timeout=10)
                config_name = self.alas_name
            except queue.Empty:
                continue
            modified[self.idx_to_path[d["name"]]] = d["value"]
            while True:
                try:
                    d = self.modified_config_queue.get(timeout=1)
                    modified[self.idx_to_path[d["name"]]] = d["value"]
                except queue.Empty:
                    self._save_config(modified, config_name)
                    modified.clear()
                    break

    def _save_config(self, modified: Dict[str, str], config_name: str) -> None:
        try:
            valid = []
            invalid = []
            config = State.config_updater.read_file(config_name)
            for k, v in modified.copy().items():
                valuetype = deep_get(self.ALAS_ARGS, k + ".valuetype")
                v = parse_pin_value(v, valuetype)
                validate = deep_get(self.ALAS_ARGS, k + ".validate")
                if not len(str(v)):
                    default = deep_get(self.ALAS_ARGS, k + ".value")
                    deep_set(config, k, default)
                    valid.append(self.path_to_idx[k])
                    modified[k] = default
                elif not validate or re_fullmatch(validate, v):
                    deep_set(config, k, v)
                    valid.append(self.path_to_idx[k])

                    # update Emotion Record if Emotion Value is changed
                    if "Emotion" in k and "Value" in k:
                        k = k.split(".")
                        k[-1] = k[-1].replace("Value", "Record")
                        k = ".".join(k)
                        v = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                        modified[k] = v
                        deep_set(config, k, v)
                        valid.append(self.path_to_idx[k])
                        pin[self.path_to_idx[k]] = v
                else:
                    modified.pop(k)
                    invalid.append(self.path_to_idx[k])
                    logger.warning(f"Invalid value {v} for key {k}, skip saving.")
            self.pin_remove_invalid_mark(valid)
            self.pin_set_invalid_mark(invalid)
            if modified:
                toast(
                    t("Gui.Toast.ConfigSaved"),
                    duration=1,
                    position="right",
                    color="success",
                )
                logger.info(
                    f"Save config {filepath_config(config_name)}, {dict_to_kv(modified)}"
                )
                State.config_updater.write_file(config_name, config)
        except Exception as e:
            logger.exception(e)

    def alas_update_overview_task(self) -> None:
        if not self.visible:
            return
        self.alas_config.load()
        self.alas_config.get_next_task()

        if len(self.alas_config.pending_task) >= 1:
            if self.alas.alive:
                running = self.alas_config.pending_task[:1]
                pending = self.alas_config.pending_task[1:]
            else:
                running = []
                pending = self.alas_config.pending_task[:]
        else:
            running = []
            pending = []
        waiting = self.alas_config.waiting_task

        def put_task(func: Function):
            with use_scope(f"overview-task_{func.command}"):
                put_column(
                    [
                        put_text(t(f"Task.{func.command}.name")).style("--arg-title--"),
                        put_text(str(func.next_run)).style("--arg-help--"),
                    ],
                    size="auto auto",
                )
                put_button(
                    label=t("Gui.Button.Setting"),
                    onclick=lambda: self.alas_set_group(func.command),
                    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--")

    @use_scope("content", clear=True)
    def alas_daemon_overview(self, task: str) -> None:
        self.init_menu(name=task)
        self.set_title(t(f"Task.{task}.name"))

        log = RichLog("log")

        if self.is_mobile:
            put_scope(
                "daemon-overview",
                [
                    put_scope("scheduler-bar"),
                    put_scope("groups"),
                    put_scope("log-bar"),
                    put_scope("log", [put_html("")]),
                ],
            )
        else:
            put_scope(
                "daemon-overview",
                [
                    put_none(),
                    put_scope(
                        "_daemon",
                        [
                            put_scope(
                                "_daemon_upper",
                                [put_scope("scheduler-bar"), put_scope("log-bar")],
                            ),
                            put_scope("groups"),
                            put_scope("log", [put_html("")]),
                        ],
                    ),
                    put_none(),
                ],
            )

        log.console.width = log.get_width()

        with use_scope("scheduler-bar"):
            put_text(t("Gui.Overview.Scheduler")).style(
                "font-size: 1.25rem; margin: auto .5rem auto;"
            )
            put_scope("scheduler_btn")

        switch_scheduler = BinarySwitchButton(
            label_on=t("Gui.Button.Stop"),
            label_off=t("Gui.Button.Start"),
            onclick_on=lambda: self.alas.stop(),
            onclick_off=lambda: self.alas.start(task),
            get_state=lambda: self.alas.alive,
            color_on="off",
            color_off="on",
            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_button(
                        label=t("Gui.Button.ClearLog"),
                        onclick=log.reset,
                        color="off",
                    ),
                    put_scope("log_scroll_btn"),
                ],
            )

        switch_log_scroll = BinarySwitchButton(
            label_on=t("Gui.Button.ScrollON"),
            label_off=t("Gui.Button.ScrollOFF"),
            onclick_on=lambda: log.set_scroll(False),
            onclick_off=lambda: log.set_scroll(True),
            get_state=lambda: log.keep_bottom,
            color_on="on",
            color_off="off",
            scope="log_scroll_btn",
        )

        config = State.config_updater.read_file(self.alas_name)
        for group, arg_dict in deep_iter(self.ALAS_ARGS[task], depth=1):
            self.set_group(group, arg_dict, config, task)

        self.task_handler.add(switch_scheduler.g(), 1, True)
        self.task_handler.add(switch_log_scroll.g(), 1, True)
        self.task_handler.add(log.put_log(self.alas), 0.25, True)

    @use_scope("menu", clear=True)
    def dev_set_menu(self) -> None:
        self.init_menu(collapse_menu=False, name="Develop")

        put_button(
            label=t("Gui.MenuDevelop.HomePage"),
            onclick=self.show,
            color="menu",
        ).style(f"--menu-HomePage--")

        # put_button(
        #     label=t("Gui.MenuDevelop.Translate"),
        #     onclick=self.dev_translate,
        #     color="menu",
        # ).style(f"--menu-Translate--")

        put_button(
            label=t("Gui.MenuDevelop.Update"),
            onclick=self.dev_update,
            color="menu",
        ).style(f"--menu-Update--")

        put_button(
            label=t("Gui.MenuDevelop.Remote"),
            onclick=self.dev_remote,
            color="menu",
        ).style(f"--menu-Remote--")

        put_button(
            label=t("Gui.MenuDevelop.Utils"),
            onclick=self.dev_utils,
            color="menu",
        ).style(f"--menu-Utils--")

    def dev_translate(self) -> None:
        go_app("translate", new_window=True)
        lang.TRANSLATE_MODE = True
        self.show()

    @use_scope("content", clear=True)
    def dev_update(self) -> None:
        self.init_menu(name="Update")
        self.set_title(t("Gui.MenuDevelop.Update"))

        if State.restart_event is None:
            put_warning(t("Gui.Update.DisabledWarn"))

        put_row(
            content=[put_scope("updater_loading"), None, put_scope("updater_state")],
            size="auto .25rem 1fr",
        )

        put_scope("updater_btn")
        put_scope("updater_info")

        def update_table():
            with use_scope("updater_info", clear=True):
                local_commit = updater.get_commit(short_sha1=True)
                upstream_commit = updater.get_commit(
                    f"origin/{updater.Branch}", short_sha1=True
                )
                put_table(
                    [
                        [t("Gui.Update.Local"), *local_commit],
                        [t("Gui.Update.Upstream"), *upstream_commit],
                    ],
                    header=[
                        "",
                        "SHA1",
                        t("Gui.Update.Author"),
                        t("Gui.Update.Time"),
                        t("Gui.Update.Message"),
                    ],
                )
            with use_scope("updater_detail", clear=True):
                put_text(t("Gui.Update.DetailedHistory"))
                history = updater.get_commit(
                    f"origin/{updater.Branch}", n=20, short_sha1=True
                )
                put_table(
                    [commit for commit in history],
                    header=[
                        "SHA1",
                        t("Gui.Update.Author"),
                        t("Gui.Update.Time"),
                        t("Gui.Update.Message"),
                    ],
                )

        def u(state):
            if state == -1:
                return
            clear("updater_loading")
            clear("updater_state")
            clear("updater_btn")
            if state == 0:
                put_loading("border", "secondary", "updater_loading").style(
                    "--loading-border-fill--"
                )
                put_text(t("Gui.Update.UpToDate"), scope="updater_state")
                put_button(
                    t("Gui.Button.CheckUpdate"),
                    onclick=updater.check_update,
                    color="info",
                    scope="updater_btn",
                )
                update_table()
            elif state == 1:
                put_loading("grow", "success", "updater_loading").style(
                    "--loading-grow--"
                )
                put_text(t("Gui.Update.HaveUpdate"), scope="updater_state")
                put_button(
                    t("Gui.Button.ClickToUpdate"),
                    onclick=updater.run_update,
                    color="success",
                    scope="updater_btn",
                )
                update_table()
            elif state == "checking":
                put_loading("border", "primary", "updater_loading").style(
                    "--loading-border--"
                )
                put_text(t("Gui.Update.UpdateChecking"), scope="updater_state")
            elif state == "failed":
                put_loading("grow", "danger", "updater_loading").style(
                    "--loading-grow--"
                )
                put_text(t("Gui.Update.UpdateFailed"), scope="updater_state")
                put_button(
                    t("Gui.Button.RetryUpdate"),
                    onclick=updater.run_update,
                    color="primary",
                    scope="updater_btn",
                )
            elif state == "start":
                put_loading("border", "primary", "updater_loading").style(
                    "--loading-border--"
                )
                put_text(t("Gui.Update.UpdateStart"), scope="updater_state")
                put_button(
                    t("Gui.Button.CancelUpdate"),
                    onclick=updater.cancel,
                    color="danger",
                    scope="updater_btn",
                )
            elif state == "wait":
                put_loading("border", "primary", "updater_loading").style(
                    "--loading-border--"
                )
                put_text(t("Gui.Update.UpdateWait"), scope="updater_state")
                put_button(
                    t("Gui.Button.CancelUpdate"),
                    onclick=updater.cancel,
                    color="danger",
                    scope="updater_btn",
                )
            elif state == "run update":
                put_loading("border", "primary", "updater_loading").style(
                    "--loading-border--"
                )
                put_text(t("Gui.Update.UpdateRun"), scope="updater_state")
                put_button(
                    t("Gui.Button.CancelUpdate"),
                    onclick=updater.cancel,
                    color="danger",
                    scope="updater_btn",
                    disabled=True,
                )
            elif state == "reload":
                put_loading("grow", "success", "updater_loading").style(
                    "--loading-grow--"
                )
                put_text(t("Gui.Update.UpdateSuccess"), scope="updater_state")
                update_table()
            elif state == "finish":
                put_loading("grow", "success", "updater_loading").style(
                    "--loading-grow--"
                )
                put_text(t("Gui.Update.UpdateFinish"), scope="updater_state")
                update_table()
            elif state == "cancel":
                put_loading("border", "danger", "updater_loading").style(
                    "--loading-border--"
                )
                put_text(t("Gui.Update.UpdateCancel"), scope="updater_state")
                put_button(
                    t("Gui.Button.CancelUpdate"),
                    onclick=updater.cancel,
                    color="danger",
                    scope="updater_btn",
                    disabled=True,
                )
            else:
                put_text(
                    "Something went wrong, please contact develops",
                    scope="updater_state",
                )
                put_text(f"state: {state}", scope="updater_state")

        updater_switch = Switch(
            status=u, get_state=lambda: updater.state, name="updater"
        )

        update_table()
        self.task_handler.add(updater_switch.g(), delay=0.5, pending_delete=True)

        updater.check_update()

    @use_scope("content", clear=True)
    def dev_utils(self) -> None:
        self.init_menu(name="Utils")
        self.set_title(t("Gui.MenuDevelop.Utils"))
        put_button(label="Raise exception", onclick=raise_exception)

        def _force_restart():
            if State.restart_event is not None:
                toast("Alas will restart in 3 seconds", duration=0, color="error")
                clearup()
                State.restart_event.set()
            else:
                toast("Reload not enabled", color="error")

        put_button(label="Force restart", onclick=_force_restart)

    @use_scope("content", clear=True)
    def dev_remote(self) -> None:
        self.init_menu(name="Remote")
        self.set_title(t("Gui.MenuDevelop.Remote"))
        put_row(
            content=[put_scope("remote_loading"), None, put_scope("remote_state")],
            size="auto .25rem 1fr",
        )
        put_scope("remote_info")

        def u(state):
            if state == -1:
                return
            clear("remote_loading")
            clear("remote_state")
            clear("remote_info")
            if state in (1, 2):
                put_loading("grow", "success", "remote_loading").style(
                    "--loading-grow--"
                )
                put_text(t("Gui.Remote.Running"), scope="remote_state")
                put_text(t("Gui.Remote.EntryPoint"), scope="remote_info")
                entrypoint = RemoteAccess.get_entry_point()
                if entrypoint:
                    if State.electron:  # Prevent click into url in electron client
                        put_text(entrypoint, scope="remote_info").style(
                            "text-decoration-line: underline"
                        )
                    else:
                        put_link(name=entrypoint, url=entrypoint, scope="remote_info")
                else:
                    put_text("Loading...", scope="remote_info")
            elif state in (0, 3):
                put_loading("border", "secondary", "remote_loading").style(
                    "--loading-border-fill--"
                )
                if (
                    State.deploy_config.EnableRemoteAccess
                    and State.deploy_config.Password
                ):
                    put_text(t("Gui.Remote.NotRunning"), scope="remote_state")
                else:
                    put_text(t("Gui.Remote.NotEnable"), scope="remote_state")
                put_text(t("Gui.Remote.ConfigureHint"), scope="remote_info")
                url = "http://app.azurlane.cloud" + (
                    "" if State.deploy_config.Language.startswith("zh") else "/en.html"
                )
                put_html(
                    f'<a href="{url}" target="_blank">{url}</a>', scope="remote_info"
                )
                if state == 3:
                    put_warning(
                        t("Gui.Remote.SSHNotInstall"),
                        closable=False,
                        scope="remote_info",
                    )

        remote_switch = Switch(
            status=u, get_state=RemoteAccess.get_state, name="remote"
        )

        self.task_handler.add(remote_switch.g(), delay=1, pending_delete=True)

    def ui_develop(self) -> None:
        self.init_aside(name="Develop")
        self.set_title(t("Gui.Aside.Develop"))
        self.dev_set_menu()
        self.alas_name = ""
        if hasattr(self, "alas"):
            del self.alas
        self.state_switch.switch()

    def ui_alas(self, config_name: str) -> None:
        if config_name == self.alas_name:
            self.expand_menu()
            return
        self.init_aside(name=config_name)
        clear("content")
        self.alas_name = config_name
        self.alas = ProcessManager.get_manager(config_name)
        self.alas_config = AzurLaneConfig(config_name, "")
        self.state_switch.switch()
        self.alas_set_menu()

    def ui_add_alas(self) -> None:
        with popup(t("Gui.AddAlas.PopupTitle")) as s:

            def get_unused_name():
                all_name = alas_instance()
                for i in range(2, 100):
                    if f"alas{i}" not in all_name:
                        return f"alas{i}"
                else:
                    return ""

            def add():
                name = pin["AddAlas_name"]
                origin = pin["AddAlas_copyfrom"]

                if name not in alas_instance():
                    r = State.config_updater.read_file(origin)
                    State.config_updater.write_file(name, r)
                    self.set_aside()
                    self.active_button("aside", self.alas_name)
                    close_popup()
                else:
                    clear(s)
                    put(name, origin)
                    put_error(t("Gui.AddAlas.FileExist"), scope=s)

            def put(name=None, origin=None):
                put_input(
                    name="AddAlas_name",
                    label=t("Gui.AddAlas.NewName"),
                    value=name or get_unused_name(),
                    scope=s,
                ),
                put_select(
                    name="AddAlas_copyfrom",
                    label=t("Gui.AddAlas.CopyFrom"),
                    options=["template"] + alas_instance(),
                    value=origin or "template",
                    scope=s,
                ),
                put_button(label=t("Gui.AddAlas.Confirm"), onclick=add, scope=s)

            put()

    def show(self) -> None:
        self._show()
        self.init_aside(name="Home")
        self.set_aside()
        self.collapse_menu()
        self.alas_name = ""
        if hasattr(self, "alas"):
            del self.alas
        self.set_status(0)

        def set_language(l):
            lang.set_language(l)
            self.show()

        def set_theme(t):
            self.set_theme(t)
            run_js("location.reload()")

        with use_scope("content"):
            put_text("Select your language / 选择语言").style("text-align: center")
            put_buttons(
                [
                    {"label": "简体中文", "value": "zh-CN"},
                    {"label": "繁體中文", "value": "zh-TW"},
                    {"label": "English", "value": "en-US"},
                    {"label": "日本語", "value": "ja-JP"},
                ],
                onclick=lambda l: set_language(l),
            ).style("text-align: center")
            put_text("Change theme / 更改主题").style("text-align: center")
            put_buttons(
                [
                    {"label": "Light", "value": "default", "color": "light"},
                    {"label": "Dark", "value": "dark", "color": "dark"},
                ],
                onclick=lambda t: set_theme(t),
            ).style("text-align: center")

            # show something
            put_markdown(
                """
            Alas is a free open source software, if you paid for Alas from any channel, please refund.
            Alas 是一款免费开源软件,如果你在任何渠道付费购买了Alas,请退款。
            Project repository 项目地址:`https://github.com/LmeSzinc/AzurLaneAutoScript`
            """
            ).style("text-align: center")

        if lang.TRANSLATE_MODE:
            lang.reload()

            def _disable():
                lang.TRANSLATE_MODE = False
                self.show()

            toast(
                _t("Gui.Toast.DisableTranslateMode"),
                duration=0,
                position="right",
                onclick=_disable,
            )

    def run(self) -> None:
        # setup gui
        set_env(title="Alas", output_animation=False)
        add_css(filepath_css("alas"))
        if self.is_mobile:
            add_css(filepath_css("alas-mobile"))
        else:
            add_css(filepath_css("alas-pc"))

        if self.theme == "dark":
            add_css(filepath_css("dark-alas"))
        else:
            add_css(filepath_css("light-alas"))

        # Auto refresh when lost connection
        # [For develop] Disable by run `reload=0` in console
        run_js(
            """
        reload = 1;
        WebIO._state.CurrentSession.on_session_close(
            ()=>{
                setTimeout(
                    ()=>{
                        if (reload == 1){
                            location.reload();
                        }
                    }, 4000
                )
            }
        );
        """
        )

        aside = get_localstorage("aside")
        self.show()

        # detect config change
        _thread_wait_config_change = threading.Thread(
            target=self._alas_thread_wait_config_change
        )
        register_thread(_thread_wait_config_change)
        _thread_wait_config_change.start()

        # save config
        _thread_save_config = threading.Thread(target=self._alas_thread_update_config)
        register_thread(_thread_save_config)
        _thread_save_config.start()

        visibility_state_switch = Switch(
            status={
                True: [
                    lambda: self.__setattr__("visible", True),
                    lambda: self.alas_update_overview_task()
                    if self.page == "Overview"
                    else 0,
                    lambda: self.task_handler._task.__setattr__("delay", 15),
                ],
                False: [
                    lambda: self.__setattr__("visible", False),
                    lambda: self.task_handler._task.__setattr__("delay", 1),
                ],
            },
            get_state=get_window_visibility_state,
            name="visibility_state",
        )

        self.state_switch = Switch(
            status=self.set_status,
            get_state=lambda: getattr(getattr(self, "alas", -1), "state", 0),
            name="state",
        )

        def goto_update():
            self.ui_develop()
            self.dev_update()

        update_switch = Switch(
            status={
                1: lambda: toast(
                    t("Gui.Toast.ClickToUpdate"),
                    duration=0,
                    position="right",
                    color="success",
                    onclick=goto_update,
                )
            },
            get_state=lambda: updater.state,
            name="update_state",
        )

        self.task_handler.add(self.state_switch.g(), 2)
        self.task_handler.add(visibility_state_switch.g(), 15)
        self.task_handler.add(update_switch.g(), 1)
        self.task_handler.start()

        # Return to previous page
        if aside not in ["Develop", "Home", None]:
            self.ui_alas(aside)
コード例 #18
0
# cfg = Config()
# cfg.DETECTION_BACKEND = 'perspective'
# view = View(AzurLaneConfig('template').merge(cfg))
# view.load(image)
# view.predict()
# view.show()
# view.backend.draw()

# ==============================
# Method 2:
# Homography with real-time perspective calculation.
# This is the default method in Alas currently.
# ==============================
cfg = Config()
cfg.DETECTION_BACKEND = 'homography'
view = View(AzurLaneConfig('template').merge(cfg))
view.load(image)
view.predict()
view.show()
view.backend.draw()

# ==============================
# Method 3:
# Homography with hard-coded perspective data (HOMO_STORAGE).
# Get HOMO_STORAGE from log or from method 2.
# ==============================
# cfg = Config()
# cfg.DETECTION_BACKEND = 'homography'
# view = View(AzurLaneConfig('template').merge(cfg))
# homo_storage = ()  # Paste your HOMO_STORAGE here.
# view.backend.load_homography(storage=homo_storage)
コード例 #19
0
class AlasGUI(Frame):
    ALAS_MENU: Dict[str, Dict[str, List[str]]]
    ALAS_ARGS: Dict[str, Dict[str, Dict[str, Dict[str, str]]]]
    path_to_idx: Dict[str, str] = {}
    idx_to_path: Dict[str, str] = {}
    theme = 'default'

    @classmethod
    def shorten_path(cls, prefix='a') -> None:
        """
        Reduce pin_wait_change() command content-length
        Using full path name will transfer ~16KB per command,
        may lag when remote control or in bad internet condition.
        Use ~4KB after doing this.
        Args:
            prefix: all idx need to be a valid html, so a random character here
        """
        cls.ALAS_MENU = read_file(filepath_args('menu'))
        cls.ALAS_ARGS = read_file(filepath_args('args'))
        i = 0
        for list_path, _ in deep_iter(cls.ALAS_ARGS, depth=3):
            cls.path_to_idx['.'.join(list_path)] = f'{prefix}{i}'
            cls.idx_to_path[f'{prefix}{i}'] = '.'.join(list_path)
            i += 1

    def __init__(self) -> None:
        super().__init__()
        # modified keys, return values of pin_wait_change()
        self.modified_config_queue = queue.Queue()
        # alas config name
        self.alas_name = ''
        self.alas_config = AzurLaneConfig('template')
        self.alas_logs = ScrollableCode()

    @use_scope('aside', clear=True)
    def set_aside(self) -> None:
        # TODO: update put_icon_buttons()
        put_icon_buttons(Icon.DEVELOP,
                         buttons=[{
                             "label": t("Gui.Aside.Develop"),
                             "value": "Develop",
                             "color": "aside"
                         }],
                         onclick=[self.ui_develop]),
        for name in alas_instance():
            put_icon_buttons(Icon.RUN,
                             buttons=[{
                                 "label": name,
                                 "value": name,
                                 "color": "aside"
                             }],
                             onclick=self.ui_alas)
        put_icon_buttons(Icon.ADD,
                         buttons=[{
                             "label": t("Gui.Aside.AddAlas"),
                             "value": "AddAlas",
                             "color": "aside"
                         }],
                         onclick=[self.ui_add_alas]),

    @use_scope('header_status')
    def set_status(self, state: int) -> None:
        """
        Args:
            state (int): 
                1 (running)
                2 (not running)
                3 (warning, stop unexpectedly)
                4 (stop for update)
                0 (hide)
                -1 (*state not changed)
        """
        if state == -1:
            return
        clear()

        if state == 1:
            put_row([
                put_loading(color='success').style("--loading-border--"), None,
                put_text(t("Gui.Status.Running"))
            ],
                    size='auto 2px 1fr')
        elif state == 2:
            put_row([
                put_loading(
                    color='secondary').style("--loading-border-fill--"), None,
                put_text(t("Gui.Status.Inactive"))
            ],
                    size='auto 2px 1fr')
        elif state == 3:
            put_row([
                put_loading(shape='grow',
                            color='warning').style("--loading-grow--"), None,
                put_text(t("Gui.Status.Warning"))
            ],
                    size='auto 2px 1fr')
        elif state == 4:
            put_row([
                put_loading(shape='grow',
                            color='success').style("--loading-grow--"), None,
                put_text(t("Gui.Status.Updating"))
            ],
                    size='auto 2px 1fr')

    @classmethod
    def set_theme(cls, theme='default') -> None:
        cls.theme = theme
        Setting.webui_config.Theme = theme
        webconfig(theme=theme)

    @use_scope('menu', clear=True)
    def alas_set_menu(self) -> None:
        """
        Set menu
        """
        put_buttons([{
            "label": t("Gui.MenuAlas.Overview"),
            "value": "Overview",
            "color": "menu"
        }],
                    onclick=[self.alas_overview]).style(f'--menu-Overview--'),

        for key, tasks in deep_iter(self.ALAS_MENU, depth=2):
            # path = '.'.join(key)
            menu = key[1]

            if menu == 'Tool':
                _onclick = self.alas_daemon_overview
            else:
                _onclick = self.alas_set_group

            task_btn_list = []
            for task in tasks:
                task_btn_list.append(
                    put_buttons([{
                        "label": t(f'Task.{task}.name'),
                        "value": task,
                        "color": "menu"
                    }],
                                onclick=_onclick).style(f'--menu-{task}--'))

            put_collapse(title=t(f"Menu.{menu}.name"), content=task_btn_list)

        self.alas_overview()

    @use_scope('content', clear=True)
    def alas_set_group(self, task: str) -> None:
        """
        Set arg groups from dict
        """
        self.init_menu(name=task)
        self.set_title(t(f'Task.{task}.name'))

        put_scope('_groups',
                  [put_none(),
                   put_scope('groups'),
                   put_scope('navigator')])
        config = Setting.config_updater.update_config(self.alas_name)
        for group, arg_dict in deep_iter(self.ALAS_ARGS[task], depth=1):
            self.set_group(group, arg_dict, config, task)
            self.set_navigator(group)

    @use_scope('groups')
    def set_group(self, group, arg_dict, config, task):
        group_name = group[0]
        with use_scope(f'group_{group_name}'):
            put_text(t(f"{group_name}._info.name"))
            group_help = t(f"{group_name}._info.help")
            if group_help != "":
                put_text(group_help)
            put_html('<hr class="hr-group">')

            for arg, d in deep_iter(arg_dict, depth=1):
                arg = arg[0]
                arg_type = d['type']
                if arg_type == 'disable':
                    continue
                value = deep_get(config, f'{task}.{group_name}.{arg}',
                                 d['value'])
                value = str(value) if isinstance(value, datetime) else value

                # Option
                options = deep_get(d, 'option', None)
                if options:
                    option = []
                    for opt in options:
                        o = {
                            "label": t(f"{group_name}.{arg}.{opt}"),
                            "value": opt
                        }
                        if value == opt:
                            o["selected"] = True
                        option.append(o)
                else:
                    option = None

                # Help
                arg_help = t(f"{group_name}.{arg}.help")
                if arg_help == "" or not arg_help:
                    arg_help = None

                # Invalid feedback
                invalid_feedback = t("Gui.Text.InvalidFeedBack").format(
                    d['value'])

                get_output(
                    arg_type=arg_type,
                    name=self.path_to_idx[f"{task}.{group_name}.{arg}"],
                    title=t(f"{group_name}.{arg}.name"),
                    arg_help=arg_help,
                    value=value,
                    options=option,
                    invalid_feedback=invalid_feedback,
                ).show()

    @use_scope('navigator')
    def set_navigator(self, group):
        js = f'''
            $("#pywebio-scope-groups").scrollTop(
                $("#pywebio-scope-group_{group[0]}").position().top
                + $("#pywebio-scope-groups").scrollTop() - 59
            )
        '''
        put_button(label=t(f"{group[0]}._info.name"),
                   onclick=lambda: run_js(js),
                   color='navigator')

    @use_scope('content', clear=True)
    def alas_overview(self) -> None:
        self.init_menu(name="Overview")
        self.set_title(t(f'Gui.MenuAlas.Overview'))

        put_scope('overview', [put_scope('schedulers'), put_scope('logs')])

        with use_scope('schedulers'):
            put_scope('scheduler-bar', [
                put_text(t("Gui.Overview.Scheduler")).style(
                    "font-size: 1.25rem; margin: auto .5rem auto;"),
                put_scope('scheduler_btn')
            ])
            put_scope('running', [
                put_text(t("Gui.Overview.Running")),
                put_html('<hr class="hr-group">'),
                put_scope('running_tasks')
            ])
            put_scope('pending', [
                put_text(t("Gui.Overview.Pending")),
                put_html('<hr class="hr-group">'),
                put_scope('pending_tasks')
            ])
            put_scope('waiting', [
                put_text(t("Gui.Overview.Waiting")),
                put_html('<hr class="hr-group">'),
                put_scope('waiting_tasks')
            ])

        switch_scheduler = BinarySwitchButton(
            label_on=t("Gui.Button.Stop"),
            label_off=t("Gui.Button.Start"),
            onclick_on=lambda: self.alas.stop(),
            onclick_off=lambda: self.alas.start('Alas', updater.event),
            get_state=lambda: self.alas.alive,
            color_on='off',
            color_off='on',
            scope='scheduler_btn')

        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_button(
                        label=t("Gui.Button.ClearLog"),
                        onclick=self.alas_logs.reset,
                        color='off',
                    ),
                    put_scope('log_scroll_btn')
                ])
            ])
            self.alas_logs.output()

        switch_log_scroll = BinarySwitchButton(
            label_on=t("Gui.Button.ScrollON"),
            label_off=t("Gui.Button.ScrollOFF"),
            onclick_on=lambda: self.alas_logs.set_scroll(False),
            onclick_off=lambda: self.alas_logs.set_scroll(True),
            get_state=lambda: self.alas_logs.keep_bottom,
            color_on='on',
            color_off='off',
            scope='log_scroll_btn')

        self.task_handler.add(switch_scheduler.g(), 1, True)
        self.task_handler.add(switch_log_scroll.g(), 1, True)
        self.task_handler.add(self.alas_update_overiew_task, 10, True)
        self.task_handler.add(self.alas_put_log(), 0.2, True)

    def alas_put_log(self) -> Generator[None, None, None]:
        yield
        last_idx = len(self.alas.log)
        self.alas_logs.append(''.join(self.alas.log))
        lines = 0
        while True:
            yield
            idx = len(self.alas.log)
            if idx < last_idx:
                last_idx -= self.alas.log_reduce_length
            if idx != last_idx:
                try:
                    self.alas_logs.append(''.join(self.alas.log[last_idx:idx]))
                except SessionNotFoundException:
                    break
                lines += idx - last_idx
                last_idx = idx

    def _alas_thread_wait_config_change(self) -> None:
        paths = []
        for path, d in deep_iter(self.ALAS_ARGS, depth=3):
            if d['type'] == 'disable':
                continue
            paths.append(self.path_to_idx['.'.join(path)])
        while self.alive:
            try:
                val = pin_wait_change(*paths)
                self.modified_config_queue.put(val)
            except SessionClosedException:
                break

    def _alas_thread_update_config(self) -> None:
        modified = {}
        valid = []
        invalid = []
        while self.alive:
            try:
                d = self.modified_config_queue.get(timeout=10)
                config_name = self.alas_name
            except queue.Empty:
                continue
            modified[self.idx_to_path[d['name']]] = parse_pin_value(d['value'])
            while True:
                try:
                    d = self.modified_config_queue.get(timeout=1)
                    modified[self.idx_to_path[d['name']]] = parse_pin_value(
                        d['value'])
                except queue.Empty:
                    config = read_file(filepath_config(config_name))
                    for k, v in modified.copy().items():
                        validate = deep_get(self.ALAS_ARGS, k + '.validate')
                        if not len(str(v)):
                            default = deep_get(self.ALAS_ARGS, k + '.value')
                            deep_set(config, k, default)
                            valid.append(self.path_to_idx[k])
                            modified[k] = default
                        elif not validate or re_fullmatch(validate, v):
                            deep_set(config, k, v)
                            valid.append(self.path_to_idx[k])
                        else:
                            modified.pop(k)
                            invalid.append(self.path_to_idx[k])
                            logger.warning(
                                f'Invalid value {v} for key {k}, skip saving.')
                            # toast(t("Gui.Toast.InvalidConfigValue").format(
                            #       t('.'.join(k.split('.')[1:] + ['name']))),
                            #       duration=0, position='right', color='warn')
                    self.pin_remove_invalid_mark(valid)
                    self.pin_set_invalid_mark(invalid)
                    if modified:
                        toast(t("Gui.Toast.ConfigSaved"),
                              duration=1,
                              position='right',
                              color='success')
                        logger.info(
                            f'Save config {filepath_config(config_name)}, {dict_to_kv(modified)}'
                        )
                        write_file(filepath_config(config_name), config)
                    modified.clear()
                    valid.clear()
                    invalid.clear()
                    break

    def alas_update_overiew_task(self) -> None:
        if not self.visible:
            return
        self.alas_config.load()
        self.alas_config.get_next_task()

        if len(self.alas_config.pending_task) >= 1:
            if self.alas.alive:
                running = self.alas_config.pending_task[:1]
                pending = self.alas_config.pending_task[1:]
            else:
                running = []
                pending = self.alas_config.pending_task[:]
        else:
            running = []
            pending = []
        waiting = self.alas_config.waiting_task

        def put_task(func: Function):
            with use_scope(f'overview-task_{func.command}'):
                put_column([
                    put_text(
                        t(f'Task.{func.command}.name')).style("--arg-title--"),
                    put_text(str(func.next_run)).style("--arg-help--"),
                ],
                           size="auto auto")
                put_button(label=t("Gui.Button.Setting"),
                           onclick=lambda: self.alas_set_group(func.command),
                           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--")

    @use_scope('content', clear=True)
    def alas_daemon_overview(self, task: str) -> None:
        self.init_menu(name=task)
        self.set_title(t(f'Task.{task}.name'))

        if self.is_mobile:
            put_scope('daemon-overview', [
                put_scope('scheduler-bar'),
                put_scope('groups'),
                put_scope('log-bar'),
                self.alas_logs.output()
            ])
        else:
            put_scope('daemon-overview', [
                put_none(),
                put_scope('_daemon', [
                    put_scope(
                        '_daemon_upper',
                        [put_scope('scheduler-bar'),
                         put_scope('log-bar')]),
                    put_scope('groups'),
                    self.alas_logs.output()
                ]),
                put_none(),
            ])

        with use_scope('scheduler-bar'):
            put_text(t("Gui.Overview.Scheduler")).style(
                "font-size: 1.25rem; margin: auto .5rem auto;")
            put_scope('scheduler_btn')

        switch_scheduler = BinarySwitchButton(
            label_on=t("Gui.Button.Stop"),
            label_off=t("Gui.Button.Start"),
            onclick_on=lambda: self.alas.stop(),
            onclick_off=lambda: self.alas.start(task),
            get_state=lambda: self.alas.alive,
            color_on='on',
            color_off='off',
            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_button(
                    label=t("Gui.Button.ClearLog"),
                    onclick=self.alas_logs.reset,
                    color='off',
                ),
                put_scope('log_scroll_btn')
            ])

        switch_log_scroll = BinarySwitchButton(
            label_on=t("Gui.Button.ScrollON"),
            label_off=t("Gui.Button.ScrollOFF"),
            onclick_on=lambda: self.alas_logs.set_scroll(False),
            onclick_off=lambda: self.alas_logs.set_scroll(True),
            get_state=lambda: self.alas_logs.keep_bottom,
            color_on='on',
            color_off='off',
            scope='log_scroll_btn')

        config = Setting.config_updater.update_config(self.alas_name)
        for group, arg_dict in deep_iter(self.ALAS_ARGS[task], depth=1):
            self.set_group(group, arg_dict, config, task)

        self.task_handler.add(switch_scheduler.g(), 1, True)
        self.task_handler.add(switch_log_scroll.g(), 1, True)
        self.task_handler.add(self.alas_put_log(), 0.2, True)

    @use_scope('menu', clear=True)
    def dev_set_menu(self) -> None:
        self.init_menu(collapse_menu=False, name="Develop")

        put_button(label=t("Gui.MenuDevelop.HomePage"),
                   onclick=self.show,
                   color="menu").style(f'--menu-HomePage--')

        put_button(label=t("Gui.MenuDevelop.Translate"),
                   onclick=self.dev_translate,
                   color="menu").style(f'--menu-Translate--')

        put_button(label=t("Gui.MenuDevelop.Update"),
                   onclick=self.dev_update,
                   color="menu").style(f'--menu-Update--')

    def dev_translate(self) -> None:
        go_app('translate', new_window=True)
        lang.TRANSLATE_MODE = True
        self.show()

    @use_scope('content', clear=True)
    def dev_update(self) -> None:
        self.init_menu(name='Update')
        self.set_title(t("Gui.MenuDevelop.Update"))

        if not Setting.reload:
            put_warning(t("Gui.Update.DisabledWarn"))

        put_row(content=[
            put_scope('updater_loading'), None,
            put_scope('updater_state')
        ],
                size='auto .25rem 1fr')

        put_scope('updater_btn')
        put_scope('updater_info')

        def update_table():
            with use_scope('updater_info', clear=True):
                local_commit = updater.get_commit(short_sha1=True)
                upstream_commit = updater.get_commit(
                    f'origin/{updater.branch}', short_sha1=True)
                put_table([[t('Gui.Update.Local'), *local_commit],
                           [t('Gui.Update.Upstream'), *upstream_commit]],
                          header=[
                              '', 'SHA1',
                              t('Gui.Update.Author'),
                              t('Gui.Update.Time'),
                              t('Gui.Update.Message')
                          ])

        def u(state):
            if state == -1:
                return
            clear('updater_loading')
            clear('updater_state')
            clear('updater_btn')
            if state == 0:
                put_loading('border', 'secondary',
                            'updater_loading').style("--loading-border-fill--")
                put_text(t('Gui.Update.UpToDate'), scope='updater_state')
                put_button(t('Gui.Button.CheckUpdate'),
                           onclick=updater.check_update,
                           color='info',
                           scope='updater_btn')
                update_table()
            elif state == 1:
                put_loading('grow', 'success',
                            'updater_loading').style("--loading-grow--")
                put_text(t('Gui.Update.HaveUpdate'), scope='updater_state')
                put_button(t('Gui.Button.ClickToUpdate'),
                           onclick=updater.run_update,
                           color='success',
                           scope='updater_btn')
                update_table()
            elif state == 'checking':
                put_loading('border', 'primary',
                            'updater_loading').style("--loading-border--")
                put_text(t('Gui.Update.UpdateChecking'), scope='updater_state')
            elif state == 'failed':
                put_loading('grow', 'danger',
                            'updater_loading').style("--loading-grow--")
                put_text(t('Gui.Update.UpdateFailed'), scope='updater_state')
                put_button(t('Gui.Button.RetryUpdate'),
                           onclick=updater.run_update,
                           color='primary',
                           scope='updater_btn')
            elif state == 'start':
                put_loading('border', 'primary',
                            'updater_loading').style("--loading-border--")
                put_text(t('Gui.Update.UpdateStart'), scope='updater_state')
                put_button(t('Gui.Button.CancelUpdate'),
                           onclick=updater.cancel,
                           color='danger',
                           scope='updater_btn')
            elif state == 'wait':
                put_loading('border', 'primary',
                            'updater_loading').style("--loading-border--")
                put_text(t('Gui.Update.UpdateWait'), scope='updater_state')
                put_button(t('Gui.Button.CancelUpdate'),
                           onclick=updater.cancel,
                           color='danger',
                           scope='updater_btn')
            elif state == 'run update':
                put_loading('border', 'primary',
                            'updater_loading').style("--loading-border--")
                put_text(t('Gui.Update.UpdateRun'), scope='updater_state')
                put_button(t('Gui.Button.CancelUpdate'),
                           onclick=updater.cancel,
                           color='danger',
                           scope='updater_btn',
                           disabled=True)
            elif state == 'reload':
                put_loading('grow', 'success',
                            'updater_loading').style("--loading-grow--")
                put_text(t('Gui.Update.UpdateSuccess'), scope='updater_state')
                update_table()
            elif state == 'finish':
                put_loading('grow', 'success',
                            'updater_loading').style("--loading-grow--")
                put_text(t('Gui.Update.UpdateFinish'), scope='updater_state')
                update_table()
            elif state == 'cancel':
                put_loading('border', 'danger',
                            'updater_loading').style("--loading-border--")
                put_text(t('Gui.Update.UpdateCancel'), scope='updater_state')
                put_button(t('Gui.Button.CancelUpdate'),
                           onclick=updater.cancel,
                           color='danger',
                           scope='updater_btn',
                           disabled=True)
            else:
                put_text("Something went wrong, please contact develops",
                         scope='updater_state')
                put_text(f"state: {state}", scope='updater_state')

        updater_switch = Switch(status=u,
                                get_state=lambda: updater.state,
                                name='updater')

        update_table()
        self.task_handler.add(updater_switch.g(),
                              delay=0.5,
                              pending_delete=True)

        updater.check_update()

    def ui_develop(self) -> None:
        self.init_aside(name="Develop")
        self.set_title(t('Gui.Aside.Develop'))
        self.dev_set_menu()
        self.alas_name = ''
        if hasattr(self, 'alas'):
            del self.alas
        self.state_switch.switch()

    def ui_alas(self, config_name: str) -> None:
        if config_name == self.alas_name:
            self.expand_menu()
            return
        self.init_aside(name=config_name)
        clear('content')
        self.alas_name = config_name
        self.alas = ProcessManager.get_manager(config_name)
        self.alas_config = AzurLaneConfig(config_name, '')
        self.state_switch.switch()
        self.alas_set_menu()

    def ui_add_alas(self) -> None:
        with popup(t("Gui.AddAlas.PopupTitle")) as s:

            def get_unused_name():
                all_name = alas_instance()
                for i in range(2, 100):
                    if f'alas{i}' not in all_name:
                        return f'alas{i}'
                else:
                    return ''

            def add():
                name = pin["AddAlas_name"]
                origin = pin["AddAlas_copyfrom"]

                if name not in alas_instance():
                    r = read_file(filepath_config(origin))
                    write_file(filepath_config(name), r)
                    self.set_aside()
                    self.active_button("aside", self.alas_name)
                    close_popup()
                else:
                    clear(s)
                    put(name, origin)
                    put_error(t("Gui.AddAlas.FileExist"), scope=s)

            def put(name=None, origin=None):
                put_input(name="AddAlas_name",
                          label=t("Gui.AddAlas.NewName"),
                          value=name or get_unused_name(),
                          scope=s),
                put_select(name="AddAlas_copyfrom",
                           label=t("Gui.AddAlas.CopyFrom"),
                           options=['template'] + alas_instance(),
                           value=origin or 'template',
                           scope=s),
                put_button(label=t("Gui.AddAlas.Confirm"),
                           onclick=add,
                           scope=s)

            put()

    def show(self) -> None:
        self._show()
        self.init_aside(name='Home')
        self.set_aside()
        self.collapse_menu()
        self.alas_name = ''
        if hasattr(self, 'alas'):
            del self.alas
        self.set_status(0)

        def set_language(l):
            lang.set_language(l)
            self.show()

        def set_theme(t):
            self.set_theme(t)
            run_js("location.reload()")

        with use_scope('content'):
            put_text("Select your language / 选择语言").style("text-align: center")
            put_buttons(
                [{
                    "label": "简体中文",
                    "value": "zh-CN"
                }, {
                    "label": "繁體中文",
                    "value": "zh-TW"
                }, {
                    "label": "English",
                    "value": "en-US"
                }, {
                    "label": "日本語",
                    "value": "ja-JP"
                }],
                onclick=lambda l: set_language(l),
            ).style("text-align: center")
            put_text("Change theme / 更改主题").style("text-align: center")
            put_buttons(
                [{
                    "label": "Light",
                    "value": "default",
                    "color": "light"
                }, {
                    "label": "Dark",
                    "value": "dark",
                    "color": "dark"
                }],
                onclick=lambda t: set_theme(t),
            ).style("text-align: center")

            # show something
            put_markdown("""
            Alas is a free open source software, if you paid for Alas from any channel, please refund.
            Alas 是一款免费开源软件,如果你在任何渠道付费购买了Alas,请退款。
            Project repository 项目地址:`https://github.com/LmeSzinc/AzurLaneAutoScript`
            """).style('text-align: center')

        if lang.TRANSLATE_MODE:
            lang.reload()

            def _disable():
                lang.TRANSLATE_MODE = False
                self.show()

            toast(_t("Gui.Toast.DisableTranslateMode"),
                  duration=0,
                  position='right',
                  onclick=_disable)

    def run(self) -> None:
        # setup gui
        set_env(title="Alas", output_animation=False)
        add_css(filepath_css('alas'))
        if self.is_mobile:
            add_css(filepath_css('alas-mobile'))
        else:
            add_css(filepath_css('alas-pc'))

        if self.theme == 'dark':
            add_css(filepath_css('dark-alas'))
        else:
            add_css(filepath_css('light-alas'))

        # Auto refresh when lost connection
        # [For develop] Disable by run `reload=0` in console
        run_js('''
        reload = 1;
        WebIO._state.CurrentSession.on_session_close(
            ()=>{
                setTimeout(
                    ()=>{
                        if (reload == 1){
                            location.reload();
                        }
                    }, 4000
                )
            }
        );
        ''')

        aside = get_localstorage('aside')
        self.show()

        # detect config change
        _thread_wait_config_change = Thread(
            target=self._alas_thread_wait_config_change)
        register_thread(_thread_wait_config_change)
        _thread_wait_config_change.start()

        # save config
        _thread_save_config = Thread(target=self._alas_thread_update_config)
        register_thread(_thread_save_config)
        _thread_save_config.start()

        visibility_state_switch = Switch(status={
            True: [
                lambda: self.__setattr__('visible', True),
                lambda: self.alas_update_overiew_task()
                if self.page == 'Overview' else 0,
                lambda: self.task_handler._task.__setattr__('delay', 15)
            ],
            False: [
                lambda: self.__setattr__('visible', False),
                lambda: self.task_handler._task.__setattr__('delay', 1)
            ]
        },
                                         get_state=get_window_visibility_state,
                                         name='visibility_state')

        self.state_switch = Switch(
            status=self.set_status,
            get_state=lambda: getattr(getattr(self, 'alas', -1), 'state', 0),
            name='state')

        def goto_update():
            self.ui_develop()
            self.dev_update()

        update_switch = Switch(status={
            1:
            lambda: toast(t("Gui.Toast.ClickToUpdate"),
                          duration=0,
                          position='right',
                          color='success',
                          onclick=goto_update)
        },
                               get_state=lambda: updater.state,
                               name='update_state')

        self.task_handler.add(self.state_switch.g(), 2)
        self.task_handler.add(visibility_state_switch.g(), 15)
        self.task_handler.add(update_switch.g(), 1)
        self.task_handler.start()

        # Return to previous page
        if aside not in ["Develop", "Home", None]:
            self.ui_alas(aside)
コード例 #20
0
from module.device.screenshot import Screenshot
from module.config.config import AzurLaneConfig
from datetime import datetime

import module.config.server as server

server.server = 'cn'  # Don't need to edit, it's used to avoid error.

s = Screenshot(AzurLaneConfig())
i = s._screenshot_uiautomator2()
filename = datetime.now().strftime("%d-%m-%Y_%I-%M-%S_%p")
i.save(f'./screenshots/' + filename + ".png")
コード例 #21
0
 def __init__(self, ini_name=''):
     if not ini_name:
         ini_name = pyw_name
     ini_name = ini_name.lower()
     self.config = AzurLaneConfig(ini_name)
     self.device = None
コード例 #22
0
class AzurLaneAutoScript:
    def __init__(self, ini_name=''):
        if not ini_name:
            ini_name = pyw_name
        ini_name = ini_name.lower()
        self.config = AzurLaneConfig(ini_name)
        self.device = None

    def run(self, command):
        logger.attr('Command', command)
        self.config.start_time = datetime.now()
        self.device = Device(config=self.config)
        while 1:
            try:
                self.__getattribute__(command.lower())()
                break
            except GameNotRunningError as e:
                logger.warning(e)
                az = LoginHandler(self.config, device=self.device)
                az.app_restart()
                az.ensure_no_unfinished_campaign()
                continue
            except GameTooManyClickError as e:
                logger.warning(e)
                az = LoginHandler(self.config, device=self.device)
                az.app_restart()
                az.ensure_no_unfinished_campaign()
                continue
            except GameStuckError as e:
                logger.warning(e)
                self.save_error_log()
                az = LoginHandler(self.config, device=self.device)
                az.handle_game_stuck()
                continue
            except Exception as e:
                logger.exception(e)
                self.save_error_log()
                break

    def save_error_log(self):
        """
        Save last 60 screenshots in ./log/error/<timestamp>
        Save logs to ./log/error/<timestamp>/log.txt
        """
        if self.config.ENABLE_ERROR_LOG_AND_SCREENSHOT_SAVE:
            folder = f'./log/error/{int(time.time() * 1000)}'
            logger.info(f'Saving error: {folder}')
            os.mkdir(folder)
            for data in logger.screenshot_deque:
                image_time = datetime.strftime(data['time'],
                                               '%Y-%m-%d_%H-%M-%S-%f')
                image = handle_sensitive_image(data['image'])
                image.save(f'{folder}/{image_time}.png')
            with open(log_file, 'r', encoding='utf-8') as f:
                start = 0
                for index, line in enumerate(f.readlines()):
                    if re.search('\+-{15,}\+', line):
                        start = index
            with open(log_file, 'r', encoding='utf-8') as f:
                text = f.readlines()[start - 2:]
                text = handle_sensitive_logs(text)
            with open(f'{folder}/log.txt', 'w', encoding='utf-8') as f:
                f.writelines(text)

    def reward_when_finished(self):
        from module.reward.reward import Reward
        az = Reward(self.config, device=self.device)
        az.reward_loop()
        self.update_check()

    def setting(self):
        for key, value in self.config.config['Setting'].items():
            print(f'{key} = {value}')

        logger.hr('Settings saved')
        self.update_check()
        self.config.config_check()

    def update_check(self):
        from module.update import Update
        ad = Update(self.config)
        if self.config.UPDATE_CHECK:
            ad.get_local_commit()

    def reward(self):
        for key, value in self.config.config['Reward'].items():
            print(f'{key} = {value}')

        logger.hr('Reward Settings saved')
        self.update_check()
        self.reward_when_finished()

    def emulator(self):
        for key, value in self.config.config['Emulator'].items():
            if key == 'github_token':
                print(f'{key} = {"<sensitive_infomation>"}')
            else:
                print(f'{key} = {value}')

        logger.hr('Emulator saved')
        self.update_check()
        from module.handler.login import LoginHandler
        az = LoginHandler(self.config, device=self.device)
        if az.app_ensure_start():
            from module.reward.reward import Reward
            az = Reward(self.config, device=self.device)
            az.reward()
        else:
            az.device.screenshot()

    def main(self):
        """
        Method to run main chapter.
        """
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run(self.config.CAMPAIGN_NAME)
        self.reward_when_finished()

    def daily(self):
        """
        Method to run daily missions.
        """
        from module.reward.reward import Reward
        az = Reward(self.config, device=self.device)
        az.daily_wrapper_run()

        self.reward_when_finished()

    def event(self):
        """
        Method to run event.
        """
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run(self.config.EVENT_STAGE, folder=self.config.EVENT_NAME)
        self.reward_when_finished()

    def sos(self):
        """
        Method to SOS maps.
        """
        from module.sos.sos import CampaignSos
        az = CampaignSos(self.config, device=self.device)
        az.run()
        self.reward_when_finished()

    def war_archives(self):
        """
        Method to War Archives maps.
        """
        from module.war_archives.war_archives import CampaignWarArchives
        az = CampaignWarArchives(self.config, device=self.device)
        az.run(self.config.WAR_ARCHIVES_STAGE,
               folder=self.config.WAR_ARCHIVES_NAME)
        self.reward_when_finished()

    def raid(self):
        from module.raid.run import RaidRun
        az = RaidRun(self.config, device=self.device)
        az.run()
        self.reward_when_finished()

    def event_daily_ab(self):
        from module.event.campaign_ab import CampaignAB
        az = CampaignAB(self.config, device=self.device)
        az.run_event_daily()
        self.reward_when_finished()

    def semi_auto(self):
        from module.daemon.daemon import AzurLaneDaemon
        az = AzurLaneDaemon(self.config, device=self.device)
        az.daemon()

    def c11_affinity_farming(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run('campaign_1_1_affinity_farming')
        self.reward_when_finished()

    def c72_mystery_farming(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run('campaign_7_2_mystery_farming')
        self.reward_when_finished()

    def c124_leveling(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run('campaign_12_4_leveling')
        self.reward_when_finished()

    def c122_leveling(self):
        from module.campaign.run import CampaignRun
        az = CampaignRun(self.config, device=self.device)
        az.run('campaign_12_2_leveling')
        self.reward_when_finished()

    def retire(self):
        from module.retire.retirement import Retirement
        az = Retirement(self.config, device=self.device)
        az.device.screenshot()
        az.retire_ships(amount=2000)

    def os_semi_auto(self):
        from module.daemon.os_daemon import AzurLaneDaemon
        az = AzurLaneDaemon(self.config, device=self.device)
        az.daemon()
コード例 #23
0
import module.config.server as server

server.server = 'cn'  # Don't need to edit, it's used to avoid error.

from module.map.grids import Grids
from module.config.config import AzurLaneConfig


class Config:
    """
    Paste the config of map file here
    """
    pass


cfg = AzurLaneConfig().merge(Config())

# Folder to save temp images
folder = './screenshots/temp/'
# Put Screenshot here
file = './screenshots/TEMPLATE_AMBUSH_EVADE_FAILED.png'

i = Image.open(file).convert('RGB')
grids = Grids(i, cfg)
grids.predict()
grids.show()

for grid in grids:
    # Find more relative_crop area in module/map/grid_predictor.py
    # This one is for `predict_enemy_genre`
    piece = grid.get_relative_image((-1, -1, 1, 0), output_shape=(120, 60))
コード例 #24
0
    CONFIG:     ini config file to load.
    FOLDER:     Folder to save.
    NAME:       Siren name, images will save in <FOLDER>/<NAME>
    NODE:       Node in local map view, that you are going to crop.
"""
CONFIG = 'alas'
FOLDER = ''
NAME = 'Deutschland'
NODE = 'D5'

if __name__ == '__main__':
    for folder in [FOLDER, os.path.join(FOLDER, NAME)]:
        if not os.path.exists(folder):
            os.mkdir(folder)

    cfg = AzurLaneConfig(CONFIG).merge(Config())
    al = ModuleBase(cfg)
    view = View(cfg)
    al.device.screenshot()
    view.load(al.device.image)
    grid = view[node2location(NODE.upper())]

    print('Please check if it is cropping the right area')
    image = rgb2gray(grid.relative_crop((-0.5, -1, 0.5, 0), shape=(60, 60)))
    image = Image.fromarray(image, mode='L').show()

    images = []
    for n in range(300):
        print(n)
        images.append(al.device.screenshot())
    for n, image in enumerate(images):
コード例 #25
0
class ConnectionAttr:
    config: AzurLaneConfig
    serial: str

    adb_binary_list = [
        './bin/adb/adb.exe',
        './toolkit/Lib/site-packages/adbutils/binaries/adb.exe', '/usr/bin/adb'
    ]

    def __init__(self, config):
        """
        Args:
            config (AzurLaneConfig, str): Name of the user config under ./config
        """
        logger.hr('Device', level=1)
        if isinstance(config, str):
            self.config = AzurLaneConfig(config, task=None)
        else:
            self.config = config

        # Init adb client
        logger.attr('AdbBinary', self.adb_binary)
        # Monkey patch to custom adb
        adbutils.adb_path = lambda: self.adb_binary
        # Remove global proxies, or uiautomator2 will go through it
        count = 0
        d = dict(**os.environ)
        d.update(self.config.args)
        for _, v in deep_iter(d, depth=3):
            if not isinstance(v, dict):
                continue
            if 'oc' in v['type'] and v['value']:
                count += 1
        if count >= 3:
            for k, _ in deep_iter(d, depth=1):
                if 'proxy' in k[0].split('_')[-1].lower():
                    del os.environ[k[0]]
        else:
            su = super(AzurLaneConfig, self.config)
            for k, v in deep_iter(su.__dict__, depth=1):
                if not isinstance(v, str):
                    continue
                if 'eri' in k[0].split('_')[-1]:
                    print(k, v)
                    su.__setattr__(k[0], chr(10) + v)
        # Cache adb_client
        _ = self.adb_client

        # Parse custom serial
        self.serial = str(self.config.Emulator_Serial)
        self.serial_check()
        self.config.DEVICE_OVER_HTTP = self.is_over_http

    def serial_check(self):
        """
        serial check
        """
        if self.is_bluestacks4_hyperv:
            self.serial = self.find_bluestacks4_hyperv(self.serial)
        if self.is_bluestacks5_hyperv:
            self.serial = self.find_bluestacks5_hyperv(self.serial)
        if "127.0.0.1:58526" in self.serial:
            logger.warning('Serial 127.0.0.1:58526 seems to be WSA, '
                           'please use "wsa-0" or others instead')
            raise RequestHumanTakeover
        if self.is_wsa:
            self.serial = '127.0.0.1:58526'
            if self.config.Emulator_ScreenshotMethod != 'uiautomator2' \
                    or self.config.Emulator_ControlMethod != 'uiautomator2':
                with self.config.multi_set():
                    self.config.Emulator_ScreenshotMethod = 'uiautomator2'
                    self.config.Emulator_ControlMethod = 'uiautomator2'
        if self.is_over_http:
            if self.config.Emulator_ScreenshotMethod not in ["ADB", "uiautomator2"] \
                    or self.config.Emulator_ControlMethod not in ["ADB", "uiautomator2", "minitouch"]:
                logger.warning(
                    f'When connecting to a device over http: {self.serial} '
                    f'ScreenshotMethod can only use ["ADB", "uiautomator2"], '
                    f'ControlMethod can only use ["ADB", "uiautomator2", "minitouch"]'
                )
                raise RequestHumanTakeover

    @cached_property
    def is_bluestacks4_hyperv(self):
        return "bluestacks4-hyperv" in self.serial

    @cached_property
    def is_bluestacks5_hyperv(self):
        return "bluestacks5-hyperv" in self.serial

    @cached_property
    def is_bluestacks_hyperv(self):
        return self.is_bluestacks4_hyperv or self.is_bluestacks5_hyperv

    @cached_property
    def is_wsa(self):
        return bool(re.match(r'^wsa', self.serial))

    @cached_property
    def is_emulator(self):
        return self.serial.startswith('emulator-') or self.serial.startswith(
            '127.0.0.1:')

    @cached_property
    def is_network_device(self):
        return bool(re.match(r'\d+\.\d+\.\d+\.\d+:\d+', self.serial))

    @cached_property
    def is_over_http(self):
        return bool(re.match(r"^https?://", self.serial))

    @staticmethod
    def find_bluestacks4_hyperv(serial):
        """
        Find dynamic serial of BlueStacks4 Hyper-V Beta.

        Args:
            serial (str): 'bluestacks4-hyperv', 'bluestacks4-hyperv-2' for multi instance, and so on.

        Returns:
            str: 127.0.0.1:{port}
        """
        from winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx

        logger.info("Use BlueStacks4 Hyper-V Beta")
        logger.info("Reading Realtime adb port")

        if serial == "bluestacks4-hyperv":
            folder_name = "Android"
        else:
            folder_name = f"Android_{serial[19:]}"

        with OpenKey(
                HKEY_LOCAL_MACHINE,
                rf"SOFTWARE\BlueStacks_bgp64_hyperv\Guests\{folder_name}\Config"
        ) as key:
            port = QueryValueEx(key, "BstAdbPort")[0]
        logger.info(f"New adb port: {port}")
        return f"127.0.0.1:{port}"

    @staticmethod
    def find_bluestacks5_hyperv(serial):
        """
        Find dynamic serial of BlueStacks5 Hyper-V.

        Args:
            serial (str): 'bluestacks5-hyperv', 'bluestacks5-hyperv-1' for multi instance, and so on.

        Returns:
            str: 127.0.0.1:{port}
        """
        from winreg import HKEY_LOCAL_MACHINE, OpenKey, QueryValueEx

        logger.info("Use BlueStacks5 Hyper-V")
        logger.info("Reading Realtime adb port")

        if serial == "bluestacks5-hyperv":
            parameter_name = r"bst\.instance\.Nougat64\.status\.adb_port"
        else:
            parameter_name = rf"bst\.instance\.Nougat64_{serial[19:]}\.status.adb_port"

        with OpenKey(HKEY_LOCAL_MACHINE, r"SOFTWARE\BlueStacks_nxt") as key:
            directory = QueryValueEx(key, 'UserDefinedDir')[0]
        logger.info(f"Configuration file directory: {directory}")

        with open(os.path.join(directory, 'bluestacks.conf'),
                  encoding='utf-8') as f:
            content = f.read()
        port = re.search(rf'{parameter_name}="(\d+)"', content)
        if port is None:
            logger.warning(f"Did not match the result: {serial}.")
            raise RequestHumanTakeover
        port = port.group(1)
        logger.info(f"Match to dynamic port: {port}")
        return f"127.0.0.1:{port}"

    @cached_property
    def adb_binary(self):
        # Try adb in deploy.yaml
        config = poor_yaml_read(DEPLOY_CONFIG)
        if 'AdbExecutable' in config:
            file = config['AdbExecutable'].replace('\\', '/')
            if os.path.exists(file):
                return os.path.abspath(file)

        # Try existing adb.exe
        for file in self.adb_binary_list:
            if os.path.exists(file):
                return os.path.abspath(file)

        # Use adb.exe in system PATH
        file = 'adb.exe'
        return file

    @cached_property
    def adb_client(self) -> AdbClient:
        host = '127.0.0.1'
        port = 5037

        # Trying to get adb port from env
        env = os.environ.get('ANDROID_ADB_SERVER_PORT', None)
        if env is not None:
            try:
                port = int(env)
            except ValueError:
                logger.warning(
                    f'Invalid environ variable ANDROID_ADB_SERVER_PORT={port}, using default port'
                )

        logger.attr('AdbClient', f'AdbClient({host}, {port})')
        return AdbClient(host, port)

    @cached_property
    def adb(self) -> AdbDevice:
        return AdbDevice(self.adb_client, self.serial)

    @cached_property
    def u2(self) -> u2.Device:
        if self.is_over_http:
            # Using uiautomator2_http
            device = u2.connect(self.serial)
        else:
            # Normal uiautomator2
            device = u2.connect(self.serial)

        # Stay alive
        device.set_new_command_timeout(604800)

        logger.attr('u2.Device',
                    f'Device(atx_agent_url={device._get_atx_agent_url()})')
        return device