import os
import re
import sys
from logging import getLevelName, DEBUG, ERROR

from PyQt6.QtCore import Qt, QCoreApplication, QTimer, QUrl, QSize
from PyQt6.QtGui import QIcon, QStandardItemModel, QDesktopServices, QKeySequence, QAction, QShortcut
from PyQt6.QtWidgets import (QApplication, QAbstractItemView, QHeaderView, QMenu, QStyle,
                             QPushButton, QFileDialog, QMessageBox, QSystemTrayIcon)

from lanzou.api import LanZouCloud
from lanzou.api.models import FolderList
from lanzou.api.types import RecFolder, FolderDetail, ShareItem
from lanzou.api.utils import convert_file_size_to_int as format_size_int
from lanzou.api.utils import time_format, time_stamp
from lanzou.debug import logger, USER_HOME, SRC_DIR
from lanzou.gui import version
from lanzou.gui.config import config
from lanzou.gui.dialogs import *
from lanzou.gui.models import DlJob, Tasks, FileInfos, FolderInfos, ShareFileInfos
from lanzou.gui.others import MyStandardItem as QStandardItem
from lanzou.gui.others import set_file_icon, TableDelegate
from lanzou.gui.qss import *
from lanzou.gui.ui import Ui_MainWindow
from lanzou.gui.workers import *
from lanzou.gui.workers.manager import change_size_unit

__ALL__ = ['MainWindow']


def get_logo_path():
    """返回 logo 路径"""
    return SRC_DIR + 'lanzou_logo2.png'


def get_lanzou_logo():
    return QIcon(get_logo_path())


def open_in_explorer(task):
    QDesktopServices.openUrl(QUrl.fromLocalFile(task.path))


def open_file(task):
    new_name = re.sub(r"(\.\w+)\.enc", r"\1", task.name)
    url = QUrl.fromLocalFile(task.path + "/" + new_name)
    if not QDesktopServices.openUrl(url):
        logger.error(f"open file failed{url}", )


def time_table_item(data):
    try:
        item = QStandardItem(data.time)
        item.setData(time_stamp(data.time), Qt.ItemDataRole.UserRole)
    except Exception as e:
        print("errr", e)
    return item


class MainWindow(Ui_MainWindow):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setupUi(self)
        self.init_variables()
        self.init_workers()
        self.update_lanzoucloud_settings()

        self.main_menu_add_slot()
        self.init_extract_share_ui()
        self.init_disk_ui()
        self.init_rec_ui()
        self.init_jobs_ui()
        self.create_left_menus()

        self.setWindowTitle("蓝奏云客户端 - {}".format(version))
        self.setStyleSheet(qssStyle)
        if self.upgrade:  # 检测新版
            self.check_update_worker.set_values(version, False)
        self.clipboard_monitor()  # 系统粘贴板
        # self.call_login_launcher()

    def create_tray(self):
        """创建 系统托盘"""
        if not self._created_tray:
            self.tray = QSystemTrayIcon(get_lanzou_logo(), parent=self)
            show_action = QAction("显示窗口", self)
            hide_action = QAction("最小化到托盘", self)
            quit_action = QAction("退出程序", self)
            show_action.triggered.connect(self.show)
            hide_action.triggered.connect(self.hide)
            quit_action.triggered.connect(self.Exit)
            show_action.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ComputerIcon))
            hide_action.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarMinButton))
            quit_action.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarCloseButton))
            self.tray.activated[QSystemTrayIcon.ActivationReason].connect(self.icon_activated)  # 托盘点击事件
            tray_menu = QMenu()
            # tray_menu = QMenu(QApplication.desktop())
            tray_menu.addAction(show_action)
            tray_menu.addAction(hide_action)
            tray_menu.addAction(quit_action)
            self.tray.setContextMenu(tray_menu)
            tip_title = f"蓝奏云客户端 <{self._user}>" if self._user else "蓝奏云客户端"
            self.tray.setToolTip(f"{tip_title}\n双击到托盘")
            self.tray.show()
            self._created_tray = True

    def icon_activated(self, reason):
        if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
            tip_title = f"蓝奏云客户端 <{self._user}>" if self._user else "蓝奏云客户端"
            if self.isHidden():
                self.show()
                self.tray.setToolTip(f"{tip_title}\n双击到托盘")
            else:
                self.hide()
                self.tray.setToolTip(f"{tip_title}\n双击显示")

    def main_menu_add_slot(self):
        """主菜单添加槽函数"""
        self.login.triggered.connect(self.show_login_dialog)  # 登录
        self.logout.triggered.connect(self.call_logout)  # 登出
        self.download.triggered.connect(self.call_download_shortcut)  # 快捷键下载
        self.delete.triggered.connect(self.call_delete_shortcut)  # 快捷键删除
        self.upload.triggered.connect(self.show_upload_dialog_menus)
        self.setting_menu.triggered.connect(lambda: self.setting_dialog.open_dialog(self._config))
        self.show_toolbar.triggered.connect(self.show_toolbar_slot)
        self.how.triggered.connect(self.open_wiki_url)
        self.about.triggered.connect(self.about_dialog.exec)
        self.tabWidget.currentChanged.connect(self.call_change_tab)  # tab 切换时更新
        self.merge_file.triggered.connect(self.merge_file_dialog.exec)  # 合并文件

    def init_variables(self):
        self._disk = LanZouCloud()
        self._config = config
        self._user = None  # 当前登录用户名
        self._folder_list = {}  # disk 工作目录文件夹
        self._file_list = {}  # disk 工作目录文件
        self._extract_folder_list = {}  # share 提取子文件夹
        self._extract_show_dir = []  # share 提取文件显示子文件夹
        self._extract_setted_head = False  # share 提取界面设置表头标示
        self._extract_count = 0  # share 提取界面文件数
        self._path_list = FolderList()
        self._path_list_old = FolderList()
        self._locs = {}
        self._parent_id = -1  # --> ..
        self._work_name = ""  # share disk rec, not use now
        self._work_id = -1  # disk folder id
        self._old_work_id = self._work_id  # 用于上传完成后判断是否需要更新disk界面
        self._show_to_tray_msg = True
        self._created_tray = False
        self._tasks = Tasks()
        self._dl_jobs_lists = {}
        self._up_jobs_lists = {}
        self._to_tray = False
        self._show_subfolder = False  # 提取界面递归展示子文件

    def set_disk(self):
        """方便切换用户更新信息"""
        self.task_manager.set_disk(self._disk)
        self.get_shared_info_thread.set_disk(self._disk)
        self.login_launcher.set_disk(self._disk)
        self.list_refresher.set_disk(self._disk)
        self.remove_files_worker.set_disk(self._disk)
        self.get_rec_lists_worker.set_disk(self._disk)
        self.rec_manipulator.set_disk(self._disk)
        self.desc_pwd_fetcher.set_disk(self._disk)
        self.logout_worker.set_disk(self._disk)
        self.rename_mkdir_worker.set_disk(self._disk)
        self.set_pwd_worker.set_disk(self._disk)
        self.more_info_worker.set_disk(self._disk)
        self.all_folders_worker.set_disk(self._disk)

    def update_lanzoucloud_settings(self):
        """更新LanzouCloud实例设置"""
        self._user = self._config.name
        settings = self._config.settings
        self._disk.set_timeout(settings["timeout"])
        self._disk.set_max_size(settings["max_size"])
        self.task_manager.set_thread(settings["download_threads"])  # 同时下载任务数量
        self.share_set_dl_path.setText(self._config.path)  # 提取界面下载路径
        self.time_fmt = settings["time_fmt"]  # 时间显示格式
        self._to_tray = settings["to_tray"]
        self.watch_clipboard = settings["watch_clipboard"]
        self.upgrade = settings["upgrade"] if 'upgrade' in settings else True  # 自动检测更新
        self.upload_dialog.set_pwd_desc_bigfile(settings)
        self.task_manager.set_allow_big_file(settings["allow_big_file"])
        if 'upload_delay' in settings:
            delay = int(settings["upload_delay"])
            if delay > 0:
                self._disk.set_upload_delay((delay / 2, delay))
            else:
                self._disk.set_upload_delay((0, 0))
        # debug
        debug = settings["debug"]
        if debug:
            if getLevelName(logger.level) != "DEBUG":
                logger.setLevel(DEBUG)
                logger.debug("\n" + "=" * 69)
                logger.debug(f"Start New Debug: version {version}")
        else:
            logger.setLevel(ERROR)
        # 托盘图标
        if self._to_tray:
            self.create_tray()
        elif self._created_tray:
            self.tray.hide()
            del self.tray
            self._created_tray = False

    def init_workers(self):
        # 登录器
        self.login_launcher = LoginLuncher()
        self.login_launcher.update_username.connect(self._config.set_username)
        self.login_launcher.update_cookie.connect(self._config.set_cookie)  # 目前失效
        self.login_launcher.code.connect(self.login_update_ui)
        # 登出器
        self.logout_worker = LogoutWorker()
        self.logout_worker.succeeded.connect(self.call_logout_update_ui)
        # 任务管理器
        self.task_manager = TaskManager()
        self.task_manager.mgr_msg.connect(self.show_status)
        self.task_manager.update.connect(self.update_jobs_info)
        self.task_manager.mgr_finished.connect(self.call_show_mgr_finished)
        # 获取更多信息，直链、下载次数等
        self.info_dialog = InfoDialog()  # 对话框
        self.info_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)  # 窗口前置
        self.more_info_worker = GetMoreInfoWorker()  # 后台更新线程
        self.more_info_worker.msg.connect(self.show_status)
        self.more_info_worker.infos.connect(lambda: self.pause_extract_clipboard(True))  # 禁用剪切板监听
        self.more_info_worker.infos.connect(self.info_dialog.set_values)
        self.more_info_worker.share_url.connect(self.call_copy_share_url)
        self.more_info_worker.dl_link.connect(self.info_dialog.set_dl_link_tx)
        self.info_dialog.get_dl_link.connect(self.more_info_worker.get_dl_link)
        self.info_dialog.closed.connect(lambda: self.pause_extract_clipboard(False))  # 恢复剪切板监听
        # 登录文件列表更新器
        self.list_refresher = ListRefresher()
        self.list_refresher.err_msg.connect(self.show_status)
        self.list_refresher.infos.connect(self.update_disk_lists)
        self.list_refresher.infos.connect(lambda: self.show_status(""))

        # 显示移动文件对话框
        self.move_file_dialog = MoveFileDialog()
        # 获取所有文件夹fid，并移动
        self.all_folders_worker = GetAllFoldersWorker()
        self.all_folders_worker.msg.connect(self.show_status)
        self.move_file_dialog.new_infos.connect(self.all_folders_worker.move_file)  # 调用移动线程
        self.all_folders_worker.infos.connect(self.move_file_dialog.set_values)
        self.all_folders_worker.moved.connect(self.on_moved)  # 更新文件列表

        # 重命名、修改简介、新建文件夹
        self.rename_mkdir_worker = RenameMkdirWorker()
        self.rename_mkdir_worker.msg.connect(self.show_status)
        self.rename_mkdir_worker.update.connect(self.list_refresher.set_values)  # 更新界面
        # 设置文件(夹)提取码
        self.set_pwd_worker = SetPwdWorker()
        self.set_pwd_worker.msg.connect(self.show_status)
        self.set_pwd_worker.update.connect(self.list_refresher.set_values)  # 更新界面
        # 删除文件(夹)
        self.remove_files_worker = RemoveFilesWorker()
        self.remove_files_worker.msg.connect(self.show_status)  # 显示错误提示
        self.remove_files_worker.finished.connect(lambda: self.list_refresher.set_values(self._work_id))  # 更新界面
        # 上传器，信号在登录更新界面设置
        self.upload_dialog = UploadDialog(USER_HOME)
        self.upload_dialog.new_infos.connect(self.call_upload)
        # 文件描述与提取码更新器
        self.desc_pwd_fetcher = DescPwdFetcher()
        self.desc_pwd_fetcher.desc.connect(self.call_update_desc_pwd)
        self.desc_pwd_fetcher.tasks.connect(self.call_task_manager_thread)  # 连接下载管理器线程

        # 重命名、修改简介与新建文件夹对话框
        self.rename_dialog = RenameDialog()
        self.rename_dialog.out.connect(self.call_rename_mkdir_worker)
        # 修改设置 提取码对话框
        self.set_pwd_dialog = SetPwdDialog()
        self.set_pwd_dialog.new_infos.connect(self.set_passwd)
        # 菜单栏关于
        self.about_dialog = AboutDialog()
        self.about_dialog.set_values(version)

        # 菜单栏设置
        self.setting_dialog = SettingDialog()
        self.setting_dialog.saved.connect(self.update_time_fmt)
        self.setting_dialog.saved.connect(self.update_lanzoucloud_settings)
        # 登录回收站信息更新器
        self.get_rec_lists_worker = GetRecListsWorker()
        self.get_rec_lists_worker.msg.connect(self.show_status)
        self.get_rec_lists_worker.infos.connect(self.update_rec_lists)
        self.get_rec_lists_worker.folders.connect(lambda: self.show_status('', 0))
        self.get_rec_lists_worker.folders.connect(self.pop_up_rec_folder_dialog)
        # 回收站操作器
        self.rec_manipulator = RecManipulator()
        self.rec_manipulator.msg.connect(self.show_status)
        self.rec_manipulator.succeeded.connect(self.get_rec_lists_worker.start)
        # 检查软件版本
        self.check_update_worker = CheckUpdateWorker()
        self.about_dialog.check_update.connect(self.check_update_worker.set_values)
        self.check_update_worker.infos.connect(self.about_dialog.show_update)
        self.check_update_worker.bg_update_infos.connect(self.show_new_version_msg)
        # 获取分享链接信息线程
        self.get_shared_info_thread = GetSharedInfo()
        # 验证码对话框
        # self.captcha_dialog = CaptchaDialog()
        # self.captcha_dialog.captcha.connect(self.set_captcha)
        # self.captcha_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)

        self.merge_file_dialog = MergeFileDialog(USER_HOME)
        self.set_disk()

    # def set_captcha(self, code):
    #     self._captcha_code = code

    # def captcha_handler(self, img_data):
    #     """处理下载时出现的验证码(暂时不需要了)"""
    #     self.captcha_dialog.handle(img_data)
    #     self._captcha_code = None
    #     self.captcha_dialog.exec()
    #     from time import sleep
    #     while True:
    #         sleep(1)
    #         if self._captcha_code:
    #             break
    #     return self._captcha_code

    def show_toolbar_slot(self):
        if self.toolbar.isVisible():
            self.toolbar.close()
            self.show_toolbar.setText("显示工具栏")
        else:
            self.toolbar.show()
            self.toolbar.setIconSize(QSize(20, 20))
            self.show_toolbar.setText("关闭工具栏")

    def show_login_dialog(self):
        """显示登录对话框"""
        login_dialog = LoginDialog(self._config)
        login_dialog.clicked_ok.connect(self.call_login_launcher)
        login_dialog.setWindowModality(Qt.WindowModality.ApplicationModal)
        login_dialog.exec()

    def show_upload_dialog(self, files):
        """显示上传文件对话框"""
        if len(self._path_list) > 0:
            self.upload_dialog.set_values(self._path_list[-1].name, self._path_list[-1].id, files)
        else:
            self.show_status("等待文件列表更新...", 2000)

    def show_upload_dialog_menus(self):
        '''菜单栏显示上传对话框槽函数'''
        self.show_upload_dialog(None)

    def update_time_fmt(self):
        """更新文件界面的时间"""
        if self.tabWidget.currentIndex() == self.tabWidget.indexOf(self.disk_tab):
            self.show_file_and_folder_lists()

    def call_task_manager_thread(self, tasks: dict):
        self.task_manager.add_tasks(tasks)
        self._tasks.add(tasks)
        self.tabWidget.insertTab(3, self.jobs_tab, "任务管理")
        self.jobs_tab.setEnabled(True)
        self.show_jobs_lists()

    def call_multi_manipulator(self, action):
        """批量操作器"""
        tab_page = self.tabWidget.currentIndex()
        if tab_page == self.tabWidget.indexOf(self.share_tab):
            listview = self.table_share
            model = self.model_share
        elif tab_page == self.tabWidget.indexOf(self.disk_tab):
            listview = self.table_disk
            model = self.model_disk
        elif tab_page == self.tabWidget.indexOf(self.rec_tab):
            listview = self.table_rec
            model = self.model_rec
        else:
            return None
        infos = []
        _indexes = listview.selectionModel().selection().indexes()
        for i in _indexes:  # 获取所选行号
            info = model.item(i.row(), 0).data()
            if info and info not in infos:
                infos.append(info)

        if tab_page == self.tabWidget.indexOf(self.share_tab):  # 提取界面下载
            if not infos:
                return None
            tasks = {}
            first_item = infos[0]
            if first_item.all is None:  # 单文件信息链接 (info, None, 1, [])
                info = first_item.item  # ShareInfo(code=0, name, url, pwd, desc, time, size)
                tasks[info.url] = DlJob(infos=info, path=self._config.path, total_file=1)
            elif len(infos) != 1 and len(infos) >= first_item.count:  # 下载整个文件夹文件 (info, all, count, parent)
                info = first_item.all.folder  # 当前文件夹信息
                tasks[info.url] = DlJob(infos=info, path=self._config.path, total_file=first_item.count)
            else:  # 下载文件夹中部分文件
                parent_dir_lst = first_item.parent  # 父文件夹信息
                parent_dir = os.sep.join(parent_dir_lst)
                path = self._config.path + os.sep + parent_dir if parent_dir_lst else self._config.path
                for info in infos:
                    info = info.item  # 文件夹中单文件信息
                    tasks[info.url] = DlJob(infos=info, path=path, total_file=1)
            logger.debug(f"manipulator, share tab tasks={tasks}")
            self.call_task_manager_thread(tasks)
        elif tab_page == self.tabWidget.indexOf(self.disk_tab):  # 登录文件界面下载
            logger.error(f"登录文件界面下载{infos}")
            if not infos:
                return None
            logger.debug(f"manipulator, disk tab infos={infos}")
            self.desc_pwd_fetcher.set_values(infos, download=True, dl_path=self._config.path)
        elif tab_page == self.tabWidget.indexOf(self.rec_tab):  # 回收站
            logger.debug(f"manipulator, rec tab action={action}")
            title = msg = ""
            if action == "recovery":
                title = "确定恢复选定文件(夹)？"
            elif action == "delete":
                title = "确定彻底删除选定文件(夹)？"
            elif action == "recovery_all":
                title = "确定还原全部文件(夹)？"
                msg = "提示: 恢复回收站中的文件将不可撤销，请确认。"
            elif action == "clean":
                title = "确定清除全部文件(夹)？"
                msg = "提示: 删除回收站中的文件将不可恢复，请确认。"

            if action == "recovery" or action == "delete":
                if not infos:
                    self.show_status("请先选中需要操作的文件！", 2999)
                    return None
                msg = "\t\t列表：\n"
                for i in infos:
                    msg += f"{i.time}\t{i.name}\t{i.size}\n"
            message_box = QMessageBox(self)
            message_box.setIcon(QMessageBox.Icon.Warning)
            message_box.setStyleSheet(btn_style)
            message_box.setWindowTitle(title)
            message_box.setText(msg)
            message_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
            buttonY = message_box.button(QMessageBox.StandardButton.Yes)
            buttonY.setText('确定')
            buttonN = message_box.button(QMessageBox.StandardButton.No)
            buttonN.setText('取消')
            message_box.exec()
            if message_box.clickedButton() == buttonY:
                self.rec_manipulator.set_values(infos, action)

    def call_logout_update_ui(self):
        """菜单栏、工具栏登出"""
        self.toolbar.removeAction(self.logout)
        self.tabWidget.setCurrentIndex(0)
        self.disk_tab.setEnabled(False)
        self.rec_tab.setEnabled(False)
        self.tabWidget.removeTab(2)
        self.tabWidget.removeTab(1)
        self.toolbar.removeAction(self.logout)  # 登出工具
        self.logout.setEnabled(False)
        self.toolbar.removeAction(self.upload)  # 上传文件工具栏
        self.upload.setEnabled(False)
        self._user = None

    def login_update_ui(self, success, msg, duration):
        """根据登录是否成功更新UI，并保持用户信息"""
        self.show_status(msg, duration)
        if success:
            self.update_lanzoucloud_settings()
            self._work_id = self._config.work_id  # 切换用户后刷新 根目录
            if self._user:
                if len(self._user) <= 6:
                    disk_tab = f"我的蓝奏<{self._user}>"
                else:
                    disk_tab = f"我的蓝奏<{self._user[:4]}..>"
            else:
                disk_tab = "我的蓝奏云"
            self.tabWidget.insertTab(1, self.disk_tab, disk_tab)
            self.tabWidget.insertTab(2, self.rec_tab, "回收站")
            self.disk_tab.setEnabled(True)
            self.rec_tab.setEnabled(True)
            # 更新快捷键与工具栏
            self.toolbar.addAction(self.logout)  # 添加登出工具栏
            self.toolbar.addAction(self.upload)  # 添加上传文件工具栏
            # 菜单栏槽
            self.logout.setEnabled(True)
            self.upload.setEnabled(True)
            self.download.setEnabled(True)
            self.delete.setEnabled(True)
            # 设置当前显示 tab
            self.tabWidget.setCurrentIndex(self.tabWidget.indexOf(self.disk_tab))
            QCoreApplication.processEvents()  # 重绘界面
            self._config.update_user()  # 存储用户信息
            # 刷新文件列表
            # self.list_refresher.set_values(self._work_id)
        else:
            self.show_status(msg, duration)
            self.tabWidget.setCurrentIndex(0)
            self.tabWidget.removeTab(2)
            self.tabWidget.removeTab(1)
            self.disk_tab.setEnabled(False)
            self.rec_tab.setEnabled(False)
            # 更新快捷键与工具栏
            self.toolbar.removeAction(self.logout)  # 登出工具栏
            self.toolbar.removeAction(self.upload)  # 上传文件工具栏
            self.logout.setEnabled(False)
            self.upload.setEnabled(False)
            self.download.setEnabled(False)
            self.delete.setEnabled(False)

    def call_login_launcher(self):
        """登录网盘"""
        self.logout_worker.set_values(update_ui=False)
        self.toolbar.removeAction(self.logout)
        try:
            username = self._config.name
            password = self._config.pwd
            cookie = self._config.cookie
            if not username and not cookie:
                return None
            self.show_status("正在登陆，稍候……", 25000)
            self.login_launcher.set_values(username, password, cookie)
        except Exception as err:
            logger.error(f"Login: err={err}")

    def call_logout(self):
        """登出确认对话框"""
        message_box = QMessageBox(self)
        message_box.setStyleSheet(btn_style)
        message_box.setIcon(QMessageBox.Icon.Question)
        message_box.setWindowTitle("确认登出")
        message_box.setText("提示：登出不会删除已经保存的用户信息！\n\n是否确认登出？")
        message_box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        buttonY = message_box.button(QMessageBox.StandardButton.Yes)
        buttonY.setText('确定')
        buttonN = message_box.button(QMessageBox.StandardButton.No)
        buttonN.setText('取消')
        message_box.accepted.connect(lambda: self.logout_worker.set_values(True))
        message_box.exec()

    def show_file_and_folder_lists(self):
        """显示用户文件和文件夹列表"""
        self.model_disk.removeRows(0, self.model_disk.rowCount())  # 清理旧的内容
        file_count = len(self._file_list)
        folder_count = len(self._folder_list)
        name_header = [f"文件夹{folder_count}个"] if folder_count else []
        if file_count:
            name_header.append(f"文件{file_count}个")
        self.model_disk.setHorizontalHeaderLabels(["/".join(name_header), "大小", "时间"])
        folder_ico = QIcon(SRC_DIR + "folder.gif")
        desc_style = ' <span style="font-size:14px;color:green;text-align:right">'
        pwd_ico = f' <img src="{SRC_DIR}keys.png" width="14" height="14" />'
        dl_count_style = ' <span style="font-size:14px;color:red;text-align:right">'
        if self._work_id != -1:
            _back = QStandardItem(folder_ico, "..")
            _back.setToolTip("双击返回上层文件夹，选中无效")
            self.model_disk.appendRow([_back, QStandardItem(""), QStandardItem("")])
        for infos in self._folder_list.values():  # 文件夹
            tips = ""
            name = QStandardItem()
            name.setData(FolderInfos(infos))
            name.setIcon(folder_ico)
            txt = infos.name + desc_style + infos.desc + "</span>" if infos.desc else infos.name
            if infos.has_pwd:
                tips = "有提取码"
                txt = txt + pwd_ico
                if infos.desc:
                    tips = tips + "，描述：" + str(infos.desc)
            elif infos.desc:
                tips = "描述：" + str(infos.desc)
            name.setText(txt)
            name.setToolTip(tips)
            self.model_disk.appendRow([name, QStandardItem(""), QStandardItem("")])
        for infos in self._file_list.values():  # 文件
            tips = ""
            name = QStandardItem(set_file_icon(infos.name), infos.name)
            name.setData(FileInfos(infos))
            txt = infos.name + desc_style + "有描述" + "</span>" if infos.has_des else infos.name
            if infos.has_pwd:
                tips = "有提取码"
                txt = txt + pwd_ico
                if infos.has_des:
                    tips = tips + "，有描述"
            elif infos.has_des:
                tips = "有描述"
            if infos.downs:
                txt = txt + dl_count_style + str(infos.downs) + "</span>"
            name.setText(txt)
            name.setToolTip(tips)
            size = QStandardItem(infos.size)
            size.setData(format_size_int(infos.size), Qt.ItemDataRole.UserRole)  # 配合MyStandardItem实现正确排序
            self.model_disk.appendRow([name, size, time_table_item(infos)])
        for row in range(self.model_disk.rowCount()):  # 右对齐
            self.model_disk.item(row, 1).setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
            self.model_disk.item(row, 2).setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

    def update_disk_lists(self, infos):
        """更新用户文件列表"""
        if not infos:
            return
        if infos['r']['files']:
            self._file_list = infos['file_list']
        if infos['r']['folders']:
            self._folder_list = infos['folder_list']
            self._path_list = infos['path_list']

        self._work_id = self._path_list[-1].id
        self._config.work_id = self._work_id
        if len(self._path_list) > 1:
            self._parent_id = self._path_list[-2].id
        self.show_file_and_folder_lists()
        if infos['r']['path']:
            self.show_full_path()

    def config_tableview(self, tab):
        """Tab 初始化"""
        if tab == "share":
            model = self.model_share
            table = self.table_share
        elif tab == "disk":
            model = self.model_disk
            table = self.table_disk
        elif tab == "rec":
            model = self.model_rec
            table = self.table_rec
        elif tab == "jobs":
            model = self.model_jobs
            table = self.table_jobs
            table.doubleClicked.connect(self.open_downloaded_file)
        else:
            logger.error(f"Gui config_tableview: tab={tab}")
            return None

        if tab == "jobs":
            model.setHorizontalHeaderLabels(["任务名", "完成度", "状态", "操作"])
            table.horizontalHeader().resizeSection(3, 40)
        else:
            model.setHorizontalHeaderLabels(["文件名", "大小", "时间"])
        table.setItemDelegateForColumn(0, TableDelegate())  # table 支持富文本
        table.setModel(model)
        # 是否显示网格线
        table.setShowGrid(False)
        # 禁止编辑表格
        table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
        # 隐藏水平表头
        table.verticalHeader().setVisible(False)
        # 设置表头可以自动排序
        if tab == 'jobs':
            table.setSortingEnabled(False)
        else:
            table.setSortingEnabled(True)
        table.setMouseTracking(False)
        # 设置表头的背景色为绿色
        table.horizontalHeader().setStyleSheet("QHeaderView::section{background:lightgray}")
        # 设置 不可选择单个单元格，只可选择一行。
        table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
        # 设置第二三列的宽度
        table.horizontalHeader().resizeSection(1, 76)
        table.horizontalHeader().resizeSection(2, 90)
        # 设置第一列宽度自动调整，充满屏幕
        table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)

        if tab != "rec" and tab != "jobs":
            table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)  # 允许右键产生子菜单
            table.customContextMenuRequested.connect(self.generateMenu)  # 右键菜单

    def open_downloaded_file(self, index):
        data = self.model_jobs.itemData(index)
        for item in data.values():
            if not isinstance(item, DlJob):
                continue
            for task in self._tasks.values():
                if not isinstance(task, DlJob):
                    continue
                logger.error("open_downloaded_file state %s %s %s %s %s %s %s " % (task.type == 'dl',
                                                                                   task.name == item.name,
                                                                                   task.path == item.path,
                                                                                   task.is_finished(),
                                                                                   task.rate,
                                                                                   task.current,
                                                                                   task.total_size)
                             )
                if task.type == 'dl' and task.name == item.name and task.path == item.path and task.is_finished():
                    open_file(task)
                    return

    def call_rename_mkdir_worker(self, infos):
        """重命名、修改简介与新建文件夹"""
        self.rename_mkdir_worker.set_values(infos, self._work_id, self._folder_list)

    def set_passwd(self, infos):
        """设置文件(夹)提取码"""
        self.set_pwd_worker.set_values(infos, self._work_id)

    def on_moved(self, r_files=True, r_folders=True, r_path=True):
        """移动文件(夹)后更新界面槽函数"""
        self.list_refresher.set_values(self._work_id, r_files, r_folders, r_path)

    def call_mkdir(self):
        """弹出新建文件夹对话框"""
        self.rename_dialog.set_values()
        self.rename_dialog.exec()

    def call_remove_files(self):
        indexes = []
        infos = []
        _indexes = self.table_disk.selectionModel().selection().indexes()
        if not _indexes:
            return
        for i in _indexes:  # 获取所选行号
            indexes.append(i.row())
        indexes = set(indexes)
        for index in indexes:
            info = self.model_disk.item(index, 0).data()  # 用于提示删除的文件名
            if info:
                infos.append(info)
        delete_dialog = DeleteDialog(infos)
        delete_dialog.new_infos.connect(self.remove_files_worker.set_values)
        delete_dialog.exec()

    def call_copy_share_url(self, infos):
        self.pause_extract_clipboard(True)
        text = infos.url + ' 提取码：' + infos.pwd if infos.has_pwd else infos.url
        self.clipboard.setText(text)
        QTimer.singleShot(500, self.pause_extract_clipboard)
        self.show_status("已复制到剪切板", 2999)

    def generateMenu(self, pos):
        """右键菜单"""
        row_nums = self.sender().selectionModel().selection().indexes()
        if not row_nums:  # 如果没有选中行，什么也不做
            return
        _model = self.sender().model()
        infos = []  # 多个选中的行，用于移动文件与...
        for one_row in row_nums:
            row_data = _model.item(one_row.row(), 0).data()
            if row_data and row_data not in infos:  # 删掉 .. 行
                if isinstance(row_data, FileInfos):
                    infos.append(row_data)
                else:
                    infos.append(row_data)
        if not infos:
            return
        info = infos[0]  # 取选中的第一行
        # 通过是否有文件 ID 判断是登录界面还是提取界面
        if isinstance(info, (FileInfos, FolderInfos)):
            self.left_menu_rename_set_desc.setEnabled(True)
            self.left_menu_set_pwd.setEnabled(True)
            self.left_menu_move.setEnabled(True)
            # 文件夹不能移动，设置不同的显示菜单名
            if isinstance(info, FileInfos):
                self.left_menu_rename_set_desc.setText("修改文件描述（支持批量）")
            else:
                self.left_menu_rename_set_desc.setText("修改文件夹名与描述")
            if info.has_pwd:
                self.left_menu_copy.setText("复制分享链接与提取码")
            else:
                self.left_menu_copy.setText("复制分享链接")
        else:
            self.left_menu_rename_set_desc.setDisabled(True)
            self.left_menu_move.setDisabled(True)
            self.left_menu_set_pwd.setDisabled(True)

        action = self.left_menus.exec(self.sender().mapToGlobal(pos))
        if action == self.left_menu_share_url:  # 显示详细信息
            # 后台跟新信息，并显示信息对话框
            if isinstance(info, ShareItem):
                info = ShareFileInfos(info.item)  # 提取界面 info 在第一个位置
            self.more_info_worker.set_values(info)
            self.info_dialog.exec()
        elif action == self.left_menu_move:  # 移动文件
            self.all_folders_worker.set_values(infos)
        elif action == self.left_menu_set_pwd:  # 修改提取码
            if len(infos) == 1:
                self.desc_pwd_fetcher.set_values([info, ])  # 兼容下载器，使用列表的列表
            self.set_pwd_dialog.set_values(infos)
            self.set_pwd_dialog.exec()
        elif action == self.left_menu_rename_set_desc:  # 重命名与修改描述
            if len(infos) == 1:
                self.desc_pwd_fetcher.set_values([info, ])  # 兼容下载器，使用列表的列表
            self.rename_dialog.set_values(infos)
            self.rename_dialog.exec()
        elif action == self.left_menu_copy:  # 复制分享链接
            if isinstance(info, tuple) and len(info) == 3:
                info = ShareFileInfos(info[0])  # 提取界面 info 在第一个位置
            self.more_info_worker.set_values(info, emit_link=True)

    def call_update_desc_pwd(self, infos):
        """更新 desc、pwd"""
        self.rename_dialog.set_values(infos)
        self.set_pwd_dialog.set_values(infos)

    def call_change_dir(self, folder_id=-1):
        """顶部路径按钮调用"""

        def callfunc():
            self.list_refresher.set_values(folder_id)

        return callfunc

    def change_disk_dir(self, dir_name):
        """双击切换工作目录"""
        item = self.model_disk.item(dir_name.row(), 0)
        if item.text() == "..":  # 返回上级路径
            self.list_refresher.set_values(self._parent_id)
            return None
        dir_name = item.data().name  # 文件夹名
        if dir_name in self._folder_list.keys():
            folder_id = self._folder_list[dir_name].id
            self.list_refresher.set_values(folder_id)
        else:
            self.call_multi_manipulator("download")

    def call_upload(self, tasks: dict):
        """上传文件(夹)"""
        self._old_work_id = self._work_id  # 记录上传文件夹id
        self.task_manager.add_tasks(tasks)
        self.tabWidget.insertTab(3, self.jobs_tab, "任务管理")
        self.jobs_tab.setEnabled(True)
        self._tasks.add(tasks)

    def show_full_path(self):
        """路径框显示当前路径"""
        index = 1
        for _ in iter(self._path_list_old):
            self._locs[index].clicked.disconnect()
            self.disk_loc_hbox.removeWidget(self._locs[index])
            self._locs[index].deleteLater()
            self._locs[index] = None
            del self._locs[index]
            index += 1
        index = 1
        for item in iter(self._path_list):
            self._locs[index] = QPushButton(item.name, self.disk_tab)
            tip = f"fid:{item.id} | 描述:{item.desc}" if item.desc else f"fid:{item.id}"
            self._locs[index].setToolTip(tip)
            self._locs[index].setIcon(QIcon(SRC_DIR + "folder.gif"))
            self._locs[index].setStyleSheet("QPushButton {border:none; background:transparent;\
                                            color: rgb(139,0,139); font-weight:bold;}")
            self.disk_loc_hbox.insertWidget(index, self._locs[index])
            self._locs[index].clicked.connect(self.call_change_dir(item.id))
            index += 1
        self._path_list_old = self._path_list

    def select_all_btn(self, action="reverse"):
        """默认反转按钮状态"""
        page = self.tabWidget.currentIndex()
        if page == self.tabWidget.indexOf(self.share_tab):
            btn = self.btn_share_select_all
            table = self.table_share
        elif page == self.tabWidget.indexOf(self.disk_tab):
            btn = self.btn_disk_select_all
            table = self.table_disk
        elif page == self.tabWidget.indexOf(self.rec_tab):
            btn = self.btn_rec_select_all
            table = self.table_rec
        else:
            return
        if btn.isEnabled():
            if action == "reverse":
                if btn.text() == "全选":
                    table.selectAll()
                    btn.setText("取消")
                    btn.setIcon(QIcon(SRC_DIR + "select_none.png"))
                elif btn.text() == "取消":
                    table.clearSelection()
                    btn.setText("全选")
                    btn.setIcon(QIcon(SRC_DIR + "select_all.png"))
            elif action == "cancel":  # 点击列表其中一个就表示放弃全选
                btn.setText("全选")
                btn.setIcon(QIcon(SRC_DIR + "select_all.png"))
            else:
                table.selectAll()
                btn.setText("取消")
                btn.setIcon(QIcon(SRC_DIR + "select_none.png"))

    def call_delete_shortcut(self):
        if self.tabWidget.currentIndex() == self.tabWidget.indexOf(self.disk_tab):
            self.call_remove_files()

    def call_download_shortcut(self):
        if self.tabWidget.currentIndex() == self.tabWidget.indexOf(self.disk_tab):
            self.call_multi_manipulator("download")

    # disk tab
    def init_disk_ui(self):
        self.model_disk = QStandardItemModel(1, 3)
        self.config_tableview("disk")
        self.btn_disk_delete.setIcon(QIcon(SRC_DIR + "delete.png"))
        self.btn_disk_delete.setToolTip("按下 Ctrl + D 删除选中文件")
        self.btn_disk_dl.setIcon(QIcon(SRC_DIR + "downloader.png"))
        self.btn_disk_dl.setToolTip("按下 Ctrl + J 下载选中文件")
        self.btn_disk_select_all.setIcon(QIcon(SRC_DIR + "select_all.png"))
        self.btn_disk_select_all.setToolTip("按下 Ctrl/Alt + A 全选或则取消全选")
        self.btn_disk_select_all.clicked.connect(lambda: self.select_all_btn("reverse"))
        self.table_disk.clicked.connect(lambda: self.select_all_btn("cancel"))
        self.btn_disk_dl.clicked.connect(lambda: self.call_multi_manipulator("download"))
        self.btn_disk_mkdir.setIcon(QIcon(SRC_DIR + "add_folder.png"))
        self.btn_disk_mkdir.clicked.connect(self.call_mkdir)
        self.btn_disk_delete.clicked.connect(self.call_remove_files)
        # 文件拖拽上传
        self.table_disk.drop_files.connect(self.show_upload_dialog)
        self.table_disk.doubleClicked.connect(self.change_disk_dir)

    # rec tab
    def pop_up_rec_folder_dialog(self, files):
        # 弹出回收站文件夹内容对话框
        if files:
            rec_file_dialog = RecFolderDialog(files)
            rec_file_dialog.exec()
        else:
            self.show_status("文件夹为空！", 2999)

    def call_rec_folder_dialog(self, dir_name):
        # 显示弹出对话框
        dir_data = self.model_rec.item(dir_name.row(), 0).data()  # 文件夹信息
        if isinstance(dir_data, RecFolder):
            self.show_status(f"正在获取文件夹 {dir_data.name} 信息，稍候", 10000)
            self.get_rec_lists_worker.set_values(dir_data.id)

    def update_rec_lists(self, dir_lists, file_lists):
        """显示回收站文件和文件夹列表"""
        self.model_rec.removeRows(0, self.model_rec.rowCount())  # 清理旧的内容
        file_count = len(file_lists)
        folder_count = len(dir_lists)
        if (not dir_lists and not file_lists) or (file_count == 0 and folder_count == 0):
            self.show_status("回收站为空！", 4000)
            return
        name_header = ["文件夹{}个".format(folder_count), ] if folder_count else []
        if file_count:
            name_header.append("文件{}个".format(file_count))
        self.model_rec.setHorizontalHeaderLabels(["/".join(name_header), "大小", "时间"])
        folder_ico = QIcon(SRC_DIR + "folder.gif")

        for item in iter(dir_lists):  # 文件夹
            name = QStandardItem(folder_ico, item.name)
            name.setData(item)
            name.setToolTip("双击查看详情")
            size_ = QStandardItem(item.size)
            print("rec time_table_item dir")
            time_ = time_table_item(item)
            # time_ = QStandardItem(item.time)
            self.model_rec.appendRow([name, size_, time_])
        for item in iter(file_lists):  # 文件
            name = QStandardItem(set_file_icon(item.name), item.name)
            name.setData(item)
            size_ = QStandardItem(item.size)
            print("rec time_table_item")
            time_ = time_table_item(item)
            # time_ = QStandardItem(item.time)
            self.model_rec.appendRow([name, size_, time_])
        for row in range(self.model_rec.rowCount()):  # 右对齐
            self.model_rec.item(row, 1).setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
            self.model_rec.item(row, 2).setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

    def init_rec_ui(self):
        """回收站ui初始化"""
        self.model_rec = QStandardItemModel(1, 3)
        self.config_tableview("rec")
        self.table_rec.doubleClicked.connect(self.call_rec_folder_dialog)
        self.btn_rec_select_all.setIcon(QIcon(SRC_DIR + "select_all.png"))
        self.btn_rec_select_all.clicked.connect(lambda: self.select_all_btn("reverse"))
        self.btn_rec_delete.clicked.connect(lambda: self.call_multi_manipulator("delete"))
        self.btn_rec_delete.setIcon(QIcon(SRC_DIR + "delete.png"))
        self.btn_recovery.clicked.connect(lambda: self.call_multi_manipulator("recovery"))
        self.btn_recovery.setIcon(QIcon(SRC_DIR + "rec_folder.png"))
        self.btn_rec_delete.setToolTip("彻底删除选中文件(夹)")
        self.btn_recovery.setToolTip("恢复选中文件(夹)")
        self.btn_recovery_all.clicked.connect(lambda: self.call_multi_manipulator("recovery_all"))
        self.btn_recovery_all.setIcon(QIcon(SRC_DIR + "rec_folder.png"))
        self.btn_recovery_all.setToolTip("恢复全部")
        self.btn_rec_clean.clicked.connect(lambda: self.call_multi_manipulator("clean"))
        self.btn_rec_clean.setIcon(QIcon(SRC_DIR + "rec_bin.png"))
        self.btn_rec_clean.setToolTip("清理回收站全部")
        self.btn_rec_expire_files.setToolTip("暂时无效！")

    # shared url
    def call_get_shared_info(self):
        """提取分享链接槽函数"""
        if not self.get_shared_info_thread.isRunning():  # 防止快速多次调用
            self._extract_show_dir = []
            self._extract_folder_list = {}
            self._extract_setted_head = False
            self._extract_count = 0
            self.line_share_url.setEnabled(False)
            self.line_share_pwd.setEnabled(False)
            self.btn_extract.setEnabled(False)
            text = self.line_share_url.text().strip()
            input_pwd = self.line_share_pwd.text().strip()
            self.model_share.setHorizontalHeaderLabels(['文件(夹)', "大小", "时间"])
            self.get_shared_info_thread.set_values(text, input_pwd)

    def show_share_folder_url_lists(self, infos, root_dir=[], show_dir=[]):
        # infos: 文件夹 FolderDetail -> code, folder, files, sub_folders
        folder_ico = QIcon(SRC_DIR + "folder.gif")
        if not (show_dir or self._extract_setted_head):
            self._extract_setted_head = True
            all_file = infos.folder.count  # 包括子文件夹的所有文件
            file_count = len(infos.files)  # 父文件夹中的文件数量
            folder_count = len(infos.sub_folders) or 0
            if self._show_subfolder:
                self._extract_count = all_file + folder_count  # 标记是否为全选下载
            else:
                self._extract_count = file_count + folder_count  # 标记是否为全选下载
            dots = '...' if len(infos.folder.desc) > 60 else ''
            desc = "| " + infos.folder.desc.replace('\n', ' ')[:60] + dots if infos.folder.desc else ""
            title = f"{infos.folder.name} | 文件{file_count}个 "
            if folder_count:
                title += f"| 子文件夹{folder_count}个 "
            if self._show_subfolder and all_file != file_count:
                title += f"| 递归文件{all_file}个 "
            title += f"｜ 总共 {infos.folder.size} {desc}"
            self.model_share.setHorizontalHeaderLabels([title, "大小", "时间"])

        if len(show_dir) == 1:
            _back = QStandardItem(folder_ico, "..")
            _back.setToolTip("双击返回上层文件夹，选中无效")
            _back.setData(ShareItem(all=infos))
            self.model_share.appendRow([_back, QStandardItem(""), QStandardItem("")])
        for sub_folder in iter(infos.sub_folders):  # 展示子文件夹
            if sub_folder.folder.name:  # 没有密码的子文件夹不展示在提取界面
                name = QStandardItem(folder_ico, sub_folder.folder.name)
                if root_dir:
                    root_path = [*show_dir, infos.folder.url]
                    post_root_dir = [*root_dir, sub_folder.folder.name]
                    pre_root_dir = f'<span style="font-size:14px;color:pink;text-align:right">{"/".join(root_dir)}/</span>'
                else:
                    root_path = [infos.folder.url, ]
                    pre_root_dir = ''
                    post_root_dir = [sub_folder.folder.name, ]
                if sub_folder.folder.desc:
                    text = ' <span style="font-size:14px;color:green;text-align:right">{}</span>'.format(
                        sub_folder.folder.desc.replace("\n", " "))
                else:
                    text = ''
                set_data = ShareItem(sub_folder.folder, infos, self._extract_count, root_dir)
                if not show_dir:
                    name.setData(set_data)
                    name.setText(pre_root_dir + sub_folder.folder.name + text)
                    if self.time_fmt:
                        time = QStandardItem(time_format(sub_folder.folder.time))
                    else:
                        time = time_table_item(sub_folder.folder)

                    size = QStandardItem(sub_folder.folder.size)
                    size.setData(format_size_int(sub_folder.folder.size), Qt.ItemDataRole.UserRole)
                    self.model_share.appendRow([name, size, time])
                if self._show_subfolder or show_dir:  # 在当前窗口递归展示子文件夹中的文件
                    self.show_share_folder_url_lists(sub_folder, root_dir=post_root_dir, show_dir=show_dir[1:])
                else:  # 不递归展示才会记录子文件夹信息
                    self._extract_folder_list[sub_folder.folder.url] = root_path
        if show_dir:
            return None
        for item in iter(infos.files):  # 展示文件
            if item:
                if root_dir:
                    pre_root_dir = f'<span style="font-size:14px;color:pink;text-align:right">{"/".join(root_dir)}/</span>'
                else:
                    pre_root_dir = ''
                name = QStandardItem(set_file_icon(item.name), item.name)
                name.setData(ShareItem(item, infos, self._extract_count, root_dir))
                name.setText(pre_root_dir + item.name)
                size = QStandardItem(item.size)
                size.setData(format_size_int(item.size), Qt.ItemDataRole.UserRole)
                # time = QStandardItem(time_format(item.time)) if self.time_fmt else QStandardItem(item.time)
                if self.time_fmt:
                    time = QStandardItem(time_format(item.time))
                else:
                    time = time_table_item(item)
                self.model_share.appendRow([name, size, time])

    def show_share_url_file_lists(self, infos):
        if infos.code == LanZouCloud.SUCCESS:
            if isinstance(infos, FolderDetail):  # 文件夹 FolderDetail -> code, folder, files, sub_folders
                self.show_share_folder_url_lists(infos)
            else:  # 单文件
                name = QStandardItem(set_file_icon(infos.name), infos.name)
                name.setData(ShareItem(item=infos))
                print("show url dir")
                item = time_table_item(infos)
                self.model_share.appendRow([name, QStandardItem(infos.size), item])
                self.model_share.setHorizontalHeaderLabels(["文件名", "大小", "时间"])
            for r in range(self.model_share.rowCount()):  # 右对齐
                self.model_share.item(r, 1).setTextAlignment(
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
                self.model_share.item(r, 2).setTextAlignment(
                    Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
            self.table_share.setDisabled(False)
            self.btn_share_select_all.setDisabled(False)
            self.btn_share_select_all.setToolTip("按下 Ctrl/Alt + A 全选或则取消全选")
            self.btn_share_dl.setDisabled(False)
        else:
            self.btn_share_select_all.setDisabled(True)
            self.btn_share_select_all.setToolTip("")
            self.btn_share_dl.setDisabled(True)
            self.table_share.setDisabled(True)

    def set_download_path(self):
        """设置下载路径"""
        dl_path = QFileDialog.getExistingDirectory(self, "选择文件下载保存文件夹", self._config.path)
        dl_path = os.path.normpath(dl_path)  # windows backslash
        if dl_path == self._config.path or dl_path == ".":
            return None
        elif dl_path == "":
            self._config.default_path()
        else:
            self._config.path = dl_path
        self.share_set_dl_path.setText(self._config.path)

    def change_share_dir(self, dir_name):
        """双击子文件夹改变路径"""
        all_infos = self.model_share.item(dir_name.row(), 0).data()  #
        if self.model_share.item(dir_name.row(), 0).text() == "..":  # 返回父文件夹
            if self._extract_show_dir:
                self.model_share.removeRows(0, self.model_share.rowCount())
                self.show_share_folder_url_lists(all_infos.all, show_dir=self._extract_show_dir[:-1])
        elif all_infos.item.url in self._extract_folder_list.keys():
            show_url = self._extract_folder_list[all_infos.item.url]
            self._extract_show_dir = show_url
            self.model_share.removeRows(0, self.model_share.rowCount())
            self.show_share_folder_url_lists(all_infos.all, show_dir=show_url)

    def call_show_share_url_sub_folder(self, infos):
        """切换展示子文件夹按钮"""
        if self._show_subfolder:
            self._show_subfolder = False
            self._share_url_show_subfolder.setText("递归展示")
            self._share_url_show_subfolder.setToolTip("将子文件递归展示")
        else:
            self._show_subfolder = True
            self._extract_folder_list = {}  # 递归展示时不记录子文件夹信息
            self._share_url_show_subfolder.setText("收起文件")
            self._share_url_show_subfolder.setToolTip("将子文件收起，不展示")
        self.model_share.removeRows(0, self.model_share.rowCount())
        self.show_share_folder_url_lists(infos)

    def show_share_url_judge_folder(self, infos):
        """判断是否包含子文件夹"""
        try:
            if infos.sub_folders:
                logger.debug('Have subfolders!')
                self._share_url_show_subfolder = QPushButton("递归展示", self.share_tab)
                self._share_url_show_subfolder.setToolTip("将子文件递归展示")
                self._share_url_show_subfolder.setIcon(QIcon(SRC_DIR + "folder.gif"))
                self.share_hlayout_top.addWidget(self._share_url_show_subfolder)
                self._share_url_show_subfolder.clicked.connect(lambda: self.call_show_share_url_sub_folder(infos))
            else:
                logger.debug('Have No subfolders!')
                self.share_hlayout_top.removeWidget(self._share_url_show_subfolder)
                self._share_url_show_subfolder.deleteLater()
                self._share_url_show_subfolder = None
                del self._share_url_show_subfolder
        except:
            pass

    def init_extract_share_ui(self):
        self.btn_share_select_all.setDisabled(True)
        self.btn_share_dl.setDisabled(True)
        self.table_share.setDisabled(True)
        self.model_share = QStandardItemModel(1, 3)
        self.config_tableview("share")

        # 清理旧的信息
        self.get_shared_info_thread.clean.connect(lambda: self.model_share.removeRows(0, self.model_share.rowCount()))
        self.get_shared_info_thread.msg.connect(self.show_status)  # 提示信息
        self.get_shared_info_thread.infos.connect(self.show_share_url_judge_folder)  # 判断是否包含子文件夹
        self.get_shared_info_thread.infos.connect(self.show_share_url_file_lists)  # 内容信息
        self.get_shared_info_thread.update.connect(lambda: self.btn_extract.setEnabled(True))
        self.get_shared_info_thread.update.connect(lambda: self.line_share_url.setEnabled(True))
        self.get_shared_info_thread.update.connect(lambda: self.line_share_pwd.setEnabled(True))
        self.table_share.doubleClicked.connect(self.change_share_dir)  # 双击
        # 控件设置
        self.line_share_url.setPlaceholderText(
            "蓝奏云链接，如有提取码，放后面，空格或汉字等分割(汉字、特殊符号提取码手动右侧输入！)，回车键提取")
        self.line_share_url.returnPressed.connect(self.call_get_shared_info)
        self.line_share_url.setFocus()  # 光标焦点
        self.line_share_pwd.setPlaceholderText("特殊提取码")
        self.line_share_pwd.setFixedWidth(100)  # 提取码输入框 宽度
        self.btn_extract.clicked.connect(self.call_get_shared_info)
        self.btn_share_dl.clicked.connect(lambda: self.call_multi_manipulator("download"))
        self.btn_share_dl.setIcon(QIcon(SRC_DIR + "downloader.png"))
        self.btn_share_select_all.setIcon(QIcon(SRC_DIR + "select_all.png"))
        self.btn_share_select_all.clicked.connect(lambda: self.select_all_btn("reverse"))
        self.table_share.clicked.connect(lambda: self.select_all_btn("cancel"))  # 全选按钮

        self.extract_input_shortcut = QShortcut(QKeySequence("Ctrl+F"), self)
        self.extract_input_shortcut.activated.connect(self.call_extract_input_shortcut)

        # 添加文件下载路径选择器
        self.share_set_dl_path.setText(self._config.path)
        self.share_set_dl_path.clicked.connect(self.set_download_path)

    def call_extract_input_shortcut(self):
        """Ctrl+F槽函数"""
        self.tabWidget.setCurrentIndex(self.tabWidget.indexOf(self.share_tab))
        self.line_share_url.selectAll()  # 全选中
        self.line_share_url.setFocus()  # 焦点

    # jobs tab
    def call_jobs_clean_all(self):
        self._tasks.clear()
        self.show_jobs_lists()

    def call_jobs_start_all(self):
        for task in self._tasks.values():
            if task.pause:
                task.pause = False
        self.task_manager.start()
        self.show_jobs_lists()

    def call_show_mgr_finished(self, num: int):
        if num:
            self.show_status(f"所有任务已完成！ <font color='red'>{num} 个任务失败</font>", 2999)
        else:
            self.show_status("所有任务已完成！", 2999)
        # 上传完成调用
        if self._old_work_id == self._work_id:
            self.list_refresher.set_values(self._work_id, True, True, False)
        else:
            self._old_work_id = self._work_id
        self.show_jobs_lists()

    def update_jobs_info(self):
        self._tasks.update()
        self.show_jobs_lists()

    def redo_job(self, task):
        logger.debug(f"re do job task={task}")
        self.task_manager.add_task(task)

    def start_work_job(self, task):
        self.task_manager.start_task(task)

    def stop_work_job(self, task):
        self.task_manager.stop_task(task)

    def del_work_job(self, task):
        self.task_manager.del_task(task)
        self._tasks.clear(task)
        self.show_jobs_lists()

    def show_jobs_lists(self):
        """显示任务列表"""
        self.model_jobs.removeRows(0, self.model_jobs.rowCount())  # 清理旧的内容
        download_ico = QIcon(SRC_DIR + "download.png")
        upload_ico = QIcon(SRC_DIR + "upload.png")
        path_style = ' <span style="font-size:14px;color:green;text-align:right">'
        error_style = ' <span style="font-size:14px;color:red;text-align:right">'
        _index = 0
        for task in self._tasks.values():
            name = QStandardItem()
            if task.type == 'dl':
                name.setIcon(download_ico)
                txt = task.name + '  ' + path_style + task.size + '|' + task.speed + '|' + task.prog \
                      + ' ➩ ' + task.path + "</span>"
            else:
                name.setIcon(upload_ico)
                txt = str(task.url[-100:]) + '  ' + path_style + change_size_unit(task.total_size) + '|' \
                      + task.speed + '|' + task.prog + ' ➩ ' + str(task.folder) + "</span>"
            if task.info:
                txt = txt + error_style + str(task.info) + "</span>"
            name.setText(txt)
            name.setToolTip(txt)
            name.setData(task)
            rate = "{:5.1f}%".format(task.rate / 10)
            percent = QStandardItem(rate)  # percent
            self.model_jobs.appendRow([name, percent, QStandardItem(""), QStandardItem("")])

            _status = QPushButton()
            _status.resize(_status.sizeHint())
            _action = QPushButton()
            _action.resize(_action.sizeHint())

            # _status.setDisabled(True)
            if task.is_finished():
                if task.type == "dl":
                    _status.setText("打开文件夹")
                    _status.clicked.connect(lambda: open_in_explorer(task))
                else:
                    _status.setText("已完成")
                _status.setStyleSheet(jobs_btn_completed_style)
                _action.setText("移除")
                _action.clicked.connect(lambda: self.del_work_job(task))
                _action.setStyleSheet(jobs_btn_delete_style)
            elif task.info:
                _status.setText("出错了")
                _status.setStyleSheet(jobs_btn_redo_style)
                if task.run:
                    _action.setText("暂停")
                    _action.clicked.connect(lambda: self.stop_work_job(task))
                else:
                    _action.setText("重试")
                    _action.clicked.connect(lambda: self.redo_job(task))
                _action.setStyleSheet(jobs_btn_completed_style)
            else:
                if task.run:
                    if task.type == 'dl':
                        _status.setText("下载中")
                    else:
                        _status.setText("上传中")
                    _status.setStyleSheet(jobs_btn_processing_style)
                    _action.setText("暂停")
                    _action.clicked.connect(lambda: self.stop_work_job(task))
                    _action.setStyleSheet(jobs_btn_completed_style)
                else:
                    if task.added:
                        _status.setText("排队中")
                        _status.setStyleSheet(jobs_btn_queue_style)

                        _action.setText("移除")
                        _action.clicked.connect(lambda: self.del_work_job(task))
                    else:
                        _status.setText("暂停中")
                        _status.setStyleSheet(jobs_btn_redo_style)

                        _action.setText("开始")
                        _action.clicked.connect(lambda: self.start_work_job(task))
                _action.setStyleSheet(jobs_btn_completed_style)

            self.table_jobs.setIndexWidget(self.model_jobs.index(_index, 2), _status)
            self.table_jobs.setIndexWidget(self.model_jobs.index(_index, 3), _action)
            _index += 1

        for row in range(self.model_jobs.rowCount()):  # 右对齐
            self.model_jobs.item(row, 1).setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter)
            self.model_jobs.item(row, 2).setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)

    def init_jobs_ui(self):
        """初始化上传下载任务管理界面"""
        self.model_jobs = QStandardItemModel(1, 4)
        self.config_tableview("jobs")
        self.jobs_tab.setEnabled(True)
        self.btn_jobs_clean_all.clicked.connect(self.call_jobs_clean_all)  # 信号
        self.btn_jobs_start_all.clicked.connect(self.call_jobs_start_all)  # 信号

    # others
    def clean_status(self):
        self.statusbar_msg_label.setText("")
        self.statusbar_load_lb.clear()
        self.statusbar_load_movie.stop()

    def show_status(self, msg, duration=0):
        self.statusbar_msg_label.setText(msg)
        if msg and duration >= 3000:
            self.statusbar_load_lb.clear()
            self.statusbar_load_movie.stop()
            ht = self.statusbar_msg_label.size().height()
            self.statusbar_load_movie.setScaledSize(QSize(ht, ht))
            self.statusbar_load_lb.setMovie(self.statusbar_load_movie)
            self.statusbar_load_movie.start()
        else:
            self.statusbar_load_lb.clear()
            self.statusbar_load_movie.stop()
        if duration != 0:
            QTimer.singleShot(duration, self.clean_status)

    def call_change_tab(self):
        """切换标签页 动作"""
        tab_index = self.tabWidget.currentIndex()
        if tab_index == self.tabWidget.indexOf(self.share_tab):  # share 界面
            if self.get_shared_info_thread.isRunning():
                self.show_status("正在获取文件夹链接信息，可能需要几秒钟，请稍候……", 500000)
        elif tab_index == self.tabWidget.indexOf(self.rec_tab):  # rec 界面
            if not self.statusbar_msg_label.text():
                self.show_status("正在更新回收站...", 10000)
            self.get_rec_lists_worker.start()
        elif tab_index == self.tabWidget.indexOf(self.disk_tab):  # disk 界面
            if not self.statusbar_msg_label.text():
                self.show_status("正在更新当前目录...", 10000)
            self.list_refresher.set_values(self._work_id)
        elif tab_index == self.tabWidget.indexOf(self.jobs_tab):  # jobs 界面
            self.show_jobs_lists()

    def keyPressEvent(self, e):
        if e.key() == Qt.Key.Key_A:  # Ctrl/Alt + A 全选
            if e.modifiers() and Qt.ControlModifier:
                self.select_all_btn()
        elif e.key() == Qt.Key.Key_F5:  # 刷新
            self.call_change_tab()

    def open_wiki_url(self):
        # 打开使用说明页面
        url = QUrl('https://github.com/rachpt/lanzou-gui/wiki')
        if not QDesktopServices.openUrl(url):
            self.show_status('Could not open wiki page!', 5000)

    def show_new_version_msg(self, ver, msg):
        message_box = QMessageBox(self)
        message_box.setIcon(QMessageBox.Icon.Information)
        message_box.setStyleSheet(btn_style)
        message_box.setWindowTitle(f"检测到新版 {ver}")
        message_box.setText(msg)
        message_box.setStandardButtons(QMessageBox.StandardButton.Close)
        buttonC = message_box.button(QMessageBox.StandardButton.Close)
        buttonC.setText('关闭')
        message_box.exec()

    def closeEvent(self, event):
        if self._to_tray:
            event.ignore()
            self.hide()
            if self._show_to_tray_msg:
                self.tray.showMessage(
                    "已经最小化到托盘",
                    "双击显示/隐藏窗口，退出请右击",
                    QSystemTrayIcon.MessageIcon.Information,
                    3000
                )
                self._show_to_tray_msg = False  # 提示一次

    def Exit(self):
        # 点击关闭按钮或者点击退出事件会出现图标无法消失的bug，那就先隐藏吧(｡･ω･｡)
        self.tray.hide()
        del self.tray
        sys.exit(0)

    def pause_extract_clipboard(self, show=False):
        """登录文件界面屏蔽剪切板监听功能"""
        if show:
            self._watch_clipboard_old = self.watch_clipboard
            self.watch_clipboard = False
        else:
            try:
                self.watch_clipboard = self._watch_clipboard_old
            except:
                pass

    def auto_extract_clipboard(self):
        if not self.watch_clipboard:
            return
        text = self.clipboard.text()
        pat = r"(https?://(\w[-\w]*\.)?lanzou[a-z].com/[a-z]?[-/a-zA-Z0-9]+)[^a-zA-Z0-9]*([a-zA-Z0-9]+)?"
        for share_url, _, pwd in re.findall(pat, text):
            if share_url and not self.get_shared_info_thread.isRunning():
                self.line_share_url.setEnabled(False)
                self.line_share_pwd.setEnabled(False)
                self.btn_extract.setEnabled(False)
                txt = share_url + "提取码：" + pwd if pwd else share_url
                self.line_share_url.setText(txt)
                self.get_shared_info_thread.set_values(txt)
                self.tabWidget.setCurrentIndex(0)
                self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)  # 窗口最前
                self.show()
                self.setWindowFlags(Qt.WindowType.WindowCloseButtonHint | Qt.WindowType.WindowMinMaxButtonsHint)  # 窗口恢复
                self.show()
                break

    def clipboard_monitor(self):
        """监听系统剪切板"""
        self.clipboard = QApplication.clipboard()
        self.clipboard.dataChanged.connect(self.auto_extract_clipboard)
