Merge branch 'FooSoft-master'
This commit is contained in:
commit
e72891e9c3
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ dist
|
|||||||
*.pyo
|
*.pyo
|
||||||
*.mngl
|
*.mngl
|
||||||
*.cbz
|
*.cbz
|
||||||
|
*.bat
|
||||||
|
98
README.md
98
README.md
@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
## Downloads
|
## Downloads
|
||||||
- [Mangle for Windows](ftp://foosoft.net/releases/mangle/mangle_win.zip)
|
- [Mangle for Windows](ftp://foosoft.net/releases/mangle/mangle_win.zip)
|
||||||
- [Mangle for MacOS (Old)](ftp://foosoft.net/releases/mangle/mangle_osx.zip)
|
- [Mangle for MacOS (Old)](ftp://foosoft.net/releases/mangle/mangle_osx.zip)
|
||||||
@ -9,3 +10,100 @@
|
|||||||
- [ReportLab](https://pypi.python.org/pypi/reportlab)
|
- [ReportLab](https://pypi.python.org/pypi/reportlab)
|
||||||
- [natsort](https://pypi.python.org/pypi/natsort/3.0.1)
|
- [natsort](https://pypi.python.org/pypi/natsort/3.0.1)
|
||||||
- [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only)
|
- [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only)
|
||||||
|
=======
|
||||||
|
# Mangle #
|
||||||
|
|
||||||
|
Many years ago I received an [Amazon Kindle](http://en.wikipedia.org/wiki/Kindle) gift. I immediately began playing
|
||||||
|
around with it and reading about certain undocumented features that the Kindle has to offer. After a couple of hours I
|
||||||
|
discovered it to be the perfect device for reading [Manga](http://en.wikipedia.org/wiki/Manga) is almost always
|
||||||
|
grayscale, and the aspect ratio fits the Kindle's 600x800 pixel screen almost perfectly. Better yet, the Kindle's
|
||||||
|
undocumented image viewer actually keeps track of the last image you viewed and thus you are always able to return to
|
||||||
|
the page you left off on when you power on your Kindle. The device supports several popular image formats (jpeg, png,
|
||||||
|
gif, etc), and is able to dither and downscale images to fit the screen.
|
||||||
|
|
||||||
|
However... The Kindle's image viewer does have certain shortcomings:
|
||||||
|
|
||||||
|
* The Kindle is very picky about file format; any additional embedded data (thumbnails, comments, possibly even exif
|
||||||
|
data) can confuse it. As a result, images may not display properly or even not at all (which actually prevents you
|
||||||
|
from reading the given book, as one bad panel will prevent you from viewing subsequent images).
|
||||||
|
* The first image that you view in a Manga (until the Kindle first writes the "bookmark" file) seems to be arbitrary
|
||||||
|
even when files are named sequentially. About half the time it will correctly pick the first file in the batch, at
|
||||||
|
other times it will pick out some other image seemingly at random.
|
||||||
|
* Normally for Kindle to find your Manga scans you have to press Alt+Z on the home screen. I haven't always had luck
|
||||||
|
with it correctly identifying image directories. At other times, after finding an image directory the Kindle will
|
||||||
|
appear to hang while trying to access it (forcing you to return to the home screen).
|
||||||
|
* The Kindle image viewer has no functionality to rotate images. So if there is a horizontally large image (such as
|
||||||
|
what often happens with dual-page scans), it can be difficult to make out the text because the image is simply
|
||||||
|
scaled to fit (consequently leaving a lot of wasted space at the bottom of the screen).
|
||||||
|
* Scanlation images are oftentimes much larger than the 600x800 screen; not only does this make them take more space
|
||||||
|
on your memory card but it also slows down image loading (the Kindle has to read more data off of the slow SD card
|
||||||
|
and scale the image). Scanlations often also include color scans of covers and inserts which take up more space than
|
||||||
|
a grayscale equivalent (which is would be fine for the Kindle's limited display).
|
||||||
|
* Kindle's image viewer provides no way to sort images (to determine in which order they are shown). This can be very
|
||||||
|
problematic especially considering that scanlation groups have differing naming conventions, and as a result files
|
||||||
|
from later chapters may appear before earlier ones when you are reading your Manga (spoilers ftl).
|
||||||
|
|
||||||
|
I was annoyed with these issues and thus Mangle was born (the program name is a mix of "Manga" and "Kindle" in case you
|
||||||
|
haven't figured it out yet; I thought it was pretty clever at the time). Fortunately you can get all the benefits of my
|
||||||
|
work without really doing anything (and it won't even cost you anything since Mangle is free,
|
||||||
|
[GPL](http://www.gnu.org/licenses/gpl-3.0.txt) software. With Mangle you can easily:
|
||||||
|
|
||||||
|
* Sort and organize images from different directories; bulk rename feature for output to the Kindle.
|
||||||
|
* Optionally re-save images in a format Kindle will be sure to understand with no visible quality loss.
|
||||||
|
* Downsample and rotate images for optimal viewing on Kindle, convert to grayscale to save space and improve contrast.
|
||||||
|
* Automatically generate book meta-data so that your Manga is always properly detected and viewable in-order.
|
||||||
|
|
||||||
|
Here is a recent screenshot showing off some of the export options that you can configure on a per-book basis in Mangle:
|
||||||
|
|
||||||
|
![Mangle options dialog](http://foosoft.net/projects/mangle/img/options.png)
|
||||||
|
|
||||||
|
You can also check out what Mangle output looks like on the Kindle on the [action
|
||||||
|
shots](http://foosoft.net/projects/mangle/action/) page.
|
||||||
|
|
||||||
|
Mangle is cross platform, and doesn't require an install (it's a standalone executable that you can run from anywhere).
|
||||||
|
It is also "environmentally friendly" by not messing with your registry or modifying your system in any way. If you
|
||||||
|
ever want to uninstall it, just delete the executable and you're done.
|
||||||
|
|
||||||
|
## Usage Instructions ##
|
||||||
|
|
||||||
|
Mangle is pretty easy to use, so this won't be really in-depth. If you have any questions drop me a line though.
|
||||||
|
|
||||||
|
1. Add images to the current book by selecting the `Book | Add | Files` or `Book | Add | Directory` menu items.
|
||||||
|
2. If certain images are not in the order you want, select them in the window, and select the `Book | Shift | Up` or
|
||||||
|
`Book | Shift | Down` menu items.
|
||||||
|
3. Configure the book title and image processing options by selecting `Book | Options`; this will be the title you see
|
||||||
|
in the Kindle home menu.
|
||||||
|
4. Create a root-level directory on your SD memory card/Kindle called `pictures` (case might matter).
|
||||||
|
5. Once you are satisfied with the your images and options select `Book | Export` and select the `pictures` directory
|
||||||
|
you just created.
|
||||||
|
6. After the export is complete your new Manga books will show up along with all your other books (if they don't for
|
||||||
|
some reason, press `Alt+Z` while on the home menu).
|
||||||
|
|
||||||
|
## The Usual Disclaimer ##
|
||||||
|
|
||||||
|
You probably know how this goes by now... Mess around with your Kindle at your own risk. Honestly, nothing bad is going
|
||||||
|
to happen; however if something *does* then it's your problem.
|
||||||
|
|
||||||
|
## Running From Source ##
|
||||||
|
|
||||||
|
Because Mangle is written in Python, a scripting language, it's trivial to get it up and running on the operating system
|
||||||
|
of your choice. First you should make sure that you have the required dependencies installed:
|
||||||
|
|
||||||
|
* [PyQT4](http://www.riverbankcomputing.com/software/pyqt/download)
|
||||||
|
* [Python 2.7](http://www.python.org/download/releases/2.7/)
|
||||||
|
* [Python Imaging Library (PIL)](http://www.pythonware.com/products/pil/)
|
||||||
|
* [ReportLab](https://pypi.python.org/pypi/reportlab)
|
||||||
|
* [py2exe](http://www.py2exe.org/) (optional, for Windows distribution only)
|
||||||
|
|
||||||
|
Now you can fetch the [latest version of the code](https://github.com/FooSoft/mangle/) and run the `mangle.pyw` script
|
||||||
|
to execute Mangle.
|
||||||
|
|
||||||
|
## Downloads ##
|
||||||
|
|
||||||
|
If you don't want to run Mangle from source, you can use the following pre-built binaries. As I don't have the means to
|
||||||
|
make MacOS X releases myself, I am providing the slightly out of date (and unsupported) package built by Rob White in
|
||||||
|
its place. Linux users should execute the Python scripts with the interpreter and libraries installed on their system.
|
||||||
|
|
||||||
|
* [magnle_win.zip](http://dl.foosoft.net/mangle/mangle_win.zip)
|
||||||
|
* [mangle_osx.zip](http://dl.foosoft.net/mangle/mangle_osx.zip)
|
||||||
|
>>>>>>> cedfe3fc512a8f58680d8fa0618143d423c3112d
|
||||||
|
@ -13,14 +13,13 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import re
|
||||||
from os.path import basename
|
from os.path import basename
|
||||||
import os.path
|
import os.path
|
||||||
import tempfile
|
import tempfile
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
from PyQt4 import QtGui, QtCore, QtXml, uic
|
from PyQt4 import QtGui, QtCore, QtXml, uic
|
||||||
from natsort import natsorted
|
|
||||||
|
|
||||||
from about import DialogAbout
|
from about import DialogAbout
|
||||||
from convert import DialogConvert
|
from convert import DialogConvert
|
||||||
@ -29,6 +28,13 @@ from options import DialogOptions
|
|||||||
import util
|
import util
|
||||||
|
|
||||||
|
|
||||||
|
# Sort function use to sort files in a natural order, by lowering
|
||||||
|
# characters, and manage multi levels of integers (tome 1/ page 1.jpg, etc etc)
|
||||||
|
def natural_key(string_):
|
||||||
|
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
||||||
|
return [int(s) if s.isdigit() else s.lower() for s in re.split(r'(\d+)', string_)]
|
||||||
|
|
||||||
|
|
||||||
class Book(object):
|
class Book(object):
|
||||||
DefaultDevice = 'Kindle Paperwhite'
|
DefaultDevice = 'Kindle Paperwhite'
|
||||||
DefaultOutputFormat = 'CBZ only'
|
DefaultOutputFormat = 'CBZ only'
|
||||||
@ -368,13 +374,15 @@ class MainWindowBook(QtGui.QMainWindow):
|
|||||||
for i in xrange(0, self.listWidgetFiles.count()):
|
for i in xrange(0, self.listWidgetFiles.count()):
|
||||||
filenamesListed.append(self.listWidgetFiles.item(i).text())
|
filenamesListed.append(self.listWidgetFiles.item(i).text())
|
||||||
|
|
||||||
for filename in natsorted(filenames):
|
# Get files but in a natural sorted order
|
||||||
|
for filename in sorted(filenames, key=natural_key):
|
||||||
if filename not in filenamesListed:
|
if filename not in filenamesListed:
|
||||||
filename = QtCore.QString(filename)
|
filename = QtCore.QString(filename)
|
||||||
self.listWidgetFiles.addItem(filename)
|
self.listWidgetFiles.addItem(filename)
|
||||||
self.book.images.append(filename)
|
self.book.images.append(filename)
|
||||||
self.book.modified = True
|
self.book.modified = True
|
||||||
|
|
||||||
|
|
||||||
def addImageDirs(self, directories):
|
def addImageDirs(self, directories):
|
||||||
filenames = []
|
filenames = []
|
||||||
|
|
||||||
@ -387,6 +395,7 @@ class MainWindowBook(QtGui.QMainWindow):
|
|||||||
|
|
||||||
self.addImageFiles(filenames)
|
self.addImageFiles(filenames)
|
||||||
|
|
||||||
|
|
||||||
def addCBZFiles(self, filenames):
|
def addCBZFiles(self, filenames):
|
||||||
directories = []
|
directories = []
|
||||||
tempDir = tempfile.gettempdir()
|
tempDir = tempfile.gettempdir()
|
||||||
|
@ -129,6 +129,14 @@ class DialogConvert(QtGui.QProgressDialog):
|
|||||||
# Change target once again for left page
|
# Change target once again for left page
|
||||||
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1))
|
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1))
|
||||||
|
|
||||||
|
# For right page (if requested), but in inverted mode
|
||||||
|
if(self.book.imageFlags & ImageFlags.SplitInverse):
|
||||||
|
# New path based on modified index
|
||||||
|
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 0))
|
||||||
|
self.convertAndSave(source, target, device, flags ^ ImageFlags.SplitInverse | ImageFlags.SplitLeft, archive, pdf)
|
||||||
|
# Change target once again for left page
|
||||||
|
target = os.path.join(self.bookPath, '%05d.png' % (index * 2 + 1))
|
||||||
|
|
||||||
# Convert page
|
# Convert page
|
||||||
self.convertAndSave(source, target, device, flags, archive, pdf)
|
self.convertAndSave(source, target, device, flags, archive, pdf)
|
||||||
|
|
||||||
|
@ -19,14 +19,17 @@ import os
|
|||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageFlags:
|
class ImageFlags:
|
||||||
Orient = 1 << 0
|
Orient = 1 << 0
|
||||||
Resize = 1 << 1
|
Resize = 1 << 1
|
||||||
Frame = 1 << 2
|
Frame = 1 << 2
|
||||||
Quantize = 1 << 3
|
Quantize = 1 << 3
|
||||||
Stretch = 1 << 4
|
Stretch = 1 << 4
|
||||||
Split = 1 << 5
|
Split = 1 << 5 # split right then left
|
||||||
SplitRight = 1 << 6
|
SplitRight = 1 << 6 # split only the right page
|
||||||
|
SplitLeft = 1 << 7 # split only the left page
|
||||||
|
SplitInverse = 1 << 8 # split left then right page
|
||||||
|
|
||||||
|
|
||||||
class KindleData:
|
class KindleData:
|
||||||
@ -82,22 +85,39 @@ class KindleData:
|
|||||||
'Kindle DX': ((824, 1200), Palette15a),
|
'Kindle DX': ((824, 1200), Palette15a),
|
||||||
'Kindle DXG': ((824, 1200), Palette15a),
|
'Kindle DXG': ((824, 1200), Palette15a),
|
||||||
'Kindle Touch': ((600, 800), Palette15a),
|
'Kindle Touch': ((600, 800), Palette15a),
|
||||||
'Kindle Paperwhite': ((758, 1024), Palette15b) # resolution given in manual, see http://kindle.s3.amazonaws.com/Kindle_Paperwhite_Users_Guide.pdf
|
'Kindle Paperwhite': ((758, 1024), Palette15b), # resolution given in manual, see http://kindle.s3.amazonaws.com/Kindle_Paperwhite_Users_Guide.pdf
|
||||||
|
'KoBo Aura H2o': ((1080, 1430), Palette15a), # resolution from http://www.fnac.com/Liseuse-Numerique-Kobo-by-Fnac-Kobo-Aura-H2O-Noir/a7745120/w-4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# decorate a function that use image, *** and if there
|
||||||
|
# is an exception raise by PIL (IOError) then return
|
||||||
|
# the original image because PIL cannot manage it
|
||||||
|
def protect_bad_image(func):
|
||||||
|
def func_wrapper(*args, **kwargs):
|
||||||
|
# If cannot convert (like a bogus image) return the original one
|
||||||
|
# args will be "image" and other params are after
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except IOError: # Exception from PIL about bad image
|
||||||
|
return args[0]
|
||||||
|
return func_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def splitLeft(image):
|
def splitLeft(image):
|
||||||
widthImg, heightImg = image.size
|
widthImg, heightImg = image.size
|
||||||
|
|
||||||
return image.crop((0, 0, widthImg / 2, heightImg))
|
return image.crop((0, 0, widthImg / 2, heightImg))
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def splitRight(image):
|
def splitRight(image):
|
||||||
widthImg, heightImg = image.size
|
widthImg, heightImg = image.size
|
||||||
|
|
||||||
return image.crop((widthImg / 2, 0, widthImg, heightImg))
|
return image.crop((widthImg / 2, 0, widthImg, heightImg))
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def quantizeImage(image, palette):
|
def quantizeImage(image, palette):
|
||||||
colors = len(palette) / 3
|
colors = len(palette) / 3
|
||||||
if colors < 256:
|
if colors < 256:
|
||||||
@ -109,10 +129,14 @@ def quantizeImage(image, palette):
|
|||||||
return image.quantize(palette=palImg)
|
return image.quantize(palette=palImg)
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def stretchImage(image, size):
|
def stretchImage(image, size):
|
||||||
widthDev, heightDev = size
|
widthDev, heightDev = size
|
||||||
|
|
||||||
return image.resize((widthDev, heightDev), Image.ANTIALIAS)
|
return image.resize((widthDev, heightDev), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def resizeImage(image, size):
|
def resizeImage(image, size):
|
||||||
widthDev, heightDev = size
|
widthDev, heightDev = size
|
||||||
widthImg, heightImg = image.size
|
widthImg, heightImg = image.size
|
||||||
@ -136,19 +160,21 @@ def resizeImage(image, size):
|
|||||||
return image.resize((widthImg, heightImg), Image.ANTIALIAS)
|
return image.resize((widthImg, heightImg), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def formatImage(image):
|
def formatImage(image):
|
||||||
if image.mode == 'RGB':
|
if image.mode == 'RGB':
|
||||||
return image
|
return image
|
||||||
|
|
||||||
return image.convert('RGB')
|
return image.convert('RGB')
|
||||||
|
|
||||||
|
|
||||||
|
@protect_bad_image
|
||||||
def orientImage(image, size):
|
def orientImage(image, size):
|
||||||
widthDev, heightDev = size
|
widthDev, heightDev = size
|
||||||
widthImg, heightImg = image.size
|
widthImg, heightImg = image.size
|
||||||
|
|
||||||
if (widthImg > heightImg) != (widthDev > heightDev):
|
if (widthImg > heightImg) != (widthDev > heightDev):
|
||||||
return image.rotate(90, Image.BICUBIC, True)
|
return image.rotate(90, Image.BICUBIC, True)
|
||||||
|
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
|
||||||
@ -204,10 +230,20 @@ def convertImage(source, target, device, flags):
|
|||||||
# Format according to palette
|
# Format according to palette
|
||||||
image = formatImage(image)
|
image = formatImage(image)
|
||||||
# Apply flag transforms
|
# Apply flag transforms
|
||||||
|
|
||||||
|
# Second pass of first split
|
||||||
if flags & ImageFlags.SplitRight:
|
if flags & ImageFlags.SplitRight:
|
||||||
image = splitRight(image)
|
image = splitRight(image)
|
||||||
if flags & ImageFlags.Split:
|
# First pass of first split option
|
||||||
|
if (flags & ImageFlags.Split):
|
||||||
image = splitLeft(image)
|
image = splitLeft(image)
|
||||||
|
# First pass of second splitting option
|
||||||
|
if flags & ImageFlags.SplitLeft:
|
||||||
|
image = splitLeft(image)
|
||||||
|
# second pass of second splitting option
|
||||||
|
if (flags & ImageFlags.SplitInverse):
|
||||||
|
image = splitRight(image)
|
||||||
|
|
||||||
if flags & ImageFlags.Orient:
|
if flags & ImageFlags.Orient:
|
||||||
image = orientImage(image, size)
|
image = orientImage(image, size)
|
||||||
if flags & ImageFlags.Resize:
|
if flags & ImageFlags.Resize:
|
||||||
|
BIN
mangle/img/book.png
Normal file
BIN
mangle/img/book.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 398 B |
Binary file not shown.
Before Width: | Height: | Size: 827 B After Width: | Height: | Size: 545 B |
@ -66,6 +66,8 @@ class DialogOptions(QtGui.QDialog):
|
|||||||
imageFlags |= ImageFlags.Frame
|
imageFlags |= ImageFlags.Frame
|
||||||
if self.checkboxSplit.isChecked():
|
if self.checkboxSplit.isChecked():
|
||||||
imageFlags |= ImageFlags.Split
|
imageFlags |= ImageFlags.Split
|
||||||
|
if self.checkboxSplitInverse.isChecked():
|
||||||
|
imageFlags |= ImageFlags.SplitInverse
|
||||||
|
|
||||||
modified = (
|
modified = (
|
||||||
self.book.title != title or
|
self.book.title != title or
|
||||||
|
@ -180,6 +180,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="actionBookOptions">
|
<action name="actionBookOptions">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>../img/book.png</normaloff>../img/book.png</iconset>
|
||||||
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Options...</string>
|
<string>&Options...</string>
|
||||||
</property>
|
</property>
|
||||||
|
@ -101,6 +101,11 @@
|
|||||||
<string>Kindle Paperwhite</string>
|
<string>Kindle Paperwhite</string>
|
||||||
</property>
|
</property>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>KoBo Aura H2o</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
@ -170,6 +175,13 @@
|
|||||||
<string>Split images into two pages (right, left)</string>
|
<string>Split images into two pages (right, left)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="checkboxSplitInverse">
|
||||||
|
<property name="text">
|
||||||
|
<string>Split images into two pages (left, right)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
|
1
setup.py
1
setup.py
@ -32,6 +32,7 @@ setup(
|
|||||||
('mangle/img', ['mangle/img/add_directory.png',
|
('mangle/img', ['mangle/img/add_directory.png',
|
||||||
'mangle/img/add_file.png',
|
'mangle/img/add_file.png',
|
||||||
'mangle/img/banner_about.png',
|
'mangle/img/banner_about.png',
|
||||||
|
'mangle/img/book.png',
|
||||||
'mangle/img/export_book.png',
|
'mangle/img/export_book.png',
|
||||||
'mangle/img/file_new.png',
|
'mangle/img/file_new.png',
|
||||||
'mangle/img/file_open.png',
|
'mangle/img/file_open.png',
|
||||||
|
Loading…
Reference in New Issue
Block a user