mirror of
https://github.com/spalencsar/bearhub.git
synced 2026-07-07 01:14:15 +02:00
screenshots button
This commit is contained in:
@@ -247,3 +247,10 @@ class SoftwareManager(ABC):
|
||||
@abstractmethod
|
||||
def launch(self, pkg: SoftwarePackage):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
|
||||
"""
|
||||
:return: screenshot urls for the given package
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -169,6 +169,12 @@ class SoftwarePackage(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def has_screenshots(self) -> bool:
|
||||
"""
|
||||
:return: if there are screenshots to be displayed
|
||||
"""
|
||||
return not self.installed
|
||||
|
||||
def __str__(self):
|
||||
return '{} (id={}, name={})'.format(self.__class__.__name__, self.id, self.name)
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ def main():
|
||||
display_limit=args.max_displayed,
|
||||
config=user_config,
|
||||
context=context,
|
||||
http_client=http_client,
|
||||
notifications=bool(args.system_notifications))
|
||||
|
||||
if args.tray:
|
||||
|
||||
@@ -364,3 +364,9 @@ class AppImageManager(SoftwareManager):
|
||||
|
||||
def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool):
|
||||
self.serialize_to_disk(pkg, icon_bytes, only_icon)
|
||||
|
||||
def get_screenshots(self, pkg: AppImage) -> List[str]:
|
||||
if pkg.has_screenshots():
|
||||
return [pkg.url_screenshot]
|
||||
|
||||
return []
|
||||
|
||||
@@ -10,7 +10,7 @@ CACHED_ATTRS = {'name', 'description', 'version', 'url_download', 'author', 'lic
|
||||
class AppImage(SoftwarePackage):
|
||||
|
||||
def __init__(self, name: str = None, description: str = None, github: str = None, source: str = None, version: str = None,
|
||||
url_download: str = None, url_icon: str = None, license: str = None, author: str = None,
|
||||
url_download: str = None, url_icon: str = None, url_screenshot: str = None, license: str = None, author: str = None,
|
||||
pictures: List[str] = None, icon_path: str = None, installed: bool = False,
|
||||
url_download_latest_version: str = None):
|
||||
super(AppImage, self).__init__(id=name, name=name, version=version, latest_version=version,
|
||||
@@ -21,6 +21,7 @@ class AppImage(SoftwarePackage):
|
||||
self.pictures = pictures
|
||||
self.url_download = url_download
|
||||
self.icon_path = icon_path
|
||||
self.url_screenshot = url_screenshot
|
||||
self.author = author
|
||||
self.url_download_latest_version = url_download_latest_version
|
||||
|
||||
@@ -80,3 +81,6 @@ class AppImage(SoftwarePackage):
|
||||
|
||||
def get_disk_icon_path(self):
|
||||
return self.icon_path
|
||||
|
||||
def has_screenshots(self):
|
||||
return not self.installed and self.url_screenshot
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
APP_ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'license', 'author')
|
||||
APP_ATTRS = ('name', 'description', 'github', 'source', 'version', 'url_download', 'url_icon', 'url_screenshot', 'license', 'author')
|
||||
RELEASE_ATTRS = ('version', 'url_download', 'published_at')
|
||||
|
||||
|
||||
|
||||
@@ -731,3 +731,6 @@ class ArchManager(SoftwareManager):
|
||||
def launch(self, pkg: ArchPackage):
|
||||
if pkg.command:
|
||||
subprocess.Popen(pkg.command.split(' '))
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
|
||||
pass
|
||||
|
||||
@@ -272,4 +272,14 @@ class FlatpakManager(SoftwareManager):
|
||||
return True
|
||||
|
||||
def launch(self, pkg: SoftwarePackage):
|
||||
flatpak.run(pkg.id)
|
||||
flatpak.run(str(pkg.id))
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
|
||||
res = self.http_client.get_json('{}/apps/{}'.format(FLATHUB_API_URL, pkg.id))
|
||||
urls = []
|
||||
if res and res.get('screenshots'):
|
||||
for s in res['screenshots']:
|
||||
if s.get('imgDesktopUrl'):
|
||||
urls.append(s['imgDesktopUrl'])
|
||||
|
||||
return urls
|
||||
|
||||
@@ -191,3 +191,21 @@ class SnapManager(SoftwareManager):
|
||||
|
||||
def launch(self, pkg: SnapApplication):
|
||||
snap.run(pkg, self.context.logger)
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage) -> List[str]:
|
||||
res = self.http_client.get_json('{}/search?q={}'.format(SNAP_API_URL, pkg.name))
|
||||
|
||||
if res:
|
||||
if res.get('_embedded') and res['_embedded'].get('clickindex:package'):
|
||||
snap_data = res['_embedded']['clickindex:package'][0]
|
||||
|
||||
if snap_data.get('screenshot_urls'):
|
||||
return snap_data['screenshot_urls']
|
||||
else:
|
||||
self.logger.warning("No 'screenshots_urls' defined for {}".format(pkg))
|
||||
else:
|
||||
self.logger.error('It seems the API is returning a different response: {}'.format(res))
|
||||
else:
|
||||
self.logger.warning('Could not retrieve data for {}'.format(pkg))
|
||||
|
||||
return []
|
||||
|
||||
@@ -369,3 +369,9 @@ class GenericSoftwareManager(SoftwareManager):
|
||||
if man:
|
||||
self.logger.info('Launching {}'.format(pkg))
|
||||
man.launch(pkg)
|
||||
|
||||
def get_screenshots(self, pkg: SoftwarePackage):
|
||||
man = self._get_manager_for(pkg)
|
||||
|
||||
if man:
|
||||
return man.get_screenshots(pkg)
|
||||
|
||||
@@ -417,6 +417,12 @@ class AppsTable(QTableWidget):
|
||||
|
||||
item.addWidget(IconButton(icon_path=resource.get_path('img/app_info.svg'), action=get_info, background='#2E68D3', tooltip=self.i18n['action.info.tooltip']))
|
||||
|
||||
if pkg.model.has_screenshots():
|
||||
def get_screenshots():
|
||||
self.window.get_screenshots(pkg)
|
||||
|
||||
item.addWidget(IconButton(icon_path=resource.get_path('img/camera.svg'), action=get_screenshots, background='purple', tooltip=self.i18n['action.screenshots.tooltip']))
|
||||
|
||||
def handle_click():
|
||||
self.show_pkg_settings(pkg)
|
||||
|
||||
|
||||
30
bauh/view/qt/screenshots.py
Normal file
30
bauh/view/qt/screenshots.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtGui import QIcon, QPixmap
|
||||
from PyQt5.QtWidgets import QDialog, QLabel, QVBoxLayout
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
|
||||
|
||||
class ScreenshotsDialog(QDialog):
|
||||
|
||||
def __init__(self, pkg: PackageView, icon_cache: MemoryCache, i18n: dict, screenshots: List[QPixmap]):
|
||||
super(ScreenshotsDialog, self).__init__()
|
||||
self.setWindowTitle(str(pkg))
|
||||
|
||||
icon_data = icon_cache.get(pkg.model.icon_url)
|
||||
|
||||
if icon_data and icon_data.get('icon'):
|
||||
self.setWindowIcon(icon_data.get('icon'))
|
||||
else:
|
||||
self.setWindowIcon(QIcon(pkg.model.get_type_icon_path()))
|
||||
|
||||
self.setLayout(QVBoxLayout())
|
||||
|
||||
if screenshots:
|
||||
lb = QLabel()
|
||||
lb.setPixmap(screenshots[0])
|
||||
self.layout().addWidget(lb)
|
||||
|
||||
self.adjustSize()
|
||||
@@ -5,6 +5,7 @@ from typing import List, Type, Set
|
||||
|
||||
import requests
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
|
||||
from bauh.api.abstract.cache import MemoryCache
|
||||
from bauh.api.abstract.controller import SoftwareManager
|
||||
@@ -12,6 +13,7 @@ from bauh.api.abstract.handler import ProcessWatcher
|
||||
from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction
|
||||
from bauh.api.abstract.view import InputViewComponent, MessageType
|
||||
from bauh.api.exception import NoInternetException
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.view.qt import commons
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
|
||||
@@ -480,3 +482,32 @@ class CustomAction(AsyncAction):
|
||||
self.pkg = None
|
||||
self.custom_action = None
|
||||
self.root_password = None
|
||||
|
||||
|
||||
class GetScreenshots(AsyncAction):
|
||||
|
||||
def __init__(self, manager: SoftwareManager, http_client: HttpClient, pkg: PackageView = None):
|
||||
super(GetScreenshots, self).__init__()
|
||||
self.pkg = pkg
|
||||
self.manager = manager
|
||||
self.http_client = http_client
|
||||
|
||||
def run(self):
|
||||
if self.pkg:
|
||||
imgs = []
|
||||
screenshots = self.manager.get_screenshots(self.pkg.model)
|
||||
|
||||
if screenshots:
|
||||
for url in screenshots:
|
||||
res = self.http_client.get(url)
|
||||
|
||||
if res and res.status_code == 200:
|
||||
pixmap = QPixmap()
|
||||
pixmap.loadFromData(res.content)
|
||||
imgs.append(pixmap)
|
||||
break # TODO load all
|
||||
|
||||
self.notify_finished({'pkg': self.pkg, 'screenshots': imgs})
|
||||
|
||||
self.pkg = None
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ from bauh.api.abstract.context import ApplicationContext
|
||||
from bauh.api.abstract.controller import SoftwareManager
|
||||
from bauh.api.abstract.model import SoftwarePackage, PackageAction
|
||||
from bauh.api.abstract.view import MessageType
|
||||
from bauh.api.http import HttpClient
|
||||
from bauh.commons.html import bold
|
||||
from bauh.view.core.config import Configuration
|
||||
from bauh.view.core.controller import GenericSoftwareManager
|
||||
from bauh.view.qt.screenshots import ScreenshotsDialog
|
||||
from bauh.view.util import util, resource
|
||||
from bauh.view.qt import dialog, commons, qt_utils
|
||||
from bauh.view.qt.about import AboutDialog
|
||||
@@ -29,7 +32,7 @@ from bauh.view.qt.root import is_root, ask_root_password
|
||||
from bauh.view.qt.styles import StylesComboBox
|
||||
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
|
||||
GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
|
||||
AsyncAction, LaunchApp, ApplyFilters, CustomAction
|
||||
AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
|
||||
from bauh.view.qt.view_model import PackageView
|
||||
from bauh.view.qt.view_utils import load_icon
|
||||
|
||||
@@ -48,7 +51,7 @@ class ManageWindow(QWidget):
|
||||
|
||||
def __init__(self, i18n: dict, icon_cache: MemoryCache, manager: SoftwareManager, disk_cache: bool,
|
||||
download_icons: bool, screen_size, suggestions: bool, display_limit: int, config: Configuration,
|
||||
context: ApplicationContext, notifications: bool, tray_icon=None):
|
||||
context: ApplicationContext, notifications: bool, http_client: HttpClient, tray_icon=None):
|
||||
super(ManageWindow, self).__init__()
|
||||
self.i18n = i18n
|
||||
self.manager = manager
|
||||
@@ -65,6 +68,7 @@ class ManageWindow(QWidget):
|
||||
self.config = config
|
||||
self.context = context
|
||||
self.notifications = notifications
|
||||
self.http_client = http_client
|
||||
|
||||
self.icon_app = QIcon(resource.get_path('img/logo.svg'))
|
||||
self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
|
||||
@@ -213,6 +217,7 @@ class ManageWindow(QWidget):
|
||||
self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_search, only_finished=True)
|
||||
self.thread_run_app = self._bind_async_action(LaunchApp(self.manager), finished_call=self._finish_run_app, only_finished=False)
|
||||
self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager), finished_call=self._finish_custom_action)
|
||||
self.thread_screenshots = self._bind_async_action(GetScreenshots(self.manager, http_client), finished_call=self._finish_get_screenshots)
|
||||
|
||||
self.thread_apply_filters = ApplyFilters()
|
||||
self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters_async)
|
||||
@@ -811,6 +816,24 @@ class ManageWindow(QWidget):
|
||||
self.thread_get_info.app = pkg
|
||||
self.thread_get_info.start()
|
||||
|
||||
def get_screenshots(self, pkg: PackageView):
|
||||
self._handle_console_option(False)
|
||||
self._begin_action(self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name)))
|
||||
|
||||
self.thread_screenshots.pkg = pkg
|
||||
self.thread_screenshots.start()
|
||||
|
||||
def _finish_get_screenshots(self, res: dict):
|
||||
self.finish_action()
|
||||
|
||||
if res.get('screenshots'):
|
||||
diag = ScreenshotsDialog(pkg=res['pkg'], icon_cache=self.icon_cache, i18n=self.i18n, screenshots=res['screenshots'])
|
||||
diag.exec_()
|
||||
else:
|
||||
dialog.show_message(title=self.i18n['error'],
|
||||
body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)),
|
||||
type_=MessageType.ERROR)
|
||||
|
||||
def get_app_history(self, app: dict):
|
||||
self._handle_console_option(False)
|
||||
self._begin_action(self.i18n['manage_window.status.history'])
|
||||
|
||||
126
bauh/view/resources/img/camera.svg
Executable file
126
bauh/view/resources/img/camera.svg
Executable file
@@ -0,0 +1,126 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 47.999996 44"
|
||||
xml:space="preserve"
|
||||
sodipodi:docname="camera_2.svg"
|
||||
width="47.999996"
|
||||
height="44"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14"><metadata
|
||||
id="metadata945"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||
id="defs943">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1366"
|
||||
inkscape:window-height="739"
|
||||
id="namedview941"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0"
|
||||
inkscape:zoom="0.78647591"
|
||||
inkscape:cx="-389.13442"
|
||||
inkscape:cy="-153.68157"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="432"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Capa_1" />
|
||||
<g
|
||||
id="g903"><path
|
||||
style="fill:#ffffff;stroke-width:1.88976378;stroke:#ffffff;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path900"
|
||||
d="M 0,13.46442 V 37.38396 C 0,41.03495 2.625306,44 5.857959,44 h 36.28408 c 3.232653,0 5.857959,-2.96505 5.857959,-6.61604 V 13.46442 c 0,-3.47398 -2.497959,-6.2952 -5.573877,-6.2952 H 34.62857 L 34.442448,6.25094 C 33.688162,2.56676 30.817958,0 27.467754,0 H 20.522448 C 17.18204,0 14.311836,2.56676 13.547755,6.25094 L 13.361632,7.16922 H 5.5738773 C 2.4979591,7.16922 0,10.00151 0,13.46442 Z M 14.321632,9.87981 c 0.558367,0 1.038367,-0.43148 1.165714,-1.05105 l 0.401633,-1.96932 c 0.509388,-2.44506 2.409796,-4.14886 4.633469,-4.14886 h 6.945306 c 2.223673,0 4.124082,1.7038 4.633469,4.14886 l 0.401633,1.96932 c 0.127347,0.6085 0.607347,1.05105 1.165714,1.05105 h 8.747755 c 1.753469,0 3.173877,1.60422 3.173877,3.58461 v 23.91954 c 0,2.1574 -1.547755,3.90545 -3.457959,3.90545 H 5.857959 c -1.910204,0 -3.4579591,-1.74805 -3.4579591,-3.90545 V 13.46442 c 0,-1.98039 1.4204081,-3.58461 3.1738774,-3.58461 z" /><path
|
||||
style="fill:#ffffff;stroke-width:1.88976378;stroke:#ffffff;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path904"
|
||||
d="m 23.999999,37.04099 c 5.554286,0 10.08,-5.1114 10.08,-11.38447 0,-6.27307 -4.525714,-11.38446 -10.08,-11.38446 -5.554285,0 -10.08,5.10033 -10.08,11.38446 0,6.28414 4.525715,11.38447 10.08,11.38447 z m 0,-20.05834 c 4.231837,0 7.68,3.89439 7.68,8.67387 0,4.77949 -3.448163,8.67388 -7.68,8.67388 -4.231836,0 -7.68,-3.89439 -7.68,-8.67388 0,-4.77948 3.448164,-8.67387 7.68,-8.67387 z" /></g>
|
||||
<g
|
||||
id="g910"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g912"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g914"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g916"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g918"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g920"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g922"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g924"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g926"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g928"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g930"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g932"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g934"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g936"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
<g
|
||||
id="g938"
|
||||
transform="translate(0,-410.99999)">
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -27,6 +27,7 @@ manage_window.status.installing=Installing
|
||||
manage_window.status.refreshing_app=Refreshing
|
||||
manage_window.status.running_app=Launching {}
|
||||
manage_window.status.filtering=Filtering
|
||||
manage_window.status.screenshots=Retrieving {} screenshots
|
||||
manage_window.bt.refresh.text=Refresh
|
||||
manage_window.bt.refresh.tooltip=Reload the data about installed applications
|
||||
manage_window.bt.upgrade.text=Upgrade
|
||||
@@ -47,6 +48,7 @@ tray.action.exit=Quit
|
||||
tray.action.about=About
|
||||
tray.action.refreshing=Refreshing
|
||||
action.info.tooltip=Click here to see information about this application
|
||||
action.screenshots.tooltip=Click here to see some pictures of this application
|
||||
action.settings.tooltip=Click here to open extra actions
|
||||
notification.update={} available update
|
||||
notification.updates={} available updates
|
||||
@@ -121,4 +123,5 @@ author=author
|
||||
source=source
|
||||
size=size
|
||||
categories=categories
|
||||
summary=summary
|
||||
summary=summary
|
||||
popup.screenshots.no_screenshot.body=it was not possible to retrieve {} screenshots
|
||||
@@ -123,4 +123,7 @@ author=autor
|
||||
source=origen
|
||||
size=tamaño
|
||||
categories=categorías
|
||||
summary=sumario
|
||||
summary=sumario
|
||||
manage_window.status.screenshots=Obteniendo imágenes de {}
|
||||
action.screenshots.tooltip=Haga clic aquí para ver algunas fotos de esta aplicación
|
||||
popup.screenshots.no_screenshot.body=No fue posible recuperar las fotos de {}
|
||||
@@ -123,4 +123,7 @@ author=autor
|
||||
source=fonte
|
||||
size=tamanho
|
||||
categories=categorias
|
||||
summary=resumo
|
||||
summary=resumo
|
||||
manage_window.status.screenshots=Obtendo imagens de {}
|
||||
action.screenshots.tooltip=Clique aqui para ver algumas fotos desse aplicativo
|
||||
popup.screenshots.no_screenshot.body=Não foi possível obter as fotos de {}
|
||||
Reference in New Issue
Block a user