profiles.py 13 KB

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