profiles.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. ########################################################################
  2. # Searx-Qt - Lightweight desktop application for Searx.
  3. # Copyright (C) 2020-2022 CYBERDEViL
  4. #
  5. # This file is part of Searx-Qt.
  6. #
  7. # Searx-Qt 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. # Searx-Qt 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. from uuid import uuid4
  22. from PyQt5.QtWidgets import (
  23. QLabel,
  24. QDialog,
  25. QHBoxLayout,
  26. QVBoxLayout,
  27. QFormLayout,
  28. QListWidget,
  29. QComboBox,
  30. QLineEdit,
  31. QListWidgetItem,
  32. QCheckBox,
  33. QMessageBox
  34. )
  35. from PyQt5.QtCore import Qt, QVariant
  36. from searxqt.widgets.buttons import Button
  37. from searxqt.models.profiles import ProfileItem
  38. from searxqt.translations import _
  39. class AddProfileDialog(QDialog):
  40. def __init__(self, profiles, parent=None):
  41. QDialog.__init__(self, parent=parent)
  42. self.setWindowTitle(_("Add profile"))
  43. self._profiles = profiles
  44. self._id = None
  45. layout = QVBoxLayout(self)
  46. # Top message
  47. labelTxt = _("There are two types of profiles:\n"
  48. " 1. stats2 profile\n"
  49. " 2. user profile\n"
  50. "\n"
  51. "Choose stats2 profile type when you want to get a\n"
  52. "list of searx-instances from a stats2-instance.\n"
  53. "For example from the default https://searx.space\n"
  54. "instance."
  55. "\n"
  56. "Choose user profile type when you want to manage\n"
  57. "your own searx-instance's list.")
  58. label = QLabel(labelTxt, self)
  59. layout.addWidget(label)
  60. # Form layout
  61. formLayout = QFormLayout()
  62. layout.addLayout(formLayout)
  63. # Profile type
  64. self._typeBox = QComboBox(self)
  65. formLayout.addRow(QLabel(_("Type") + ":"), self._typeBox)
  66. # Do these in order to match InstancesModelTypes -1
  67. self._typeBox.addItem("stats2")
  68. self._typeBox.addItem("user")
  69. # Profile name
  70. self._nameEdit = QLineEdit(self)
  71. self._nameEdit.setMaxLength(32)
  72. formLayout.addRow(QLabel(_("Name") + ":"), self._nameEdit)
  73. # Profile preset
  74. from searxqt.defaults import Presets
  75. self._presetCombo = QComboBox(self)
  76. self._presetCombo.addItem("None")
  77. index = 1
  78. webIndex = 0
  79. for preset in Presets:
  80. if preset == "Web":
  81. webIndex = index
  82. index += 1
  83. self._presetCombo.addItem(preset)
  84. del Presets
  85. self._presetCombo.setCurrentIndex(webIndex)
  86. formLayout.addRow(QLabel(_("Preset") + ":"), self._presetCombo)
  87. # Feedback info label
  88. self._infoLabel = QLabel("", self)
  89. formLayout.addRow("", self._infoLabel)
  90. # Add / Cancel button
  91. buttonLayout = QHBoxLayout()
  92. layout.addLayout(buttonLayout)
  93. self._addButton = Button(_("Add"), self)
  94. self._addButton.setEnabled(False)
  95. buttonLayout.addWidget(self._addButton, 1, Qt.AlignRight)
  96. self._cancelButton = Button(_("Cancel"), self)
  97. buttonLayout.addWidget(self._cancelButton, 0, Qt.AlignRight)
  98. # Signals
  99. self._addButton.clicked.connect(self.__addButtonClicked)
  100. self._cancelButton.clicked.connect(self.reject)
  101. self._nameEdit.textEdited.connect(self.__nameEdited)
  102. def id(self): return self._id
  103. def __getSanitizedName(self):
  104. return self._nameEdit.text().rstrip().lstrip()
  105. def __addButtonClicked(self, state):
  106. self._id = str(uuid4())
  107. presetKey = self._presetCombo.currentText()
  108. preset = None if presetKey == "None" else presetKey
  109. profile = ProfileItem(
  110. self._id,
  111. self.__getSanitizedName(),
  112. # +1 to match InstancesModelTypes
  113. self._typeBox.currentIndex() + 1,
  114. preset=preset)
  115. self._profiles.add(profile)
  116. self.accept()
  117. def __doChecks(self):
  118. # Check if input name already exists.
  119. # profiles.conf is not re-read! So possible duplicate name if the
  120. # user where to add the same name at the same time in 2 different
  121. # instances of searx-qt.
  122. sanName = self.__getSanitizedName()
  123. if not sanName:
  124. self._addButton.setEnabled(False)
  125. self._infoLabel.setText("")
  126. elif sanName in self._profiles.names():
  127. self._addButton.setEnabled(False)
  128. self._infoLabel.setText(_("Name already exists."))
  129. else:
  130. self._addButton.setEnabled(True)
  131. self._infoLabel.setText("")
  132. def __nameEdited(self, name):
  133. self.__doChecks()
  134. class ProfileChooserDialog(QDialog):
  135. def __init__(self, profiles, parent=None):
  136. QDialog.__init__(self, parent=parent)
  137. self.setWindowTitle(_("Profile select"))
  138. self._profiles = profiles
  139. self._items = {}
  140. self._selectedProfile = ProfileItem()
  141. self._activeProfiles = []
  142. # current profile index in the profile list (self._profileListWidget)
  143. self._currentIndex = -1
  144. # keep track of removed profile id's so their files can be
  145. # deleted on save.
  146. self._removedIds = []
  147. # keep track of new profile id's, these profiles don't have a
  148. # .conf file stored yet. When one of these id's get deleted we
  149. # don't have to remove the file because there is none.
  150. self._newIds = []
  151. layout = QVBoxLayout(self)
  152. # Top message
  153. labelTxt = _("Please select a profile")
  154. label = QLabel(labelTxt, self)
  155. layout.addWidget(label)
  156. hLayout = QHBoxLayout()
  157. layout.addLayout(hLayout)
  158. # Actions
  159. actionsLayout = QVBoxLayout()
  160. hLayout.addLayout(actionsLayout)
  161. self._addButton = Button(_("Add"), self)
  162. actionsLayout.addWidget(self._addButton, 0, Qt.AlignTop)
  163. self._delButton = Button(_("Delete"), self)
  164. self._delButton.setEnabled(False)
  165. actionsLayout.addWidget(self._delButton, 1, Qt.AlignTop)
  166. self._resetButton = Button(_("Force\nrelease\nprofiles"), self)
  167. self._resetButton.setEnabled(False)
  168. actionsLayout.addWidget(self._resetButton, 0, Qt.AlignBottom)
  169. # Profile list
  170. self._profileListWidget = QListWidget(self)
  171. hLayout.addWidget(self._profileListWidget)
  172. # Bottom stuff
  173. bottomLayout = QHBoxLayout()
  174. layout.addLayout(bottomLayout)
  175. self._defaultCheckBox = QCheckBox(_("Use as default"), self)
  176. bottomLayout.addWidget(self._defaultCheckBox, 1, Qt.AlignRight)
  177. exitButtonTxt = ""
  178. if profiles.current().id:
  179. exitButtonTxt = _("Cancel")
  180. else:
  181. exitButtonTxt = _("Exit")
  182. self._exitButton = Button(exitButtonTxt, self)
  183. bottomLayout.addWidget(self._exitButton, 0, Qt.AlignRight)
  184. self._selectButton = Button(_("Load profile"), self)
  185. self._selectButton.setEnabled(False)
  186. bottomLayout.addWidget(self._selectButton, 0, Qt.AlignRight)
  187. # Signals
  188. self._addButton.clicked.connect(self.__addButtonClicked)
  189. self._delButton.clicked.connect(self.__delButtonClicked)
  190. self._exitButton.clicked.connect(self.reject)
  191. self._selectButton.clicked.connect(self.__selectButtonClicked)
  192. self._profileListWidget.currentItemChanged.connect(self.__itemChanged)
  193. # Only show the 'force release profiles' button when there is
  194. # currently no profile set for this searx-qt instance.
  195. if not profiles.current().id:
  196. self._resetButton.clicked.connect(self.__resetButtonClicked)
  197. else:
  198. self._resetButton.hide()
  199. if not len(profiles):
  200. self.__addDialog()
  201. else:
  202. self.refresh()
  203. def selectedProfile(self): return self._selectedProfile
  204. def removedProfiles(self): return self._removedIds
  205. def promtProfileLoadFailed(self, msg):
  206. """
  207. @param msg: Message with what went wrong.
  208. @type msg: str
  209. """
  210. QMessageBox.warning(
  211. self,
  212. _("Failed to load profile."),
  213. msg
  214. )
  215. def __selectButtonClicked(self):
  216. item = self._profileListWidget.currentItem()
  217. selectedProfile = item.data(Qt.UserRole)
  218. profiles = self._profiles
  219. # Check if profile didn't get active in meantime.
  220. if profiles.profileActive(selectedProfile):
  221. self.promtProfileLoadFailed(
  222. _("Profile already in use."),
  223. )
  224. self.refresh()
  225. return
  226. # Check if profile still exists
  227. if (selectedProfile.id not in self._newIds and
  228. not profiles.profileExists(selectedProfile)):
  229. self.promtProfileLoadFailed(
  230. _("Profile doesn't exist anymore.")
  231. )
  232. self.refresh()
  233. return
  234. # Set default profile
  235. if self._defaultCheckBox.isChecked():
  236. self._profiles.setDefault(selectedProfile)
  237. else:
  238. self._profiles.setDefault(None)
  239. self._selectedProfile = selectedProfile
  240. self.accept()
  241. def __addDialog(self):
  242. dialog = AddProfileDialog(self._profiles)
  243. if dialog.exec():
  244. self._newIds.append(dialog.id())
  245. self.refresh()
  246. def __addButtonClicked(self):
  247. self.__addDialog()
  248. def __delButtonClicked(self):
  249. item = self._profileListWidget.currentItem()
  250. profile = item.data(Qt.UserRole)
  251. profiles = self._profiles
  252. # Confirmation
  253. confirmDialog = QMessageBox()
  254. confirmDialog.setWindowTitle(_(f"Delete profile {profile.name}"))
  255. confirmDialog.setText(_("Are you sure you want to delete profile " \
  256. f"'{profile.name}'?"))
  257. confirmDialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  258. confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
  259. confirmDialog.button(QMessageBox.No).setText(_("No"))
  260. if confirmDialog.exec() != QMessageBox.Yes:
  261. return
  262. if profiles.profileActive(profile):
  263. QMessageBox.warning(
  264. self,
  265. _("Failed to delete profile."),
  266. _("Cannot remove active profiles!")
  267. )
  268. self.refresh()
  269. return
  270. if (profiles.default() is not None and
  271. profiles.default().id == profile.id):
  272. # Default got removed.
  273. profiles.setDefault(None)
  274. # Remove profile from profiles.conf
  275. profiles.remove(profile)
  276. if profile.id in self._newIds:
  277. self._newIds.remove(profile.id)
  278. else:
  279. # Register the profile id as removed.
  280. self._removedIds.append(profile.id)
  281. self._profileListWidget.currentItemChanged.disconnect(
  282. self.__itemChanged
  283. )
  284. self.refresh()
  285. self._profileListWidget.currentItemChanged.connect(
  286. self.__itemChanged
  287. )
  288. def __resetButtonClicked(self):
  289. # Confirmation
  290. confirmDialog = QMessageBox()
  291. confirmDialog.setIcon(QMessageBox.Warning)
  292. confirmDialog.setWindowTitle(_("Force release active profiles"))
  293. confirmDialog.setText(
  294. _("This will force release all active profiles.\n"
  295. "\n"
  296. "This should only be run if Searx-Qt didn't exit properly\n"
  297. "and you can't access your profile anymore.\n"
  298. "\n"
  299. "Make sure to close all other running Searx-Qt instances\n"
  300. "before continuing!\n"
  301. "\n"
  302. "Do you want to continue?")
  303. )
  304. confirmDialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  305. confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
  306. confirmDialog.button(QMessageBox.No).setText(_("No"))
  307. if confirmDialog.exec() != QMessageBox.Yes:
  308. return
  309. self._profiles.releaseAll()
  310. self.refresh()
  311. def __itemChanged(self, item, previousItem):
  312. if item:
  313. profile = item.data(Qt.UserRole)
  314. if (self._profiles.current().id == profile.id or
  315. profile.id in self._activeProfiles):
  316. # Can't remove current profile
  317. self._delButton.setEnabled(False)
  318. self._selectButton.setEnabled(False)
  319. else:
  320. self._delButton.setEnabled(True)
  321. self._selectButton.setEnabled(True)
  322. else:
  323. self._delButton.setEnabled(False)
  324. self._selectButton.setEnabled(False)
  325. self._currentIndex = self._profileListWidget.currentRow()
  326. def refresh(self):
  327. self._profileListWidget.clear()
  328. self._items.clear()
  329. self._resetButton.setEnabled(False)
  330. profiles = self._profiles
  331. settings = profiles.settings()
  332. self._activeProfiles = profiles.getActiveProfiles(settings)
  333. for profile in profiles:
  334. itemTxt = f"{profile.type} - {profile.name}"
  335. item = QListWidgetItem()
  336. currentProfile = self._profiles.current()
  337. if currentProfile.id == profile.id:
  338. itemTxt = f"* {itemTxt}"
  339. item.setText(itemTxt)
  340. if profile.id in self._activeProfiles:
  341. item.setFlags(item.flags() & Qt.ItemIsSelectable)
  342. self._resetButton.setEnabled(True)
  343. item.setData(Qt.UserRole, QVariant(profile))
  344. self._items.update({profile.id: item})
  345. self._profileListWidget.addItem(item)
  346. # Restore index
  347. if self._currentIndex >= 0:
  348. self._currentIndex = min(
  349. self._currentIndex, self._profileListWidget.count() - 1
  350. )
  351. self._profileListWidget.setCurrentRow(self._currentIndex)