profiles.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. ########################################################################
  2. # Searx-qt - Lightweight desktop application for SearX.
  3. # Copyright (C) 2020 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. if self.__getSanitizedName() in self._profiles.names():
  117. self._addButton.setEnabled(False)
  118. self._infoLabel.setText(_("Name already exists."))
  119. else:
  120. self._addButton.setEnabled(True)
  121. self._infoLabel.setText("")
  122. def __nameEdited(self, name):
  123. self.__doChecks()
  124. class ProfileChooserDialog(QDialog):
  125. def __init__(self, profiles, parent=None):
  126. QDialog.__init__(self, parent=parent)
  127. self.setWindowTitle(_("Profile select"))
  128. self._profiles = profiles
  129. self._items = {}
  130. self._selectedProfile = ProfileItem()
  131. self._activeProfiles = []
  132. # current profile index in the profile list (self._profileListWidget)
  133. self._currentIndex = -1
  134. # keep track of removed profile id's so their files can be
  135. # deleted on save.
  136. self._removedIds = []
  137. # keep track of new profile id's, these profiles don't have a
  138. # .conf file stored yet. When one of these id's get deleted we
  139. # don't have to remove the file because there is none.
  140. self._newIds = []
  141. layout = QVBoxLayout(self)
  142. # Top message
  143. labelTxt = _("Please select a profile")
  144. label = QLabel(labelTxt, self)
  145. layout.addWidget(label)
  146. hLayout = QHBoxLayout()
  147. layout.addLayout(hLayout)
  148. # Actions
  149. actionsLayout = QVBoxLayout()
  150. hLayout.addLayout(actionsLayout)
  151. self._addButton = Button(_("Add"), self)
  152. actionsLayout.addWidget(self._addButton, 0, Qt.AlignTop)
  153. self._delButton = Button(_("Delete"), self)
  154. self._delButton.setEnabled(False)
  155. actionsLayout.addWidget(self._delButton, 1, Qt.AlignTop)
  156. self._resetButton = Button(_("Force\nrelease\nprofiles"), self)
  157. self._resetButton.setEnabled(False)
  158. actionsLayout.addWidget(self._resetButton, 0, Qt.AlignBottom)
  159. # Profile list
  160. self._profileListWidget = QListWidget(self)
  161. hLayout.addWidget(self._profileListWidget)
  162. # Bottom stuff
  163. bottomLayout = QHBoxLayout()
  164. layout.addLayout(bottomLayout)
  165. self._defaultCheckBox = QCheckBox(_("Use as default"), self)
  166. bottomLayout.addWidget(self._defaultCheckBox, 1, Qt.AlignRight)
  167. exitButtonTxt = ""
  168. if profiles.current().id:
  169. exitButtonTxt = _("Cancel")
  170. else:
  171. exitButtonTxt = _("Exit")
  172. self._exitButton = Button(exitButtonTxt, self)
  173. bottomLayout.addWidget(self._exitButton, 0, Qt.AlignRight)
  174. self._selectButton = Button(_("Load profile"), self)
  175. self._selectButton.setEnabled(False)
  176. bottomLayout.addWidget(self._selectButton, 0, Qt.AlignRight)
  177. # Signals
  178. self._addButton.clicked.connect(self.__addButtonClicked)
  179. self._delButton.clicked.connect(self.__delButtonClicked)
  180. self._exitButton.clicked.connect(self.reject)
  181. self._selectButton.clicked.connect(self.__selectButtonClicked)
  182. self._profileListWidget.currentItemChanged.connect(self.__itemChanged)
  183. # Only show the 'force release profiles' button when there is
  184. # currently no profile set for this searx-qt instance.
  185. if not profiles.current().id:
  186. self._resetButton.clicked.connect(self.__resetButtonClicked)
  187. else:
  188. self._resetButton.hide()
  189. if not len(profiles):
  190. self.__addDialog()
  191. else:
  192. self.refresh()
  193. def selectedProfile(self): return self._selectedProfile
  194. def removedProfiles(self): return self._removedIds
  195. def promtProfileLoadFailed(self, msg):
  196. """
  197. @param msg: Message with what went wrong.
  198. @type msg: str
  199. """
  200. QMessageBox.warning(
  201. self,
  202. _("Failed to load profile."),
  203. msg
  204. )
  205. def __selectButtonClicked(self):
  206. item = self._profileListWidget.currentItem()
  207. selectedProfile = item.data(Qt.UserRole)
  208. profiles = self._profiles
  209. # Check if profile didn't get active in meantime.
  210. if profiles.profileActive(selectedProfile):
  211. self.promtProfileLoadFailed(
  212. _("Profile already in use."),
  213. )
  214. self.refresh()
  215. return
  216. # Check if profile still exists
  217. if (selectedProfile.id not in self._newIds and
  218. not profiles.profileExists(selectedProfile)):
  219. self.promtProfileLoadFailed(
  220. _("Profile doesn't exist anymore.")
  221. )
  222. self.refresh()
  223. return
  224. # TODO
  225. if self._defaultCheckBox.isChecked():
  226. self._profiles.setDefault(selectedProfile)
  227. else:
  228. self._profiles.setDefault(ProfileItem())
  229. self._selectedProfile = selectedProfile
  230. self.accept()
  231. def __addDialog(self):
  232. dialog = AddProfileDialog(self._profiles)
  233. if dialog.exec():
  234. self._newIds.append(dialog.id())
  235. self.refresh()
  236. def __addButtonClicked(self):
  237. self.__addDialog()
  238. def __delButtonClicked(self):
  239. item = self._profileListWidget.currentItem()
  240. profile = item.data(Qt.UserRole)
  241. profiles = self._profiles
  242. # Confirmation
  243. confirmDialog = QMessageBox()
  244. confirmDialog.setWindowTitle(
  245. _("Delete profile {profileName}")
  246. .format(profileName=profile.name)
  247. )
  248. confirmDialog.setText(
  249. _("Are you sure you want to delete profile '{profileName}'?")
  250. .format(profileName=profile.name)
  251. )
  252. confirmDialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
  253. confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
  254. confirmDialog.button(QMessageBox.No).setText(_("No"))
  255. if confirmDialog.exec() != QMessageBox.Yes:
  256. return
  257. if profiles.profileActive(profile):
  258. QMessageBox.warning(
  259. self,
  260. _("Failed to delete profile."),
  261. _("Cannot remove active profiles!")
  262. )
  263. self.refresh()
  264. return
  265. if profiles.default().id == profile.id:
  266. # Default got removed.
  267. profiles.setDefault(ProfileItem())
  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)