Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32028a54b9 | |||
| b6aa3083d2 | |||
| 0c55fdc44b | |||
| 750456642c | |||
| 27dbc9085d | |||
| 7b0d7f8807 | |||
| bbedaf73de | |||
| 6040b1633d | |||
| 1ddd589cc2 | |||
| 5e81d90c1f | |||
| 46a49f1b90 | |||
| 3772e696ce | |||
| f9d10d37ec | |||
| 4a082ab8ee | |||
| fcc570d75f | |||
| 7732d95626 | |||
| fc0922c661 |
@@ -1,4 +1,4 @@
|
||||
#  KrowLog
|
||||
#  KrowLog
|
||||
|
||||
KrowLog is a viewer for text files of arbitrary size.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
0.2-alpha
|
||||
@@ -1,3 +1,3 @@
|
||||
krow_icon = "icons/icon.png"
|
||||
krow_icon = "icons/krowlog.svg"
|
||||
|
||||
tab_width = 4
|
||||
|
||||
46
icons/krowlog.svg
Normal file
46
icons/krowlog.svg
Normal file
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="64" height="64" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="bg1" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#9f8700"/>
|
||||
<stop offset="100%" style="stop-color:#ffdb00"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fg1" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#8caec7"/>
|
||||
<stop offset="100%" style="stop-color:#6289a3"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="rg1">
|
||||
<stop offset="0%" stop-color="white"/>
|
||||
<stop offset="100%" stop-color="black"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect x="8" y="8" width="112" height="112" style="fill:url(#fg1)" rx="10"/>
|
||||
<path style="fill:black"
|
||||
d="M76,113
|
||||
L15,113
|
||||
L15,70
|
||||
C15,70 25,55 45,36
|
||||
C53,29 60,26 78,32
|
||||
C85,34 90,32 90,32
|
||||
C95,30 108,35 110,40
|
||||
L110,40
|
||||
C83,48 80,60 76,71
|
||||
C74,80 76,113 76,113
|
||||
z"/>
|
||||
<rect x="8" y="8" width="112" height="112"
|
||||
style="fill:none; stroke:url(#bg1); stroke-width:14" rx="10"/>
|
||||
|
||||
<circle cx="65" cy="40" r="6" fill="url(#rg1)"/>
|
||||
<circle cx="65" cy="40" r="3" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,16 +1,15 @@
|
||||
import logging
|
||||
import signal
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtGui import QIcon
|
||||
import sys
|
||||
|
||||
import constants
|
||||
from src.pluginregistry import PluginRegistry
|
||||
|
||||
import gettext
|
||||
from src.ui.icon import Icon
|
||||
|
||||
__version__ = '0.2.0'
|
||||
|
||||
gettext.install('krowlog', 'locale')
|
||||
|
||||
@@ -31,7 +30,7 @@ def stop_signal(signum, _stackframe):
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QIcon(constants.krow_icon)) # works only for Linux
|
||||
app.setWindowIcon(Icon(constants.krow_icon)) # works only for Linux
|
||||
|
||||
# make icon appear in Windows
|
||||
# see https://stackoverflow.com/questions/1551605/how-to-set-applications-taskbar-icon-in-windows-7/1552105#1552105
|
||||
Binary file not shown.
@@ -5,8 +5,8 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: RavenLog\n"
|
||||
"POT-Creation-Date: 2022-08-23 20:08+0200\n"
|
||||
"PO-Revision-Date: 2022-08-23 20:09+0200\n"
|
||||
"POT-Creation-Date: 2022-08-25 19:21+0200\n"
|
||||
"PO-Revision-Date: 2022-08-25 19:23+0200\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: de\n"
|
||||
@@ -54,30 +54,30 @@ msgstr "&Suchtreffer Hervorheben"
|
||||
msgid "Open Tab on Save As File"
|
||||
msgstr "Öffne neues Tab wenn Selektion als neue Datei gespeichert wird"
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:35
|
||||
msgid "&Files Browser"
|
||||
msgstr "&Dateibrowser"
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:41
|
||||
msgid "Files Browser"
|
||||
msgstr "Dateibrowser"
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:37
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:37
|
||||
msgid "Focus on current file"
|
||||
msgstr "Auf aktuelle Datei fokussieren"
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:40
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:40
|
||||
msgid "Folder:"
|
||||
msgstr "Ordner:"
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:45
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:45
|
||||
msgid "Filter:"
|
||||
msgstr "Filter:"
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:73
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:73
|
||||
msgid "Open Directory"
|
||||
msgstr "Ordner öffnen"
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:30
|
||||
msgid "&Files Browser"
|
||||
msgstr "&Dateibrowser"
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:36
|
||||
msgid "Files Browser"
|
||||
msgstr "Dateibrowser"
|
||||
|
||||
#: src/plugins/krowlog/aboutdialog.py:19
|
||||
msgid "About KrowLog"
|
||||
msgstr "Über KrowLog"
|
||||
@@ -146,22 +146,26 @@ msgstr "&Über KrowLog"
|
||||
msgid "E&xit"
|
||||
msgstr "&Beenden"
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:137
|
||||
#: src/plugins/logfile/filterwidget.py:149
|
||||
msgid "Cancel"
|
||||
msgstr "Abbrechen"
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:143
|
||||
#: src/plugins/logfile/filterwidget.py:155
|
||||
msgid "save query"
|
||||
msgstr "suche speichern"
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:148
|
||||
#: src/plugins/logfile/filterwidget.py:160
|
||||
msgid "ignore case"
|
||||
msgstr "Groß-/Kleinschreibung ignorieren"
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:152
|
||||
#: src/plugins/logfile/filterwidget.py:164
|
||||
msgid "regex"
|
||||
msgstr "RegExp"
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:251
|
||||
msgid "({hits} lines)"
|
||||
msgstr "({hits} Zeilen)"
|
||||
|
||||
#: src/plugins/logfileplugin.py:26
|
||||
msgid "File not found"
|
||||
msgstr "Datei nicht gefunden"
|
||||
@@ -312,11 +316,11 @@ msgstr "Echtes Blau"
|
||||
|
||||
#: src/ui/colorbutton.py:33
|
||||
msgid "Fairy Topia"
|
||||
msgstr ""
|
||||
msgstr "Fairy Topia"
|
||||
|
||||
#: src/ui/colorbutton.py:34
|
||||
msgid "Magenta Bachiego"
|
||||
msgstr ""
|
||||
msgstr "Magenta Bachiego"
|
||||
|
||||
#: src/ui/colorbutton.py:36
|
||||
msgid "Breeze of Mist"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"POT-Creation-Date: 2022-08-23 20:08+0200\n"
|
||||
"POT-Creation-Date: 2022-08-25 19:21+0200\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -52,30 +52,30 @@ msgstr ""
|
||||
msgid "Open Tab on Save As File"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:35
|
||||
msgid "&Files Browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:41
|
||||
msgid "Files Browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:37
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:37
|
||||
msgid "Focus on current file"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:40
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:40
|
||||
msgid "Folder:"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:45
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:45
|
||||
msgid "Filter:"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/findInFiles/filesbrowserwidget.py:73
|
||||
#: src/plugins/filesbrowser/filesbrowserwidget.py:73
|
||||
msgid "Open Directory"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:30
|
||||
msgid "&Files Browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/filesbrowserplugin.py:36
|
||||
msgid "Files Browser"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/krowlog/aboutdialog.py:19
|
||||
msgid "About KrowLog"
|
||||
msgstr ""
|
||||
@@ -140,22 +140,26 @@ msgstr ""
|
||||
msgid "E&xit"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:137
|
||||
#: src/plugins/logfile/filterwidget.py:149
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:143
|
||||
#: src/plugins/logfile/filterwidget.py:155
|
||||
msgid "save query"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:148
|
||||
#: src/plugins/logfile/filterwidget.py:160
|
||||
msgid "ignore case"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:152
|
||||
#: src/plugins/logfile/filterwidget.py:164
|
||||
msgid "regex"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfile/filterwidget.py:251
|
||||
msgid "({hits} lines)"
|
||||
msgstr ""
|
||||
|
||||
#: src/plugins/logfileplugin.py:26
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pip==22.2.2
|
||||
PySide6==6.3.1
|
||||
setuptools==62.1.0
|
||||
urllib3==1.26.11
|
||||
PySide6-Essentials==6.3.1
|
||||
setuptools==65.3.0
|
||||
urllib3==1.26.12
|
||||
watchdog==2.1.9
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
|
||||
class PluginBase():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def copy(self):
|
||||
"""
|
||||
Subclasses that use state must implement this method and return a new instance of themselves.
|
||||
|
||||
@@ -3,6 +3,8 @@ from typing import Callable
|
||||
from PySide6.QtGui import QAction, QIcon
|
||||
from PySide6.QtWidgets import QMenu, QPushButton, QWidget
|
||||
|
||||
from src.ui.icon import Icon
|
||||
|
||||
|
||||
class RAction():
|
||||
|
||||
@@ -65,9 +67,9 @@ class RAction():
|
||||
def _update_check_state(self):
|
||||
if self._action:
|
||||
if self.checked:
|
||||
self._action.setIcon(QIcon("icons/ionicons/checkbox-outline.svg"))
|
||||
self._action.setIcon(Icon("icons/ionicons/checkbox-outline.svg"))
|
||||
else:
|
||||
self._action.setIcon(QIcon("icons/ionicons/square-outline.svg"))
|
||||
self._action.setIcon(Icon("icons/ionicons/square-outline.svg"))
|
||||
|
||||
def set_label(self, label: str):
|
||||
if self._action:
|
||||
@@ -77,9 +79,9 @@ class RAction():
|
||||
action = QAction(self.label, qmenu)
|
||||
self._action = action
|
||||
if self.icon_from_theme:
|
||||
action.setIcon(QIcon.fromTheme(self.icon_from_theme))
|
||||
action.setIcon(Icon.fromTheme(self.icon_from_theme))
|
||||
if self.icon_file:
|
||||
action.setIcon(QIcon(self.icon_file))
|
||||
action.setIcon(Icon(self.icon_file))
|
||||
if self.shortcut:
|
||||
action.setShortcut(self.shortcut)
|
||||
if self.action:
|
||||
@@ -95,9 +97,9 @@ class RAction():
|
||||
if self.label:
|
||||
button.setText(self.label)
|
||||
if self.icon_from_theme:
|
||||
button.setIcon(QIcon.fromTheme(self.icon_from_theme))
|
||||
button.setIcon(Icon.fromTheme(self.icon_from_theme))
|
||||
if self.icon_file:
|
||||
button.setIcon(QIcon(self.icon_file))
|
||||
button.setIcon(Icon(self.icon_file))
|
||||
if self.shortcut:
|
||||
button.setShortcut(self.shortcut)
|
||||
if self.action:
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from abc import ABC
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
from src.pluginbase import PluginBase
|
||||
from src.pluginregistry import PluginRegistry
|
||||
from src.plugins.domain.menucontribution import MenuContribution
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.findInFiles.filesbrowserwidget import FilesBrowserWidget
|
||||
from src.plugins.filesbrowser.filesbrowserwidget import FilesBrowserWidget
|
||||
from src.i18n import _
|
||||
from src.settings.settings import Settings
|
||||
|
||||
@@ -17,9 +15,6 @@ class FilesBrowserPlugin(PluginBase):
|
||||
super(FilesBrowserPlugin, self).__init__()
|
||||
self.settings = None
|
||||
|
||||
def copy(self):
|
||||
return self
|
||||
|
||||
def set_settings(self, settings: Settings):
|
||||
self.settings = settings
|
||||
if not self.settings.session.has_section("filesBrowser"):
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import textwrap
|
||||
|
||||
import PySide6
|
||||
import urllib3
|
||||
from watchdog import version as watchdog_version
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont, QPixmap
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import constants
|
||||
|
||||
import krowlog
|
||||
from src.ui.icon import Icon
|
||||
from src.ui.label import Label
|
||||
from src.ui.vbox import VBox
|
||||
from src.i18n import _
|
||||
@@ -21,16 +26,16 @@ class AboutDialog(QDialog):
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
|
||||
heading_app_name = QLabel(_("KrowLog"))
|
||||
heading_app_name = Label(_("KrowLog"))
|
||||
heading_app_name.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
heading_app_name.setFont(QFont("default", 25))
|
||||
heading_app_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
# heading_app_name.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
|
||||
version = QLabel(_("Version: {0}").format(self._version()))
|
||||
version = Label(_("Version: {0}").format(krowlog.__version__))
|
||||
version.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
app_icon = QLabel()
|
||||
app_icon.setPixmap(QPixmap(constants.krow_icon))
|
||||
app_icon.setPixmap(Icon(constants.krow_icon).pixmap(64, 64))
|
||||
heading = QWidget(self)
|
||||
hbox = QHBoxLayout(heading)
|
||||
hbox.addWidget(app_icon)
|
||||
@@ -66,21 +71,18 @@ class AboutDialog(QDialog):
|
||||
dependencies = """
|
||||
<ul>
|
||||
<li>Ionicons (MIT) - <a href="https://github.com/ionic-team/ionicons">https://github.com/ionic-team/ionicons</a></li>
|
||||
<li>PySide6 {pyside} (LGPL v3) - <a href="https://doc.qt.io/qtforpython-6/">https://doc.qt.io/qtforpython-6/</a></li>
|
||||
<li>PySide6-Essentials {pyside} (LGPL v3) - <a href="https://doc.qt.io/qtforpython-6/">https://doc.qt.io/qtforpython-6/</a></li>
|
||||
<li>Qt6 {qt} (LGPL v3) - <a href="https://code.qt.io/cgit/qt/qtbase.git/">https://code.qt.io/cgit/qt/qtbase.git/</a></li>
|
||||
<li>urllib3 (MIT) - <a href="https://github.com/urllib3/urllib3">https://github.com/urllib3/urllib3</a></li>
|
||||
<li>watchdog 2.16 (Apache 2.0) - <a href="https://github.com/gorakhargosh/watchdog">https://github.com/gorakhargosh/watchdog</a></li>
|
||||
</ul>""".format(pyside=PySide6.__version__, qt=PySide6.QtCore.__version__)
|
||||
<li>urllib3 {urllib3} (MIT) - <a href="https://github.com/urllib3/urllib3">https://github.com/urllib3/urllib3</a></li>
|
||||
<li>watchdog {watchdog} (Apache 2.0) - <a href="https://github.com/gorakhargosh/watchdog">https://github.com/gorakhargosh/watchdog</a></li>
|
||||
</ul>""".format(
|
||||
pyside=PySide6.__version__,
|
||||
qt=PySide6.QtCore.__version__,
|
||||
urllib3=urllib3.__version__,
|
||||
watchdog=watchdog_version.VERSION_STRING)
|
||||
label = textwrap.dedent(dependencies)
|
||||
|
||||
result = QWidget()
|
||||
result.layout = QVBoxLayout(result)
|
||||
result.layout.addWidget(Label(label))
|
||||
return result
|
||||
|
||||
def _version(self):
|
||||
with open('VERSION.info', "rt") as f:
|
||||
line = f.readline()
|
||||
version = line.strip()
|
||||
return version
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Optional, Callable
|
||||
from PySide6.QtCore import QRunnable, QThreadPool, Signal
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QCheckBox, QPushButton, QComboBox, \
|
||||
QSizePolicy, QProgressBar, QMenu, QMenuBar
|
||||
QSizePolicy, QProgressBar
|
||||
|
||||
from src.plugins.domain.raction import RAction
|
||||
from src.plugins.logfile.preprocesslineshook import PreProcessLinesHook
|
||||
@@ -18,6 +18,8 @@ from src.ui.bigtext.logFileModel import LogFileModel
|
||||
from src.i18n import _
|
||||
from src.pluginregistry import PluginRegistry
|
||||
from src.ui.hbox import HBox
|
||||
from src.ui.icon import Icon
|
||||
from src.ui.label import Label
|
||||
from src.zonedpluginregistry import ZonedPluginRegistry
|
||||
|
||||
|
||||
@@ -33,6 +35,7 @@ class FilterTask(QRunnable):
|
||||
filter_match_found_listeners: Callable[[int], None],
|
||||
pre_process_lines_hooks: [PreProcessLinesHook],
|
||||
progress_handler: Callable[[float], None],
|
||||
update_hits_handler: Callable[[int], None],
|
||||
on_before: Callable[[], None],
|
||||
on_finish: Callable[[], None]
|
||||
):
|
||||
@@ -41,6 +44,7 @@ class FilterTask(QRunnable):
|
||||
self.filter_model = filter_model
|
||||
self.regex = regex
|
||||
self.progress_handler = progress_handler
|
||||
self.update_hits_handler = update_hits_handler
|
||||
self.pre_process_lines_hooks = pre_process_lines_hooks
|
||||
self.on_before = on_before
|
||||
self.on_finish = on_finish
|
||||
@@ -61,19 +65,20 @@ class FilterTask(QRunnable):
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(-1, -1) # notify listeners that a new search started
|
||||
|
||||
hits_count = 0
|
||||
last_progress_report = time.time()
|
||||
try:
|
||||
with open(self.source_model.get_file(), "rb") as source:
|
||||
with open(self.filter_model.get_file(), "w+b") as target:
|
||||
line_count = 0
|
||||
lines_written = 0
|
||||
while l := source.readline():
|
||||
while line_encoded := source.readline():
|
||||
line_count = line_count + 1
|
||||
line = l.decode("utf8", errors="ignore")
|
||||
line = line_encoded.decode("utf8", errors="ignore")
|
||||
|
||||
if self.regex.findall(line):
|
||||
lines_written = lines_written + 1
|
||||
source_line_offset = source.tell() - len(l)
|
||||
source_line_offset = source.tell() - len(line_encoded)
|
||||
target_line_offset = target.tell()
|
||||
for listener in self.filter_match_found_listeners:
|
||||
listener(target_line_offset, source_line_offset)
|
||||
@@ -83,6 +88,7 @@ class FilterTask(QRunnable):
|
||||
line = h.pre_process_line(line, target)
|
||||
|
||||
target.write(line.encode("utf8"))
|
||||
hits_count = hits_count + 1
|
||||
|
||||
# sometime buffering can hide results for a while
|
||||
# We force a flush periodically.
|
||||
@@ -95,12 +101,15 @@ class FilterTask(QRunnable):
|
||||
if now - last_progress_report > 0.2:
|
||||
progress = source.tell() / os.stat(self.source_model.get_file()).st_size
|
||||
self.progress_handler(progress)
|
||||
self.update_hits_handler(hits_count)
|
||||
last_progress_report = now
|
||||
|
||||
if self.aborted:
|
||||
self.update_hits_handler(hits_count)
|
||||
# print("aborted ", time.time())
|
||||
break
|
||||
finally:
|
||||
self.update_hits_handler(hits_count)
|
||||
self.on_finish()
|
||||
# print("dome thread ", threading.currentThread())
|
||||
|
||||
@@ -110,6 +119,7 @@ class FilterWidget(QWidget):
|
||||
filter_task: Optional[FilterTask] = None
|
||||
search_is_running = Signal(bool)
|
||||
signal_update_progress = Signal(float)
|
||||
signal_update_hits = Signal(int)
|
||||
|
||||
def __init__(self, source_model: LogFileModel, zoned_plugin_registry: ZonedPluginRegistry):
|
||||
super(FilterWidget, self).__init__()
|
||||
@@ -129,6 +139,9 @@ class FilterWidget(QWidget):
|
||||
self.query_field.lineEdit().returnPressed.connect(self.filter_changed)
|
||||
self.query_field.setInsertPolicy(QComboBox.NoInsert)
|
||||
|
||||
self.hits_field = Label("")
|
||||
self.signal_update_hits.connect(self._update_hits)
|
||||
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setVisible(False)
|
||||
self.progress_bar.setMaximumWidth(50)
|
||||
@@ -139,7 +152,7 @@ class FilterWidget(QWidget):
|
||||
self.btn_cancel_search.pressed.connect(self._cancel_search)
|
||||
self.search_is_running.connect(self.search_running_status_changed)
|
||||
|
||||
self.btn_bookmark = QPushButton(QIcon("icons/ionicons/star.svg"), "")
|
||||
self.btn_bookmark = QPushButton(Icon("icons/ionicons/star.svg"), "")
|
||||
self.btn_bookmark.setToolTip(_("save query"))
|
||||
self.btn_bookmark.pressed.connect(self._save_query)
|
||||
|
||||
@@ -157,16 +170,17 @@ class FilterWidget(QWidget):
|
||||
filter_bar.layout = QHBoxLayout(filter_bar)
|
||||
filter_bar.layout.setContentsMargins(0, 0, 0, 0)
|
||||
filter_bar.layout.addWidget(self.query_field)
|
||||
filter_bar.layout.addWidget(self.btn_bookmark)
|
||||
filter_bar.layout.addWidget(self.hits_field)
|
||||
filter_bar.layout.addWidget(self.progress_bar)
|
||||
filter_bar.layout.addWidget(self.btn_cancel_search)
|
||||
filter_bar.layout.addWidget(self.btn_bookmark)
|
||||
filter_bar.layout.addWidget(self.menu)
|
||||
filter_bar.layout.addWidget(self.ignore_case)
|
||||
filter_bar.layout.addWidget(self.is_regex)
|
||||
|
||||
(handle, self.tmpfilename) = tempfile.mkstemp()
|
||||
(handle, self.tmp_filename) = tempfile.mkstemp()
|
||||
os.close(handle)
|
||||
self.filter_model = LogFileModel(self.tmpfilename, self.source_model.settings)
|
||||
self.filter_model = LogFileModel(self.tmp_filename, self.source_model.settings)
|
||||
self.hits_view = BigText(self.filter_model)
|
||||
|
||||
self.layout.addWidget(filter_bar)
|
||||
@@ -208,9 +222,8 @@ class FilterWidget(QWidget):
|
||||
self._reload_save_queries()
|
||||
|
||||
def destruct(self):
|
||||
# print("cleanup: ", self.tmpfilename)
|
||||
self._cancel_search()
|
||||
os.remove(self.tmpfilename)
|
||||
os.remove(self.tmp_filename)
|
||||
|
||||
def _cancel_search(self):
|
||||
if self.filter_task:
|
||||
@@ -224,6 +237,7 @@ class FilterWidget(QWidget):
|
||||
self.filter_model.truncate()
|
||||
self.source_model.clear_query_highlight()
|
||||
self.filter_model.clear_query_highlight()
|
||||
self._update_hits(-1)
|
||||
PluginRegistry.execute("update_ui")
|
||||
|
||||
def search_running_status_changed(self, is_running: bool):
|
||||
@@ -233,9 +247,18 @@ class FilterWidget(QWidget):
|
||||
def update_progress(self, progress: float):
|
||||
self.progress_bar.setValue(progress * 100)
|
||||
|
||||
def _update_hits(self, hits: int):
|
||||
if hits >= 0:
|
||||
self.hits_field.setText(_("({hits} lines)").format(hits=hits))
|
||||
else:
|
||||
self.hits_field.setText("")
|
||||
|
||||
def progress_handler(self, progress: float):
|
||||
self.signal_update_progress.emit(progress)
|
||||
|
||||
def update_hits_handler(self, hits: int):
|
||||
self.signal_update_hits.emit(hits)
|
||||
|
||||
def filter_changed(self):
|
||||
query = self.query_field.currentText()
|
||||
ignore_case = self.ignore_case.isChecked()
|
||||
@@ -260,6 +283,7 @@ class FilterWidget(QWidget):
|
||||
return
|
||||
|
||||
self.progress_bar.setValue(0)
|
||||
self._update_hits(-1)
|
||||
|
||||
self.source_model.set_query_highlight(query, ignore_case, is_regex)
|
||||
self.filter_model.set_query_highlight(query, ignore_case, is_regex)
|
||||
@@ -274,6 +298,7 @@ class FilterWidget(QWidget):
|
||||
self.filter_match_found_listeners,
|
||||
pre_process_lines_hooks,
|
||||
self.progress_handler,
|
||||
self.update_hits_handler,
|
||||
lambda: self.search_is_running.emit(True),
|
||||
lambda: self.search_is_running.emit(False)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import sys
|
||||
|
||||
import math
|
||||
import os
|
||||
import time
|
||||
@@ -12,6 +10,7 @@ from PySide6.QtGui import QMouseEvent
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from src.ui.ScaledScrollBar import ScaledScrollBar
|
||||
from src.ui.bigtext.highlight_regex import HighlightRegex
|
||||
from src.ui.bigtext.highlight_selection import HighlightSelection
|
||||
from src.ui.bigtext.highlighted_range import HighlightedRange
|
||||
from src.ui.bigtext.highlightingdialog import HighlightingDialog
|
||||
@@ -97,7 +96,6 @@ class BigText(QWidget):
|
||||
|
||||
def add_line_click_listener(self, listener: Callable[[int], None]):
|
||||
"""
|
||||
|
||||
:param listener: a callable, the parameter is the byte offset of the clicked line
|
||||
:return:
|
||||
"""
|
||||
@@ -111,6 +109,7 @@ class BigText(QWidget):
|
||||
pass
|
||||
|
||||
|
||||
# noinspection PyArgumentList,PyTypeChecker
|
||||
class InnerBigText(QWidget):
|
||||
_byte_offset = 0
|
||||
_left_offset = 0
|
||||
@@ -119,6 +118,8 @@ class InnerBigText(QWidget):
|
||||
|
||||
def __init__(self, parent: BigText, model: LogFileModel):
|
||||
super(InnerBigText, self).__init__()
|
||||
self.char_height = None
|
||||
self.char_width = None
|
||||
self.model = model
|
||||
self.parent = parent
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
@@ -132,6 +133,12 @@ class InnerBigText(QWidget):
|
||||
self._last_double_click_time = 0
|
||||
self._last_double_click_line_number = -1
|
||||
|
||||
self.highlight_selected_text = HighlightRegex(
|
||||
"",
|
||||
is_regex=False,
|
||||
ignore_case=True,
|
||||
hit_background_color="d7efffc0") # same blue as the selection hightlight, but with lower saturation
|
||||
|
||||
self.line_click_listeners: [Callable[[int], None]] = []
|
||||
|
||||
def keyPressEvent(self, e: QKeyEvent) -> None:
|
||||
@@ -203,10 +210,12 @@ class InnerBigText(QWidget):
|
||||
self.update()
|
||||
self.parent.v_scroll_bar.setValue(self._byte_offset)
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
|
||||
if e.buttons() == Qt.MouseButton.LeftButton and e.modifiers() == Qt.KeyboardModifier.ShiftModifier:
|
||||
offset = self.to_byte_offset(e)
|
||||
self.selection_highlight.set_end_byte(offset)
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
return
|
||||
|
||||
@@ -217,6 +226,7 @@ class InnerBigText(QWidget):
|
||||
line: Line = self.lines[line_number]
|
||||
self.selection_highlight.set_start(line.byte_offset())
|
||||
self.selection_highlight.set_end_byte(line.byte_end())
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
return
|
||||
|
||||
@@ -224,6 +234,7 @@ class InnerBigText(QWidget):
|
||||
offset = self.to_byte_offset(e)
|
||||
self.selection_highlight.set_start(offset)
|
||||
self.selection_highlight.set_end_byte(offset)
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
|
||||
line_number = self.y_pos_to_line(e.pos().y())
|
||||
@@ -245,6 +256,8 @@ class InnerBigText(QWidget):
|
||||
else:
|
||||
self.selection_highlight.set_start(offset)
|
||||
self.selection_highlight.set_end_byte(offset)
|
||||
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
|
||||
def mouseMoveEvent(self, e: QMouseEvent):
|
||||
@@ -256,8 +269,10 @@ class InnerBigText(QWidget):
|
||||
|
||||
if self.selection_highlight.end_byte != current_byte:
|
||||
self.selection_highlight.set_end_byte(current_byte)
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
# print("-> %s,%s" %(self._selection_start_byte, self._selection_end_byte))
|
||||
|
||||
line_number = self.y_pos_to_line(e.pos().y())
|
||||
column_in_line = self.x_pos_to_column(e.pos().x())
|
||||
if line_number < 0:
|
||||
@@ -310,7 +325,7 @@ class InnerBigText(QWidget):
|
||||
column_in_line = self.x_pos_to_column(e.pos().x()) + self._left_offset
|
||||
column_in_line = min(column_in_line, line.length_in_columns()) # x was behind the last column of this line
|
||||
char_in_line = line.column_to_char(column_in_line)
|
||||
# print("%s in line %s lcolumn_in_line=%s" % (char_in_line, line_number, column_in_line))
|
||||
# print("%s in line %s column_in_line=%s" % (char_in_line, line_number, column_in_line))
|
||||
byte_in_line = line.char_index_to_byte(char_in_line)
|
||||
current_byte = line.byte_offset() + byte_in_line
|
||||
# print("%s + %s = %s" % (line.byte_offset(), char_in_line, current_byte))
|
||||
@@ -332,6 +347,7 @@ class InnerBigText(QWidget):
|
||||
_("data selection"),
|
||||
_(
|
||||
"You have selected <b>{0}</b> of data.").format(bytes_human_readable))
|
||||
# noinspection PyTypeChecker
|
||||
you_sure.setStandardButtons(QMessageBox.Cancel)
|
||||
copy_btn = you_sure.addButton(_("Copy {0} to Clipboard").format(bytes_human_readable),
|
||||
QMessageBox.ActionRole)
|
||||
@@ -357,6 +373,7 @@ class InnerBigText(QWidget):
|
||||
end = max(self.selection_highlight.start_byte, self.selection_highlight.end_byte)
|
||||
dialog = QFileDialog(self)
|
||||
(selected_file, _filter) = dialog.getSaveFileName(
|
||||
parent=self,
|
||||
caption=_("Save File"),
|
||||
dir=os.path.dirname(self.model.get_file())
|
||||
)
|
||||
@@ -369,19 +386,28 @@ class InnerBigText(QWidget):
|
||||
def _select_all(self):
|
||||
self.selection_highlight.start_byte = 0
|
||||
self.selection_highlight.end_byte = self.model.byte_count()
|
||||
self._update_highlight_selected_text()
|
||||
self.update()
|
||||
|
||||
def _update_highlight_selected_text(self):
|
||||
start_byte=self.selection_highlight.start_byte
|
||||
end_byte=self.selection_highlight.end_byte
|
||||
if abs(start_byte - end_byte) < 1024:
|
||||
query = self.model.read_range(start_byte, end_byte)
|
||||
if query.find("\n") < 0:
|
||||
self.highlight_selected_text.set_query(query)
|
||||
return
|
||||
|
||||
self.highlight_selected_text.set_query("")
|
||||
|
||||
def paintEvent(self, event: QPaintEvent) -> None:
|
||||
# print("paintEvent %s" % (self.model.get_file()))
|
||||
painter = QPainter(self)
|
||||
# painter.setFont(self.model.settings.font())
|
||||
# Courier New, DejaVu Sans Mono, Monospace, Liberation Mono, Noto Mono, Nimbus Mono L, Tlwg Mono, Ubuntu Mono, FreeMono, Mitra Mono
|
||||
font = "Courier New" if sys.platform == 'win32' or sys.platform == 'cygwin' else "Monospace"
|
||||
# font = "Courier New" if sys.platform == 'win32' or sys.platform == 'cygwin' else "Monospace"
|
||||
painter.setFont(QFont("Courier New", self.model.settings.getint_session('general', "font_size")))
|
||||
painter.setPen(QColor(0, 0, 0))
|
||||
self.update_font_metrics(painter)
|
||||
|
||||
lines_to_show = self.lines_shown()
|
||||
lines_to_show = math.ceil(self.lines_shown())
|
||||
# print("%s / %s = %s" %(self.height(), float(self.char_height), lines_to_show))
|
||||
|
||||
self.lines = self.model.data(self._byte_offset, self.scroll_lines, lines_to_show)
|
||||
@@ -392,36 +418,33 @@ class InnerBigText(QWidget):
|
||||
# document length == maximum + pageStep + aFewBytesSoThatTheLastLineIsShown
|
||||
self.parent.v_scroll_bar.setMaximum(self.model.byte_count() - 1)
|
||||
|
||||
for l in self.lines:
|
||||
self.update_longest_line(len(l.line()))
|
||||
for line in self.lines:
|
||||
self.update_longest_line(len(line.line()))
|
||||
|
||||
highlighters = self.model.highlighters()
|
||||
if self.model.get_query_highlight():
|
||||
highlighters = highlighters + [self.model.get_query_highlight()]
|
||||
highlighters = highlighters + [self.highlight_selected_text]
|
||||
highlighters = highlighters + [self.selection_highlight] # selection highlight should be last
|
||||
|
||||
# draw hightlights first - some characters may overlap to the next line
|
||||
# by drawing the background hightlights first we prevent that the hightlight
|
||||
# draw highlights first - some characters may overlap to the next line
|
||||
# by drawing the background highlights first we prevent that the highlight
|
||||
# draws over a character
|
||||
start = time.time()
|
||||
y_line_offset = self.char_height;
|
||||
for l in self.lines:
|
||||
y_line_offset = self.char_height
|
||||
for line in self.lines:
|
||||
highlight_ranges = []
|
||||
for h in highlighters:
|
||||
optional_highlight_range = h.compute_highlight(l)
|
||||
optional_highlight_range = h.compute_highlight(line)
|
||||
if optional_highlight_range:
|
||||
highlight_ranges = highlight_ranges + optional_highlight_range
|
||||
|
||||
self.draw_highlights(highlight_ranges, painter, y_line_offset)
|
||||
y_line_offset = y_line_offset + self.char_height
|
||||
|
||||
end = time.time()
|
||||
# print("highlight duration: %.3f" %((end-start)*1000))
|
||||
|
||||
left_offset = int(-1 * self._left_offset * self.char_width)
|
||||
y_line_offset = self.char_height;
|
||||
for l in self.lines:
|
||||
text = l.line_prepared_for_display()
|
||||
y_line_offset = self.char_height
|
||||
for line in self.lines:
|
||||
text = line.line_prepared_for_display()
|
||||
painter.drawText(left_offset, y_line_offset, text)
|
||||
y_line_offset = y_line_offset + self.char_height
|
||||
|
||||
@@ -461,6 +484,6 @@ class InnerBigText(QWidget):
|
||||
fm: QFontMetrics = painter.fontMetrics()
|
||||
self.char_height = fm.height()
|
||||
self.char_width = fm.averageCharWidth() # all chars have same width for monospace font
|
||||
text = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
|
||||
text = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012"
|
||||
self.char_width = fm.horizontalAdvance(text) / float(len(text))
|
||||
# print("font width=%s height=%s" % (self.char_width, self.char_height))
|
||||
|
||||
@@ -29,6 +29,10 @@ class HighlightRegex(Highlight):
|
||||
else:
|
||||
return re.compile(re.escape(self.query), flags=flags)
|
||||
|
||||
def set_query(self, query: str) -> None:
|
||||
self.query = query
|
||||
self.regex = self._get_regex()
|
||||
|
||||
def compute_highlight(self, line: Line) -> Optional[List[HighlightedRange]]:
|
||||
result = []
|
||||
# print("execute regex: %s in %s" % (self.regex, line.line()))
|
||||
@@ -56,9 +60,15 @@ class HighlightRegex(Highlight):
|
||||
|
||||
@staticmethod
|
||||
def brush(color: str) -> QBrush:
|
||||
if re.match("[0-9a-f]{6}", color, flags=re.IGNORECASE):
|
||||
if re.match("^[0-9a-f]{6}$", color, flags=re.IGNORECASE):
|
||||
red = int(color[0:2], 16)
|
||||
green = int(color[2:4], 16)
|
||||
blue = int(color[4:6], 16)
|
||||
return QBrush(QColor(red, green, blue))
|
||||
if re.match("^[0-9a-f]{8}$", color, flags=re.IGNORECASE):
|
||||
red = int(color[0:2], 16)
|
||||
green = int(color[2:4], 16)
|
||||
blue = int(color[4:6], 16)
|
||||
alpha = int(color[6:8], 16)
|
||||
return QBrush(QColor(red, green, blue, alpha))
|
||||
return QBrush()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtWidgets import QDialog, QLineEdit, QLabel, QGridLayout, QCheckBox, QListWidget, QListWidgetItem, \
|
||||
QPushButton, QDialogButtonBox, QMessageBox, QSizePolicy
|
||||
|
||||
@@ -9,6 +8,7 @@ from src.ui.hbox import HBox
|
||||
from src.settings.settings import Settings
|
||||
|
||||
from src.i18n import _
|
||||
from src.ui.icon import Icon
|
||||
|
||||
|
||||
class PayloadItem(QListWidgetItem):
|
||||
@@ -32,23 +32,23 @@ class HighlightingDialog(QDialog):
|
||||
form_grid.addWidget(self.list, row, 0, 1, 2)
|
||||
|
||||
row = row + 1
|
||||
self.btn_add = QPushButton(QIcon.fromTheme("list-add"), _("Add"))
|
||||
self.btn_add = QPushButton(Icon.fromTheme("list-add"), _("Add"))
|
||||
self.btn_add.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
self.btn_add.pressed.connect(self._add)
|
||||
|
||||
self.btn_update = QPushButton(QIcon.fromTheme("stock_edit"), _("Update"))
|
||||
self.btn_update = QPushButton(Icon.fromTheme("stock_edit"), _("Update"))
|
||||
self.btn_update.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
self.btn_update.pressed.connect(self._update)
|
||||
|
||||
self.btn_delete = QPushButton(QIcon.fromTheme("list-remove"), _("Remove"))
|
||||
self.btn_delete = QPushButton(Icon.fromTheme("list-remove"), _("Remove"))
|
||||
self.btn_delete.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
self.btn_delete.pressed.connect(self._delete)
|
||||
|
||||
self.btn_move_up = QPushButton(QIcon.fromTheme("go-up"), _("Up"))
|
||||
self.btn_move_up = QPushButton(Icon.fromTheme("go-up"), _("Up"))
|
||||
self.btn_move_up.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
self.btn_move_up.pressed.connect(self._move_up)
|
||||
|
||||
self.btn_move_down = QPushButton(QIcon.fromTheme("go-down"), _("Down"))
|
||||
self.btn_move_down = QPushButton(Icon.fromTheme("go-down"), _("Down"))
|
||||
self.btn_move_down.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed))
|
||||
self.btn_move_down.pressed.connect(self._move_down)
|
||||
button_box = HBox(self.btn_update, self.btn_add, self.btn_delete, self.btn_move_up, self.btn_move_down)
|
||||
|
||||
11
src/ui/icon.py
Normal file
11
src/ui/icon.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from PySide6.QtGui import QIcon
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class Icon(QIcon):
|
||||
def __init__(self, file_name: str):
|
||||
super(Icon, self).__init__("%s" % Path(__file__).parent.parent.parent.joinpath(file_name).absolute())
|
||||
print("%s -> %s" % (file_name, Path(__file__).parent.parent.parent.joinpath(file_name).absolute()))
|
||||
|
||||
def fromTheme(icon_from_theme: str) -> QIcon:
|
||||
return QIcon.fromTheme(icon_from_theme)
|
||||
@@ -11,9 +11,12 @@ def urls_to_path(urls: str) -> [str]:
|
||||
result.append(path)
|
||||
return result
|
||||
|
||||
|
||||
def url_to_path(url: str) -> str:
|
||||
p = urlparse(url)
|
||||
if sys.platform == 'win32' or sys.platform == 'cygwin':
|
||||
if p.netloc:
|
||||
return f"//{p.netloc}{p.path}"
|
||||
return os.path.abspath(p.path[1:])
|
||||
return os.path.abspath(os.path.join(p.netloc, p.path))
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import sys
|
||||
|
||||
from PyQt6.QtWidgets import QApplication, QMainWindow, QDockWidget, QLabel, QTextEdit
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
|
||||
class DockWindow(QMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DockWindow, self).__init__(*args, **kwargs)
|
||||
|
||||
self.dock_1 = QDockWidget("Dock1", self)
|
||||
self.dock_1.layout().addWidget(QLabel("dock1"))
|
||||
|
||||
self.dock_2 = QDockWidget("Dock2", self)
|
||||
self.dock_2.layout().addWidget(QLabel("dock2"))
|
||||
|
||||
self.dock_3 = QDockWidget("Dock3", self)
|
||||
self.dock_3.layout().addWidget(QLabel("dock3"))
|
||||
|
||||
self.setCentralWidget(QTextEdit())
|
||||
self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dock_1)
|
||||
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.dock_2)
|
||||
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.dock_3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
window = DockWindow()
|
||||
window.show()
|
||||
|
||||
app.exec()
|
||||
Reference in New Issue
Block a user