123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615 |
- ########################################################################
- # Searx-Qt - Lightweight desktop application for Searx.
- # Copyright (C) 2020-2022 CYBERDEViL
- #
- # This file is part of Searx-Qt.
- #
- # Searx-Qt is free software: you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation, either version 3 of the License, or
- # (at your option) any later version.
- #
- # Searx-Qt is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program. If not, see <https://www.gnu.org/licenses/>.
- #
- ########################################################################
- from PyQt5.QtWidgets import (
- QWidget,
- QFrame,
- QVBoxLayout,
- QFormLayout,
- QGridLayout,
- QCheckBox,
- QLabel,
- QDoubleSpinBox,
- QLineEdit,
- QComboBox,
- QHBoxLayout,
- QSizePolicy,
- QTabWidget,
- QPlainTextEdit,
- QSpacerItem
- )
- from PyQt5.QtCore import Qt, pyqtSignal, QVariant
- from searxqt.views.guard import GuardSettings
- from searxqt.widgets.buttons import Button
- from searxqt.widgets.dialogs import UrlDialog
- from searxqt.translations import _
- from searxqt.themes import Themes
- from searxqt.core import log
- from searxqt.core.requests import HAVE_SOCKS, ProxyProtocol
- class ProxyWidget(QWidget):
- """
- @param model:
- @type model: searxqt.models.RequestSettingsModel
- """
- def __init__(self, model, parent=None):
- QWidget.__init__(self, parent=parent)
- self.__model = model
- layout = QFormLayout(self)
- self._proxyEnabledCheck = QCheckBox(self)
- self._proxyDNSCheck = QCheckBox(self)
- self._proxyType = QComboBox(self)
- self._proxyType.addItem("http")
- if HAVE_SOCKS:
- self._proxyType.addItem("socks4")
- self._proxyType.addItem("socks5")
- self._proxyStr = QLineEdit(self)
- self._proxyStr.setPlaceholderText(_("user:pass@host:port"))
- layout.addRow(QLabel(_("Enabled")), self._proxyEnabledCheck)
- layout.addRow(QLabel(_("Proxy DNS")), self._proxyDNSCheck)
- layout.addRow(QLabel(_("Protocol")), self._proxyType)
- layout.addRow(QLabel(_("Address")), self._proxyStr)
- # Set values
- self._setValuesFromModel(model)
- # Connections
- self._proxyEnabledCheck.toggled.connect(self.__enabledToggled)
- self._proxyDNSCheck.toggled.connect(self.__dnsToggled)
- self._proxyType.currentIndexChanged.connect(self.__protocolChanged)
- self._proxyStr.textEdited.connect(self.__hostTextEdited)
- def _setValuesFromModel(self, model):
- self._proxyEnabledCheck.setChecked(model.proxyEnabled)
- self.enableWidgets(model.proxyEnabled) # enable/disable widgets
- self._proxyDNSCheck.setChecked(model.proxyDNS)
- if model.proxyProtocol == ProxyProtocol.HTTP:
- self._proxyType.setCurrentIndex(0)
- elif HAVE_SOCKS:
- if model.proxyProtocol == ProxyProtocol.SOCKS4:
- self._proxyType.setCurrentIndex(1)
- elif model.proxyProtocol == ProxyProtocol.SOCKS5:
- self._proxyType.setCurrentIndex(2)
- self._proxyStr.setText(model.proxyHost)
- def __enabledToggled(self, state):
- self.__model.proxyEnabled = bool(state)
- self.enableWidgets(state)
- def enableWidgets(self, state):
- self._proxyDNSCheck.setEnabled(state)
- self._proxyType.setEnabled(state)
- self._proxyStr.setEnabled(state)
- def __dnsToggled(self, state):
- self.__model.proxyDNS = bool(state)
- def __protocolChanged(self, index):
- if index == 0:
- self.__model.proxyProtocol = ProxyProtocol.HTTP
- self._proxyDNSCheck.setEnabled(False)
- self._proxyDNSCheck.setToolTip(_("Not available for http proxy."))
- return
- elif index == 1:
- self.__model.proxyProtocol = ProxyProtocol.SOCKS4
- else: # index == 2
- self.__model.proxyProtocol = ProxyProtocol.SOCKS5
- self._proxyDNSCheck.setEnabled(True)
- def __hostTextEdited(self, newText):
- # TODO verify format
- self.__model.proxyHost = newText
- class TextEdit(QFrame):
- def __init__(self, model, parent=None):
- QFrame.__init__(self, parent)
- self._model = model
- layout = QVBoxLayout(self)
- buttonLayout = QHBoxLayout()
- self._textEdit = QPlainTextEdit(self)
- self._textEdit.setReadOnly(True)
- self._textEdit.setLineWrapMode(QPlainTextEdit.NoWrap)
- self._editButton = Button(_("Edit"), self)
- self._editButton.setCheckable(True)
- self._editButton.setChecked(False)
- self._cancelButton = Button(_("Cancel"), self)
- self._cancelButton.setCheckable(False)
- self._cancelButton.hide()
- buttonLayout.addWidget(self._cancelButton, 0, Qt.AlignLeft)
- buttonLayout.addWidget(self._editButton, 0, Qt.AlignLeft)
- buttonLayout.addStretch(1)
- layout.addWidget(self._textEdit)
- layout.addLayout(buttonLayout)
- self._editButton.toggled.connect(self.__toggleEdit)
- self._cancelButton.clicked.connect(self.__cancelEdit)
- def __toggleEdit(self, state):
- if state:
- self._editButton.setText(_("Save"))
- self._textEdit.setReadOnly(False)
- self._cancelButton.show()
- else:
- self._editButton.setText(_("Edit"))
- self._textEdit.setReadOnly(True)
- self._cancelButton.hide()
- self.updateToModel()
- def __cancelEdit(self, state):
- self._editButton.setText(_("Edit"))
- self._cancelButton.hide()
- self._textEdit.setReadOnly(True)
- self._editButton.setChecked(False)
- self.updateFromModel()
- def updateFromModel(self):
- pass # reimplement this
- def updateToModel(self):
- pass # reimplement this
- class RequestsHeaderEdit(TextEdit):
- def __init__(self, model, parent=None):
- TextEdit.__init__(self, model, parent)
- warningLabel = QLabel(_("WARNING! Edit below only when you know what "
- "you are doing!"), self)
- warningLabel.setWordWrap(True)
- self.layout().insertWidget(0, warningLabel)
- self.updateFromModel()
- def updateFromModel(self):
- text = ""
- for key, value in self._model.extraHeaders.items():
- text += f"\"{key}\" \"{value}\"\n"
- self._textEdit.setPlainText(text)
- def updateToModel(self):
- self._model.extraHeaders.clear()
- for line in self._textEdit.toPlainText().split("\n"):
- if " " not in line:
- continue
- key, value = line.split(" ", 1)
- value = value.lstrip().rstrip()
- if not key.startswith("\"") or not key.endswith("\""):
- # TODO malformed, inform the user
- continue
- if not value.startswith("\"") or not value.endswith("\""):
- # TODO malformed, inform the user
- continue
- key = key[1:len(key)-1]
- value = value[1:len(value)-1]
- if key.lower() == "user-agent":
- # TODO inform the user
- continue
- if "\"" in key:
- # TODO malformed, inform the user
- continue
- if "\"" in value:
- # TODO malformed, inform the user
- continue
- self._model.extraHeaders.update({key: value})
- self.updateFromModel()
- class UserAgentsEdit(TextEdit):
- def __init__(self, model, parent=None):
- TextEdit.__init__(self, model, parent)
- self._textEdit.setToolTip(
- """- One user-agent string per line.
- - Default user-agent string is the first (top) line.
- - Empty lines will be removed.
- - Leave empty to not send any user-agent string."""
- )
- self._randomUserAgent = QCheckBox(_("Random"), self)
- self._randomUserAgent.setToolTip(
- """When checked it will pick a random
- user-agent from the list for each request."""
- )
- self.layout().addWidget(self._randomUserAgent)
- self.updateFromModel()
- self._randomUserAgent.stateChanged.connect(self._randomUserAgentEdited)
- def _randomUserAgentEdited(self, state):
- self._model.randomUserAgent = bool(state)
- def updateToModel(self):
- text = self._textEdit.toPlainText()
- self._model.useragents = [s for s in text.split('\n') if s]
- self.updateFromModel()
- def updateFromModel(self):
- text = ""
- for userAgentStr in self._model.useragents:
- if not text:
- text = userAgentStr
- else:
- text += f"\n{userAgentStr}"
- self._textEdit.setPlainText(text)
- class RequestsSettings(QWidget):
- def __init__(self, model, parent=None):
- """
- @param model:
- @type model: searxqt.models.RequestSettingsModel
- """
- QWidget.__init__(self, parent=parent)
- self._model = model
- layout = QFormLayout(self)
- # Verify checkbox
- self._verifyCheck = QCheckBox(self)
- layout.addRow(QLabel(_("Verify") + " (SSL):"), self._verifyCheck)
- # Timeout double spinbox
- self._timeoutSpin = QDoubleSpinBox(self)
- self._timeoutSpin.setSuffix(" sec")
- self._timeoutSpin.setMinimum(3)
- self._timeoutSpin.setMaximum(300)
- layout.addRow(QLabel(_("Timeout") + ":"), self._timeoutSpin)
- # Proxy
- self._proxyWidget = ProxyWidget(self._model, self)
- layout.addRow(QLabel(_("Proxy") + ":"), self._proxyWidget)
- # Headers
- # User-agent
- self._useragents = UserAgentsEdit(model, self)
- layout.addRow(QLabel(_("User-Agents") + ":"), self._useragents)
- # Additional headers
- self._extraHeaders = RequestsHeaderEdit(model, self)
- layout.addRow(QLabel(_("Extra headers") + ":"), self._extraHeaders)
- # Init values for view
- self._changed()
- # Connections
- self._verifyCheck.stateChanged.connect(self.__verifyEdited)
- self._timeoutSpin.valueChanged.connect(self.__timeoutEdited)
- def __timeoutEdited(self, value):
- self._model.timeout = value
- def __verifyEdited(self, state):
- self._model.verifySSL = bool(state)
- def _changed(self):
- self._verifyCheck.setChecked(self._model.verifySSL)
- self._timeoutSpin.setValue(self._model.timeout)
- class SearxngSettings(QWidget):
- def __init__(self, model, parent=None):
- """
- model: SearchModel
- """
- QWidget.__init__(self, parent=parent)
- layout = QGridLayout(self)
- self.__searchModel = model
- self._useHtmlParser = QCheckBox(self)
- self._useHtmlParser.setChecked(model.parseHtml)
- self._safeSearch = QCheckBox(self)
- self._safeSearch.setChecked(model.safeSearch)
- infoLabel = QLabel(
- _("Since many SearXNG instances block API requests "
- "we now have a option to not use it and parse HTML "
- "response instead. Check \"Parse HTML\" below for "
- "that, when left unchecked it will use the API "
- "instead."), self)
- infoLabel.setWordWrap(True)
- layout.addWidget(infoLabel, 0, 0, 1, 2, Qt.AlignTop)
- layout.addWidget(QLabel(_("Parse HTML"), self), 1, 0,
- Qt.AlignLeft|Qt.AlignTop)
- layout.addWidget(self._useHtmlParser, 1, 1,
- Qt.AlignLeft|Qt.AlignTop)
- infoLabel = QLabel(_("Enable \"Safe search\" for search engines that "
- "support it."), self)
- infoLabel.setWordWrap(True)
- layout.addWidget(infoLabel, 2, 0, 1, 2, Qt.AlignTop)
- layout.addWidget(QLabel(_("Safe Search"), self), 3, 0,
- Qt.AlignLeft|Qt.AlignTop)
- layout.addWidget(self._safeSearch, 3, 1, Qt.AlignLeft|Qt.AlignTop)
- layout.setRowStretch(3, 1)
- layout.setColumnStretch(1, 1)
- self._useHtmlParser.stateChanged.connect(self.__useHtmlParserChanged)
- self._safeSearch.stateChanged.connect(self.__safeSearchChanged)
- def __useHtmlParserChanged(self, state):
- self.__searchModel.parseHtml = bool(state)
- def __safeSearchChanged(self, state):
- self.__searchModel.safeSearch = bool(state)
- class Stats2Settings(QWidget): # TODO rename to SearxSpaceSettings and rename stats2 to searxspace or something..
- def __init__(self, model, parent=None):
- """
- @type model: SearxStats2Model
- """
- QWidget.__init__(self, parent=parent)
- self._model = model
- layout = QVBoxLayout(self)
- infoLabel = QLabel(_(
- "The Searx-Stats2 project lists public Searx instances with"
- " statistics. The original instance is running at"
- " https://searx.space/. This is where Searx-Qt will request"
- " a list with instances when the update button is pressed."),
- self
- )
- infoLabel.setWordWrap(True)
- layout.addWidget(infoLabel, 0, Qt.AlignTop)
- hLayout = QHBoxLayout()
- label = QLabel("URL:", self)
- label.setSizePolicy(
- QSizePolicy(
- QSizePolicy.Maximum, QSizePolicy.Maximum
- )
- )
- self._urlLabel = QLabel(model.url, self)
- self._urlEditButton = Button(_("Edit"), self)
- self._urlResetButton = Button(_("Reset"), self)
- hLayout.addWidget(label, 0, Qt.AlignTop)
- hLayout.addWidget(self._urlLabel, 0, Qt.AlignTop)
- hLayout.addWidget(self._urlEditButton, 0, Qt.AlignTop)
- hLayout.addWidget(self._urlResetButton, 0, Qt.AlignTop)
- spacer = QSpacerItem(
- 20, 40, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding
- )
- layout.addLayout(hLayout)
- layout.addItem(spacer)
- self._urlEditButton.clicked.connect(self.__urlEditClicked)
- self._urlResetButton.clicked.connect(self.__urlResetClicked)
- model.changed.connect(self.__modelChanged)
- def __modelChanged(self):
- self._urlLabel.setText(self._model.url)
- def __urlEditClicked(self):
- dialog = UrlDialog(self._model.url)
- if dialog.exec():
- self._model.url = dialog.url
- def __urlResetClicked(self):
- self._model.reset()
- class LogLevelSettings(QWidget):
- def __init__(self, parent=None):
- QWidget.__init__(self, parent=parent)
- layout = QVBoxLayout(self)
- label = QLabel(_("<h2>CLI output level</h2>"), self)
- self.__cbInfo = QCheckBox(_("Info"), self)
- self.__cbWarning = QCheckBox(_("Warning"), self)
- self.__cbDebug = QCheckBox(_("Debug"), self)
- self.__cbError = QCheckBox(_("Error"), self)
- if log.LogLevel & log.LogLevels.INFO:
- self.__cbInfo.setChecked(True)
- if log.LogLevel & log.LogLevels.WARNING:
- self.__cbWarning.setChecked(True)
- if log.LogLevel & log.LogLevels.DEBUG:
- self.__cbDebug.setChecked(True)
- if log.LogLevel & log.LogLevels.ERROR:
- self.__cbError.setChecked(True)
- layout.addWidget(label)
- if log.DebugMode == True:
- label = QLabel(
- _("Debug mode enabled via environment variable"
- " 'SEARXQT_DEBUG'. The settings below are ignored,"
- " unset 'SEARXQT_DEBUG' and restart Searx-Qt to disable"
- " debug mode."),
- parent=self
- )
- label.setWordWrap(True)
- layout.addWidget(label)
- layout.addWidget(self.__cbInfo)
- layout.addWidget(self.__cbWarning)
- layout.addWidget(self.__cbDebug)
- layout.addWidget(self.__cbError)
- self.__cbInfo.stateChanged.connect(self.__stateChangedInfo)
- self.__cbWarning.stateChanged.connect(self.__stateChangedWarning)
- self.__cbDebug.stateChanged.connect(self.__stateChangedDebug)
- self.__cbError.stateChanged.connect(self.__stateChangedError)
- def __stateChanged(self, logLevel, state):
- if state:
- log.LogLevel |= logLevel
- else:
- log.LogLevel &= ~logLevel
- def __stateChangedInfo(self, state):
- self.__stateChanged(log.LogLevels.INFO, state)
- def __stateChangedWarning(self, state):
- self.__stateChanged(log.LogLevels.WARNING, state)
- def __stateChangedDebug(self, state):
- self.__stateChanged(log.LogLevels.DEBUG, state)
- def __stateChangedError(self, state):
- self.__stateChanged(log.LogLevels.ERROR, state)
- class GeneralSettings(QWidget):
- def __init__(self, parent=None):
- QWidget.__init__(self, parent=parent)
- layout = QVBoxLayout(self)
- # Theme
- label = QLabel(f"<h2>{_('Theme')}</h2>", self)
- layout.addWidget(label, 0, Qt.AlignTop)
- formLayout = QFormLayout()
- layout.addLayout(formLayout)
- self.__themesCombo = QComboBox(self)
- currentTheme = Themes.currentTheme
- indexOfCurrentTheme = 0
- index = 1
- self.__themesCombo.addItem("None", QVariant(None))
- for theme in Themes.themes:
- data = QVariant(theme)
- self.__themesCombo.addItem(theme.name, data)
- if theme.key == currentTheme:
- indexOfCurrentTheme = index
- index += 1
- self.__themesCombo.setCurrentIndex(indexOfCurrentTheme)
- formLayout.addRow(
- QLabel(_("Theme:"), self),
- self.__themesCombo
- )
- self.__stylesCombo = QComboBox(self)
- currentStyle = Themes.currentStyle
- indexOfCurrentStyle = 0
- index = 0
- for style in Themes.styles:
- self.__stylesCombo.addItem(style, QVariant(style))
- if style == currentStyle:
- indexOfCurrentStyle = index
- index += 1
- self.__stylesCombo.setCurrentIndex(indexOfCurrentStyle)
- formLayout.addRow(
- QLabel(_("Base style:"), self),
- self.__stylesCombo
- )
- applyButton = Button("Apply", self)
- applyButton.clicked.connect(self.__applyTheme)
- layout.addWidget(applyButton, 0, Qt.AlignTop)
- # Log level
- logLevelSettings = LogLevelSettings(self)
- layout.addWidget(logLevelSettings, 1, Qt.AlignTop)
- def __applyTheme(self):
- index = self.__stylesCombo.currentIndex()
- style = self.__stylesCombo.itemData(index, Qt.UserRole)
- Themes.setStyle(style)
- index = self.__themesCombo.currentIndex()
- theme = self.__themesCombo.itemData(index, Qt.UserRole)
- Themes.setTheme(theme.key if theme is not None else "")
- Themes.repolishAllWidgets()
- class SettingsWindow(QTabWidget):
- closed = pyqtSignal()
- def __init__(self, model, searchModel, guard, parent=None):
- """
- @type model: SettingsModel
- """
- QTabWidget.__init__(self, parent=parent)
- self.setWindowTitle(_("Settings"))
- # General settings
- self._generalView = GeneralSettings(self)
- self.addTab(self._generalView, _("General"))
- # Requests settings
- self._requestsView = RequestsSettings(model.requests, self)
- self.addTab(self._requestsView, _("Connection"))
- # SearXNG settings
- self._searxView = SearxngSettings(searchModel, self)
- self.addTab(self._searxView, "SearXNG")
- # Stats2 settings
- if model.stats2:
- self._stats2View = Stats2Settings(model.stats2, self)
- self.addTab(self._stats2View, "Searx-Stats2")
- # Guard settings
- self._guardView = GuardSettings(guard, self)
- self.addTab(self._guardView, _("Guard"))
- def closeEvent(self, event):
- QTabWidget.closeEvent(self, event)
- self.closed.emit()
|