forked from openlp/openlp
Created a new SearchEdit class, and implemented it in the song search.
bzr-revno: 1172
This commit is contained in:
commit
5e730c3138
191
openlp/core/lib/searchedit.py
Normal file
191
openlp/core/lib/searchedit.py
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OpenLP - Open Source Lyrics Projection #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Copyright (c) 2008-2011 Raoul Snyman #
|
||||||
|
# Portions copyright (c) 2008-2011 Tim Bentley, Jonathan Corwin, Michael #
|
||||||
|
# Gorven, Scott Guerrieri, Meinert Jordan, Andreas Preikschat, Christian #
|
||||||
|
# Richter, Philip Ridout, Maikel Stuivenberg, Martin Thompson, Jon Tibble, #
|
||||||
|
# Carsten Tinggaard, Frode Woldsund #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# This program is free software; you can redistribute it and/or modify it #
|
||||||
|
# under the terms of the GNU General Public License as published by the Free #
|
||||||
|
# Software Foundation; version 2 of the License. #
|
||||||
|
# #
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||||
|
# more details. #
|
||||||
|
# #
|
||||||
|
# You should have received a copy of the GNU General Public License along #
|
||||||
|
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||||
|
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from PyQt4 import QtCore, QtGui
|
||||||
|
|
||||||
|
from openlp.core.lib import build_icon
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SearchEdit(QtGui.QLineEdit):
|
||||||
|
"""
|
||||||
|
This is a specialised QLineEdit with a "clear" button inside for searches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
"""
|
||||||
|
Constructor.
|
||||||
|
"""
|
||||||
|
QtGui.QLineEdit.__init__(self, parent)
|
||||||
|
self._currentSearchType = -1
|
||||||
|
self.clearButton = QtGui.QToolButton(self)
|
||||||
|
self.clearButton.setIcon(build_icon(u':/system/clear_shortcut.png'))
|
||||||
|
self.clearButton.setCursor(QtCore.Qt.ArrowCursor)
|
||||||
|
self.clearButton.setStyleSheet(
|
||||||
|
u'QToolButton { border: none; padding: 0px; }')
|
||||||
|
self.clearButton.resize(18, 18)
|
||||||
|
self.clearButton.hide()
|
||||||
|
QtCore.QObject.connect(
|
||||||
|
self.clearButton,
|
||||||
|
QtCore.SIGNAL(u'clicked()'),
|
||||||
|
self._onClearButtonClicked
|
||||||
|
)
|
||||||
|
QtCore.QObject.connect(
|
||||||
|
self,
|
||||||
|
QtCore.SIGNAL(u'textChanged(const QString&)'),
|
||||||
|
self._onSearchEditTextChanged
|
||||||
|
)
|
||||||
|
self._updateStyleSheet()
|
||||||
|
|
||||||
|
def _updateStyleSheet(self):
|
||||||
|
"""
|
||||||
|
Internal method to update the stylesheet depending on which widgets are
|
||||||
|
available and visible.
|
||||||
|
"""
|
||||||
|
frameWidth = self.style().pixelMetric(
|
||||||
|
QtGui.QStyle.PM_DefaultFrameWidth)
|
||||||
|
rightPadding = self.clearButton.sizeHint().width() + frameWidth
|
||||||
|
if hasattr(self, u'menuButton'):
|
||||||
|
leftPadding = self.menuButton.width()
|
||||||
|
self.setStyleSheet(
|
||||||
|
u'QLineEdit { padding-left: %spx; padding-right: %spx; } ' % \
|
||||||
|
(leftPadding, rightPadding))
|
||||||
|
else:
|
||||||
|
self.setStyleSheet(u'QLineEdit { padding-right: %spx; } ' % \
|
||||||
|
rightPadding)
|
||||||
|
msz = self.minimumSizeHint();
|
||||||
|
self.setMinimumSize(
|
||||||
|
max(msz.width(),
|
||||||
|
self.clearButton.sizeHint().width() + (frameWidth * 2) + 2),
|
||||||
|
max(msz.height(),
|
||||||
|
self.clearButton.height() + (frameWidth * 2) + 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
def resizeEvent(self, event):
|
||||||
|
"""
|
||||||
|
Reimplemented method to react to resizing of the widget.
|
||||||
|
|
||||||
|
``event``
|
||||||
|
The event that happened.
|
||||||
|
"""
|
||||||
|
sz = self.clearButton.sizeHint()
|
||||||
|
frameWidth = self.style().pixelMetric(
|
||||||
|
QtGui.QStyle.PM_DefaultFrameWidth)
|
||||||
|
self.clearButton.move(self.rect().right() - frameWidth - sz.width(),
|
||||||
|
(self.rect().bottom() + 1 - sz.height()) / 2)
|
||||||
|
if hasattr(self, u'menuButton'):
|
||||||
|
sz = self.menuButton.sizeHint()
|
||||||
|
self.menuButton.move(self.rect().left() + frameWidth + 2,
|
||||||
|
(self.rect().bottom() + 1 - sz.height()) / 2)
|
||||||
|
|
||||||
|
def currentSearchType(self):
|
||||||
|
"""
|
||||||
|
Readonly property to return the current search type.
|
||||||
|
"""
|
||||||
|
return self._currentSearchType
|
||||||
|
|
||||||
|
def setSearchTypes(self, items):
|
||||||
|
"""
|
||||||
|
A list of tuples to be used in the search type menu. The first item in
|
||||||
|
the list will be preselected as the default.
|
||||||
|
|
||||||
|
``items``
|
||||||
|
The list of tuples to use. The tuples should contain an integer
|
||||||
|
identifier, an icon (QIcon instance or string) and a title for the
|
||||||
|
item in the menu. In short, they should look like this::
|
||||||
|
|
||||||
|
(<identifier>, <icon>, <title>)
|
||||||
|
|
||||||
|
For instance::
|
||||||
|
|
||||||
|
(1, <QIcon instance>, "Titles")
|
||||||
|
|
||||||
|
Or::
|
||||||
|
|
||||||
|
(2, ":/songs/authors.png", "Authors")
|
||||||
|
"""
|
||||||
|
menu = QtGui.QMenu(self)
|
||||||
|
first = None
|
||||||
|
for identifier, icon, title in items:
|
||||||
|
action = QtGui.QAction(build_icon(icon), title, menu)
|
||||||
|
action.setData(QtCore.QVariant(identifier))
|
||||||
|
menu.addAction(action)
|
||||||
|
QtCore.QObject.connect(action, QtCore.SIGNAL(u'triggered(bool)'),
|
||||||
|
self._onMenuActionTriggered)
|
||||||
|
if first is None:
|
||||||
|
first = action
|
||||||
|
self._currentSearchType = identifier
|
||||||
|
if not hasattr(self, u'menuButton'):
|
||||||
|
self.menuButton = QtGui.QToolButton(self)
|
||||||
|
self.menuButton.setIcon(build_icon(u':/system/clear_shortcut.png'))
|
||||||
|
self.menuButton.setCursor(QtCore.Qt.ArrowCursor)
|
||||||
|
self.menuButton.setPopupMode(QtGui.QToolButton.InstantPopup)
|
||||||
|
self.menuButton.setStyleSheet(
|
||||||
|
u'QToolButton { border: none; padding: 0px 10px 0px 0px; }')
|
||||||
|
self.menuButton.resize(QtCore.QSize(28, 18))
|
||||||
|
self.menuButton.setMenu(menu)
|
||||||
|
self.menuButton.setDefaultAction(first)
|
||||||
|
self.menuButton.show()
|
||||||
|
self._updateStyleSheet()
|
||||||
|
|
||||||
|
def _onSearchEditTextChanged(self, text):
|
||||||
|
"""
|
||||||
|
Internally implemented slot to react to when the text in the line edit
|
||||||
|
has changed so that we can show or hide the clear button.
|
||||||
|
|
||||||
|
``text``
|
||||||
|
A :class:`~PyQt4.QtCore.QString` instance which represents the text
|
||||||
|
in the line edit.
|
||||||
|
"""
|
||||||
|
self.clearButton.setVisible(not text.isEmpty())
|
||||||
|
|
||||||
|
def _onClearButtonClicked(self):
|
||||||
|
"""
|
||||||
|
Internally implemented slot to react to the clear button being pressed
|
||||||
|
to clear the line edit. Once it has cleared the line edit, it emits the
|
||||||
|
``cleared()`` signal so that an application can react to the clearing
|
||||||
|
of the line edit.
|
||||||
|
"""
|
||||||
|
self.clear()
|
||||||
|
self.emit(QtCore.SIGNAL(u'cleared()'))
|
||||||
|
|
||||||
|
def _onMenuActionTriggered(self):
|
||||||
|
"""
|
||||||
|
Internally implemented slot to react to the select of one of the search
|
||||||
|
types in the menu. Once it has set the correct action on the button,
|
||||||
|
and set the current search type (using the list of identifiers provided
|
||||||
|
by the developer), the ``searchTypeChanged(int)`` signal is emitted
|
||||||
|
with the identifier.
|
||||||
|
"""
|
||||||
|
sender = self.sender()
|
||||||
|
for action in self.menuButton.menu().actions():
|
||||||
|
action.setChecked(False)
|
||||||
|
self.menuButton.setDefaultAction(sender)
|
||||||
|
self._currentSearchType = sender.data().toInt()[0]
|
||||||
|
self.emit(QtCore.SIGNAL(u'searchTypeChanged(int)'),
|
||||||
|
self._currentSearchType)
|
@ -29,6 +29,7 @@ import locale
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from PyQt4 import QtCore, QtGui
|
from PyQt4 import QtCore, QtGui
|
||||||
|
from sqlalchemy.sql import or_
|
||||||
|
|
||||||
from openlp.core.lib import MediaManagerItem, BaseListWithDnD, Receiver, \
|
from openlp.core.lib import MediaManagerItem, BaseListWithDnD, Receiver, \
|
||||||
ItemCapabilities, translate, check_item_selected
|
ItemCapabilities, translate, check_item_selected
|
||||||
@ -36,6 +37,7 @@ from openlp.plugins.songs.forms import EditSongForm, SongMaintenanceForm, \
|
|||||||
SongImportForm
|
SongImportForm
|
||||||
from openlp.plugins.songs.lib import SongXMLParser, OpenLyricsParser
|
from openlp.plugins.songs.lib import SongXMLParser, OpenLyricsParser
|
||||||
from openlp.plugins.songs.lib.db import Author, Song
|
from openlp.plugins.songs.lib.db import Author, Song
|
||||||
|
from openlp.core.lib.searchedit import SearchEdit
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -88,20 +90,10 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
self.SearchTextLabel.setObjectName(u'SearchTextLabel')
|
self.SearchTextLabel.setObjectName(u'SearchTextLabel')
|
||||||
self.SearchLayout.setWidget(
|
self.SearchLayout.setWidget(
|
||||||
0, QtGui.QFormLayout.LabelRole, self.SearchTextLabel)
|
0, QtGui.QFormLayout.LabelRole, self.SearchTextLabel)
|
||||||
self.SearchTextEdit = QtGui.QLineEdit(self)
|
self.SearchTextEdit = SearchEdit(self)
|
||||||
self.SearchTextEdit.setObjectName(u'SearchTextEdit')
|
self.SearchTextEdit.setObjectName(u'SearchTextEdit')
|
||||||
self.SearchLayout.setWidget(
|
self.SearchLayout.setWidget(
|
||||||
0, QtGui.QFormLayout.FieldRole, self.SearchTextEdit)
|
0, QtGui.QFormLayout.FieldRole, self.SearchTextEdit)
|
||||||
self.SearchTypeLabel = QtGui.QLabel(self)
|
|
||||||
self.SearchTypeLabel.setAlignment(
|
|
||||||
QtCore.Qt.AlignBottom|QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft)
|
|
||||||
self.SearchTypeLabel.setObjectName(u'SearchTypeLabel')
|
|
||||||
self.SearchLayout.setWidget(
|
|
||||||
1, QtGui.QFormLayout.LabelRole, self.SearchTypeLabel)
|
|
||||||
self.SearchTypeComboBox = QtGui.QComboBox(self)
|
|
||||||
self.SearchTypeComboBox.setObjectName(u'SearchTypeComboBox')
|
|
||||||
self.SearchLayout.setWidget(
|
|
||||||
1, QtGui.QFormLayout.FieldRole, self.SearchTypeComboBox)
|
|
||||||
self.pageLayout.addLayout(self.SearchLayout)
|
self.pageLayout.addLayout(self.SearchLayout)
|
||||||
self.SearchButtonLayout = QtGui.QHBoxLayout()
|
self.SearchButtonLayout = QtGui.QHBoxLayout()
|
||||||
self.SearchButtonLayout.setMargin(0)
|
self.SearchButtonLayout.setMargin(0)
|
||||||
@ -113,9 +105,6 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
self.SearchTextButton = QtGui.QPushButton(self)
|
self.SearchTextButton = QtGui.QPushButton(self)
|
||||||
self.SearchTextButton.setObjectName(u'SearchTextButton')
|
self.SearchTextButton.setObjectName(u'SearchTextButton')
|
||||||
self.SearchButtonLayout.addWidget(self.SearchTextButton)
|
self.SearchButtonLayout.addWidget(self.SearchTextButton)
|
||||||
self.ClearTextButton = QtGui.QPushButton(self)
|
|
||||||
self.ClearTextButton.setObjectName(u'ClearTextButton')
|
|
||||||
self.SearchButtonLayout.addWidget(self.ClearTextButton)
|
|
||||||
self.pageLayout.addLayout(self.SearchButtonLayout)
|
self.pageLayout.addLayout(self.SearchButtonLayout)
|
||||||
# Signals and slots
|
# Signals and slots
|
||||||
QtCore.QObject.connect(Receiver.get_receiver(),
|
QtCore.QObject.connect(Receiver.get_receiver(),
|
||||||
@ -124,8 +113,6 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
QtCore.SIGNAL(u'returnPressed()'), self.onSearchTextButtonClick)
|
QtCore.SIGNAL(u'returnPressed()'), self.onSearchTextButtonClick)
|
||||||
QtCore.QObject.connect(self.SearchTextButton,
|
QtCore.QObject.connect(self.SearchTextButton,
|
||||||
QtCore.SIGNAL(u'pressed()'), self.onSearchTextButtonClick)
|
QtCore.SIGNAL(u'pressed()'), self.onSearchTextButtonClick)
|
||||||
QtCore.QObject.connect(self.ClearTextButton,
|
|
||||||
QtCore.SIGNAL(u'pressed()'), self.onClearTextButtonClick)
|
|
||||||
QtCore.QObject.connect(self.SearchTextEdit,
|
QtCore.QObject.connect(self.SearchTextEdit,
|
||||||
QtCore.SIGNAL(u'textChanged(const QString&)'),
|
QtCore.SIGNAL(u'textChanged(const QString&)'),
|
||||||
self.onSearchTextEditChanged)
|
self.onSearchTextEditChanged)
|
||||||
@ -139,6 +126,11 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
QtCore.SIGNAL(u'songs_edit'), self.onRemoteEdit)
|
QtCore.SIGNAL(u'songs_edit'), self.onRemoteEdit)
|
||||||
QtCore.QObject.connect(Receiver.get_receiver(),
|
QtCore.QObject.connect(Receiver.get_receiver(),
|
||||||
QtCore.SIGNAL(u'songs_edit_clear'), self.onRemoteEditClear)
|
QtCore.SIGNAL(u'songs_edit_clear'), self.onRemoteEditClear)
|
||||||
|
QtCore.QObject.connect(self.SearchTextEdit,
|
||||||
|
QtCore.SIGNAL(u'cleared()'), self.onClearTextButtonClick)
|
||||||
|
QtCore.QObject.connect(self.SearchTextEdit,
|
||||||
|
QtCore.SIGNAL(u'searchTypeChanged(int)'),
|
||||||
|
self.onSearchTextButtonClick)
|
||||||
|
|
||||||
def configUpdated(self):
|
def configUpdated(self):
|
||||||
self.searchAsYouType = QtCore.QSettings().value(
|
self.searchAsYouType = QtCore.QSettings().value(
|
||||||
@ -154,39 +146,44 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
def retranslateUi(self):
|
def retranslateUi(self):
|
||||||
self.SearchTextLabel.setText(
|
self.SearchTextLabel.setText(
|
||||||
translate('SongsPlugin.MediaItem', 'Search:'))
|
translate('SongsPlugin.MediaItem', 'Search:'))
|
||||||
self.SearchTypeLabel.setText(
|
|
||||||
translate('SongsPlugin.MediaItem', 'Type:'))
|
|
||||||
self.ClearTextButton.setText(
|
|
||||||
translate('SongsPlugin.MediaItem', 'Clear'))
|
|
||||||
self.SearchTextButton.setText(
|
self.SearchTextButton.setText(
|
||||||
translate('SongsPlugin.MediaItem', 'Search'))
|
translate('SongsPlugin.MediaItem', 'Search'))
|
||||||
|
|
||||||
def initialise(self):
|
def initialise(self):
|
||||||
self.SearchTypeComboBox.addItem(
|
self.SearchTextEdit.setSearchTypes([
|
||||||
translate('SongsPlugin.MediaItem', 'Titles'))
|
(1, u':/songs/song_search_all.png', translate('SongsPlugin.MediaItem', 'Entire Song')),
|
||||||
self.SearchTypeComboBox.addItem(
|
(2, u':/songs/song_search_title.png', translate('SongsPlugin.MediaItem', 'Titles')),
|
||||||
translate('SongsPlugin.MediaItem', 'Lyrics'))
|
(3, u':/songs/song_search_lyrics.png', translate('SongsPlugin.MediaItem', 'Lyrics')),
|
||||||
self.SearchTypeComboBox.addItem(
|
(4, u':/songs/song_search_author.png', translate('SongsPlugin.MediaItem', 'Authors'))
|
||||||
translate('SongsPlugin.MediaItem', 'Authors'))
|
])
|
||||||
self.configUpdated()
|
self.configUpdated()
|
||||||
|
|
||||||
def onSearchTextButtonClick(self):
|
def onSearchTextButtonClick(self):
|
||||||
search_keywords = unicode(self.SearchTextEdit.displayText())
|
search_keywords = unicode(self.SearchTextEdit.displayText())
|
||||||
search_results = []
|
search_results = []
|
||||||
search_type = self.SearchTypeComboBox.currentIndex()
|
# search_type = self.SearchTypeComboBox.currentIndex()
|
||||||
if search_type == 0:
|
search_type = self.SearchTextEdit.currentSearchType()
|
||||||
|
if search_type == 1:
|
||||||
|
log.debug(u'Entire Song Search')
|
||||||
|
search_results = self.parent.manager.get_all_objects(Song,
|
||||||
|
or_(Song.search_title.like(u'%' + self.whitespace.sub(u' ',
|
||||||
|
search_keywords.lower()) + u'%'),
|
||||||
|
Song.search_lyrics.like(u'%' + search_keywords.lower() + \
|
||||||
|
u'%')), Song.search_title.asc())
|
||||||
|
self.displayResultsSong(search_results)
|
||||||
|
if search_type == 2:
|
||||||
log.debug(u'Titles Search')
|
log.debug(u'Titles Search')
|
||||||
search_results = self.parent.manager.get_all_objects(Song,
|
search_results = self.parent.manager.get_all_objects(Song,
|
||||||
Song.search_title.like(u'%' + self.whitespace.sub(u' ',
|
Song.search_title.like(u'%' + self.whitespace.sub(u' ',
|
||||||
search_keywords.lower()) + u'%'), Song.search_title.asc())
|
search_keywords.lower()) + u'%'), Song.search_title.asc())
|
||||||
self.displayResultsSong(search_results)
|
self.displayResultsSong(search_results)
|
||||||
elif search_type == 1:
|
elif search_type == 3:
|
||||||
log.debug(u'Lyrics Search')
|
log.debug(u'Lyrics Search')
|
||||||
search_results = self.parent.manager.get_all_objects(Song,
|
search_results = self.parent.manager.get_all_objects(Song,
|
||||||
Song.search_lyrics.like(u'%' + search_keywords.lower() + u'%'),
|
Song.search_lyrics.like(u'%' + search_keywords.lower() + u'%'),
|
||||||
Song.search_lyrics.asc())
|
Song.search_lyrics.asc())
|
||||||
self.displayResultsSong(search_results)
|
self.displayResultsSong(search_results)
|
||||||
elif search_type == 2:
|
elif search_type == 4:
|
||||||
log.debug(u'Authors Search')
|
log.debug(u'Authors Search')
|
||||||
search_results = self.parent.manager.get_all_objects(Author,
|
search_results = self.parent.manager.get_all_objects(Author,
|
||||||
Author.display_name.like(u'%' + search_keywords + u'%'),
|
Author.display_name.like(u'%' + search_keywords + u'%'),
|
||||||
@ -255,7 +252,9 @@ class SongMediaItem(MediaManagerItem):
|
|||||||
"""
|
"""
|
||||||
if self.searchAsYouType:
|
if self.searchAsYouType:
|
||||||
search_length = 1
|
search_length = 1
|
||||||
if self.SearchTypeComboBox.currentIndex() == 1:
|
if self.SearchTextEdit.currentSearchType() == 1:
|
||||||
|
search_length = 3
|
||||||
|
elif self.SearchTextEdit.currentSearchType() == 3:
|
||||||
search_length = 7
|
search_length = 7
|
||||||
if len(text) > search_length:
|
if len(text) > search_length:
|
||||||
self.onSearchTextButtonClick()
|
self.onSearchTextButtonClick()
|
||||||
|
BIN
resources/images/general_search_clear.png
Normal file
BIN
resources/images/general_search_clear.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 644 B |
@ -1,5 +1,9 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="songs">
|
<qresource prefix="songs">
|
||||||
|
<file>song_search_all.png</file>
|
||||||
|
<file>song_search_author.png</file>
|
||||||
|
<file>song_search_lyrics.png</file>
|
||||||
|
<file>song_search_title.png</file>
|
||||||
<file>topic_edit.png</file>
|
<file>topic_edit.png</file>
|
||||||
<file>author_add.png</file>
|
<file>author_add.png</file>
|
||||||
<file>author_delete.png</file>
|
<file>author_delete.png</file>
|
||||||
|
BIN
resources/images/song_search_all.png
Normal file
BIN
resources/images/song_search_all.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 607 B |
BIN
resources/images/song_search_author.png
Normal file
BIN
resources/images/song_search_author.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 409 B |
BIN
resources/images/song_search_lyrics.png
Normal file
BIN
resources/images/song_search_lyrics.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 335 B |
BIN
resources/images/song_search_title.png
Normal file
BIN
resources/images/song_search_title.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 245 B |
Loading…
Reference in New Issue
Block a user