main_gui.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3. '''
  4. @Contact : liuyuqi.gov@msn.cn
  5. @Time : 2023/04/27 02:55:59
  6. @License : Copyright © 2017-2022 liuyuqi. All Rights Reserved.
  7. @Desc : repo_sync GUI入口
  8. 打包说明:
  9. 使用PyInstaller可以将此应用打包为单个可执行文件:
  10. 1. 安装必要的包:
  11. pip install pyinstaller
  12. pip install -e . # 以开发模式安装当前包
  13. 2. 打包命令:
  14. pyinstaller --onefile --windowed --icon=icon.ico --add-data "repo_sync/config.yml;repo_sync" main_gui.py
  15. (如果有图标文件,可以通过--icon参数指定)
  16. 3. 打包后的可执行文件将位于dist目录中
  17. 注意:确保在打包前先安装repo_sync包,这样PyInstaller才能正确找到所有依赖。
  18. '''
  19. import sys
  20. import os
  21. import threading
  22. import subprocess
  23. import yaml
  24. try:
  25. from PyQt5.QtWidgets import (
  26. QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
  27. QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox,
  28. QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
  29. QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton, QListWidget, QListWidgetItem,
  30. QSpacerItem
  31. )
  32. from PyQt5.QtCore import Qt, pyqtSignal, QObject
  33. HAS_QT = True
  34. except ImportError:
  35. HAS_QT = False
  36. print("PyQt5 not installed, GUI mode not available")
  37. # 直接导入repo_sync模块
  38. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  39. try:
  40. from repo_sync.repo_sync import RepoSync
  41. from repo_sync.version import __version__
  42. from repo_sync.utils.config_reader import ConfigReader
  43. except ImportError:
  44. print("无法导入repo_sync模块,尝试直接导入...")
  45. try:
  46. from repo_sync import RepoSync
  47. from version import __version__
  48. from utils.config_reader import ConfigReader
  49. except ImportError:
  50. print("导入repo_sync模块失败,请确保repo_sync已正确安装")
  51. __version__ = "未知"
  52. # 创建一个空的RepoSync类作为替代
  53. class RepoSync:
  54. def __init__(self, params):
  55. self.params = params
  56. def run(self):
  57. print("RepoSync模块未找到,无法执行操作")
  58. # 确保config.yml文件存在
  59. def ensure_config_file():
  60. config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yml')
  61. if not os.path.exists(config_file):
  62. # 创建空的config.yml文件
  63. default_config = {
  64. 'accounts': {
  65. 'github': {
  66. 'enable': 1,
  67. '1': {
  68. 'username': '',
  69. 'token': '',
  70. 'private': True
  71. }
  72. }
  73. },
  74. 'log': {
  75. 'level': 'debug',
  76. 'file': '/tmp/repo_sync.log',
  77. 'max_size': '100MB',
  78. 'max_backups': 3,
  79. 'max_age': 7,
  80. 'console_formatter': {
  81. 'level': 'debug',
  82. 'format': '%(asctime)s - %(levelname)s - %(message)s'
  83. },
  84. 'file_formatter': {
  85. 'level': 'debug',
  86. 'format': '%(asctime)s - %(levelname)s - %(message)s'
  87. }
  88. }
  89. }
  90. with open(config_file, 'w', encoding='utf-8') as f:
  91. yaml.dump(default_config, f, default_flow_style=False)
  92. return config_file
  93. class SettingsTab(QWidget):
  94. def __init__(self, parent=None):
  95. super().__init__(parent)
  96. # 确保config.yml文件存在
  97. ensure_config_file()
  98. # 初始化account_lists字典
  99. self.account_lists = {}
  100. self.config_reader = ConfigReader()
  101. self.init_ui()
  102. def init_ui(self):
  103. layout = QVBoxLayout()
  104. # 创建平台选择标签页
  105. self.platform_tabs = QTabWidget()
  106. # 平台配置
  107. self.platform_configs = {
  108. "github": ["username", "token", "private"],
  109. "gitlab": ["host", "username", "token", "private"],
  110. "gitee": ["username", "token", "private"],
  111. "gitcode": ["username", "token", "private"],
  112. "git.yoq.me": ["username", "token", "private"],
  113. "coding": ["username", "token", "project", "private"],
  114. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  115. "cnb": ["group", "username", "token", "private"]
  116. }
  117. # 为每个平台创建标签页
  118. self.platform_pages = {}
  119. for platform in self.platform_configs.keys():
  120. page = QWidget()
  121. page_layout = QVBoxLayout()
  122. page_layout.setSpacing(15) # 增加垂直间距
  123. # 创建水平布局,使账户列表和账户详情并排显示
  124. accounts_details_layout = QHBoxLayout()
  125. accounts_details_layout.setSpacing(20) # 增加水平间距
  126. # 账户列表
  127. account_group = QGroupBox("Accounts")
  128. account_layout = QVBoxLayout()
  129. account_layout.setSpacing(10) # 增加垂直间距
  130. account_list = QListWidget()
  131. account_list.setMinimumWidth(200) # 设置最小宽度
  132. self.account_lists[platform] = account_list
  133. account_list.currentItemChanged.connect(lambda current, previous, p=platform: self.select_account(p, current))
  134. account_buttons = QHBoxLayout()
  135. add_btn = QPushButton("Add Account")
  136. add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
  137. delete_btn = QPushButton("Delete Account")
  138. delete_btn.clicked.connect(lambda checked, p=platform: self.delete_account(p))
  139. enable_btn = QPushButton("Enable Account")
  140. enable_btn.clicked.connect(lambda checked, p=platform: self.enable_account(p))
  141. account_buttons.addWidget(add_btn)
  142. account_buttons.addWidget(delete_btn)
  143. account_buttons.addWidget(enable_btn)
  144. account_layout.addWidget(account_list)
  145. account_layout.addLayout(account_buttons)
  146. account_group.setLayout(account_layout)
  147. # 账户详情表单
  148. form_group = QGroupBox("Account Details")
  149. form_layout = QFormLayout()
  150. form_layout.setSpacing(10) # 增加表单项之间的间距
  151. # 创建一个垂直布局来组合表单和保存按钮
  152. form_container = QVBoxLayout()
  153. form_container.addLayout(form_layout)
  154. # 添加弹性空间,将保存按钮推到底部
  155. form_container.addStretch(1)
  156. # 保存按钮
  157. save_btn = QPushButton("Save Settings")
  158. save_btn.clicked.connect(self.save_settings)
  159. save_btn.setFixedHeight(30) # 设置按钮高度
  160. save_btn.setMinimumWidth(200) # 设置最小宽度
  161. form_container.addWidget(save_btn)
  162. form_group.setLayout(form_container)
  163. # 将账户列表和账户详情添加到水平布局中
  164. accounts_details_layout.addWidget(account_group, 1) # 1是伸缩因子
  165. accounts_details_layout.addWidget(form_group, 2) # 2是伸缩因子,使表单区域更宽
  166. # 添加水平布局到页面布局
  167. page_layout.addLayout(accounts_details_layout)
  168. page.setLayout(page_layout)
  169. self.platform_pages[platform] = {
  170. "form": form_layout
  171. }
  172. self.platform_tabs.addTab(page, platform)
  173. layout.addWidget(self.platform_tabs)
  174. self.setLayout(layout)
  175. # 加载设置
  176. self.load_settings()
  177. def load_settings(self):
  178. # 为每个平台加载账户
  179. for platform in self.platform_configs.keys():
  180. account_list = self.account_lists[platform]
  181. account_list.clear()
  182. # 获取该平台的所有账户
  183. accounts = self.config_reader.get_platform_accounts(platform)
  184. # 获取启用的账户ID
  185. enabled_account = str(self.config_reader.config['accounts'].get(platform, {}).get('enable', '1'))
  186. # 添加到列表
  187. for account in accounts:
  188. item = QListWidgetItem(account)
  189. # 如果是启用的账户,设置背景色以突出显示
  190. if account == enabled_account:
  191. item.setBackground(Qt.green)
  192. item.setText(f"{account} (启用中)")
  193. account_list.addItem(item)
  194. # 选择第一个账户
  195. if account_list.count() > 0:
  196. account_list.setCurrentRow(0)
  197. self.select_account(platform, account_list.item(0))
  198. def select_account(self, platform, item):
  199. if not item:
  200. return
  201. account = item.text()
  202. # 如果账户名包含 "(启用中)" 后缀,去掉后缀
  203. if " (启用中)" in account:
  204. account = account.split(" (")[0]
  205. form = self.platform_pages[platform]["form"]
  206. # 清空表单
  207. while form.rowCount() > 0:
  208. form.removeRow(0)
  209. # 获取账户配置
  210. account_config = self.config_reader.get_account_config(platform, account)
  211. # 创建表单项
  212. self.field_widgets = {}
  213. for field in self.platform_configs[platform]:
  214. value = account_config.get(field, "")
  215. if field == "private":
  216. widget = QCheckBox()
  217. widget.setChecked(value if isinstance(value, bool) else value.lower() != "false")
  218. else:
  219. widget = QLineEdit()
  220. if field in ["token", "password"]:
  221. widget.setEchoMode(QLineEdit.Password)
  222. widget.setText(str(value))
  223. self.field_widgets[field] = widget
  224. form.addRow(f"{field.capitalize()}:", widget)
  225. def add_account(self, platform):
  226. dialog = AddAccountDialog(platform, self)
  227. if dialog.exec_() == QDialog.Accepted:
  228. account_data = dialog.get_account_data()
  229. account_name = account_data["name"]
  230. if not account_name:
  231. QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
  232. return
  233. # 更新config.yml文件
  234. config = self.config_reader.config
  235. if platform not in config['accounts']:
  236. config['accounts'][platform] = {'enable': 1}
  237. # 添加新账户
  238. config['accounts'][platform][account_name] = {
  239. field: value for field, value in account_data.items() if field != 'name'
  240. }
  241. # 保存配置
  242. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  243. yaml.dump(config, f, default_flow_style=False)
  244. # 重新加载设置
  245. self.config_reader = ConfigReader()
  246. self.load_settings()
  247. # 选择新账户
  248. account_list = self.account_lists[platform]
  249. for i in range(account_list.count()):
  250. if account_list.item(i).text() == account_name:
  251. account_list.setCurrentRow(i)
  252. self.select_account(platform, account_list.item(i))
  253. break
  254. def delete_account(self, platform):
  255. account_list = self.account_lists[platform]
  256. current_item = account_list.currentItem()
  257. if not current_item:
  258. return
  259. account = current_item.text()
  260. # 如果账户名包含 "(启用中)" 后缀,去掉后缀
  261. if " (启用中)" in account:
  262. account = account.split(" (")[0]
  263. reply = QMessageBox.question(
  264. self,
  265. "Confirm Deletion",
  266. f"Are you sure you want to delete the account '{account}' for {platform}?",
  267. QMessageBox.Yes | QMessageBox.No,
  268. QMessageBox.No
  269. )
  270. if reply == QMessageBox.Yes:
  271. # 更新config.yml文件
  272. config = self.config_reader.config
  273. if platform in config['accounts'] and account in config['accounts'][platform]:
  274. del config['accounts'][platform][account]
  275. # 保存配置
  276. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  277. yaml.dump(config, f, default_flow_style=False)
  278. # 重新加载设置
  279. self.config_reader = ConfigReader()
  280. self.load_settings()
  281. def enable_account(self, platform):
  282. account_list = self.account_lists[platform]
  283. current_item = account_list.currentItem()
  284. if not current_item:
  285. return
  286. account = current_item.text()
  287. # 如果账户名包含 "(启用中)" 后缀,去掉后缀
  288. if " (启用中)" in account:
  289. account = account.split(" (")[0]
  290. # 更新config.yml文件
  291. config = self.config_reader.config
  292. if platform in config['accounts']:
  293. # 将当前账户设置为启用账户
  294. config['accounts'][platform]['enable'] = account
  295. # 保存配置
  296. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  297. yaml.dump(config, f, default_flow_style=False)
  298. QMessageBox.information(
  299. self,
  300. "Success",
  301. f"Account '{account}' has been enabled for {platform}."
  302. )
  303. # 重新加载设置
  304. self.config_reader = ConfigReader()
  305. self.load_settings()
  306. def save_settings(self):
  307. # 保存当前显示的账户配置
  308. platform = list(self.platform_configs.keys())[self.platform_tabs.currentIndex()]
  309. account_list = self.account_lists[platform]
  310. current_item = account_list.currentItem()
  311. if current_item and hasattr(self, 'field_widgets'):
  312. account = current_item.text()
  313. # 如果账户名包含 "(启用中)" 后缀,去掉后缀
  314. if " (启用中)" in account:
  315. account = account.split(" (")[0]
  316. # 更新config.yml文件
  317. config = self.config_reader.config
  318. if platform not in config['accounts']:
  319. config['accounts'][platform] = {'enable': 1}
  320. # 更新账户配置
  321. account_config = {}
  322. for field, widget in self.field_widgets.items():
  323. if isinstance(widget, QCheckBox):
  324. value = widget.isChecked()
  325. else:
  326. value = widget.text().strip()
  327. if value:
  328. account_config[field] = value
  329. config['accounts'][platform][account] = account_config
  330. # 保存配置
  331. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  332. yaml.dump(config, f, default_flow_style=False)
  333. QMessageBox.information(self, "Success", "Settings saved successfully!")
  334. class AddAccountDialog(QDialog):
  335. def __init__(self, platform, parent=None):
  336. super().__init__(parent)
  337. self.platform = platform
  338. self.init_ui()
  339. def init_ui(self):
  340. self.setWindowTitle(f"Add {self.platform} Account")
  341. layout = QFormLayout()
  342. layout.setSpacing(10) # 增加表单项之间的间距
  343. # 账户名称
  344. self.name_edit = QLineEdit()
  345. layout.addRow("Account Name:", self.name_edit)
  346. # 其他字段
  347. self.field_widgets = {}
  348. platform_configs = {
  349. "github": ["username", "token", "private"],
  350. "gitlab": ["host", "username", "token", "private"],
  351. "gitee": ["username", "token", "private"],
  352. "gitcode": ["username", "token", "private"],
  353. "git.yoq.me": ["username", "token", "private"],
  354. "coding": ["username", "token", "project", "private"],
  355. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  356. "cnb": ["group", "username", "token", "private"]
  357. }
  358. for field in platform_configs[self.platform]:
  359. if field == "private":
  360. widget = QCheckBox()
  361. widget.setChecked(True)
  362. else:
  363. widget = QLineEdit()
  364. if field in ["token", "password"]:
  365. widget.setEchoMode(QLineEdit.Password)
  366. self.field_widgets[field] = widget
  367. layout.addRow(f"{field.capitalize()}:", widget)
  368. # 添加一些空间
  369. layout.addItem(QSpacerItem(20, 20))
  370. # 按钮
  371. buttons = QDialogButtonBox(
  372. QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
  373. Qt.Horizontal, self)
  374. buttons.accepted.connect(self.accept)
  375. buttons.rejected.connect(self.reject)
  376. layout.addRow(buttons)
  377. self.setLayout(layout)
  378. self.setMinimumWidth(400) # 设置对话框最小宽度
  379. def get_account_data(self):
  380. data = {"name": self.name_edit.text().strip()}
  381. for field, widget in self.field_widgets.items():
  382. if isinstance(widget, QCheckBox):
  383. data[field] = widget.isChecked()
  384. else:
  385. data[field] = widget.text().strip()
  386. return data
  387. class MainTab(QWidget):
  388. def __init__(self, parent=None):
  389. super().__init__(parent)
  390. layout = QVBoxLayout()
  391. # 路径选择
  392. path_layout = QHBoxLayout()
  393. self.path_label = QLabel("Local Path: ")
  394. self.path_edit = QLineEdit()
  395. self.path_edit.setText(get_active_explorer_path())
  396. self.path_btn = QPushButton("Browse...")
  397. self.path_btn.clicked.connect(self.choose_path)
  398. path_layout.addWidget(self.path_label)
  399. path_layout.addWidget(self.path_edit)
  400. path_layout.addWidget(self.path_btn)
  401. layout.addLayout(path_layout)
  402. # 操作
  403. op_group = QGroupBox("Operation:")
  404. op_layout = QHBoxLayout()
  405. self.op_buttons = QButtonGroup(self)
  406. for i, op in enumerate(["create", "push", "pull", "clone", "delete"]):
  407. btn = QRadioButton(op.capitalize())
  408. if i == 1:
  409. btn.setChecked(True)
  410. self.op_buttons.addButton(btn, i)
  411. op_layout.addWidget(btn)
  412. op_group.setLayout(op_layout)
  413. layout.addWidget(op_group)
  414. # 平台
  415. pf_group = QGroupBox("Platform:")
  416. pf_layout = QHBoxLayout()
  417. self.pf_buttons = QButtonGroup(self)
  418. self.platforms = ["github", "gitlab", "gitee", "gitcode", "git.yoq.me", "coding", "aliyun", "cnb"]
  419. for i, pf in enumerate(self.platforms):
  420. btn = QRadioButton(pf.capitalize())
  421. if i == 0:
  422. btn.setChecked(True)
  423. self.pf_buttons.addButton(btn, i)
  424. pf_layout.addWidget(btn)
  425. pf_group.setLayout(pf_layout)
  426. layout.addWidget(pf_group)
  427. # 账户选择
  428. account_layout = QHBoxLayout()
  429. account_layout.addWidget(QLabel("Account:"))
  430. self.account_combo = QComboBox()
  431. self.account_combo.setMinimumWidth(200)
  432. account_layout.addWidget(self.account_combo)
  433. account_layout.addStretch()
  434. layout.addLayout(account_layout)
  435. # 按钮区域 (run和cancel)
  436. btn_layout = QHBoxLayout()
  437. self.run_btn = QPushButton("run it")
  438. self.run_btn.clicked.connect(self.run_repo_sync)
  439. self.cancel_btn = QPushButton("Cancel")
  440. self.cancel_btn.clicked.connect(self.cancel_operation)
  441. self.cancel_btn.setEnabled(False)
  442. btn_layout.addWidget(self.run_btn)
  443. btn_layout.addWidget(self.cancel_btn)
  444. layout.addLayout(btn_layout)
  445. # 命令执行结果
  446. result_group = QGroupBox("Execution Result:")
  447. result_layout = QVBoxLayout()
  448. self.result_text = QTextEdit()
  449. self.result_text.setReadOnly(True)
  450. result_layout.addWidget(self.result_text)
  451. result_group.setLayout(result_layout)
  452. layout.addWidget(result_group)
  453. self.setLayout(layout)
  454. # 命令执行相关
  455. self.process = None
  456. self.command_signals = CommandSignals()
  457. self.command_signals.output.connect(self.update_output)
  458. self.command_signals.finished.connect(self.process_finished)
  459. # 初始化配置读取器
  460. self.config_reader = ConfigReader()
  461. # 平台变更时更新账户列表
  462. self.pf_buttons.buttonClicked.connect(self.update_account_list)
  463. self.update_account_list()
  464. def choose_path(self):
  465. path = QFileDialog.getExistingDirectory(self, "Select Directory")
  466. if path:
  467. self.path_edit.setText(path)
  468. def update_account_list(self):
  469. self.account_combo.clear()
  470. pf_id = self.pf_buttons.checkedId()
  471. platform = self.platforms[pf_id]
  472. # 读取所有账户
  473. accounts = self.get_platform_accounts(platform)
  474. # 找出启用的账户
  475. enabled_account = self.get_enabled_account(platform)
  476. # 添加账户到下拉框,启用的账户放在最前面
  477. if enabled_account in accounts:
  478. accounts.remove(enabled_account)
  479. self.account_combo.addItem(f"{enabled_account} (启用中)")
  480. # 设置启用账户的背景颜色为绿色
  481. model = self.account_combo.model()
  482. index = model.index(0, 0)
  483. model.setData(index, Qt.green, Qt.BackgroundRole)
  484. for account in accounts:
  485. self.account_combo.addItem(account)
  486. # 默认选择启用的账户
  487. self.account_combo.setCurrentIndex(0)
  488. def get_platform_accounts(self, platform):
  489. # 读取config.yml文件中的所有配置
  490. accounts = set()
  491. # 获取该平台的所有账户
  492. try:
  493. platform_accounts = self.config_reader.get_platform_accounts(platform)
  494. for account in platform_accounts:
  495. accounts.add(account)
  496. except Exception as e:
  497. print(f"Error getting platform accounts: {str(e)}")
  498. # 确保至少有一个默认账户
  499. if not accounts:
  500. accounts.add("1")
  501. return sorted(list(accounts))
  502. def get_enabled_account(self, platform):
  503. """获取平台的启用账户ID"""
  504. try:
  505. if platform in self.config_reader.config['accounts']:
  506. return str(self.config_reader.config['accounts'][platform].get('enable', '1'))
  507. return '1' # 默认返回'1'
  508. except Exception as e:
  509. print(f"Error getting enabled account: {str(e)}")
  510. return '1' # 出错时默认返回'1'
  511. def run_repo_sync(self):
  512. repo_path = self.path_edit.text().strip()
  513. if not repo_path:
  514. QMessageBox.warning(self, "Warning", "Please select a local path.")
  515. return
  516. op_id = self.op_buttons.checkedId()
  517. pf_id = self.pf_buttons.checkedId()
  518. op = ["create", "push", "pull", "clone", "delete"][op_id]
  519. pf = self.platforms[pf_id]
  520. # 获取选择的账户名(去掉可能的"(启用中)"后缀)
  521. account_text = self.account_combo.currentText()
  522. account = account_text.split(" (")[0] if " (" in account_text else account_text
  523. # 清空结果区域
  524. self.result_text.clear()
  525. # 检查平台配置
  526. account_config = self.config_reader.get_account_config(pf, account)
  527. if not account_config.get('token'):
  528. QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
  529. return
  530. # 构建参数
  531. args = {
  532. 'command': op,
  533. 'platform': pf,
  534. 'repo_path': repo_path
  535. }
  536. # 获取启用的账户
  537. enabled_account = self.get_enabled_account(pf)
  538. # 如果不是启用账户,需要设置环境变量
  539. if account != enabled_account:
  540. # 保存原始环境变量
  541. self.original_env = {}
  542. for field, value in account_config.items():
  543. env_var = f"{pf}_{field}"
  544. if env_var in os.environ:
  545. self.original_env[env_var] = os.environ[env_var]
  546. os.environ[env_var] = str(value)
  547. # 显示执行命令
  548. cmd_str = f"repo_sync {op} -p {pf} -repo_path {repo_path}"
  549. self.result_text.append(f"Running: {cmd_str}\n")
  550. # 执行命令
  551. self.run_btn.setEnabled(False)
  552. self.cancel_btn.setEnabled(True)
  553. # 重定向标准输出和标准错误
  554. self.original_stdout = sys.stdout
  555. self.original_stderr = sys.stderr
  556. sys.stdout = self.OutputRedirector(self.command_signals)
  557. sys.stderr = self.OutputRedirector(self.command_signals)
  558. # 在新线程中执行命令
  559. self.process_thread = threading.Thread(
  560. target=self.run_module,
  561. args=(args,)
  562. )
  563. self.process_thread.daemon = True
  564. self.process_thread.start()
  565. def run_module(self, args):
  566. try:
  567. # 正确导入repo_sync模块的main函数
  568. from repo_sync import main
  569. self.running = True
  570. return_code = 0
  571. try:
  572. main(args)
  573. except SystemExit as e:
  574. return_code = e.code if isinstance(e.code, int) else 1
  575. except Exception as e:
  576. self.command_signals.output.emit(f"Error: {str(e)}")
  577. return_code = 1
  578. finally:
  579. self.command_signals.finished.emit(return_code)
  580. # 恢复标准输出和标准错误
  581. sys.stdout = self.original_stdout
  582. sys.stderr = self.original_stderr
  583. # 恢复环境变量
  584. if hasattr(self, 'original_env'):
  585. for var, value in self.original_env.items():
  586. os.environ[var] = value
  587. delattr(self, 'original_env')
  588. except Exception as e:
  589. self.command_signals.output.emit(f"Error: {str(e)}")
  590. self.command_signals.finished.emit(1)
  591. # 恢复标准输出和标准错误
  592. sys.stdout = self.original_stdout
  593. sys.stderr = self.original_stderr
  594. # 输出重定向类
  595. class OutputRedirector:
  596. def __init__(self, signals):
  597. self.signals = signals
  598. self.buffer = ""
  599. def write(self, text):
  600. self.buffer += text
  601. if '\n' in self.buffer:
  602. lines = self.buffer.split('\n')
  603. for line in lines[:-1]:
  604. self.signals.output.emit(line)
  605. self.buffer = lines[-1]
  606. def flush(self):
  607. if self.buffer:
  608. self.signals.output.emit(self.buffer)
  609. self.buffer = ""
  610. def run_process(self, cmd, env=None):
  611. try:
  612. self.process = subprocess.Popen(
  613. cmd,
  614. stdout=subprocess.PIPE,
  615. stderr=subprocess.STDOUT,
  616. text=True,
  617. bufsize=1,
  618. universal_newlines=True,
  619. env=env
  620. )
  621. # 读取输出
  622. for line in self.process.stdout:
  623. self.command_signals.output.emit(line)
  624. self.process.wait()
  625. self.command_signals.finished.emit(self.process.returncode)
  626. except Exception as e:
  627. self.command_signals.output.emit(f"Error: {str(e)}")
  628. self.command_signals.finished.emit(1)
  629. def update_output(self, text):
  630. self.result_text.append(text)
  631. # 自动滚动到底部
  632. self.result_text.verticalScrollBar().setValue(
  633. self.result_text.verticalScrollBar().maximum()
  634. )
  635. def process_finished(self, return_code):
  636. self.process = None
  637. self.run_btn.setEnabled(True)
  638. self.cancel_btn.setEnabled(False)
  639. if return_code == 0:
  640. self.result_text.append("\nOperation completed successfully.")
  641. else:
  642. self.result_text.append(f"\nOperation failed with return code {return_code}.")
  643. def cancel_operation(self):
  644. if self.process:
  645. self.process.terminate()
  646. self.result_text.append("\nOperation cancelled by user.")
  647. else:
  648. # 直接调用模式下,通过退出标志通知线程结束
  649. self.running = False
  650. self.result_text.append("\nOperation cancelled by user.")
  651. # 恢复标准输出和标准错误
  652. if hasattr(self, 'original_stdout') and hasattr(self, 'original_stderr'):
  653. sys.stdout = self.original_stdout
  654. sys.stderr = self.original_stderr
  655. # 恢复环境变量
  656. if hasattr(self, 'original_env'):
  657. for var, value in self.original_env.items():
  658. os.environ[var] = value
  659. delattr(self, 'original_env')
  660. # Explorer路径获取
  661. try:
  662. import win32com.client
  663. def get_active_explorer_path():
  664. shell = win32com.client.Dispatch("Shell.Application")
  665. for window in shell.Windows():
  666. if window.Name in ["文件资源管理器", "Windows Explorer"]:
  667. return window.Document.Folder.Self.Path
  668. return os.getcwd()
  669. except ImportError:
  670. def get_active_explorer_path():
  671. return os.getcwd()
  672. # 命令执行信号类
  673. class CommandSignals(QObject):
  674. output = pyqtSignal(str)
  675. finished = pyqtSignal(int)
  676. class AboutTab(QWidget):
  677. def __init__(self, parent=None):
  678. super().__init__(parent)
  679. layout = QVBoxLayout()
  680. layout.addWidget(QLabel(f"repo_sync tools v{__version__}"))
  681. layout.addWidget(QLabel("作者: liuyuqi.gov@msn.cn"))
  682. layout.addWidget(QLabel("GitHub: https://github.com/jianboy/repo_sync"))
  683. layout.addWidget(QLabel("\n功能说明:"))
  684. layout.addWidget(QLabel("- 支持多个代码托管平台"))
  685. layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
  686. layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
  687. layout.addWidget(QLabel("- 配置信息保存在config.yml文件中"))
  688. layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
  689. layout.addWidget(QLabel("- 命令行执行结果实时显示"))
  690. self.setLayout(layout)
  691. class RepoSyncMainWindow(QTabWidget):
  692. def __init__(self):
  693. super().__init__()
  694. self.setWindowTitle('repo_sync tools v1.12')
  695. self.resize(800, 700)
  696. self.main_tab = MainTab()
  697. self.addTab(self.main_tab, '主界面')
  698. self.settings_tab = SettingsTab()
  699. self.addTab(self.settings_tab, '设置')
  700. self.about_tab = AboutTab()
  701. self.addTab(self.about_tab, '关于')
  702. def main():
  703. """GUI主入口函数"""
  704. if not HAS_QT:
  705. print("PyQt5 not installed. Please install it with: pip install PyQt5")
  706. print("Running in fallback mode...")
  707. # 这里可以添加一个简单的命令行界面作为后备
  708. return
  709. try:
  710. # 确保config.yml文件存在
  711. ensure_config_file()
  712. app = QApplication(sys.argv)
  713. window = RepoSyncMainWindow()
  714. window.show()
  715. sys.exit(app.exec_())
  716. except Exception as e:
  717. print(f"Error starting GUI: {str(e)}")
  718. import traceback
  719. traceback.print_exc()
  720. if __name__ == '__main__':
  721. main()