search.py 48 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451
  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. import html # For escaping html text
  22. from PyQt5.QtWidgets import (
  23. QWidget,
  24. QVBoxLayout,
  25. QHBoxLayout,
  26. QLineEdit,
  27. QTextBrowser,
  28. QCheckBox,
  29. QLabel,
  30. QComboBox,
  31. QFrame,
  32. QMenu,
  33. QWidgetAction,
  34. QSpacerItem,
  35. QSizePolicy,
  36. QDialog,
  37. QListWidget,
  38. QTableView,
  39. QSplitter,
  40. QAbstractItemView,
  41. QHeaderView,
  42. QFormLayout,
  43. QMessageBox
  44. )
  45. from PyQt5.QtCore import pyqtSignal, Qt, QVariant, QByteArray
  46. from PyQt5.QtGui import QPalette, QColor
  47. from searxqt.core.guard import ConsequenceType
  48. from searxqt.models.search import (
  49. SearchStatus,
  50. UserCategoriesModel,
  51. CategoriesModel
  52. )
  53. from searxqt.models.instances import EnginesTableModel
  54. from searxqt.widgets.buttons import Button, CheckboxOptionsButton
  55. from searxqt.thread import Thread
  56. from searxqt.translations import _
  57. class SearchNavigation(QWidget):
  58. requestPage = pyqtSignal(int) # pageno
  59. def __init__(self, parent=None):
  60. QWidget.__init__(self, parent=parent)
  61. layout = QHBoxLayout(self)
  62. self.prevPageButton = Button("<<", self)
  63. self.pageNoLabel = QLabel("1", self)
  64. self.nextPageButton = Button(">>", self)
  65. layout.addWidget(self.prevPageButton, 0, Qt.AlignLeft)
  66. layout.addWidget(self.pageNoLabel, 0, Qt.AlignCenter)
  67. layout.addWidget(self.nextPageButton, 0, Qt.AlignRight)
  68. self.prevPageButton.setEnabled(False)
  69. self.nextPageButton.clicked.connect(self._nextPage)
  70. self.prevPageButton.clicked.connect(self._prevPage)
  71. self.reset()
  72. def _updateLabel(self):
  73. self.pageNoLabel.setText(str(self._pageno))
  74. def _nextPage(self):
  75. self._pageno += 1
  76. if self._pageno > 1 and not self.prevPageButton.isEnabled():
  77. self.prevPageButton.setEnabled(True)
  78. self._updateLabel()
  79. self.requestPage.emit(self._pageno)
  80. def _prevPage(self):
  81. self._pageno -= 1
  82. if self._pageno == 1:
  83. self.prevPageButton.setEnabled(False)
  84. self.setNextEnabled(True)
  85. self._updateLabel()
  86. self.requestPage.emit(self._pageno)
  87. def reset(self):
  88. self._pageno = 1
  89. self.prevPageButton.setEnabled(False)
  90. self._updateLabel()
  91. def setNextEnabled(self, state):
  92. self.nextPageButton.setEnabled(state)
  93. class SearchEngines(CheckboxOptionsButton):
  94. def __init__(self, searchModel, instancesModel, parent=None):
  95. """
  96. @param searchModel: needed for getting and setting current
  97. enabled/disabled engines.
  98. @type searchModel: SearchModel
  99. @param instancesModel: needed for listing current available
  100. engines and update it's current filter
  101. to filter out instances without atleast
  102. one of the required engine(s).
  103. @type instancesModel: InstanceModelFilter
  104. """
  105. self._instancesModel = instancesModel
  106. self._searchModel = searchModel
  107. CheckboxOptionsButton.__init__(
  108. self,
  109. labelName=_("Engines"),
  110. parent=parent
  111. )
  112. instancesModel.parentModel().changed.connect(self.reGenerate)
  113. def updateFilter(self):
  114. """ Filter out instances that don't support atleast one of the
  115. enabled engines.
  116. """
  117. self._instancesModel.updateKwargs(
  118. {'engines': self.getCheckedOptionNames()}
  119. )
  120. """ Below are re-implementations.
  121. """
  122. def getCheckedOptionNames(self):
  123. """ Should return a list with checked option names. This will
  124. be used to generate the label.
  125. @return: should return a list with strings.
  126. @rtype: list
  127. """
  128. return self._searchModel.engines
  129. def getOptions(self):
  130. """ Should return a list with options tuple(key, name, state)
  131. This will be used to generate the options.
  132. """
  133. list_ = []
  134. tmp = []
  135. for url, instance in self._instancesModel.items():
  136. for engine in instance.engines:
  137. if engine.name not in tmp:
  138. state = bool(engine.name in self._searchModel.engines)
  139. list_.append((engine.name, engine.name, state))
  140. tmp.append(engine.name)
  141. return sorted(list_)
  142. def optionToggled(self, key, state):
  143. if state:
  144. self._searchModel.engines.append(key)
  145. else:
  146. self._searchModel.engines.remove(key)
  147. self.updateFilter()
  148. class CategoryEditor(QDialog):
  149. def __init__(self, enginesModel, categoriesModel,
  150. userCategoriesModel, parent=None):
  151. QDialog.__init__(self, parent=parent)
  152. self._categoriesModel = categoriesModel
  153. self._userCategoriesModel = userCategoriesModel
  154. self.setWindowTitle(_("Category manager"))
  155. layout = QHBoxLayout(self)
  156. # Splitter to horizontal split the categories widget and the engines
  157. # widget so their width becomes adjustable.
  158. self.splitter = QSplitter(self)
  159. self.splitter.setOrientation(Qt.Horizontal)
  160. layout.addWidget(self.splitter)
  161. # Categories
  162. catWidget = QWidget(self.splitter)
  163. catLayout = QVBoxLayout(catWidget)
  164. label = QLabel("<h2>{0}</h2>".format(_("Categories")), self)
  165. # Categories toolbuttons
  166. catToolLayout = QHBoxLayout()
  167. catAddButton = Button("+", self)
  168. self._catDelButton = Button("-", self)
  169. catToolLayout.addWidget(catAddButton, 0, Qt.AlignLeft)
  170. catToolLayout.addWidget(self._catDelButton, 1, Qt.AlignLeft)
  171. self._categoryListWidget = QListWidget(self)
  172. catLayout.addWidget(label)
  173. catLayout.addLayout(catToolLayout)
  174. catLayout.addWidget(self._categoryListWidget)
  175. # Engines
  176. engWidget = QWidget(self.splitter)
  177. engLayout = QVBoxLayout(engWidget)
  178. label = QLabel("<h2>{0}</h2>".format(_("Engines")), self)
  179. # Engines filter
  180. filterLayout = QHBoxLayout()
  181. self._enginesCategoryFilterBox = QComboBox(self)
  182. self._enginesCategoryFilterBox.addItem(_("All"))
  183. for key, cat in categoriesModel.items():
  184. self._enginesCategoryFilterBox.addItem(cat.name)
  185. filterLayout.addWidget(
  186. QLabel(_("Category") + ":", self), 1, Qt.AlignRight
  187. )
  188. filterLayout.addWidget(
  189. self._enginesCategoryFilterBox, 0, Qt.AlignRight
  190. )
  191. # Engines table
  192. self._enginesTableView = QTableView(self)
  193. self._enginesTableView.setAlternatingRowColors(True)
  194. self._enginesTableView.setSelectionBehavior(
  195. QAbstractItemView.SelectRows
  196. )
  197. self._enginesTableView.setEditTriggers(
  198. QAbstractItemView.NoEditTriggers
  199. )
  200. self._enginesTableView.setSortingEnabled(True)
  201. self._enginesTableView.setHorizontalScrollMode(
  202. QAbstractItemView.ScrollPerPixel
  203. )
  204. header = self._enginesTableView.horizontalHeader()
  205. header.setSectionResizeMode(QHeaderView.ResizeToContents)
  206. header.setSectionsMovable(True)
  207. self._enginesTableModel = EnginesTableModel(enginesModel, self)
  208. self._enginesTableView.setModel(self._enginesTableModel)
  209. engLayout.addWidget(label)
  210. engLayout.addLayout(filterLayout)
  211. engLayout.addWidget(self._enginesTableView)
  212. # Connections
  213. catAddButton.clicked.connect(self.__addCategoryClicked)
  214. self._catDelButton.clicked.connect(self.__delCategoryClicked)
  215. self._categoryListWidget.currentRowChanged.connect(
  216. self.__currentUserCategoryChanged
  217. )
  218. self._enginesCategoryFilterBox.currentIndexChanged.connect(
  219. self.__enginesCategoryFilterChanged
  220. )
  221. self._enginesTableView.setEnabled(False)
  222. self._catDelButton.setEnabled(False)
  223. self.__addUserCategories()
  224. self.__selectFirst()
  225. def __currentUserCategoryChanged(self, index):
  226. if index < 0:
  227. self._enginesTableView.setEnabled(False)
  228. self._catDelButton.setEnabled(False)
  229. else:
  230. self._enginesTableView.setEnabled(True)
  231. self._catDelButton.setEnabled(True)
  232. if self._userCategoriesModel:
  233. key = list(self._userCategoriesModel.keys())[index]
  234. self._enginesTableModel.setUserModel(
  235. self._userCategoriesModel[key]
  236. )
  237. def __addUserCategories(self):
  238. for catKey, cat in self._userCategoriesModel.items():
  239. self._categoryListWidget.addItem(cat.name)
  240. def __selectFirst(self):
  241. if self._categoryListWidget.count():
  242. self._categoryListWidget.setCurrentRow(0)
  243. def __selectLast(self):
  244. self._categoryListWidget.setCurrentRow(
  245. self._categoryListWidget.count() - 1
  246. )
  247. def __reloadUserCategories(self):
  248. self._categoryListWidget.currentRowChanged.disconnect(
  249. self.__currentUserCategoryChanged
  250. )
  251. self._enginesTableModel.setUserModel(None)
  252. self._categoryListWidget.clear()
  253. self.__addUserCategories()
  254. self._categoryListWidget.currentRowChanged.connect(
  255. self.__currentUserCategoryChanged
  256. )
  257. def __addCategoryClicked(self, state):
  258. dialog = AddUserCategoryDialog(
  259. self._userCategoriesModel.keys()
  260. )
  261. if dialog.exec():
  262. self._userCategoriesModel.addCategory(
  263. dialog.name.lower(),
  264. dialog.name
  265. )
  266. self.__reloadUserCategories()
  267. self.__selectLast()
  268. def __delCategoryClicked(self, state):
  269. index = self._categoryListWidget.currentRow()
  270. key = list(self._userCategoriesModel.keys())[index]
  271. confirmDialog = QMessageBox()
  272. confirmDialog.setWindowTitle(
  273. _("Delete category")
  274. )
  275. confirmDialog.setText(
  276. _("Are you sure you want to delete the category `{0}`?")
  277. .format(self._userCategoriesModel[key].name)
  278. )
  279. confirmDialog.setStandardButtons(
  280. QMessageBox.Yes | QMessageBox.No
  281. )
  282. confirmDialog.button(QMessageBox.Yes).setText(_("Yes"))
  283. confirmDialog.button(QMessageBox.No).setText(_("No"))
  284. if confirmDialog.exec() != QMessageBox.Yes:
  285. return
  286. self._userCategoriesModel.removeCategory(key)
  287. self.__reloadUserCategories()
  288. self.__selectLast()
  289. self.__currentUserCategoryChanged(
  290. self._categoryListWidget.count() - 1
  291. )
  292. def __enginesCategoryFilterChanged(self, index):
  293. if not index: # All
  294. self._enginesTableModel.setCatFilter()
  295. else:
  296. key = list(self._categoriesModel.keys())[index-1]
  297. self._enginesTableModel.setCatFilter(key)
  298. class AddUserCategoryDialog(QDialog):
  299. def __init__(self, existingNames=[], text="", parent=None):
  300. QDialog.__init__(self, parent=parent)
  301. self._existingNames = existingNames
  302. layout = QFormLayout(self)
  303. label = QLabel(_("Name") + ":")
  304. self._nameEdit = QLineEdit(self)
  305. if text:
  306. self._nameEdit.setText(text)
  307. self._nameEdit.setPlaceholderText(text)
  308. else:
  309. self._nameEdit.setPlaceholderText(_("My category"))
  310. self._cancelButton = Button(_("Cancel"), self)
  311. self._addButton = Button(_("Add"), self)
  312. self._addButton.setEnabled(False)
  313. # Add stuff to layout
  314. layout.addRow(label, self._nameEdit)
  315. layout.addRow(self._cancelButton, self._addButton)
  316. # Connections
  317. self._nameEdit.textChanged.connect(self.__inputChanged)
  318. self._addButton.clicked.connect(self.accept)
  319. self._cancelButton.clicked.connect(self.reject)
  320. def __inputChanged(self, text):
  321. if self.isValid():
  322. self._addButton.setEnabled(True)
  323. else:
  324. self._addButton.setEnabled(False)
  325. def isValid(self):
  326. name = self._nameEdit.text().lower()
  327. if not name:
  328. return False
  329. for existingName in self._existingNames:
  330. if name == existingName.lower():
  331. return False
  332. return True
  333. @property
  334. def name(self):
  335. return self._nameEdit.text()
  336. class SearchCategories(CheckboxOptionsButton):
  337. def __init__(self, categoriesModel, enginesModel,
  338. userCategoriesModel, parent=None):
  339. """
  340. @param categoriesModel:
  341. @type categoriesModel: searxqt.models.search.CategoriesModel
  342. @param enginesModel:
  343. @type enginesModel: searxqt.models.instances.EnginesModel
  344. @param userCategoriesModel:
  345. @type userCategoriesModel: searxqt.models.search.UserCategoriesModel
  346. @param parent:
  347. @type parent: QObject or None
  348. """
  349. self._categoriesModel = categoriesModel
  350. self._enginesModel = enginesModel
  351. self._userCategoriesModel = userCategoriesModel
  352. CheckboxOptionsButton.__init__(
  353. self,
  354. labelName=_("Categories"),
  355. parent=parent
  356. )
  357. self._categoriesModel.dataChanged.connect(self.__categoryDataChanged)
  358. def __categoryDataChanged(self):
  359. # This happends after CategoriesModel.setData
  360. self.reGenerate()
  361. def __openUserCategoryEditor(self):
  362. window = CategoryEditor(
  363. self._enginesModel,
  364. self._categoriesModel,
  365. self._userCategoriesModel, self)
  366. window.exec()
  367. def __userCategoryToggled(self, key, state):
  368. if state:
  369. self._userCategoriesModel[key].check()
  370. else:
  371. self._userCategoriesModel[key].uncheck()
  372. self.reGenerate()
  373. """ Below are re-implementations.
  374. """
  375. def addCustomWidgetsTop(self, menu):
  376. menu.addSection(_("Custom"))
  377. action = QWidgetAction(menu)
  378. manageCustomButton = Button(_("Manage"), menu)
  379. action.setDefaultWidget(manageCustomButton)
  380. menu.addAction(action)
  381. for catKey, cat in self._userCategoriesModel.items():
  382. action = QWidgetAction(menu)
  383. widget = QCheckBox(cat.name, menu)
  384. widget.setTristate(False)
  385. widget.setChecked(
  386. cat.isChecked()
  387. )
  388. action.setDefaultWidget(widget)
  389. widget.stateChanged.connect(
  390. lambda state, key=catKey:
  391. self.__userCategoryToggled(key, state)
  392. )
  393. menu.addAction(action)
  394. menu.addSection(_("Default"))
  395. manageCustomButton.clicked.connect(self.__openUserCategoryEditor)
  396. def hasEnabledCheckedKeys(self):
  397. """ Same as CheckboxOptionsButton.hasEnabledCheckedKeys(self) but with
  398. User Categories. Categories don't get enabled/disabled so we can skip
  399. that check.
  400. @rtype: bool
  401. """
  402. if self._userCategoriesModel.checkedCategories():
  403. return True
  404. return CheckboxOptionsButton.hasEnabledCheckedKeys(self)
  405. def uncheckAllEnabledKeys(self):
  406. """ Unchecks all checked keys that are enabled.
  407. """
  408. for catKey in self._userCategoriesModel.checkedCategories():
  409. self._userCategoriesModel[catKey].uncheck()
  410. CheckboxOptionsButton.uncheckAllEnabledKeys(self)
  411. def getCheckedOptionNames(self):
  412. """ Should return a list with checked option names. This will
  413. be used to generate the label.
  414. @return: should return a list with strings.
  415. @rtype: list
  416. """
  417. return(
  418. [
  419. self._categoriesModel[catKey].name
  420. for catKey in self._categoriesModel.checkedCategories()
  421. ] +
  422. [
  423. self._userCategoriesModel[catKey].name
  424. for catKey in self._userCategoriesModel.checkedCategories()
  425. ]
  426. )
  427. def getOptions(self):
  428. """ Should return a list with options tuple(key, name, state)
  429. This will be used to generate the options.
  430. """
  431. list_ = []
  432. for catKey in self._categoriesModel:
  433. list_.append(
  434. (
  435. catKey,
  436. self._categoriesModel[catKey].name,
  437. self._categoriesModel[catKey].isChecked()
  438. )
  439. )
  440. return list_
  441. def optionToggled(self, key, state):
  442. if state:
  443. self._categoriesModel[key].check()
  444. else:
  445. self._categoriesModel[key].uncheck()
  446. class SearchPeriod(QComboBox):
  447. def __init__(self, model, parent=None):
  448. QComboBox.__init__(self, parent=parent)
  449. self._model = model
  450. self.setMinimumContentsLength(2)
  451. for period in model.Periods:
  452. self.addItem(model.Periods[period], QVariant(period))
  453. self.currentIndexChanged.connect(self.__indexChanged)
  454. def __indexChanged(self, index):
  455. self._model.timeRange = self.currentData()
  456. class SearchLanguage(QComboBox):
  457. def __init__(self, model, parent=None):
  458. QComboBox.__init__(self, parent=parent)
  459. self._model = model
  460. self.setMinimumContentsLength(2)
  461. for lang in model.Languages:
  462. self.addItem(model.Languages[lang], QVariant(lang))
  463. self.currentIndexChanged.connect(self.__indexChanged)
  464. def __indexChanged(self, index):
  465. self._model.lang = self.currentData()
  466. def loadSettings(self, lang):
  467. # Find and set the index that matches lang
  468. self.setCurrentIndex(self.findData(QVariant(lang), Qt.UserRole))
  469. def saveSettings(self):
  470. return str(self.currentData())
  471. class SearchOptionsContainer(QFrame):
  472. """ Custom QFrame to be able to show or hide certain widgets.
  473. """
  474. def __init__(self, searchModel, instancesModel, enginesModel, parent=None):
  475. """
  476. @param searchModel:
  477. @type searchModel: searxqt.models.search.SearchModel
  478. @param instancesModel:
  479. @type instancesModel: searxqt.models.instances.InstanceModelFilter
  480. @param enginesModel:
  481. @type enginesModel: searxqt.models.instances.EnginesModel
  482. @param parent:
  483. @type parent: QObject or None
  484. """
  485. QFrame.__init__(self, parent=parent)
  486. self._enginesModel = enginesModel
  487. self._searchModel = searchModel
  488. self._categoriesModel = CategoriesModel()
  489. self._userCategoriesModel = UserCategoriesModel()
  490. # Backup user checked engines.
  491. self.__userCheckedBackup = []
  492. # Keep track of disabled engines (these engines are disabled
  493. # because they are part of one or more checked categories).
  494. #
  495. # @key: engine name (str)
  496. # @value : list with category keys (str)
  497. self.__disabledByCat = {}
  498. layout = QHBoxLayout(self)
  499. self._widgets = {
  500. 'categories': SearchCategories(
  501. self._categoriesModel,
  502. enginesModel,
  503. self._userCategoriesModel,
  504. self
  505. ),
  506. 'engines': SearchEngines(searchModel, instancesModel, self),
  507. 'period': SearchPeriod(searchModel, self),
  508. 'lang': SearchLanguage(searchModel, self)
  509. }
  510. for widget in self._widgets.values():
  511. layout.addWidget(widget, 0, Qt.AlignTop)
  512. # Keep widgets left aligned.
  513. spacer = QSpacerItem(
  514. 40, 20, QSizePolicy.MinimumExpanding, QSizePolicy.Minimum
  515. )
  516. layout.addItem(spacer)
  517. # Connections
  518. self._categoriesModel.stateChanged.connect(
  519. self.__categoriesStateChanged
  520. )
  521. self._userCategoriesModel.stateChanged.connect(
  522. self.__userCategoriesStateChanged
  523. )
  524. self._userCategoriesModel.changed.connect(self.__userCategoriesChanged)
  525. self._userCategoriesModel.removed.connect(self.__userCategoryRemoved)
  526. self._enginesModel.changed.connect(self.__enginesModelChanged)
  527. def __enginesModelChanged(self):
  528. """Settings loaded or data updated
  529. """
  530. # Remove deleted engines from __disabledByCat
  531. for engine in list(self.__disabledByCat.keys()):
  532. if engine not in self._enginesModel:
  533. del self.__disabledByCat[engine]
  534. self._widgets['engines'].setKeyEnabled(engine)
  535. # Add new categories
  536. for catKey in self._enginesModel.categories():
  537. if catKey not in self._categoriesModel:
  538. name = ""
  539. if catKey in self._searchModel.categories.types:
  540. # Default pre-defined categories are translatable
  541. name = self._searchModel.categories.types[catKey][0]
  542. else:
  543. name = catKey.capitalize()
  544. print("[DEBUG] Found non default category `{0}`"
  545. .format(name))
  546. self._categoriesModel.addCategory(catKey, name)
  547. # Remove old categories
  548. for catKey in self._categoriesModel.copy():
  549. if catKey not in self._enginesModel.categories():
  550. self._categoriesModel.removeCategory(catKey)
  551. # Release potentialy checked engines
  552. self.__processCategoriesStateChange(
  553. [engine.name for engine in
  554. self._enginesModel.getByCategory(catKey)],
  555. catKey,
  556. False
  557. )
  558. self._widgets['categories'].reGenerate()
  559. self.__finalizeCategoriesStateChange()
  560. def __userCategoryRemoved(self, catKey):
  561. for engineKey, catList in self.__disabledByCat.copy().items():
  562. if catKey in catList:
  563. self.__uncheckEngineByCat(catKey, engineKey)
  564. self._widgets['categories'].reGenerate()
  565. self.__finalizeCategoriesStateChange()
  566. def __userCategoriesChanged(self, catKey):
  567. """ When the user edited a existing user-category this should
  568. check freshly added engines to this category and uncheck engines
  569. that have been removed from this category.
  570. """
  571. if self._userCategoriesModel[catKey].isChecked():
  572. engines = self._userCategoriesModel[catKey].engines
  573. # Uncheck removed engines
  574. for engineKey, categories in self.__disabledByCat.copy().items():
  575. if catKey in categories:
  576. if engineKey not in engines:
  577. self.__uncheckEngineByCat(catKey, engineKey)
  578. # Check newly added engines
  579. for engineKey in engines:
  580. if engineKey not in self.__disabledByCat:
  581. self.__checkEngineByCat(catKey, engineKey)
  582. self.__finalizeCategoriesStateChange()
  583. def __checkEngineByCat(self, catKey, engineKey):
  584. """ This method handles checking of a engine by a category.
  585. @param catKey: Category key
  586. @type catKey: str
  587. @param engineKey: Engine key
  588. @type engineKey: str
  589. """
  590. if engineKey not in self._searchModel.engines:
  591. # User did not check this engine so we are going to.
  592. self._searchModel.engines.append(
  593. engineKey
  594. )
  595. elif(engineKey not in self.__userCheckedBackup and
  596. not self._widgets['engines'].keyDisabled(engineKey)):
  597. # User did check this engine, so we backup that.
  598. self.__userCheckedBackup.append(engineKey)
  599. if not self._widgets['engines'].keyDisabled(engineKey):
  600. # Disable the engine from being toggled by the user.
  601. self._widgets['engines'].setKeyDisabled(engineKey)
  602. if engineKey not in self.__disabledByCat:
  603. self.__disabledByCat.update({engineKey: []})
  604. # Backup that this category is blocking this engine from
  605. # being toggled by the user.
  606. self.__disabledByCat[engineKey].append(catKey)
  607. def __uncheckEngineByCat(self, catKey, engineKey):
  608. """ This method handles the unchecking of a engine by a category.
  609. @param catKey: Category key
  610. @type catKey: str
  611. @param engineKey: Engine key
  612. @type engineKey: str
  613. """
  614. if engineKey in self.__disabledByCat:
  615. if catKey in self.__disabledByCat[engineKey]:
  616. # This category no longer blocks this engine from
  617. # being edited by the user.
  618. self.__disabledByCat[engineKey].remove(catKey)
  619. if not self.__disabledByCat[engineKey]:
  620. # No category left that blocks this engine from
  621. # user-toggleing.
  622. self._widgets['engines'].setKeyEnabled(engineKey)
  623. self.__disabledByCat.pop(engineKey)
  624. if engineKey not in self.__userCheckedBackup:
  625. # User didn't check this engine, so we can
  626. # uncheck it.
  627. self._searchModel.engines.remove(
  628. engineKey
  629. )
  630. else:
  631. # User did check this engine before checking
  632. # this category so we won't uncheck it.
  633. self.__userCheckedBackup.remove(engineKey)
  634. def __userCategoriesStateChanged(self, catKey, state):
  635. """ The user checked or unchecked a user-category.
  636. @param catKey: Category key
  637. @type catKey: str
  638. @param state:Category enabled or disabled (checked or unchecked)
  639. @type state: bool
  640. """
  641. self.__processCategoriesStateChange(
  642. self._userCategoriesModel[catKey].engines,
  643. catKey,
  644. state
  645. )
  646. self._widgets['categories'].reGenerate()
  647. self.__finalizeCategoriesStateChange()
  648. def __categoriesStateChanged(self, catKey, state):
  649. """ The user checked or unchecked a default-category.
  650. @param catKey: Category key
  651. @type catKey: str
  652. @param state: Category enabled or disabled (checked or unchecked)
  653. @type state: bool
  654. """
  655. self.__processCategoriesStateChange(
  656. [engine.name for engine in
  657. self._enginesModel.getByCategory(catKey)],
  658. catKey,
  659. state
  660. )
  661. self._widgets['categories'].reGenerate()
  662. self.__finalizeCategoriesStateChange()
  663. def __processCategoriesStateChange(self, engines, catKey, state):
  664. """ The user checked or unchecked a category, depending on the
  665. `state` variable.
  666. When a category gets checked all the engines in that category
  667. will be checked and disabled so that the user can't toggle the
  668. engine.
  669. On uncheck of a category all engines in that category should be
  670. re-enabled. And those engines should be unchecked if they weren't
  671. checked by the user before checking this category.
  672. @param engines: A list with engineKeys (str) that are part of the
  673. category (catKey).
  674. @type engines: list
  675. @param catKey: Category key
  676. @type catKey: str
  677. @param state: Category enabled or disabled (checked or unchecked)
  678. @type state: bool
  679. """
  680. if state:
  681. # Category checked.
  682. for engine in engines:
  683. self.__checkEngineByCat(catKey, engine)
  684. else:
  685. # Category unchecked.
  686. for engine in engines:
  687. self.__uncheckEngineByCat(catKey, engine)
  688. def __finalizeCategoriesStateChange(self):
  689. # Re-generate the engines label
  690. self._widgets['engines'].reGenerate()
  691. # Update the instances filter.
  692. self._widgets['engines'].updateFilter()
  693. def saveSettings(self):
  694. data = {}
  695. # Store widgets visible state.
  696. for key, widget in self._widgets.items():
  697. data.update({
  698. '{0}Visible'.format(key): not widget.isHidden()
  699. })
  700. # Store category states and CheckboxOptionsButton states (label expanded
  701. # or collapsed)
  702. data.update({
  703. 'userCatModel': self._userCategoriesModel.data(),
  704. 'defaultCatModel': self._categoriesModel.data(),
  705. 'categoriesButton': self._widgets['categories'].saveSettings(),
  706. 'enginesButton': self._widgets['engines'].saveSettings(),
  707. 'language': self._widgets['lang'].saveSettings()
  708. })
  709. return data
  710. def loadSettings(self, data):
  711. # Set widgets visible or hidden depending on their state.
  712. for key, widget in self._widgets.items():
  713. if data.get('{0}Visible'.format(key), True):
  714. widget.show()
  715. else:
  716. widget.hide()
  717. # Load category states
  718. self._userCategoriesModel.setData(data.get('userCatModel', {}))
  719. self._categoriesModel.setData(data.get('defaultCatModel', {}))
  720. # Load CheckboxOptionsButton states (categories and engines label states
  721. # , expanded or collapsed.)
  722. self._widgets['categories'].loadSettings(data.get('categoriesButton', {}))
  723. self._widgets['engines'].loadSettings(data.get('enginesButton', {}))
  724. # Load search language.
  725. self._widgets['lang'].loadSettings(data.get('language', ''))
  726. def __checkBoxStateChanged(self, key, state):
  727. if state:
  728. self._widgets[key].show()
  729. else:
  730. self._widgets[key].hide()
  731. """ QFrame re-implementations
  732. """
  733. def contextMenuEvent(self, event):
  734. menu = QMenu(self)
  735. menu.addSection(_("Show / Hide"))
  736. for key, widget in self._widgets.items():
  737. action = QWidgetAction(menu)
  738. checkbox = QCheckBox(key, menu)
  739. checkbox.setTristate(False)
  740. checkbox.setChecked(not widget.isHidden())
  741. action.setDefaultWidget(checkbox)
  742. checkbox.stateChanged.connect(
  743. lambda state, key=key:
  744. self.__checkBoxStateChanged(key, state)
  745. )
  746. menu.addAction(action)
  747. menu.exec_(self.mapToGlobal(event.pos()))
  748. class SearchContainer(QWidget):
  749. def __init__(self, searchModel, instancesModel,
  750. instanceSelecter, enginesModel, guard, parent=None):
  751. """
  752. @type searchModel: models.search.SearchModel
  753. @type instancesModel: models.instances.InstancesModelFilter
  754. @type instanceSelecter: models.instances.InstanceSelecterModel
  755. @type enginesModel: models.instances.EnginesModel
  756. @type guard: core.guard.Guard
  757. """
  758. QWidget.__init__(self, parent=parent)
  759. layout = QVBoxLayout(self)
  760. self._model = searchModel
  761. self._instancesModel = instancesModel
  762. self._instanceSelecter = instanceSelecter
  763. self._guard = guard
  764. self._searchThread = None
  765. # Maximum other instances to try on fail.
  766. self._maxSearchFailCount = 10
  767. # Set `_useFallback` to True to try another instance when the
  768. # search failed somehow or set to False to try same instance or
  769. # for pagination which also should use the same instance.
  770. self._useFallback = True
  771. # `_fallbackActive` should be False when a fresh list of fallback
  772. # instances should be picked on failed search and should be True
  773. # when search(es) fail and `_fallbackInstancesQueue` is beeing
  774. # used.
  775. self._fallbackActive = False
  776. # Every first request that has `_useFallback` set to True will
  777. # use this list as a resource for next instance(s) to try until
  778. # it is out of instance url's. Also on the first request this
  779. # list will be cleared and filled again with `_maxSearchFailCount`
  780. # of random instances.
  781. self._fallbackInstancesQueue = []
  782. # Set to True to break out of the fallback loop.
  783. # This is used for the Stop action.
  784. self._breakFallback = False
  785. searchLayout = QHBoxLayout()
  786. layout.addLayout(searchLayout)
  787. # -- Start search bar
  788. self.queryEdit = QLineEdit(self)
  789. self.queryEdit.setPlaceholderText(_("Search for .."))
  790. searchLayout.addWidget(self.queryEdit)
  791. self.searchButton = Button(_("Searx"), self)
  792. self.searchButton.setToolTip(_("Preform search."))
  793. searchLayout.addWidget(self.searchButton)
  794. self.reloadButton = Button("♻", self)
  795. self.reloadButton.setToolTip(_("Reload"))
  796. searchLayout.addWidget(self.reloadButton)
  797. self.randomButton = Button("⤳", self)
  798. self.randomButton.setToolTip(_(
  799. "Search with random instance.\n"
  800. "(Obsolete when 'Random Every is checked')"
  801. ))
  802. searchLayout.addWidget(self.randomButton)
  803. rightLayout = QVBoxLayout()
  804. rightLayout.setSpacing(0)
  805. searchLayout.addLayout(rightLayout)
  806. self._fallbackCheck = QCheckBox(_("Fallback"), self)
  807. self._fallbackCheck.setToolTip(_("Try random other instance on fail."))
  808. rightLayout.addWidget(self._fallbackCheck)
  809. self._randomCheckEvery = QCheckBox(_("Random every"), self)
  810. self._randomCheckEvery.setToolTip(_("Pick a random instance for "
  811. "every request."))
  812. rightLayout.addWidget(self._randomCheckEvery)
  813. # -- End search bar
  814. # -- Start splitter
  815. self.splitter = QSplitter(self)
  816. self.splitter.setOrientation(Qt.Vertical)
  817. layout.addWidget(self.splitter)
  818. # ---- Start search options toolbar
  819. self._optionsContainer = SearchOptionsContainer(
  820. searchModel, instancesModel, enginesModel, self.splitter
  821. )
  822. # ---- End search options toolbar
  823. # --- Start search results container
  824. searchBottomWidget = QWidget(self.splitter)
  825. searchBottomWidgetLayout = QVBoxLayout(searchBottomWidget)
  826. self.resultsContainer = QTextBrowser(self)
  827. self.resultsContainer.setOpenExternalLinks(True)
  828. self.resultsContainer.setLineWrapMode(1)
  829. searchBottomWidgetLayout.addWidget(self.resultsContainer)
  830. self.navBar = SearchNavigation(self)
  831. self.navBar.setEnabled(False)
  832. searchBottomWidgetLayout.addWidget(self.navBar)
  833. # --- End search results container
  834. # -- End splitter
  835. self.queryEdit.textChanged.connect(self.__queryChanged)
  836. self._model.statusChanged.connect(self.__searchStatusChanged)
  837. self._model.optionsChanged.connect(self.__searchOptionsChanged)
  838. self._fallbackCheck.stateChanged.connect(self.__useFallbackChanged)
  839. self._randomCheckEvery.stateChanged.connect(
  840. self.__randomEveryRequestChanged)
  841. self._instanceSelecter.instanceChanged.connect(
  842. self.__instanceChanged)
  843. self.queryEdit.returnPressed.connect(self.__searchButtonClicked)
  844. self.searchButton.clicked.connect(self.__searchButtonClicked)
  845. self.reloadButton.clicked.connect(self.__reloadButtonClicked)
  846. self.randomButton.clicked.connect(self.__randomSearchButtonClicked)
  847. self.navBar.requestPage.connect(self.__navBarRequest)
  848. self.__queryChanged("")
  849. def isBusy(self):
  850. return bool(self._searchThread is not None)
  851. def cancelAll(self):
  852. self._breakFallback = True
  853. self._searchThread.wait()
  854. def __searchButtonClicked(self, checked=0):
  855. # Set to use fallback
  856. self._useFallback = self._model.useFallback
  857. if (self._model.randomEvery or
  858. (self._useFallback and
  859. not self._instanceSelecter.currentUrl)):
  860. self._instanceSelecter.randomInstance()
  861. self._resetPagination()
  862. self._newSearch(self._instanceSelecter.currentUrl)
  863. def __stopButtonClicked(self):
  864. self._breakFallback = True
  865. self.searchButton.setEnabled(False)
  866. def __reloadButtonClicked(self):
  867. self._useFallback = False
  868. self._newSearch(self._instanceSelecter.currentUrl)
  869. def __randomSearchButtonClicked(self):
  870. self._useFallback = self._model.useFallback
  871. self._instanceSelecter.randomInstance()
  872. self._newSearch(self._instanceSelecter.currentUrl)
  873. def __navBarRequest(self, pageNo):
  874. self._useFallback = False
  875. self._model.pageno = pageNo
  876. self._newSearch(self._instanceSelecter.currentUrl)
  877. def _resetPagination(self):
  878. self.navBar.reset()
  879. self._model.pageno = 1
  880. def __instanceChanged(self):
  881. self._resetPagination()
  882. def __searchOptionsChanged(self):
  883. """ From the model (on load settings)
  884. """
  885. self._randomCheckEvery.stateChanged.disconnect(
  886. self.__randomEveryRequestChanged)
  887. self._fallbackCheck.stateChanged.disconnect(
  888. self.__useFallbackChanged)
  889. self._randomCheckEvery.setChecked(self._model.randomEvery)
  890. self._fallbackCheck.setChecked(self._model.useFallback)
  891. self.__handleRandomButtonState()
  892. self._randomCheckEvery.stateChanged.connect(
  893. self.__randomEveryRequestChanged)
  894. self._fallbackCheck.stateChanged.connect(self.__useFallbackChanged)
  895. def __handleRandomButtonState(self):
  896. """ Hides or shows the 'Random search button'.
  897. We don't need the button when the model it's randomEvery is True.
  898. """
  899. if self._model.randomEvery:
  900. self.randomButton.hide()
  901. else:
  902. self.randomButton.show()
  903. def __randomEveryRequestChanged(self, state):
  904. """ From the checkbox
  905. """
  906. self._model.optionsChanged.disconnect(self.__searchOptionsChanged)
  907. self._model.randomEvery = bool(state)
  908. self.__handleRandomButtonState()
  909. self._model.optionsChanged.connect(self.__searchOptionsChanged)
  910. def __useFallbackChanged(self, state):
  911. """ From the checkbox
  912. """
  913. self._model.optionsChanged.disconnect(self.__searchOptionsChanged)
  914. self._model.useFallback = bool(state)
  915. self._model.optionsChanged.connect(self.__searchOptionsChanged)
  916. def __queryChanged(self, q):
  917. if self._model.status() == SearchStatus.Busy:
  918. return
  919. if q:
  920. self.searchButton.setEnabled(True)
  921. self.reloadButton.setEnabled(True)
  922. self.randomButton.setEnabled(True)
  923. else:
  924. self.searchButton.setEnabled(False)
  925. self.reloadButton.setEnabled(False)
  926. self.randomButton.setEnabled(False)
  927. def _setOptionsState(self, state=True):
  928. if self._useFallback:
  929. if state:
  930. # Search stopped
  931. self.searchButton.setText(_("Search"))
  932. self.searchButton.clicked.disconnect(
  933. self.__stopButtonClicked
  934. )
  935. self.searchButton.clicked.connect(self.__searchButtonClicked)
  936. self.searchButton.setEnabled(True)
  937. else:
  938. # Searching
  939. self.searchButton.setText(_("Stop"))
  940. self.searchButton.clicked.disconnect(
  941. self.__searchButtonClicked
  942. )
  943. self.searchButton.clicked.connect(self.__stopButtonClicked)
  944. else:
  945. self.searchButton.setEnabled(state)
  946. self.reloadButton.setEnabled(state)
  947. self.randomButton.setEnabled(state)
  948. self._randomCheckEvery.setEnabled(state)
  949. self._fallbackCheck.setEnabled(state)
  950. self.queryEdit.setEnabled(state)
  951. self._optionsContainer.setEnabled(state)
  952. def __searchStatusChanged(self, status):
  953. if status == SearchStatus.Busy:
  954. self._setOptionsState(False)
  955. elif status == SearchStatus.Done:
  956. self._setOptionsState()
  957. @property
  958. def query(self): return self.queryEdit.text()
  959. def _newSearch(self, url, query=''):
  960. self.resultsContainer.clear()
  961. self._search(url, query)
  962. def _search(self, url, query=''):
  963. if self._searchThread:
  964. return
  965. if not query:
  966. query = self.query
  967. if not query:
  968. self.resultsContainer.setHtml(_("Please enter a search query."))
  969. return
  970. if not url:
  971. self.resultsContainer.setHtml(_("Please select a instance first."))
  972. return
  973. self.navBar.setEnabled(False)
  974. self._model.url = url
  975. self._model.query = query
  976. self._searchThread = Thread(
  977. self._model.search,
  978. args=[self._model],
  979. parent=self
  980. )
  981. self._searchThread.finished.connect(self._searchFinished)
  982. self._searchThread.start()
  983. def _searchFailed(self, result):
  984. currentUrl = self._instanceSelecter.currentUrl # backup
  985. if self._guard.isEnabled():
  986. # See if the Guard has any consequence for this instance.
  987. consequence = self._guard.getConsequence(currentUrl)
  988. if consequence:
  989. # Apply the consequence.
  990. if consequence.type == ConsequenceType.Blacklist:
  991. # Blacklist the instance.
  992. self._instancesModel.putInstanceOnBlacklist(
  993. currentUrl,
  994. reason=result.error()
  995. )
  996. else:
  997. # Put the instance on a timeout.
  998. self._instancesModel.putInstanceOnTimeout(
  999. currentUrl,
  1000. duration=consequence.duration,
  1001. reason=result.error()
  1002. )
  1003. self._instancesModel.apply() # Apply the changed filter.
  1004. if self._useFallback: # Re-try another instance
  1005. if self._breakFallback: # Stop button pressed
  1006. self._breakFallback = False
  1007. return
  1008. if not self._fallbackActive:
  1009. # Get new list with instances to try same request.
  1010. self._fallbackActive = True
  1011. self._fallbackInstancesQueue.clear()
  1012. self._fallbackInstancesQueue = (
  1013. self._instanceSelecter.getRandomInstances(
  1014. amount=self._maxSearchFailCount))
  1015. if not self._fallbackInstancesQueue:
  1016. self.resultsContainer.setHtml(
  1017. "{0} ({1})".format(
  1018. _("Max fail count reached!"),
  1019. self._maxSearchFailCount))
  1020. self._fallbackActive = False
  1021. return
  1022. # Set next instance url to try.
  1023. self._instanceSelecter.currentUrl = (
  1024. self._fallbackInstancesQueue.pop(0))
  1025. self._search(self._instanceSelecter.currentUrl)
  1026. return
  1027. if self._model.pageno > 1:
  1028. self.navBar.setEnabled(True)
  1029. self.navBar.setNextEnabled(False)
  1030. failTemplate = """<html>
  1031. <head>
  1032. <style>
  1033. h3{{
  1034. color: red;
  1035. }}
  1036. p{{
  1037. background-color: {colorAlternate};
  1038. margin: 3px 3px 3px 20px;
  1039. padding: 3px;
  1040. }}
  1041. </style>
  1042. </head>
  1043. <body>
  1044. <h3>Search failed.</h3>
  1045. <h4>Instance:</h4>
  1046. <p>{url}</p>
  1047. <h4>Error:</h4>
  1048. <p>{errMsg}</p>
  1049. <h4>Response content:</h4>
  1050. <p>{text}</p>
  1051. </body>
  1052. </html>"""
  1053. self.resultsContainer.setHtml(
  1054. failTemplate.format(
  1055. url = currentUrl,
  1056. errMsg = result.error(),
  1057. text = html.escape(result.text()),
  1058. colorAlternate = self.palette().color(QPalette.AlternateBase).name(QColor.HexRgb)
  1059. )
  1060. )
  1061. def _searchFinished(self):
  1062. result = self._searchThread.result()
  1063. self._clearSearchThread()
  1064. # Guard
  1065. if self._guard.isEnabled():
  1066. currentUrl = self._instanceSelecter.currentUrl
  1067. # Report the search result to Guard.
  1068. self._guard.reportSearchResult(currentUrl, result)
  1069. if not bool(result): # Failed
  1070. self._searchFailed(result)
  1071. return
  1072. self._fallbackActive = False
  1073. # Create HTML from results
  1074. elemStr = ""
  1075. for item in result.json().get('results', {}):
  1076. elemStr += self.createResultElement(item)
  1077. self.resultsContainer.setHtml(self.compileHtml(elemStr))
  1078. self.navBar.setEnabled(True)
  1079. self.navBar.setNextEnabled(True)
  1080. def _clearSearchThread(self):
  1081. self._searchThread.finished.disconnect(self._searchFinished)
  1082. # Wait before deleting because the `finished` signal is emited
  1083. # from the thread itself, so this method could be called before the
  1084. # thread is actually finished and result in a crash.
  1085. self._searchThread.wait()
  1086. self._searchThread.deleteLater()
  1087. self._searchThread = None
  1088. def createResultElement(self, data):
  1089. # Create general elements
  1090. elem = """<div class="result" id="result-{id}">
  1091. <h4 class="result-title"><i>{engine}: </i><a href="{url}">{title}</a></h4>
  1092. <div style="margin-left: 10px;">
  1093. <p class="result-description">{content}</p>
  1094. <p class="result-url">{url}</p>
  1095. """.format(id="TODO-id", title=data.get('title', ''),
  1096. url=data.get('url', ''), content=data.get('content', ''),
  1097. engine=data.get('engine', '?'))
  1098. # Add file data elements
  1099. elem += self.htmlFileSection(data)
  1100. elem += "</div></div>"
  1101. return elem
  1102. def compileHtml(self, elemStr=""):
  1103. return """<html>
  1104. <head>
  1105. <title>Yeah</title>
  1106. </head>
  1107. <body>
  1108. {0}
  1109. </body>
  1110. </html>""".format(elemStr)
  1111. def formatFileSize(self, fileSize):
  1112. sizeStr = ""
  1113. _TiB = 1024 ** 4
  1114. _GiB = 1024 ** 3
  1115. _MiB = 1024 ** 2
  1116. _KiB = 1024
  1117. if fileSize > _TiB: # TiB
  1118. sizeStr = "{0:.2f} TB".format(fileSize / _TiB)
  1119. elif fileSize > _GiB: # GiB
  1120. sizeStr = "{0:.2f} GB".format(fileSize / _GiB)
  1121. elif fileSize > _MiB: # MiB
  1122. sizeStr = "{0:.2f} MB".format(fileSize / _MiB)
  1123. elif fileSize > _KiB: # KiB
  1124. sizeStr = "{0:.2f} KB".format(fileSize / _KiB)
  1125. else:
  1126. sizeStr = "{0} Bytes".format(fileSize)
  1127. return sizeStr
  1128. def formatFileCount(self, fileCount):
  1129. if fileCount == 1:
  1130. return "1" + _("file")
  1131. return "{0} {1}".format(fileCount, _("files"))
  1132. def htmlFileSection(self, data):
  1133. elem = ""
  1134. if data.get('magnetlink', ''):
  1135. elem += "<a href=\"{0}\">Magnet</a> ".format(data['magnetlink'])
  1136. if data.get('torrentfile', ''):
  1137. elem += "<a href=\"{0}\">Torrent</a> ".format(data['torrentfile'])
  1138. if data.get('filesize', 0):
  1139. elem += self.formatFileSize(data['filesize']) + " "
  1140. if data.get('files', 0):
  1141. elem += self.formatFileCount(data['files']) + " "
  1142. if data.get('seed', None) is not None:
  1143. elem += "seeders: {0} ".format(data['seed'])
  1144. if data.get('leech', None) is not None:
  1145. elem += "leechers: {0} ".format(data['leech'])
  1146. if data.get('img_format', ""):
  1147. elem += "format: {0} ".format(data['img_format'])
  1148. if elem:
  1149. elem = "<p class=\"result-file\">" + elem + "</p>"
  1150. return elem
  1151. def saveSettings(self):
  1152. return {
  1153. 'searchOptions': self._optionsContainer.saveSettings(),
  1154. 'splitterState': self.splitter.saveState()
  1155. }
  1156. def loadSettings(self, data):
  1157. self._optionsContainer.loadSettings(
  1158. data.get('searchOptions', {})
  1159. )
  1160. self.splitter.restoreState(
  1161. data.get('splitterState', QByteArray())
  1162. )