Fixes: Performance fix in gamlist when reloading
* prg and crt vic-20 games loaded properly in Vice
This commit is contained in:
parent
f5f6430ec7
commit
d1341ee332
|
@ -14,7 +14,6 @@ import java.sql.ResultSet;
|
|||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
@ -109,10 +108,9 @@ public class GamebaseImporter
|
|||
{
|
||||
Statement statement = connection.createStatement();
|
||||
|
||||
String sql =
|
||||
"SELECT Games.Name, Musicians.Musician, Genres.Genre, Publishers.Publisher, Games.Filename, Games.ScrnshotFilename, Years.Year, Games.GA_Id, Games.Control, Games.V_PalNTSC, Games.V_TrueDriveEmu, Games.Gemus\r\n" +
|
||||
"FROM Years INNER JOIN (Publishers INNER JOIN ((Games INNER JOIN Musicians ON Games.MU_Id = Musicians.MU_Id) INNER JOIN Genres ON Games.GE_Id = Genres.GE_Id) ON Publishers.PU_Id = Games.PU_Id) ON Years.YE_Id = Games.YE_Id\r\n";
|
||||
|
||||
String sql = "SELECT Games.Name, Musicians.Musician, PGenres.ParentGenre, Publishers.Publisher, Games.Filename, Games.ScrnshotFilename, Years.Year, Games.GA_Id, Games.Control, Games.V_PalNTSC, Games.V_TrueDriveEmu, Games.Gemus\r\n" +
|
||||
"FROM PGenres INNER JOIN (Years INNER JOIN (Publishers INNER JOIN ((Games INNER JOIN Musicians ON Games.MU_Id = Musicians.MU_Id) INNER JOIN Genres ON Games.GE_Id = Genres.GE_Id) ON Publishers.PU_Id = Games.PU_Id) ON Years.YE_Id = Games.YE_Id) ON PGenres.PG_Id = Genres.PG_Id\r\n";
|
||||
|
||||
String condition = "";
|
||||
switch (selectedOption)
|
||||
{
|
||||
|
@ -139,14 +137,16 @@ public class GamebaseImporter
|
|||
String gamefile = result.getString("Filename");
|
||||
String screen1 = result.getString("ScrnShotFileName");
|
||||
String musician = result.getString("Musician");
|
||||
String genre = result.getString("Genre");
|
||||
String genre = result.getString("ParentGenre");
|
||||
String publisher = result.getString("Publisher");
|
||||
int control = result.getInt("Control");
|
||||
int palOrNtsc = result.getInt("V_PalNTSC");
|
||||
int trueDriveEmu = result.getInt("V_TrueDriveEmu");
|
||||
String gemus = result.getString("Gemus");
|
||||
|
||||
if (gamefile.isEmpty())
|
||||
//GB64 includes game files for all games, no extras available. GbVic20 can have empty
|
||||
//game files but available TAP or CART images
|
||||
if (isC64 && gamefile.isEmpty())
|
||||
{
|
||||
builder.append("Ignoring " + title + " (No game file available)\n");
|
||||
continue;
|
||||
|
@ -184,46 +184,29 @@ public class GamebaseImporter
|
|||
genre = GamebaseScraper.mapGenre(genre);
|
||||
|
||||
//Get cover
|
||||
String coverFile = "";
|
||||
String coverSql =
|
||||
"SELECT Extras.Name, Extras.Path\r\n" + "FROM Games INNER JOIN Extras ON Games.GA_Id = Extras.GA_Id\r\n" +
|
||||
"WHERE (((Games.GA_Id)=" + gameId + ") AND ((Extras.Name) Like \"Cover*\"));";
|
||||
|
||||
ResultSet sqlResult = statement.executeQuery(coverSql);
|
||||
if (sqlResult.next())
|
||||
{
|
||||
coverFile = sqlResult.getString("Path");
|
||||
}
|
||||
if (!coverFile.isEmpty())
|
||||
{
|
||||
coverFile = gbParentPath.toString() + "\\extras\\" + coverFile;
|
||||
}
|
||||
|
||||
String coverFile = getCoverPath(gameId, statement);
|
||||
|
||||
//Get cartridge if available, easyflash is preferred
|
||||
String cartridgeSql =
|
||||
"SELECT Extras.Name, Extras.Path\r\n" + "FROM Games INNER JOIN Extras ON Games.GA_Id = Extras.GA_Id\r\n" +
|
||||
"WHERE (((Games.GA_Id)=" + gameId + ") AND ((Extras.Name) Like \"*Cartridge*\"));";
|
||||
|
||||
sqlResult = statement.executeQuery(cartridgeSql);
|
||||
String cartridgePath = "";
|
||||
while (sqlResult.next())
|
||||
{
|
||||
if (cartridgePath.isEmpty())
|
||||
{
|
||||
cartridgePath = sqlResult.getString("Path");
|
||||
}
|
||||
//Pick easyflash if available
|
||||
String name = sqlResult.getString("Name");
|
||||
if (name.contains("EasyFlash"))
|
||||
{
|
||||
cartridgePath = sqlResult.getString("Path");
|
||||
}
|
||||
}
|
||||
|
||||
String cartridgePath = getCartridgePath(gameId, statement);
|
||||
if (!cartridgePath.isEmpty())
|
||||
{
|
||||
gamefile = gbParentPath.toString() + "\\extras\\" + cartridgePath;
|
||||
}
|
||||
//Check tap for VIC-20
|
||||
if (!isC64 && gamefile.isEmpty())
|
||||
{
|
||||
String tapFile = getTapPath(gameId, statement);
|
||||
if (!tapFile.isEmpty())
|
||||
{
|
||||
gamefile = gbParentPath.toString() + "\\extras\\" + tapFile;
|
||||
}
|
||||
|
||||
if (gamefile.isEmpty())
|
||||
{
|
||||
builder.append("Ignoring " + title + " (No game file available)\n");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
GbGameInfo info = new GbGameInfo(title,
|
||||
year,
|
||||
|
@ -256,6 +239,85 @@ public class GamebaseImporter
|
|||
}
|
||||
return builder;
|
||||
}
|
||||
|
||||
private String getCoverPath(int gameId, Statement statement) throws SQLException
|
||||
{
|
||||
String coverFile = "";
|
||||
String coverSql =
|
||||
"SELECT Extras.Name, Extras.Path, Extras.DisplayOrder\r\n" + "FROM Games INNER JOIN Extras ON Games.GA_Id = Extras.GA_Id\r\n" +
|
||||
"WHERE (((Games.GA_Id)=" + gameId + ") AND ((Extras.Name) Like \"Cover*\"));";
|
||||
|
||||
ResultSet sqlResult = statement.executeQuery(coverSql);
|
||||
int displayOrder = -1;
|
||||
//Get the one with the lowest display order (probably the best one)
|
||||
while (sqlResult.next())
|
||||
{
|
||||
int currentDisplayOrder = sqlResult.getInt("DisplayOrder");
|
||||
if (displayOrder == -1 || currentDisplayOrder < displayOrder)
|
||||
{
|
||||
displayOrder = currentDisplayOrder;
|
||||
coverFile = sqlResult.getString("Path");
|
||||
}
|
||||
}
|
||||
if (!coverFile.isEmpty())
|
||||
{
|
||||
coverFile = gbParentPath.toString() + "\\extras\\" + coverFile;
|
||||
}
|
||||
return coverFile;
|
||||
}
|
||||
|
||||
private String getCartridgePath(int gameId, Statement statement) throws SQLException
|
||||
{
|
||||
//Get cartridge if available, easyflash is preferred
|
||||
String cartridgeSql =
|
||||
"SELECT Extras.Name, Extras.Path\r\n" + "FROM Games INNER JOIN Extras ON Games.GA_Id = Extras.GA_Id\r\n" +
|
||||
"WHERE (((Games.GA_Id)=" + gameId + ") AND ((Extras.Name) Like ";
|
||||
|
||||
if (isC64)
|
||||
{
|
||||
cartridgeSql = cartridgeSql + "\"*Cartridge*\"));";
|
||||
}
|
||||
else
|
||||
{
|
||||
cartridgeSql = cartridgeSql + "\"*CART\"));";
|
||||
}
|
||||
|
||||
ResultSet sqlResult = statement.executeQuery(cartridgeSql);
|
||||
String cartridgePath = "";
|
||||
while (sqlResult.next())
|
||||
{
|
||||
if (cartridgePath.isEmpty())
|
||||
{
|
||||
cartridgePath = sqlResult.getString("Path");
|
||||
}
|
||||
//Pick easyflash if available
|
||||
String name = sqlResult.getString("Name");
|
||||
if (name.contains("EasyFlash"))
|
||||
{
|
||||
cartridgePath = sqlResult.getString("Path");
|
||||
}
|
||||
}
|
||||
return cartridgePath;
|
||||
}
|
||||
|
||||
private String getTapPath(int gameId, Statement statement) throws SQLException
|
||||
{
|
||||
//Get TAP file
|
||||
String tapSql =
|
||||
"SELECT Extras.Name, Extras.Path\r\n" + "FROM Games INNER JOIN Extras ON Games.GA_Id = Extras.GA_Id\r\n" +
|
||||
"WHERE (((Games.GA_Id)=" + gameId + ") AND ((Extras.Name) Like \"*TAP*\"));";
|
||||
|
||||
ResultSet sqlResult = statement.executeQuery(tapSql);
|
||||
String cartridgePath = "";
|
||||
if (sqlResult.next())
|
||||
{
|
||||
if (cartridgePath.isEmpty())
|
||||
{
|
||||
cartridgePath = sqlResult.getString("Path");
|
||||
}
|
||||
}
|
||||
return cartridgePath;
|
||||
}
|
||||
|
||||
public List<List<GbGameInfo>> getGbGameInfoChunks()
|
||||
{
|
||||
|
@ -281,7 +343,8 @@ public class GamebaseImporter
|
|||
gbGameInfo.getScreen2(),
|
||||
gbGameInfo.getJoy1config(),
|
||||
gbGameInfo.getJoy2config(),
|
||||
gbGameInfo.getAdvanced());
|
||||
gbGameInfo.getAdvanced(),
|
||||
isC64);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -348,5 +411,6 @@ public class GamebaseImporter
|
|||
public void clearAfterImport()
|
||||
{
|
||||
gbGameInfoList.clear();
|
||||
FileManager.deleteTempFolder();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@ import java.net.URI;
|
|||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuItem;
|
||||
|
@ -37,6 +41,7 @@ import se.lantz.manager.RestoreManager;
|
|||
import se.lantz.model.MainViewModel;
|
||||
import se.lantz.model.data.GameListData;
|
||||
import se.lantz.model.data.GameView;
|
||||
import se.lantz.util.ExceptionHandler;
|
||||
import se.lantz.util.FileManager;
|
||||
import se.lantz.util.VersionChecker;
|
||||
|
||||
|
@ -261,7 +266,15 @@ public class MenuManager
|
|||
}
|
||||
//Save properties before exit
|
||||
FileManager.storeProperties();
|
||||
FileManager.deleteTempFolder();
|
||||
Future<?> deleteTempFolder = FileManager.deleteTempFolder();
|
||||
try
|
||||
{
|
||||
deleteTempFolder.get(10, TimeUnit.SECONDS);
|
||||
}
|
||||
catch (Exception e1)
|
||||
{
|
||||
ExceptionHandler.logException(e1, "Could not delete temp folder");
|
||||
}
|
||||
System.exit(0);
|
||||
});
|
||||
return exitItem;
|
||||
|
|
|
@ -337,7 +337,8 @@ public class ImportManager
|
|||
String screen2file,
|
||||
String joy1config,
|
||||
String joy2config,
|
||||
String advanced)
|
||||
String advanced,
|
||||
boolean isC64)
|
||||
{
|
||||
//Generate proper names for files
|
||||
int duplicateIndex = getDuplicateIndexForImportedGame(title);
|
||||
|
@ -348,6 +349,11 @@ public class ImportManager
|
|||
//Ignore first "." when finding file ending
|
||||
String strippedGameFile = gamefile.substring(1);
|
||||
String fileEnding = strippedGameFile.substring(strippedGameFile.indexOf("."));
|
||||
if (!isC64 && fileEnding.contains(".crt"))
|
||||
{
|
||||
//A Vic-20 cartridge. Add the flag indicating the cartridge type to the name
|
||||
fileEnding = strippedGameFile.substring(strippedGameFile.indexOf("-"));
|
||||
}
|
||||
String newGamefile = fileName + fileEnding;
|
||||
addToDbRowList(title,
|
||||
year,
|
||||
|
|
|
@ -1,13 +1,38 @@
|
|||
package se.lantz.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.DefaultListModel;
|
||||
|
||||
import se.lantz.model.data.GameListData;
|
||||
|
||||
public class GameListModel extends DefaultListModel<GameListData>
|
||||
{
|
||||
private boolean disableIntervalChange = false;
|
||||
|
||||
void notifyChange()
|
||||
{
|
||||
fireContentsChanged(this, 0, getSize()-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fireIntervalAdded(Object source, int index0, int index1)
|
||||
{
|
||||
if (!disableIntervalChange)
|
||||
{
|
||||
super.fireIntervalAdded(source, index0, index1);;
|
||||
}
|
||||
}
|
||||
|
||||
void addAllGames(List<GameListData> gamesList)
|
||||
{
|
||||
clear();
|
||||
disableIntervalChange = true;
|
||||
for (GameListData gameListData : gamesList)
|
||||
{
|
||||
addElement(gameListData);
|
||||
}
|
||||
disableIntervalChange = false;
|
||||
notifyChange();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,12 +247,11 @@ public class MainViewModel extends AbstractModel
|
|||
{
|
||||
this.disableChangeNotification(true);
|
||||
logger.debug("Fetching games for view {}...", gameView);
|
||||
gameListModel.clear();
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
List<GameListData> gamesList = dbConnector.fetchGamesByView(gameView);
|
||||
for (GameListData gameListData : gamesList)
|
||||
{
|
||||
gameListModel.addElement(gameListData);
|
||||
}
|
||||
logger.debug("Fetched all games from db in " + (System.currentTimeMillis() - start) + " ms");
|
||||
gameListModel.addAllGames(gamesList);
|
||||
gameView.setGameCount(gamesList.size());
|
||||
if (gameView.getGameViewId() == GameView.ALL_GAMES_ID)
|
||||
{
|
||||
|
|
|
@ -168,7 +168,7 @@ public class GamebaseScraper implements Scraper
|
|||
//Map towards available genres, return first one found
|
||||
for (Map.Entry<String, String> entry : genreMap.entrySet())
|
||||
{
|
||||
if (entry.getKey().contains(parentGenre))
|
||||
if (entry.getKey().toLowerCase().contains(parentGenre.toLowerCase()))
|
||||
{
|
||||
return entry.getValue();
|
||||
}
|
||||
|
|
|
@ -18,8 +18,13 @@ import java.nio.file.Paths;
|
|||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.stream.Collectors;
|
||||
|
@ -56,7 +61,7 @@ public class FileManager
|
|||
private MainViewModel model;
|
||||
private InfoModel infoModel;
|
||||
private SystemModel systemModel;
|
||||
|
||||
private static ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
static
|
||||
{
|
||||
try
|
||||
|
@ -327,7 +332,7 @@ public class FileManager
|
|||
//Just add the duplicate index if there are several games with the same name
|
||||
newNameString = newNameString + "0" + duplicateIndex;
|
||||
}
|
||||
|
||||
|
||||
logger.debug("Game title: \"{}\" ---- New fileName: \"{}\"", title, newNameString);
|
||||
return newNameString;
|
||||
}
|
||||
|
@ -479,37 +484,40 @@ public class FileManager
|
|||
|
||||
if (appendGame)
|
||||
{
|
||||
|
||||
//Append game file
|
||||
Path gamePath = infoModel.getGamesPath();
|
||||
if (gamePath != null)
|
||||
{
|
||||
if (gamePath.toString().contains("crt"))
|
||||
{
|
||||
command.append("-cartcrt \"" + gamePath.toString() + "\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
command.append("-autostart \"" + gamePath.toString() + "\"");
|
||||
}
|
||||
appendCorrectFlagForGameFile(gamePath.toString(), command);
|
||||
// if (gamePath.toString().contains("crt"))
|
||||
// {
|
||||
// command.append("-cartcrt \"" + gamePath.toString() + "\"");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// command.append("-autostart \"" + gamePath.toString() + "\"");
|
||||
// }
|
||||
}
|
||||
else
|
||||
{
|
||||
//For Vic-20 treat prg's as carts. TODO: many cart alternatives for Vic-20, see GEMUS for GB-VIC20.
|
||||
if (!systemModel.isC64() && gameFile.contains("prg"))
|
||||
{
|
||||
command.append("-cartA \"" + gameFile + "\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameFile.contains("crt"))
|
||||
{
|
||||
command.append("-cartcrt \"" + decompressIfNeeded(gameFile) + "\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
command.append("-autostart \"" + gameFile + "\"");
|
||||
}
|
||||
}
|
||||
appendCorrectFlagForGameFile(gameFile, command);
|
||||
// //For Vic-20 treat prg's as carts. TODO: many cart alternatives for Vic-20, see GEMUS for GB-VIC20.
|
||||
// if (!systemModel.isC64() && gameFile.contains("prg"))
|
||||
// {
|
||||
// command.append("-cartA \"" + gameFile + "\"");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if (gameFile.contains("crt"))
|
||||
// {
|
||||
// command.append("-cartcrt \"" + decompressIfNeeded(gameFile) + "\"");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// command.append("-autostart \"" + gameFile + "\"");
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -558,10 +566,62 @@ public class FileManager
|
|||
}
|
||||
}
|
||||
|
||||
private void appendCorrectFlagForGameFile(String gameFile, StringBuilder command)
|
||||
{
|
||||
Map<String, String> nameToFlagMap = new HashMap<>();
|
||||
nameToFlagMap.put("a0", "-cartA ");
|
||||
nameToFlagMap.put("a0", "-cartA ");
|
||||
if (!systemModel.isC64())
|
||||
{
|
||||
//VIC-20
|
||||
if (gameFile.contains("crt"))
|
||||
{
|
||||
//Get the file flag
|
||||
String fileFlag = gameFile.substring(gameFile.lastIndexOf("-") + 1, gameFile.indexOf("crt.gz") - 1);
|
||||
if (fileFlag.contains("a0"))
|
||||
{
|
||||
command.append("-cartA \"" + gameFile + "\"");
|
||||
}
|
||||
else if (fileFlag.contains("b0"))
|
||||
{
|
||||
command.append("-cartB \"" + gameFile + "\"");
|
||||
}
|
||||
else if (fileFlag.contains("20"))
|
||||
{
|
||||
command.append("-cart2 \"" + gameFile + "\"");
|
||||
}
|
||||
else if (fileFlag.contains("40"))
|
||||
{
|
||||
command.append("-cart4 \"" + gameFile + "\"");
|
||||
}
|
||||
else if (fileFlag.contains("60"))
|
||||
{
|
||||
command.append("-cart6 \"" + gameFile + "\"");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
command.append("-autostart \"" + decompressIfNeeded(gameFile) + "\"");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//C64
|
||||
if (gameFile.contains("crt"))
|
||||
{
|
||||
command.append("-cartcrt \"" + decompressIfNeeded(gameFile) + "\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
command.append("-autostart \"" + gameFile + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String decompressIfNeeded(String path)
|
||||
{
|
||||
String returnPath = path;
|
||||
if (path.contains("crt.gz") || path.contains("CRT.gz"))
|
||||
if (path.contains("crt.gz") || path.contains("CRT.gz") || path.contains("prg.gz"))
|
||||
{
|
||||
Path targetFile = Paths.get("./temp/" + path.substring(path.lastIndexOf("/") + 1, path.lastIndexOf(".")));
|
||||
try
|
||||
|
@ -572,7 +632,7 @@ public class FileManager
|
|||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ExceptionHandler.handleException(e, "Could not decomrpess file: " + path);
|
||||
ExceptionHandler.handleException(e, "Could not decompress file: " + path);
|
||||
}
|
||||
}
|
||||
return returnPath;
|
||||
|
@ -704,7 +764,8 @@ public class FileManager
|
|||
{
|
||||
for (File file : dir.listFiles())
|
||||
{
|
||||
if (!file.isDirectory() && (deleteAll || !(file.getName().contains("THEC64") || file.getName().contains("VIC20"))))
|
||||
if (!file.isDirectory() &&
|
||||
(deleteAll || !(file.getName().contains("THEC64") || file.getName().contains("VIC20"))))
|
||||
{
|
||||
file.delete();
|
||||
}
|
||||
|
@ -845,21 +906,24 @@ public class FileManager
|
|||
return returnValue;
|
||||
}
|
||||
|
||||
public static void deleteTempFolder()
|
||||
public static Future<?> deleteTempFolder()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Files.exists(TEMP_PATH))
|
||||
//Delete temp folder. It may be very large, do it in a separate thread
|
||||
return executor.submit(() -> {
|
||||
try
|
||||
{
|
||||
Files.walk(TEMP_PATH).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
|
||||
if (Files.exists(TEMP_PATH))
|
||||
{
|
||||
Files.walk(TEMP_PATH).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ExceptionHandler.handleException(e, "Could not delete temp folder");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ExceptionHandler.logException(e, "Could not delete temp folder");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void scaleCoverImageAndSave(Path coverImagePath)
|
||||
{
|
||||
try
|
||||
|
@ -878,7 +942,7 @@ public class FileManager
|
|||
ExceptionHandler.handleException(e, "Could not scale and cover");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void scaleScreenshotImageAndSave(Path screenshotImagePath)
|
||||
{
|
||||
try
|
||||
|
|
Loading…
Reference in New Issue