Initial commit of working code
This commit is contained in:
20
.classpath
Normal file
20
.classpath
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-13">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/classes" path="src">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
bin/
|
||||
target/
|
||||
.DS_Store
|
||||
23
.project
Normal file
23
.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>ImageWitch</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
15
.settings/org.eclipse.jdt.core.prefs
Normal file
15
.settings/org.eclipse.jdt.core.prefs
Normal file
@@ -0,0 +1,15 @@
|
||||
eclipse.preferences.version=1
|
||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=13
|
||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||
org.eclipse.jdt.core.compiler.compliance=13
|
||||
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
|
||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
|
||||
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
|
||||
org.eclipse.jdt.core.compiler.release=enabled
|
||||
org.eclipse.jdt.core.compiler.source=13
|
||||
4
.settings/org.eclipse.m2e.core.prefs
Normal file
4
.settings/org.eclipse.m2e.core.prefs
Normal file
@@ -0,0 +1,4 @@
|
||||
activeProfiles=
|
||||
eclipse.preferences.version=1
|
||||
resolveWorkspaceProjects=true
|
||||
version=1
|
||||
30
pom.xml
Normal file
30
pom.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>ImageWitch</groupId>
|
||||
<artifactId>ImageWitch</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<release>13</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.drewnoakes</groupId>
|
||||
<artifactId>metadata-extractor</artifactId>
|
||||
<version>2.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.imgscalr</groupId>
|
||||
<artifactId>imgscalr-lib</artifactId>
|
||||
<version>4.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
486
src/ImageWitchGui.java
Normal file
486
src/ImageWitchGui.java
Normal file
@@ -0,0 +1,486 @@
|
||||
import java.awt.Dimension;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.GridBagLayout;
|
||||
import java.awt.Image;
|
||||
import java.awt.Insets;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.image.AffineTransformOp;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ImageObserver;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.InputVerifier;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JFileChooser;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JProgressBar;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||
|
||||
import org.imgscalr.Scalr.Rotation;
|
||||
|
||||
import com.drew.imaging.ImageMetadataReader;
|
||||
import com.drew.metadata.Metadata;
|
||||
import com.drew.metadata.MetadataException;
|
||||
import com.drew.metadata.exif.ExifIFD0Directory;
|
||||
|
||||
public class ImageWitchGui extends JFrame {
|
||||
private static final long serialVersionUID = 1L;
|
||||
public static final String[] supportedExtensions =
|
||||
{"jpg","jpeg","png","gif"};
|
||||
|
||||
File[] selectedFiles = null;
|
||||
|
||||
JTextField heightField;
|
||||
JTextField widthField;
|
||||
|
||||
Integer maxWidth = 0;
|
||||
Integer maxHeight = 0;
|
||||
Boolean errorNotified = false;
|
||||
|
||||
JButton loadButton;
|
||||
JButton resizeButton;
|
||||
JProgressBar progressBar;
|
||||
volatile boolean conversionRunning;
|
||||
|
||||
public static void main(String[] args) {
|
||||
//Schedule a job for the event-dispatching thread:
|
||||
//creating and showing this application's GUI.
|
||||
javax.swing.SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
ImageWitchGui gui = new ImageWitchGui();
|
||||
gui.setDefaultCloseOperation(EXIT_ON_CLOSE);
|
||||
gui.createAndShowGui();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ImageWitchGui() {
|
||||
setTitle("ImageWitch");
|
||||
try {
|
||||
UIManager.setLookAndFeel(
|
||||
UIManager.getCrossPlatformLookAndFeelClassName());
|
||||
} catch (Exception e) {}
|
||||
|
||||
InputVerifier verifier = new NumberInputVerifier();
|
||||
|
||||
heightField = new JTextField("0");
|
||||
heightField.setColumns(10);
|
||||
heightField.setInputVerifier(verifier);
|
||||
heightField.getDocument().addDocumentListener(
|
||||
new FieldListener(heightField));
|
||||
|
||||
widthField = new JTextField("0");
|
||||
widthField.setColumns(10);
|
||||
widthField.setInputVerifier(verifier);
|
||||
widthField.getDocument().addDocumentListener(
|
||||
new FieldListener(widthField));
|
||||
|
||||
loadButton = new JButton("Load Files");
|
||||
loadButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
|
||||
chooser.setMultiSelectionEnabled(true);
|
||||
chooser.setFileFilter(
|
||||
new FileNameExtensionFilter("Image Files",
|
||||
supportedExtensions));
|
||||
int returnVal = chooser.showOpenDialog(ImageWitchGui.this);
|
||||
if (returnVal == JFileChooser.APPROVE_OPTION) {
|
||||
selectedFiles = chooser.getSelectedFiles();
|
||||
resizeButton.setEnabled(true);
|
||||
errorNotified = false;
|
||||
}
|
||||
|
||||
progressBar.setString("Ready");
|
||||
progressBar.setValue(0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
resizeButton = new JButton("Resize");
|
||||
resizeButton.setEnabled(false);
|
||||
resizeButton.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
if (conversionRunning) {
|
||||
conversionRunning = false;
|
||||
loadButton.setEnabled(true);
|
||||
resizeButton.setText("Resize");
|
||||
} else {
|
||||
errorNotified = false;
|
||||
loadButton.setEnabled(false);
|
||||
conversionRunning = true;
|
||||
resizeButton.setText("Cancel");
|
||||
Thread conversionThread = new Thread() {
|
||||
public void run() {
|
||||
convertFiles(selectedFiles);
|
||||
}
|
||||
};
|
||||
conversionThread.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
progressBar = new JProgressBar();
|
||||
progressBar.setIndeterminate(false);
|
||||
progressBar.setStringPainted(true);
|
||||
progressBar.setString("Ready");
|
||||
progressBar.setValue(0);
|
||||
progressBar.setMaximum(1);
|
||||
}
|
||||
|
||||
public void createAndShowGui() {
|
||||
JLabel label;
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
getContentPane().add(panel);
|
||||
GridBagConstraints c = new GridBagConstraints();
|
||||
c.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
label = new JLabel("Max Width: ");
|
||||
c.gridx = 0;
|
||||
c.gridy = 0;
|
||||
c.insets = new Insets(2, 2, 2, 2);
|
||||
c.gridheight = 1;
|
||||
c.gridwidth = 1;
|
||||
c.weightx = 1.0;
|
||||
c.weighty = 1.0;
|
||||
panel.add(label, c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 0;
|
||||
panel.add(widthField, c);
|
||||
|
||||
label = new JLabel("Max Height: ");
|
||||
c.gridx = 0;
|
||||
c.gridy = 1;
|
||||
panel.add(label, c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 1;
|
||||
panel.add(heightField, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 2;
|
||||
panel.add(loadButton, c);
|
||||
|
||||
c.gridx = 1;
|
||||
c.gridy = 2;
|
||||
panel.add(resizeButton, c);
|
||||
|
||||
c.gridx = 0;
|
||||
c.gridy = 3;
|
||||
c.gridwidth = 2;
|
||||
c.insets = new Insets(0, 0, 0, 0);
|
||||
c.fill = GridBagConstraints.BOTH;
|
||||
panel.add(progressBar, c);
|
||||
|
||||
this.pack();
|
||||
this.setVisible(true);
|
||||
}
|
||||
|
||||
private String getExtension(String filename) {
|
||||
String extension;
|
||||
|
||||
extension = filename.substring(filename.lastIndexOf(".")+1).toLowerCase();
|
||||
if (extension.equals("jpeg")) {
|
||||
extension = "jpg";
|
||||
}
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
private String getNewFilename(String filename) {
|
||||
String newFilename;
|
||||
|
||||
int dotIdx = filename.lastIndexOf(".");
|
||||
newFilename = filename.substring(0, dotIdx) +
|
||||
"_scaled." + getExtension(filename);
|
||||
|
||||
return newFilename;
|
||||
}
|
||||
|
||||
private Dimension getNewDimensions(int maxWidth, int maxHeight,
|
||||
BufferedImage image) {
|
||||
Dimension dim = new Dimension();
|
||||
double sourceRatio, targetRatio;
|
||||
int targetWidth, targetHeight;
|
||||
|
||||
sourceRatio = (double) image.getWidth() / (double) image.getHeight();
|
||||
targetRatio = (double) maxWidth / (double) maxHeight;
|
||||
|
||||
if (maxWidth < 0 && maxHeight < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"At least one of width and height must be selected");
|
||||
} else if (maxWidth <= 0 ||
|
||||
(maxHeight > 0 && targetRatio > sourceRatio)) {
|
||||
targetHeight = maxHeight;
|
||||
targetWidth = (int) (targetHeight * sourceRatio);
|
||||
|
||||
} else if (maxHeight <= 0 ||
|
||||
(maxWidth > 0 && targetRatio < sourceRatio)) {
|
||||
targetWidth = maxWidth;
|
||||
targetHeight = (int) (targetWidth / sourceRatio);
|
||||
} else {
|
||||
targetWidth = maxWidth;
|
||||
targetHeight = maxHeight;
|
||||
}
|
||||
|
||||
dim.height = targetHeight;
|
||||
dim.width = targetWidth;
|
||||
|
||||
return dim;
|
||||
}
|
||||
|
||||
private BufferedImage rotateUpright(BufferedImage image, File file) {
|
||||
Metadata metadata;
|
||||
ExifIFD0Directory exifIFD0;
|
||||
int orientation;
|
||||
AffineTransform transform;
|
||||
BufferedImage uprightImage;
|
||||
AffineTransformOp transformOp;
|
||||
|
||||
try {
|
||||
metadata = ImageMetadataReader.readMetadata(file);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return image;
|
||||
}
|
||||
|
||||
exifIFD0 = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
|
||||
|
||||
try {
|
||||
orientation = exifIFD0.getInt(ExifIFD0Directory.TAG_ORIENTATION);
|
||||
} catch (Exception e) {
|
||||
return image;
|
||||
}
|
||||
|
||||
transform = new AffineTransform();
|
||||
|
||||
switch (orientation) {
|
||||
case 6: // [Exif IFD0] Orientation - Right side, top (Rotate 90 CW)
|
||||
uprightImage = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
|
||||
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
||||
transform.rotate(Math.PI/2);
|
||||
transform.translate(-image.getWidth() / 2, -image.getHeight() / 2);
|
||||
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
||||
transformOp.filter(image, uprightImage);
|
||||
break;
|
||||
case 3: // [Exif IFD0] Orientation - Bottom, right side (Rotate 180)
|
||||
uprightImage = new BufferedImage(image.getWidth(), image.getHeight(), image.getType());
|
||||
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
||||
transform.rotate(Math.PI);
|
||||
transform.translate(-image.getHeight() / 2, -image.getWidth() / 2);
|
||||
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
||||
transformOp.filter(image, uprightImage);
|
||||
break;
|
||||
case 8: // [Exif IFD0] Orientation - Left side, bottom (Rotate 270 CW)
|
||||
uprightImage = new BufferedImage(image.getHeight(), image.getWidth(), image.getType());
|
||||
transform.translate(image.getHeight() / 2, image.getWidth() / 2);
|
||||
transform.rotate(-Math.PI/2);
|
||||
transform.translate(-image.getWidth() / 2, -image.getHeight() / 2);
|
||||
transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
|
||||
transformOp.filter(image, uprightImage);
|
||||
break;
|
||||
default:
|
||||
uprightImage = image;
|
||||
}
|
||||
|
||||
return uprightImage;
|
||||
}
|
||||
|
||||
private void updateProgress(int progress, int maxValue, String text) {
|
||||
javax.swing.SwingUtilities.invokeLater(new Runnable() {
|
||||
public void run() {
|
||||
System.out.println(text);
|
||||
if (maxValue != progressBar.getMaximum()) {
|
||||
progressBar.setMaximum(maxValue);
|
||||
}
|
||||
progressBar.setValue(progress);
|
||||
progressBar.setString(text);
|
||||
|
||||
if (progress == maxValue) {
|
||||
loadButton.setEnabled(true);
|
||||
resizeButton.setText("Resize");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void convertFiles(File[] files) {
|
||||
BufferedImage oldImage;
|
||||
String newFilename;
|
||||
int index = 0;
|
||||
int numFiles = files.length;
|
||||
|
||||
progressBar.setMaximum(numFiles);
|
||||
for (File oldFile : files) {
|
||||
if (!conversionRunning) {
|
||||
updateProgress(0, 1, "Cancelled");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
oldImage = ImageIO.read(oldFile);
|
||||
newFilename = getNewFilename(oldFile.getAbsolutePath());
|
||||
oldImage = rotateUpright(oldImage, oldFile);
|
||||
resizeAndSaveImage(oldImage, maxWidth, maxHeight, newFilename);
|
||||
|
||||
progressBar.setValue(index);
|
||||
index++;
|
||||
updateProgress(index, numFiles,
|
||||
"Processing " + Integer.toString(index) +
|
||||
" of " + Integer.toString(numFiles));
|
||||
} catch (IOException e) {
|
||||
if (!errorNotified) {
|
||||
errorNotified = true;
|
||||
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
||||
"<html>Unable to read file: <br>" +
|
||||
oldFile.getAbsolutePath() +
|
||||
"<br><br>Other files may be affected.",
|
||||
"Read Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateProgress(numFiles, numFiles, "Done");
|
||||
}
|
||||
|
||||
private void resizeAndSaveImage(BufferedImage image,
|
||||
int maxWidth, int maxHeight,
|
||||
String newFilename)
|
||||
throws IllegalArgumentException
|
||||
{
|
||||
Image newImage;
|
||||
BufferedImage renderedImage;
|
||||
ImageSaver saver;
|
||||
Dimension newDim;
|
||||
|
||||
newDim = getNewDimensions(maxWidth, maxHeight, image);
|
||||
newImage = image.getScaledInstance(
|
||||
newDim.width, newDim.height, Image.SCALE_SMOOTH);
|
||||
renderedImage = new BufferedImage(
|
||||
newDim.width, newDim.height, BufferedImage.TYPE_INT_RGB);
|
||||
saver = new ImageSaver(newFilename, renderedImage);
|
||||
renderedImage.getGraphics().drawImage(newImage, 0, 0, saver);
|
||||
saver.saveImage();
|
||||
}
|
||||
|
||||
private class NumberInputVerifier extends InputVerifier {
|
||||
|
||||
@Override
|
||||
public boolean verify(JComponent input) {
|
||||
String text = ((JTextField) input).getText();
|
||||
try {
|
||||
Integer.parseInt(text);
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ImageSaver implements ImageObserver {
|
||||
|
||||
private String filename;
|
||||
private BufferedImage image;
|
||||
private String formatName;
|
||||
|
||||
public ImageSaver(String filename, BufferedImage image) {
|
||||
this.filename = filename;
|
||||
this.image = image;
|
||||
this.formatName = getExtension(filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean imageUpdate(Image img, int infoflags, int x, int y,
|
||||
int width, int height) {
|
||||
if (infoflags == ImageObserver.ERROR) {
|
||||
if (!errorNotified) {
|
||||
errorNotified = true;
|
||||
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
||||
"<html>Unable to resize file: <br>" + filename +
|
||||
"<br><br>Other files may be affected.",
|
||||
"Resize Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
saveImage();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void saveImage() {
|
||||
File file = new File(filename);
|
||||
try {
|
||||
ImageIO.write(image, formatName, file);
|
||||
} catch (IOException e) {
|
||||
if (!errorNotified) {
|
||||
errorNotified = true;
|
||||
JOptionPane.showMessageDialog(ImageWitchGui.this,
|
||||
"<html>Unable to save file: <br>" + filename +
|
||||
"<br><br>Other files may be affected.",
|
||||
"Save Error",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class FieldListener implements DocumentListener {
|
||||
|
||||
private JTextField field;
|
||||
|
||||
public FieldListener(JTextField field) {
|
||||
this.field = field;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
Integer value = 0;
|
||||
|
||||
if (!field.getText().isBlank()) {
|
||||
value = Integer.parseInt(field.getText());
|
||||
}
|
||||
if (field == heightField) {
|
||||
maxHeight = value;
|
||||
} else {
|
||||
maxWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
update();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
update();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user