mirror of
https://github.com/bancika/diy-layout-creator
synced 2026-11-10 17:22:09 +10:00
Ability to tag components and building blocks as favorites
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
DIY Layout Creator (DIYLC).
|
||||||
|
Copyright (c) 2009-2018 held jointly by the individual authors.
|
||||||
|
|
||||||
|
This file is part of DIYLC.
|
||||||
|
|
||||||
|
DIYLC 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, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
DIYLC 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 DIYLC. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
*/
|
||||||
|
package org.diylc.common;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class Favorite implements Serializable, Comparable<Favorite> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private FavoriteType type;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public Favorite(FavoriteType type, String name) {
|
||||||
|
super();
|
||||||
|
this.type = type;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FavoriteType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((name == null) ? 0 : name.hashCode());
|
||||||
|
result = prime * result + ((type == null) ? 0 : type.hashCode());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (obj == null)
|
||||||
|
return false;
|
||||||
|
if (getClass() != obj.getClass())
|
||||||
|
return false;
|
||||||
|
Favorite other = (Favorite) obj;
|
||||||
|
if (name == null) {
|
||||||
|
if (other.name != null)
|
||||||
|
return false;
|
||||||
|
} else if (!name.equals(other.name))
|
||||||
|
return false;
|
||||||
|
if (type != other.type)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type + ":" + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FavoriteType {
|
||||||
|
Component, Block
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Favorite o) {
|
||||||
|
return name.compareToIgnoreCase(o.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,7 +74,8 @@ public interface IPlugInPort extends ISelectionProcessor, IMouseProcessor, IKeyP
|
|||||||
public static final String SHOW_GRID_KEY = "showGrid";
|
public static final String SHOW_GRID_KEY = "showGrid";
|
||||||
public static final String HIGHLIGHT_CONTINUITY_AREA = "highlightContinuityArea";
|
public static final String HIGHLIGHT_CONTINUITY_AREA = "highlightContinuityArea";
|
||||||
public static final String HARDWARE_ACCELERATION = "hardwareAcceleration";
|
public static final String HARDWARE_ACCELERATION = "hardwareAcceleration";
|
||||||
public static final String EXTRA_SPACE_KEY = "extraSpace";
|
public static final String EXTRA_SPACE_KEY = "extraSpace";
|
||||||
|
public static final String FAVORITES_KEY = "favorites";
|
||||||
|
|
||||||
public static final int DND_TOGGLE_STICKY = 0x1;
|
public static final int DND_TOGGLE_STICKY = 0x1;
|
||||||
public static final int DND_TOGGLE_SNAP = 0x40000000;
|
public static final int DND_TOGGLE_SNAP = 0x40000000;
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ public enum IconLoader {
|
|||||||
"component_add.png"), History("history.png"), DocumentPlain("document_plain.png"), FitToSize("fit_to_size.png"), DocumentsGear(
|
"component_add.png"), History("history.png"), DocumentPlain("document_plain.png"), FitToSize("fit_to_size.png"), DocumentsGear(
|
||||||
"documents_gear.png"), SplashResistor("splash_resistor.png"), SplashCeramic("splash_ceramic.png"), SplashElectrolytic(
|
"documents_gear.png"), SplashResistor("splash_resistor.png"), SplashCeramic("splash_ceramic.png"), SplashElectrolytic(
|
||||||
"splash_electrolytic.png"), SplashFilm("splash_film.png"), Splash("splash.png"), Help("help2.png"), ScrollInformation("scroll_information.png"),
|
"splash_electrolytic.png"), SplashFilm("splash_film.png"), Splash("splash.png"), Help("help2.png"), ScrollInformation("scroll_information.png"),
|
||||||
Node("node.png"), Web("web.png"), Guitar("guitar.png"), Scientist("scientist.png");
|
Node("node.png"), Web("web.png"), Guitar("guitar.png"), Scientist("scientist.png"), StarGrey("star_grey.png"), StarBlue("star_blue.png");
|
||||||
|
|
||||||
protected String name;
|
protected String name;
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 877 B |
@@ -36,7 +36,6 @@ import java.awt.event.MouseAdapter;
|
|||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.event.MouseListener;
|
import java.awt.event.MouseListener;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -63,6 +62,7 @@ import javax.swing.event.DocumentEvent;
|
|||||||
import javax.swing.event.DocumentListener;
|
import javax.swing.event.DocumentListener;
|
||||||
import javax.swing.event.PopupMenuEvent;
|
import javax.swing.event.PopupMenuEvent;
|
||||||
import javax.swing.event.PopupMenuListener;
|
import javax.swing.event.PopupMenuListener;
|
||||||
|
import javax.swing.event.TreeModelEvent;
|
||||||
import javax.swing.event.TreeSelectionEvent;
|
import javax.swing.event.TreeSelectionEvent;
|
||||||
import javax.swing.event.TreeSelectionListener;
|
import javax.swing.event.TreeSelectionListener;
|
||||||
import javax.swing.tree.DefaultMutableTreeNode;
|
import javax.swing.tree.DefaultMutableTreeNode;
|
||||||
@@ -72,9 +72,12 @@ import javax.swing.tree.TreePath;
|
|||||||
import javax.swing.tree.TreeSelectionModel;
|
import javax.swing.tree.TreeSelectionModel;
|
||||||
|
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
|
import org.apache.log4j.lf5.viewer.categoryexplorer.TreeModelAdapter;
|
||||||
import org.diylc.appframework.miscutils.ConfigurationManager;
|
import org.diylc.appframework.miscutils.ConfigurationManager;
|
||||||
import org.diylc.appframework.miscutils.IConfigListener;
|
import org.diylc.appframework.miscutils.IConfigListener;
|
||||||
import org.diylc.common.ComponentType;
|
import org.diylc.common.ComponentType;
|
||||||
|
import org.diylc.common.Favorite;
|
||||||
|
import org.diylc.common.Favorite.FavoriteType;
|
||||||
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
import org.diylc.common.IBlockProcessor.InvalidBlockException;
|
||||||
import org.diylc.common.IPlugInPort;
|
import org.diylc.common.IPlugInPort;
|
||||||
import org.diylc.core.IDIYComponent;
|
import org.diylc.core.IDIYComponent;
|
||||||
@@ -99,11 +102,14 @@ public class TreePanel extends JPanel {
|
|||||||
private JTextField searchField;
|
private JTextField searchField;
|
||||||
private DefaultMutableTreeNode recentNode;
|
private DefaultMutableTreeNode recentNode;
|
||||||
private DefaultMutableTreeNode blocksNode;
|
private DefaultMutableTreeNode blocksNode;
|
||||||
|
private DefaultMutableTreeNode favoritesNode;
|
||||||
private List<String> recentComponents;
|
private List<String> recentComponents;
|
||||||
|
private List<Favorite> favorites;
|
||||||
private List<String> blocks;
|
private List<String> blocks;
|
||||||
|
|
||||||
private IPlugInPort plugInPort;
|
private IPlugInPort plugInPort;
|
||||||
private ISwingUI swingUI;
|
private ISwingUI swingUI;
|
||||||
|
private boolean initializing = false;
|
||||||
|
|
||||||
private JPopupMenu popup;
|
private JPopupMenu popup;
|
||||||
|
|
||||||
@@ -142,7 +148,7 @@ public class TreePanel extends JPanel {
|
|||||||
LOG.info("Detected recent component change");
|
LOG.info("Detected recent component change");
|
||||||
refreshRecentComponents(newComponents);
|
refreshRecentComponents(newComponents);
|
||||||
} else
|
} else
|
||||||
LOG.info("Detected no recent component change");
|
LOG.info("Detected no recent component change");
|
||||||
TreePanel.this.recentComponents = new ArrayList<String>(newComponents);
|
TreePanel.this.recentComponents = new ArrayList<String>(newComponents);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -174,6 +180,20 @@ public class TreePanel extends JPanel {
|
|||||||
repaint();
|
repaint();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ConfigurationManager.getInstance().addConfigListener(IPlugInPort.FAVORITES_KEY, new IConfigListener() {
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public void valueChanged(String key, Object value) {
|
||||||
|
List<Favorite> newFavorites = (List<Favorite>) value;
|
||||||
|
if (newFavorites != null && !newFavorites.equals(TreePanel.this.favorites)) {
|
||||||
|
LOG.info("Detected favorites change");
|
||||||
|
refreshFavorites(newFavorites);
|
||||||
|
} else
|
||||||
|
LOG.info("Detected no favorites change");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
getTree().expandRow(0);
|
getTree().expandRow(0);
|
||||||
|
|
||||||
@@ -199,6 +219,7 @@ public class TreePanel extends JPanel {
|
|||||||
public DefaultMutableTreeNode getRecentNode() {
|
public DefaultMutableTreeNode getRecentNode() {
|
||||||
if (this.recentNode == null) {
|
if (this.recentNode == null) {
|
||||||
this.recentNode = new DefaultMutableTreeNode(new Payload("(Recently Used)", null), true);
|
this.recentNode = new DefaultMutableTreeNode(new Payload("(Recently Used)", null), true);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
List<String> recent =
|
List<String> recent =
|
||||||
(List<String>) ConfigurationManager.getInstance().readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
(List<String>) ConfigurationManager.getInstance().readObject(IPlugInPort.RECENT_COMPONENTS_KEY, null);
|
||||||
@@ -215,6 +236,7 @@ public class TreePanel extends JPanel {
|
|||||||
public DefaultMutableTreeNode getBlocksNode() {
|
public DefaultMutableTreeNode getBlocksNode() {
|
||||||
if (this.blocksNode == null) {
|
if (this.blocksNode == null) {
|
||||||
this.blocksNode = new DefaultMutableTreeNode(new Payload("(Building Blocks)", null), true);
|
this.blocksNode = new DefaultMutableTreeNode(new Payload("(Building Blocks)", null), true);
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, List<IDIYComponent<?>>> newBlocks =
|
Map<String, List<IDIYComponent<?>>> newBlocks =
|
||||||
(Map<String, List<IDIYComponent<?>>>) ConfigurationManager.getInstance().readObject(
|
(Map<String, List<IDIYComponent<?>>>) ConfigurationManager.getInstance().readObject(
|
||||||
@@ -223,13 +245,27 @@ public class TreePanel extends JPanel {
|
|||||||
List<String> blockNames = new ArrayList<String>(newBlocks.keySet());
|
List<String> blockNames = new ArrayList<String>(newBlocks.keySet());
|
||||||
Collections.sort(blockNames);
|
Collections.sort(blockNames);
|
||||||
refreshBuildingBlocks(blockNames);
|
refreshBuildingBlocks(blockNames);
|
||||||
this.blocks = blockNames;
|
|
||||||
} else {
|
} else {
|
||||||
this.blocks = new ArrayList<String>();
|
this.blocks = new ArrayList<String>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.blocksNode;
|
return this.blocksNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DefaultMutableTreeNode getFavoritesNode() {
|
||||||
|
if (this.favoritesNode == null) {
|
||||||
|
this.favoritesNode = new DefaultMutableTreeNode(new Payload("(Favorites)", null), true);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Favorite> favorites = (List<Favorite>) ConfigurationManager.getInstance().readObject(IPlugInPort.FAVORITES_KEY, null);
|
||||||
|
if (favorites != null) {
|
||||||
|
refreshFavorites(favorites);
|
||||||
|
} else {
|
||||||
|
this.favorites = new ArrayList<Favorite>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.favoritesNode;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private void refreshRecentComponents(List<String> recentComponentClassList) {
|
private void refreshRecentComponents(List<String> recentComponentClassList) {
|
||||||
@@ -260,7 +296,8 @@ public class TreePanel extends JPanel {
|
|||||||
LOG.error("Could not create recent component button for " + componentClassName, e);
|
LOG.error("Could not create recent component button for " + componentClassName, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getTreeModel().nodeStructureChanged(getRecentNode());
|
if (!initializing)
|
||||||
|
getTreeModel().nodeStructureChanged(getRecentNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshBuildingBlocks(List<String> blocks) {
|
private void refreshBuildingBlocks(List<String> blocks) {
|
||||||
@@ -291,14 +328,72 @@ public class TreePanel extends JPanel {
|
|||||||
payload.setVisible(visible);
|
payload.setVisible(visible);
|
||||||
getBlocksNode().add(componentNode);
|
getBlocksNode().add(componentNode);
|
||||||
}
|
}
|
||||||
getTreeModel().nodeStructureChanged(getBlocksNode());
|
if (!initializing)
|
||||||
|
getTreeModel().nodeStructureChanged(getBlocksNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshFavorites(List<Favorite> favorites) {
|
||||||
|
this.favorites = new ArrayList<Favorite>(favorites);
|
||||||
|
|
||||||
|
getFavoritesNode().removeAllChildren();
|
||||||
|
|
||||||
|
Map<String, List<ComponentType>> types = plugInPort.getComponentTypes();
|
||||||
|
Map<String, ComponentType> typesByClass = new HashMap<String, ComponentType>();
|
||||||
|
for (Map.Entry<String, List<ComponentType>> e : types.entrySet())
|
||||||
|
for (ComponentType c : e.getValue())
|
||||||
|
typesByClass.put(c.getInstanceClass().getCanonicalName(), c);
|
||||||
|
|
||||||
|
for (Favorite f : favorites) {
|
||||||
|
if (f.getType() == FavoriteType.Component) {
|
||||||
|
final ComponentType type = typesByClass.get(f.getName());
|
||||||
|
if (type != null) {
|
||||||
|
DefaultMutableTreeNode componentNode = new DefaultMutableTreeNode(new Payload(type, new MouseAdapter() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
plugInPort.setNewComponentTypeSlot(type, null, false);
|
||||||
|
}
|
||||||
|
}), false);
|
||||||
|
getFavoritesNode().add(componentNode);
|
||||||
|
}
|
||||||
|
} else if (f.getType() == FavoriteType.Block) {
|
||||||
|
final String block = f.getName();
|
||||||
|
Payload payload = new Payload(block, new MouseAdapter() {
|
||||||
|
|
||||||
|
long previousActionTime = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e == null || SwingUtilities.isLeftMouseButton(e) && System.currentTimeMillis() - previousActionTime > 100) {
|
||||||
|
previousActionTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
plugInPort.loadBlock(block);
|
||||||
|
} catch (InvalidBlockException e1) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e1.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
final DefaultMutableTreeNode componentNode = new DefaultMutableTreeNode(payload, false);
|
||||||
|
String text = getSearchField().getText();
|
||||||
|
boolean visible = text.trim().length() == 0 || block.toLowerCase().contains(text.toLowerCase());
|
||||||
|
payload.setVisible(visible);
|
||||||
|
getFavoritesNode().add(componentNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!initializing)
|
||||||
|
getTreeModel().nodeStructureChanged(getFavoritesNode());
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultTreeModel getTreeModel() {
|
public DefaultTreeModel getTreeModel() {
|
||||||
if (treeModel == null) {
|
if (treeModel == null) {
|
||||||
final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Components", true);
|
final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode("Components", true);
|
||||||
|
initializing = true;
|
||||||
|
rootNode.add(getFavoritesNode());
|
||||||
rootNode.add(getRecentNode());
|
rootNode.add(getRecentNode());
|
||||||
rootNode.add(getBlocksNode());
|
rootNode.add(getBlocksNode());
|
||||||
|
initializing = false;
|
||||||
Map<String, List<ComponentType>> componentTypes = plugInPort.getComponentTypes();
|
Map<String, List<ComponentType>> componentTypes = plugInPort.getComponentTypes();
|
||||||
List<String> categories = new ArrayList<String>(componentTypes.keySet());
|
List<String> categories = new ArrayList<String>(componentTypes.keySet());
|
||||||
Collections.sort(categories);
|
Collections.sort(categories);
|
||||||
@@ -317,6 +412,18 @@ public class TreePanel extends JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
treeModel = new DefaultTreeModel(rootNode);
|
treeModel = new DefaultTreeModel(rootNode);
|
||||||
|
treeModel.addTreeModelListener(new TreeModelAdapter() {
|
||||||
|
@Override
|
||||||
|
public void treeNodesRemoved(TreeModelEvent e) {
|
||||||
|
super.treeNodesRemoved(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void treeNodesChanged(TreeModelEvent e) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
super.treeNodesChanged(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return treeModel;
|
return treeModel;
|
||||||
}
|
}
|
||||||
@@ -459,6 +566,27 @@ public class TreePanel extends JPanel {
|
|||||||
});
|
});
|
||||||
shortcutSubmenu.add(item);
|
shortcutSubmenu.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Favorite fav = new Favorite(componentType == null ? FavoriteType.Block : FavoriteType.Component, componentType == null ? payload.toString() : componentType.getInstanceClass()
|
||||||
|
.getCanonicalName());
|
||||||
|
final boolean isFavorite = favorites != null && favorites.indexOf(fav) >= 0;
|
||||||
|
final JMenuItem favoritesItem = new JMenuItem(isFavorite ? "Remove From Favorites" : "Add To Favorites",
|
||||||
|
isFavorite ? IconLoader.StarBlue.getIcon() : IconLoader.StarGrey.getIcon());
|
||||||
|
favoritesItem.addActionListener(new ActionListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e) {
|
||||||
|
List<Favorite> favorites = new ArrayList<Favorite>(TreePanel.this.favorites);
|
||||||
|
if (isFavorite) {
|
||||||
|
favorites.remove(fav);
|
||||||
|
} else {
|
||||||
|
favorites.add(fav);
|
||||||
|
Collections.sort(favorites);
|
||||||
|
}
|
||||||
|
ConfigurationManager.getInstance().writeValue(IPlugInPort.FAVORITES_KEY, favorites);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
popup.add(favoritesItem);
|
||||||
|
|
||||||
if (componentType != null) {
|
if (componentType != null) {
|
||||||
popup.add(new SelectAllAction(plugInPort, componentType));
|
popup.add(new SelectAllAction(plugInPort, componentType));
|
||||||
|
|||||||
@@ -2577,7 +2577,11 @@
|
|||||||
</versionNumber>
|
</versionNumber>
|
||||||
<releaseDate>2019-03-02 00:00:00.000 CET</releaseDate>
|
<releaseDate>2019-03-02 00:00:00.000 CET</releaseDate>
|
||||||
<name></name>
|
<name></name>
|
||||||
<changes>
|
<changes>
|
||||||
|
<org.diylc.appframework.update.Change>
|
||||||
|
<changeType>NEW_FEATURE</changeType>
|
||||||
|
<description>Ability to tag components and building blocks as favorites</description>
|
||||||
|
</org.diylc.appframework.update.Change>
|
||||||
<org.diylc.appframework.update.Change>
|
<org.diylc.appframework.update.Change>
|
||||||
<changeType>BUG_FIX</changeType>
|
<changeType>BUG_FIX</changeType>
|
||||||
<description>"Highlight Connected Areas" doesn't follow multiple chained wires and jumpers</description>
|
<description>"Highlight Connected Areas" doesn't follow multiple chained wires and jumpers</description>
|
||||||
|
|||||||
Reference in New Issue
Block a user