manageLocalSources.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. ########################################################################
  2. # Wiizard - A Wii games manager
  3. # Copyright (C) 2023 CYBERDEViL
  4. #
  5. # This file is part of Wiizard.
  6. #
  7. # Wiizard is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Wiizard is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  19. #
  20. ########################################################################
  21. import os
  22. import shutil
  23. import math
  24. from PyQt5.QtWidgets import (
  25. QWidget,
  26. QGridLayout,
  27. QLabel,
  28. QToolBar,
  29. QListWidget,
  30. QDialog,
  31. QComboBox,
  32. QSpinBox,
  33. QPushButton,
  34. QFileDialog,
  35. QMessageBox,
  36. QVBoxLayout,
  37. QTabWidget,
  38. QListWidgetItem,
  39. QApplication,
  40. QStyle
  41. )
  42. from PyQt5.QtCore import Qt, QVariant
  43. from PyQt5.QtGui import QBrush
  44. import pywwt
  45. from wiizard import globals as Global
  46. from wiizard import const as Const
  47. from wiizard.models.source import LocalSourceModel, FilePartitionSourceModel
  48. class AbstractManageLocalWidget(QWidget):
  49. def __init__(self, source, modelType, labelText, parent=None):
  50. QWidget.__init__(self, parent=parent)
  51. self.__newPaths = []
  52. self.__delPaths = []
  53. self._source = source
  54. self.__modelType = modelType
  55. layout = QGridLayout(self)
  56. label = QLabel(labelText, self)
  57. label.setWordWrap(True)
  58. self.toolBar = QToolBar(self)
  59. self.addAction = self.toolBar.addAction("Add")
  60. self.delAction = self.toolBar.addAction("Remove")
  61. self.restoreAction = self.toolBar.addAction("Restore")
  62. self.saveAction = self.toolBar.addAction("Save")
  63. self.locationsList = QListWidget(self)
  64. layout.addWidget(label , 0, 0)
  65. layout.addWidget(self.toolBar , 1, 0)
  66. layout.addWidget(self.locationsList, 2, 0)
  67. # Add paths
  68. for path in self._source:
  69. sourceModel = self._source[path]
  70. item = QListWidgetItem(path, self.locationsList)
  71. if not sourceModel.isValid():
  72. item.setBackground(QBrush(Qt.darkRed))
  73. item.setIcon(QApplication.style().standardIcon(QStyle.SP_MessageBoxCritical))
  74. item.setToolTip("Missing")
  75. self.locationsList.addItem(item)
  76. self.__updateButtonStates()
  77. self.__selectionChanged()
  78. self.addAction.triggered.connect(self.__onAddPath)
  79. self.delAction.triggered.connect(self.__onDelPath)
  80. self.restoreAction.triggered.connect(self.restore)
  81. self.saveAction.triggered.connect(self.__onSave)
  82. self.locationsList.itemSelectionChanged.connect(self.__selectionChanged)
  83. def restore(self):
  84. self.__newPaths.clear()
  85. self.__delPaths.clear()
  86. self.locationsList.clear()
  87. for path in self._source:
  88. self.locationsList.addItem(path)
  89. self.__updateButtonStates()
  90. def hasUnsavedChanges(self):
  91. return (self.__newPaths or self.__delPaths)
  92. def __updateButtonStates(self):
  93. """ Update 'Save' and 'Restore' button states
  94. """
  95. if self.__newPaths or self.__delPaths:
  96. self.saveAction.setEnabled(True)
  97. self.restoreAction.setEnabled(True)
  98. else:
  99. self.saveAction.setEnabled(False)
  100. self.restoreAction.setEnabled(False)
  101. def __selectionChanged(self):
  102. """ Update 'Remove' button state
  103. """
  104. items = self.locationsList.selectedItems()
  105. if len(items) > 0:
  106. self.delAction.setEnabled(True)
  107. else:
  108. self.delAction.setEnabled(False)
  109. def addPath(self, path):
  110. if path:
  111. if path in self.__newPaths or path in self._source:
  112. if path in self.__delPaths and path in self._source:
  113. # In this scenario the path first got deleted by the user (so added
  114. # to __delPaths but still exists is _source), then the user adds the
  115. # same path again.
  116. self.__delPaths.remove(path)
  117. self.locationsList.addItem(path)
  118. self.__updateButtonStates()
  119. # Path already present ..
  120. return
  121. # Total new path
  122. self.__newPaths.append(path)
  123. self.locationsList.addItem(path)
  124. self.__updateButtonStates()
  125. def __onAddPath(self):
  126. path = self.getNewPath()
  127. self.addPath(path)
  128. def onDelPath(self, path):
  129. # For ManageLocalWbfsFilesWidget to be able to ask if the file should be
  130. # removed from disk.
  131. return True
  132. def save(self):
  133. self.__onSave()
  134. def __onDelPath(self):
  135. for item in self.locationsList.selectedItems():
  136. path = item.text()
  137. # Something gone wrong in the super class
  138. if not self.onDelPath(path):
  139. continue
  140. index = self.locationsList.row(item)
  141. self.locationsList.takeItem(index)
  142. if path in self.__newPaths:
  143. self.__newPaths.remove(path)
  144. continue
  145. self.__delPaths.append(path)
  146. self.__updateButtonStates()
  147. def __onSave(self):
  148. for path in self.__newPaths:
  149. model = self.__modelType(path)
  150. self._source.update({path: model})
  151. for path in self.__delPaths:
  152. del self._source[path]
  153. self.onSaved()
  154. self.parent().parent().parent().accept() # this is ugly but it works
  155. def getNewPath(self):
  156. raise Exception("Re-implement this!")
  157. def onSaved(self):
  158. raise Exception("Re-implement this!")
  159. class ManageLocalDirsWidget(AbstractManageLocalWidget):
  160. def __init__(self, parent=None):
  161. AbstractManageLocalWidget.__init__(self, Global.Sources.localDirectories,
  162. LocalSourceModel,
  163. "Add directories containing Wii games "
  164. "(.iso and .wbfs) below. It will "
  165. "recurse with a maximum depth of 4.",
  166. parent=parent)
  167. def getNewPath(self):
  168. return QFileDialog.getExistingDirectory(self, "Add Wii games directory")
  169. def onSaved(self):
  170. Global.Sources.localDirectoriesUpdated.emit()
  171. class NewWbfsFileDialog(QDialog):
  172. def __init__(self, filepath, maxGiB, parent=None):
  173. QDialog.__init__(self, parent=parent)
  174. self.setWindowTitle("New WBFS file")
  175. layout = QGridLayout(self)
  176. label = QLabel("File:", self)
  177. value = QLabel(filepath, self)
  178. layout.addWidget(label , 0, 0, 1, 1)
  179. layout.addWidget(value , 0, 1, 1, 2)
  180. label = QLabel("Free space:", self)
  181. value = QLabel(f"{maxGiB} GiB", self)
  182. layout.addWidget(label , 1, 0, 1, 1)
  183. layout.addWidget(value , 1, 1, 1, 2)
  184. label = QLabel("Size:", self)
  185. self.sizeBox = QSpinBox(self)
  186. self.sizeBox.setSuffix(" GiB")
  187. self.sizeBox.setSingleStep(1)
  188. self.sizeBox.setMinimum(1)
  189. self.sizeBox.setMaximum(maxGiB)
  190. layout.addWidget(label , 2, 0, 1, 1)
  191. layout.addWidget(self.sizeBox , 2, 1, 1, 2)
  192. label = QLabel("HD sector size:", self)
  193. self.hssCombo = QComboBox(self)
  194. self.hssCombo.addItem("auto" , QVariant(0))
  195. self.hssCombo.addItem(" 512 bytes", QVariant(512))
  196. self.hssCombo.addItem("2048 bytes", QVariant(2048))
  197. self.hssCombo.addItem("4096 bytes", QVariant(4096))
  198. layout.addWidget(label , 3, 0, 1, 1)
  199. layout.addWidget(self.hssCombo, 3, 1, 1, 2)
  200. label = QLabel("WBFS sector size:", self)
  201. self.wssCombo = QComboBox(self)
  202. self.wssCombo.addItem("auto" , QVariant(0))
  203. self.wssCombo.addItem("1024 bytes", QVariant(1024))
  204. self.wssCombo.addItem("4096 bytes", QVariant(4096))
  205. self.wssCombo.addItem("8192 bytes", QVariant(8192))
  206. layout.addWidget(label , 4, 0, 1, 1)
  207. layout.addWidget(self.wssCombo, 4, 1, 1, 2)
  208. cancelButton = QPushButton("Cancel", self)
  209. acceptButton = QPushButton("Accept", self)
  210. layout.addWidget(cancelButton, 5, 1, 1, 1)
  211. layout.addWidget(acceptButton, 5, 2, 1, 1)
  212. acceptButton.clicked.connect(self.accept)
  213. cancelButton.clicked.connect(self.reject)
  214. class ManageLocalWbfsFilesWidget(AbstractManageLocalWidget):
  215. def __init__(self, parent=None):
  216. AbstractManageLocalWidget.__init__(self, Global.Sources.filePartitions,
  217. FilePartitionSourceModel,
  218. "Add WBFS partition files below.",
  219. parent=parent)
  220. self.newAction = self.toolBar.addAction("New")
  221. self.newAction.setToolTip("Create a new WBFS partition file.")
  222. self.newAction.triggered.connect(self.__onNewClicked)
  223. def getNewPath(self):
  224. output = QFileDialog.getOpenFileName(self, "Add WBFS partition file", "",
  225. "WBFS file (*.wbfs)")
  226. return output[0]
  227. def onSaved(self):
  228. Global.Sources.filePartitionsUpdated.emit()
  229. def onDelPath(self, path):
  230. source = self._source[path]
  231. # File doesn't exist any more, so no need to ask to delete the file.
  232. if not source.isValid():
  233. return True
  234. # Ask to delete the file
  235. result = QMessageBox.question(self, "Delete file?",
  236. f"Do you want to delete {path} from disk?")
  237. # Delete the file
  238. if result == QMessageBox.Yes:
  239. try:
  240. os.remove(path)
  241. except OSError as err:
  242. QMessageBox.critical(self, "Remove failed",
  243. f"Removal of file {path} failed: {err}")
  244. return False
  245. # Don't delete the file
  246. return True
  247. def __onNewClicked(self):
  248. filepath = QFileDialog.getSaveFileName(self, "New WBFS partition file", "",
  249. "WBFS file (*.wbfs)")[0]
  250. if not filepath:
  251. return
  252. path = os.path.dirname(filepath)
  253. freeGiB = int(math.floor(shutil.disk_usage(path).free / Const.GiB))
  254. if not freeGiB:
  255. QMessageBox.critical(self, "Not enough free space",
  256. "Not enough free space at given location, the "
  257. "minimum required free space is 1 GiB.")
  258. return
  259. if not filepath.endswith(".wbfs"):
  260. filepath += ".wbfs"
  261. sizeDialog = NewWbfsFileDialog(filepath, freeGiB, parent=self)
  262. if sizeDialog.exec():
  263. size = sizeDialog.sizeBox.value()
  264. hss = sizeDialog.hssCombo.currentData()
  265. wss = sizeDialog.wssCombo.currentData()
  266. # TODO thread maybe the storage is slow?
  267. result = None
  268. try:
  269. result = pywwt.create_new_wbfs_file(filepath, size, hss=hss, wss=wss,
  270. testMode=False)
  271. except pywwt.error as err:
  272. QMessageBox.critical(self, "Failed!",
  273. f"Creating new WBFS file failed! Error: {err}")
  274. return
  275. else:
  276. if not result:
  277. QMessageBox.critical(self, "Failed!", "Creating new WBFS file failed!")
  278. return
  279. self.addPath(filepath)
  280. class ManageLocalWidget(QDialog):
  281. def __init__(self, parent=None):
  282. QDialog.__init__(self, parent=parent)
  283. self.setWindowTitle("Manage local sources")
  284. layout = QVBoxLayout(self)
  285. self.tabWidget = QTabWidget(self)
  286. self.localDirsWidget = ManageLocalDirsWidget(parent=self)
  287. self.localWbfsWidget = ManageLocalWbfsFilesWidget(parent=self)
  288. self.tabWidget.addTab(self.localDirsWidget, "Directories")
  289. self.tabWidget.addTab(self.localWbfsWidget, "WBFS Files")
  290. layout.addWidget(self.tabWidget)
  291. def closeEvent(self, closeEvent):
  292. if (self.localDirsWidget.hasUnsavedChanges()
  293. or self.localWbfsWidget.hasUnsavedChanges()):
  294. result = QMessageBox.question(self, "Save?",
  295. "There are unsaved changes, do you like "
  296. "to save them?",
  297. buttons=QMessageBox.Cancel|QMessageBox.No|QMessageBox.Yes)
  298. if result == QMessageBox.Cancel:
  299. closeEvent.ignore()
  300. return
  301. if result == QMessageBox.Yes:
  302. if self.localDirsWidget.hasUnsavedChanges():
  303. self.localDirsWidget.save()
  304. if self.localWbfsWidget.hasUnsavedChanges():
  305. self.localWbfsWidget.save()
  306. closeEvent.accept()