settings.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  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 PyQt5.QtWidgets import (
  22. QWidget,
  23. QVBoxLayout,
  24. QFormLayout,
  25. QCheckBox,
  26. QLabel,
  27. QDoubleSpinBox,
  28. QLineEdit,
  29. QComboBox,
  30. QHBoxLayout,
  31. QSizePolicy,
  32. QTabWidget,
  33. QPlainTextEdit,
  34. QSpacerItem
  35. )
  36. from PyQt5.QtCore import Qt, pyqtSignal, QVariant
  37. from searxqt.views.guard import GuardSettings
  38. from searxqt.widgets.buttons import Button
  39. from searxqt.widgets.dialogs import UrlDialog
  40. from searxqt.translations import _
  41. from searxqt.themes import Themes
  42. from searxqt.core import log
  43. from searxqt.core.requests import HAVE_SOCKS, ProxyProtocol
  44. class ProxyWidget(QWidget):
  45. """
  46. @param model:
  47. @type model: searxqt.models.RequestSettingsModel
  48. """
  49. def __init__(self, model, parent=None):
  50. QWidget.__init__(self, parent=parent)
  51. self.__model = model
  52. layout = QFormLayout(self)
  53. self._proxyEnabledCheck = QCheckBox(self)
  54. self._proxyDNSCheck = QCheckBox(self)
  55. self._proxyType = QComboBox(self)
  56. self._proxyType.addItem("http")
  57. if HAVE_SOCKS:
  58. self._proxyType.addItem("socks4")
  59. self._proxyType.addItem("socks5")
  60. self._proxyStr = QLineEdit(self)
  61. self._proxyStr.setPlaceholderText(_("user:pass@host:port"))
  62. layout.addRow(QLabel(_("Enabled")), self._proxyEnabledCheck)
  63. layout.addRow(QLabel(_("Proxy DNS")), self._proxyDNSCheck)
  64. layout.addRow(QLabel(_("Protocol")), self._proxyType)
  65. layout.addRow(QLabel(_("Address")), self._proxyStr)
  66. # Set values
  67. self._setValuesFromModel(model)
  68. # Connections
  69. self._proxyEnabledCheck.toggled.connect(self.__enabledToggled)
  70. self._proxyDNSCheck.toggled.connect(self.__dnsToggled)
  71. self._proxyType.currentIndexChanged.connect(self.__protocolChanged)
  72. self._proxyStr.textEdited.connect(self.__hostTextEdited)
  73. def _setValuesFromModel(self, model):
  74. self._proxyEnabledCheck.setChecked(model.proxyEnabled)
  75. self.enableWidgets(model.proxyEnabled) # enable/disable widgets
  76. self._proxyDNSCheck.setChecked(model.proxyDNS)
  77. if model.proxyProtocol == ProxyProtocol.HTTP:
  78. self._proxyType.setCurrentIndex(0)
  79. elif HAVE_SOCKS:
  80. if model.proxyProtocol == ProxyProtocol.SOCKS4:
  81. self._proxyType.setCurrentIndex(1)
  82. elif model.proxyProtocol == ProxyProtocol.SOCKS5:
  83. self._proxyType.setCurrentIndex(2)
  84. self._proxyStr.setText(model.proxyHost)
  85. def __enabledToggled(self, state):
  86. self.__model.proxyEnabled = bool(state)
  87. self.enableWidgets(state)
  88. def enableWidgets(self, state):
  89. self._proxyDNSCheck.setEnabled(state)
  90. self._proxyType.setEnabled(state)
  91. self._proxyStr.setEnabled(state)
  92. def __dnsToggled(self, state):
  93. self.__model.proxyDNS = bool(state)
  94. def __protocolChanged(self, index):
  95. if index == 0:
  96. self.__model.proxyProtocol = ProxyProtocol.HTTP
  97. self._proxyDNSCheck.setEnabled(False)
  98. self._proxyDNSCheck.setToolTip(_("Not available for http proxy."))
  99. return
  100. elif index == 1:
  101. self.__model.proxyProtocol = ProxyProtocol.SOCKS4
  102. else: # index == 2
  103. self.__model.proxyProtocol = ProxyProtocol.SOCKS5
  104. self._proxyDNSCheck.setEnabled(True)
  105. def __hostTextEdited(self, newText):
  106. # TODO verify format
  107. self.__model.proxyHost = newText
  108. class RequestsSettings(QWidget):
  109. def __init__(self, model, parent=None):
  110. """
  111. @param model:
  112. @type model: searxqt.models.RequestSettingsModel
  113. """
  114. QWidget.__init__(self, parent=parent)
  115. self._model = model
  116. layout = QFormLayout(self)
  117. # Verify checkbox
  118. self._verifyCheck = QCheckBox(self)
  119. layout.addRow(QLabel(_("Verify") + " (SSL):"), self._verifyCheck)
  120. # Timeout double spinbox
  121. self._timeoutSpin = QDoubleSpinBox(self)
  122. self._timeoutSpin.setSuffix(" sec")
  123. self._timeoutSpin.setMinimum(3)
  124. self._timeoutSpin.setMaximum(300)
  125. layout.addRow(QLabel(_("Timeout") + ":"), self._timeoutSpin)
  126. # Proxy
  127. self._proxyWidget = ProxyWidget(self._model, self)
  128. layout.addRow(QLabel(_("Proxy") + ":"), self._proxyWidget)
  129. # Headers
  130. # User-agent
  131. userAgentLayout = QFormLayout()
  132. layout.addRow(QLabel(_("User-agents") + ":"), userAgentLayout)
  133. self._userAgentStringsEdit = QPlainTextEdit(self)
  134. self._userAgentStringsEdit.setToolTip(
  135. """- One user-agent string per line.
  136. - Default user-agent string is the first (top) line.
  137. - Empty lines will be removed.
  138. - Leave empty to not send any user-agent string."""
  139. )
  140. userAgentLayout.addWidget(self._userAgentStringsEdit)
  141. self._userAgentEditButton = Button("", self)
  142. self._userAgentEditButton.setCheckable(True)
  143. userAgentLayout.addWidget(self._userAgentEditButton)
  144. self._randomUserAgent = QCheckBox(_("Random"), self)
  145. self._randomUserAgent.setToolTip(
  146. """When checked it will pick a random
  147. user-agent from the list for each request."""
  148. )
  149. userAgentLayout.addWidget(self._randomUserAgent)
  150. # Init values for view
  151. self._changed()
  152. # Connections
  153. self._verifyCheck.stateChanged.connect(self.__verifyEdited)
  154. self._timeoutSpin.valueChanged.connect(self.__timeoutEdited)
  155. self._userAgentEditButton.toggled.connect(self._toggleUserAgentEdit)
  156. self._randomUserAgent.stateChanged.connect(self._randomUserAgentEdited)
  157. self.__unsetWidgetsEditMode()
  158. def _randomUserAgentEdited(self, state):
  159. self._model.randomUserAgent = bool(state)
  160. def __setWidgetsEditMode(self):
  161. self._userAgentEditButton.setText(_("Save"))
  162. self._userAgentStringsEdit.setReadOnly(False)
  163. self._userAgentStringsEdit.setFocus()
  164. def __unsetWidgetsEditMode(self):
  165. self._userAgentEditButton.setText(_("Edit"))
  166. self._userAgentStringsEdit.setReadOnly(True)
  167. def _toggleUserAgentEdit(self, state):
  168. if state:
  169. self.__setWidgetsEditMode()
  170. else:
  171. self.__unsetWidgetsEditMode()
  172. self.__UserAgentStringsEdited(
  173. self._userAgentStringsEdit.toPlainText()
  174. )
  175. def __UserAgentStringsEdited(self, value):
  176. """
  177. @param value: String with the ?user-agent(s)
  178. @type value: str
  179. """
  180. self._model.useragents = [s for s in value.split('\n') if s]
  181. self._userAgentListChanged()
  182. def __timeoutEdited(self, value):
  183. self._model.timeout = value
  184. def __verifyEdited(self, state):
  185. self._model.verifySSL = bool(state)
  186. def _userAgentListChanged(self):
  187. txt = ""
  188. for userAgentStr in self._model.useragents:
  189. if not txt:
  190. txt = userAgentStr
  191. else:
  192. txt += f"\n{userAgentStr}"
  193. self._userAgentStringsEdit.setPlainText(txt)
  194. def _changed(self):
  195. self._verifyCheck.setChecked(self._model.verifySSL)
  196. self._timeoutSpin.setValue(self._model.timeout)
  197. self._userAgentListChanged()
  198. self._randomUserAgent.setChecked(self._model.randomUserAgent)
  199. class Stats2Settings(QWidget):
  200. def __init__(self, model, parent=None):
  201. """
  202. @type model: SearxStats2Model
  203. """
  204. QWidget.__init__(self, parent=parent)
  205. self._model = model
  206. layout = QVBoxLayout(self)
  207. infoLabel = QLabel(_(
  208. "The Searx-Stats2 project lists public Searx instances with"
  209. " statistics. The original instance is running at"
  210. " https://searx.space/. This is where Searx-Qt will request"
  211. " a list with instances when the update button is pressed."),
  212. self
  213. )
  214. infoLabel.setWordWrap(True)
  215. layout.addWidget(infoLabel, 0, Qt.AlignTop)
  216. hLayout = QHBoxLayout()
  217. label = QLabel("URL:", self)
  218. label.setSizePolicy(
  219. QSizePolicy(
  220. QSizePolicy.Maximum, QSizePolicy.Maximum
  221. )
  222. )
  223. self._urlLabel = QLabel(model.url, self)
  224. self._urlEditButton = Button(_("Edit"), self)
  225. self._urlResetButton = Button(_("Reset"), self)
  226. hLayout.addWidget(label, 0, Qt.AlignTop)
  227. hLayout.addWidget(self._urlLabel, 0, Qt.AlignTop)
  228. hLayout.addWidget(self._urlEditButton, 0, Qt.AlignTop)
  229. hLayout.addWidget(self._urlResetButton, 0, Qt.AlignTop)
  230. spacer = QSpacerItem(
  231. 20, 40, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding
  232. )
  233. layout.addLayout(hLayout)
  234. layout.addItem(spacer)
  235. self._urlEditButton.clicked.connect(self.__urlEditClicked)
  236. self._urlResetButton.clicked.connect(self.__urlResetClicked)
  237. model.changed.connect(self.__modelChanged)
  238. def __modelChanged(self):
  239. self._urlLabel.setText(self._model.url)
  240. def __urlEditClicked(self):
  241. dialog = UrlDialog(self._model.url)
  242. if dialog.exec():
  243. self._model.url = dialog.url
  244. def __urlResetClicked(self):
  245. self._model.reset()
  246. class LogLevelSettings(QWidget):
  247. def __init__(self, parent=None):
  248. QWidget.__init__(self, parent=parent)
  249. layout = QVBoxLayout(self)
  250. label = QLabel(_("<h2>CLI output level</h2>"), self)
  251. self.__cbInfo = QCheckBox(_("Info"), self)
  252. self.__cbWarning = QCheckBox(_("Warning"), self)
  253. self.__cbDebug = QCheckBox(_("Debug"), self)
  254. self.__cbError = QCheckBox(_("Error"), self)
  255. if log.LogLevel & log.LogLevels.INFO:
  256. self.__cbInfo.setChecked(True)
  257. if log.LogLevel & log.LogLevels.WARNING:
  258. self.__cbWarning.setChecked(True)
  259. if log.LogLevel & log.LogLevels.DEBUG:
  260. self.__cbDebug.setChecked(True)
  261. if log.LogLevel & log.LogLevels.ERROR:
  262. self.__cbError.setChecked(True)
  263. layout.addWidget(label)
  264. if log.DebugMode == True:
  265. label = QLabel(
  266. _("Debug mode enabled via environment variable"
  267. " 'SEARXQT_DEBUG'. The settings below are ignored,"
  268. " unset 'SEARXQT_DEBUG' and restart Searx-Qt to disable"
  269. " debug mode."),
  270. parent=self
  271. )
  272. label.setWordWrap(True)
  273. layout.addWidget(label)
  274. layout.addWidget(self.__cbInfo)
  275. layout.addWidget(self.__cbWarning)
  276. layout.addWidget(self.__cbDebug)
  277. layout.addWidget(self.__cbError)
  278. self.__cbInfo.stateChanged.connect(self.__stateChangedInfo)
  279. self.__cbWarning.stateChanged.connect(self.__stateChangedWarning)
  280. self.__cbDebug.stateChanged.connect(self.__stateChangedDebug)
  281. self.__cbError.stateChanged.connect(self.__stateChangedError)
  282. def __stateChanged(self, logLevel, state):
  283. if state:
  284. log.LogLevel |= logLevel
  285. else:
  286. log.LogLevel &= ~logLevel
  287. def __stateChangedInfo(self, state):
  288. self.__stateChanged(log.LogLevels.INFO, state)
  289. def __stateChangedWarning(self, state):
  290. self.__stateChanged(log.LogLevels.WARNING, state)
  291. def __stateChangedDebug(self, state):
  292. self.__stateChanged(log.LogLevels.DEBUG, state)
  293. def __stateChangedError(self, state):
  294. self.__stateChanged(log.LogLevels.ERROR, state)
  295. class GeneralSettings(QWidget):
  296. def __init__(self, parent=None):
  297. QWidget.__init__(self, parent=parent)
  298. layout = QVBoxLayout(self)
  299. # Theme
  300. label = QLabel(f"<h2>{_('Theme')}</h2>", self)
  301. layout.addWidget(label, 0, Qt.AlignTop)
  302. formLayout = QFormLayout()
  303. layout.addLayout(formLayout)
  304. self.__themesCombo = QComboBox(self)
  305. currentTheme = Themes.currentTheme
  306. indexOfCurrentTheme = 0
  307. index = 1
  308. self.__themesCombo.addItem("None", QVariant(None))
  309. for theme in Themes.themes:
  310. data = QVariant(theme)
  311. self.__themesCombo.addItem(theme.name, data)
  312. if theme.key == currentTheme:
  313. indexOfCurrentTheme = index
  314. index += 1
  315. self.__themesCombo.setCurrentIndex(indexOfCurrentTheme)
  316. formLayout.addRow(
  317. QLabel(_("Theme:"), self),
  318. self.__themesCombo
  319. )
  320. self.__stylesCombo = QComboBox(self)
  321. currentStyle = Themes.currentStyle
  322. indexOfCurrentStyle = 0
  323. index = 0
  324. for style in Themes.styles:
  325. self.__stylesCombo.addItem(style, QVariant(style))
  326. if style == currentStyle:
  327. indexOfCurrentStyle = index
  328. index += 1
  329. self.__stylesCombo.setCurrentIndex(indexOfCurrentStyle)
  330. formLayout.addRow(
  331. QLabel(_("Base style:"), self),
  332. self.__stylesCombo
  333. )
  334. applyButton = Button("Apply", self)
  335. applyButton.clicked.connect(self.__applyTheme)
  336. layout.addWidget(applyButton, 0, Qt.AlignTop)
  337. # Log level
  338. logLevelSettings = LogLevelSettings(self)
  339. layout.addWidget(logLevelSettings, 1, Qt.AlignTop)
  340. def __applyTheme(self):
  341. index = self.__stylesCombo.currentIndex()
  342. style = self.__stylesCombo.itemData(index, Qt.UserRole)
  343. Themes.setStyle(style)
  344. index = self.__themesCombo.currentIndex()
  345. theme = self.__themesCombo.itemData(index, Qt.UserRole)
  346. Themes.setTheme(theme.key if theme is not None else "")
  347. Themes.repolishAllWidgets()
  348. class SettingsWindow(QTabWidget):
  349. closed = pyqtSignal()
  350. def __init__(self, model, guard, parent=None):
  351. """
  352. @type model: SettingsModel
  353. """
  354. QTabWidget.__init__(self, parent=parent)
  355. self.setWindowTitle(_("Settings"))
  356. # General settings
  357. self._generalView = GeneralSettings(self)
  358. self.addTab(self._generalView, _("General"))
  359. # Requests settings
  360. self._requestsView = RequestsSettings(model.requests, self)
  361. self.addTab(self._requestsView, _("Connection"))
  362. # Stats2 settings
  363. if model.stats2:
  364. self._stats2View = Stats2Settings(model.stats2, self)
  365. self.addTab(self._stats2View, "Searx-Stats2")
  366. # Guard settings
  367. self._guardView = GuardSettings(guard, self)
  368. self.addTab(self._guardView, _("Guard"))
  369. def closeEvent(self, event):
  370. QTabWidget.closeEvent(self, event)
  371. self.closed.emit()