diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java index 004244280de3..aaea23cbec1d 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/DeckImport.java @@ -19,6 +19,7 @@ import java.awt.*; import java.awt.event.*; +import java.io.IOException; import java.util.*; import java.util.List; @@ -38,6 +39,7 @@ import forge.game.GameFormat; import forge.game.GameType; import forge.gui.CardPicturePanel; +import forge.gui.FThreads; import forge.item.PaperCard; import forge.screens.deckeditor.controllers.CDeckEditor; import forge.screens.deckeditor.controllers.CStatisticsImporter; @@ -172,6 +174,13 @@ public class DeckImport extends FDialog { ); private static final int PADDING_TOKEN_MSG_LENGTH = 45; + private final FLabel deckUrlLabel = new FLabel.Builder() + .text(Localizer.getInstance().getMessage("lblDeckUrlLabel")) + .fontSize(14).build(); + private final FTextField txtDeckUrl = new FTextField.Builder() + .ghostText(Localizer.getInstance().getMessage("nlDeckUrlGhostText")).build(); + private final FButton btnLoadUrl = new FButton(Localizer.getInstance().getMessage("lblLoad")); + private final FHtmlViewer htmlOutput = new FHtmlViewer(); private final FScrollPane scrollInput = new FScrollPane(this.txtInput, false); private final FScrollPane scrollOutput = new FScrollPane(this.htmlOutput, false); @@ -243,6 +252,12 @@ public DeckImport(final CDeckEditor g) { this.statsView = new VStatisticsImporter(this.controller.currentGameFormatAllowsCommander()); this.cStatsView = new CStatisticsImporter(this.statsView); initUIComponents(g, currentDeckIsNotEmpty); + addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + txtInput.requestFocusInWindow(); + } + }); } private void initUIComponents(CDeckEditor g, boolean currentDeckIsNotEmpty) { @@ -275,6 +290,10 @@ private void initUIComponents(CDeckEditor g, boolean currentDeckIsNotEmp // ---------------- this.txtInput.getDocument().addDocumentListener(new OnChangeTextUpdate()); + // == A.1 Deck URL toolbar (above the Card List) + this.btnLoadUrl.addActionListener(e -> loadFromUrl()); + this.txtDeckUrl.addActionListener(e -> loadFromUrl()); + // == B. Scroll Output (Decklist) this.scrollOutput.setBorder(new FSkin.TitledSkinBorder(BorderFactory.createEtchedBorder(), Localizer.getInstance().getMessage("lblDecklistTitle"), foreColor)); @@ -541,7 +560,18 @@ public void mouseClicked(MouseEvent e) { // === ASSEMBLING ALL PANELS TOGETHER // ================================== - this.add(this.scrollInput, "cell 0 0, w 40%, growy, pushy, spany 2"); + JPanel urlPanel = new JPanel(new MigLayout("insets 0 0 0 5, gap 5, fillx")); + urlPanel.setOpaque(false); + urlPanel.add(this.deckUrlLabel, "ax left"); + urlPanel.add(this.txtDeckUrl, "growx, pushx, h 28!"); + urlPanel.add(this.btnLoadUrl, "w 80!, h 28!, ax right"); + + JPanel inputColumn = new JPanel(new MigLayout("insets 0, gap 5, fill, wrap 1")); + inputColumn.setOpaque(false); + inputColumn.add(urlPanel, "growx, h 32!"); + inputColumn.add(this.scrollInput, "grow, push"); + + this.add(inputColumn, "cell 0 0, w 40%, growy, pushy, spany 2"); this.add(this.scrollOutput, "cell 1 0, w 60%, growy, pushy, spany 2"); this.add(statsPanel, "cell 2 0, w 480:510:550, growy, pushy, ax c"); this.add(cardPreview, "cell 2 1, w 480:510:550, h 65%, growy, pushy, ax c"); @@ -550,6 +580,43 @@ public void mouseClicked(MouseEvent e) { this.add(cmdPanel, "cell 0 3, w 100%, spanx 3"); } + private void loadFromUrl() { + final String url = this.txtDeckUrl.getText().trim(); + if (url.isEmpty()) { + return; + } + if (!this.txtInput.getText().trim().isEmpty() + && !FOptionPane.showConfirmDialog(Localizer.getInstance().getMessage("nlConfirmReplaceCardList"))) { + return; + } + setUrlControlsEnabled(false, Localizer.getInstance().getMessage("lblLoadingEllipsis")); + FThreads.invokeInBackgroundThread(() -> { + String deckText = null; + String error = null; + try { + deckText = controller.getDeckTextFromUrl(url); + } catch (IOException ex) { + error = ex.getMessage(); + } + final String resultText = deckText; + final String errorMsg = error; + FThreads.invokeInEdtLater(() -> { + setUrlControlsEnabled(true, Localizer.getInstance().getMessage("lblLoad")); + if (errorMsg != null) { + FOptionPane.showErrorDialog(errorMsg, Localizer.getInstance().getMessage("lblUnableToLoadDeckUrl")); + } else { + txtInput.setText(resultText); + } + }); + }); + } + + private void setUrlControlsEnabled(final boolean enabled, final String buttonText) { + this.txtDeckUrl.setEnabled(enabled); + this.btnLoadUrl.setEnabled(enabled); + this.btnLoadUrl.setText(buttonText); + } + private void activateCardPreview(HyperlinkEvent e) { if(e.getEventType() == HyperlinkEvent.EventType.ENTERED || e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index 8534573c2cee..0d14b82e6969 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -781,6 +781,8 @@ lblNetArchiveBlockDecks=Net Archive Block Decks lblNetEventDecks=Draft/Sealed Decks lblProvideDeckUrl=Provide Deck URL lblDeckUrlLabel=URL: +nlDeckUrlGhostText=Paste a Moxfield or Archidekt deck URL +nlConfirmReplaceCardList=Replace the current card list with the deck loaded from this URL? lblReload=Reload lblLoadingEllipsis=Loading... lblUnableToLoadDeckUrl=Unable to Load Deck URL diff --git a/forge-gui/src/main/java/forge/deck/DeckImportController.java b/forge-gui/src/main/java/forge/deck/DeckImportController.java index 0686e65b2ff4..4d6c68b336ae 100644 --- a/forge-gui/src/main/java/forge/deck/DeckImportController.java +++ b/forge-gui/src/main/java/forge/deck/DeckImportController.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; +import java.io.IOException; import java.text.DateFormatSymbols; import java.util.*; import java.util.function.Function; @@ -194,6 +195,10 @@ public void importBannedAndRestrictedCards(boolean includeBannedAndRestricted){ public boolean importBannedAndRestrictedCards(){ return this.includeBnRInDeck; } + public String getDeckTextFromUrl(final String deckUrl) throws IOException { + return DeckUrlLoader.loadImportText(deckUrl); + } + public List parseInput(String input) { tokens.clear(); cardsInTokens.clear(); diff --git a/forge-gui/src/main/java/forge/deck/DeckUrlLoader.java b/forge-gui/src/main/java/forge/deck/DeckUrlLoader.java index c5495665b4e6..7bcd1b815fea 100644 --- a/forge-gui/src/main/java/forge/deck/DeckUrlLoader.java +++ b/forge-gui/src/main/java/forge/deck/DeckUrlLoader.java @@ -43,6 +43,12 @@ public static DeckProxy load(final String deckUrl) throws IOException { return new DeckProxy(deck, localizer.getMessage("lblUrlDeck"), GameType.Constructed, storage); } + static String loadImportText(final String deckUrl) throws IOException { + final String normalizedUrl = normalizeUrl(deckUrl); + final DeckUrlProvider provider = getProvider(normalizedUrl); + return provider.load(normalizedUrl, List.of()).importText(); + } + public static List getUrlDecks() { final List decks = new ArrayList<>(); final StorageImmediatelySerialized storage = getStorage();