profiles.py 14 KB

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