package mage.client.dialog; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Worker; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import mage.client.MageFrame; import mage.utils.MageVersion; import org.apache.log4j.Logger; import org.w3c.dom.events.EventListener; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.net.*; import java.util.ArrayList; import java.util.List; import java.util.Scanner; /** * @author JayDi85 */ public class WhatsNewDialog extends MageDialog { private static final Logger logger = Logger.getLogger(WhatsNewDialog.class); private static final MageVersion clientVersion = new MageVersion(WhatsNewDialog.class); // cookies tester: http://www.html-kit.com/tools/cookietester/ private static final String WHATS_NEW_PAGE = "https://jaydi85.github.io/xmage-web-news/news.html"; private static final String WHATS_NEW_VERSION_PAGE = "https://jaydi85.github.io/xmage-web-news/news_version.html"; // increment version=123 to auto-shown for all users private static final int WHATS_NEW_LOAD_TIMEOUT_SECS = 20; // timeout for page loading final JFXPanel fxPanel; private WebEngine engine; private boolean isPageReady = false; private final SwingWorker backgroundWorker = new SwingWorker() { @Override public Void doInBackground() { try { int maxWait = WHATS_NEW_LOAD_TIMEOUT_SECS; int currentWait = 0; while (!isPageReady) { Thread.sleep(1000); currentWait++; if (currentWait > maxWait) { logger.error("Can't load news page: " + WHATS_NEW_PAGE); break; } } } catch (InterruptedException e) { logger.error("Checking news was interrupted", e); Thread.currentThread().interrupt(); } return null; } @Override public void done() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (isPageReady) { showDialog(); } } }); } }; private final SwingWorker backgroundUpdatesWorker = new SwingWorker() { private String newVersion = ""; @Override public Void doInBackground() { try { // download version URLConnection connection = new URL(WHATS_NEW_VERSION_PAGE).openConnection(); connection.setRequestProperty("user-agent", "xmage"); InputStream download = connection.getInputStream(); Scanner s = new Scanner(download).useDelimiter("\\A"); String result = s.hasNext() ? s.next() : ""; // check version (default: always have new version) if (result.startsWith("version=")) { newVersion = result.substring("version=".length()); } } catch (MalformedURLException e) { logger.error("Checking updates got wrong url " + WHATS_NEW_VERSION_PAGE, e); } catch (IOException e) { logger.error("Checking updates got error", e); } return null; } @Override public void done() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { String oldVersion = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEWS_PAGE_LAST_VERSION, "1"); boolean isHaveUpdates = newVersion.isEmpty() || !newVersion.equals(oldVersion); if (isHaveUpdates) { PreferencesDialog.saveValue(PreferencesDialog.KEY_NEWS_PAGE_LAST_VERSION, newVersion); loadURL(WHATS_NEW_PAGE); backgroundWorker.execute(); } } }); } }; public WhatsNewDialog() { initComponents(); this.setDefaultCloseOperation(HIDE_ON_CLOSE); fxPanel = new JFXPanel(); panelData.add(fxPanel); createWebView(); } public void checkUpdatesAndShow(boolean forceToShowPage) { // lazy loading in background // shows it on page ready or by force isPageReady = false; if (forceToShowPage) { // direct open loadURL(WHATS_NEW_PAGE); if (!backgroundUpdatesWorker.isDone()) backgroundUpdatesWorker.cancel(true); if (!backgroundWorker.isDone()) backgroundWorker.cancel(true); showDialog(); } else { // checks version -> loads on new backgroundUpdatesWorker.execute(); } } private void showDialog() { this.setModal(true); this.setResizable(true); getRootPane().setDefaultButton(buttonCancel); this.setSize(800, 600); // windows settings MageFrame.getDesktop().remove(this); if (this.isModal()) { MageFrame.getDesktop().add(this, JLayeredPane.MODAL_LAYER); } else { MageFrame.getDesktop().add(this, JLayeredPane.PALETTE_LAYER); } this.makeWindowCentered(); // Close on "ESC" registerKeyboardAction(e -> onCancel(), KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); this.setVisible(true); } /** * Store cookies in preferences */ private class PersistentCookieStore implements CookieStore, Runnable { private CookieStore store; public PersistentCookieStore() { store = new CookieManager().getCookieStore(); // restore data loadFromPrefs(); // save data on app close Runtime.getRuntime().addShutdownHook(new Thread(this)); } private void loadFromPrefs() { String sourceValue = PreferencesDialog.getCachedValue(PreferencesDialog.KEY_NEWS_PAGE_COOKIES, ""); Gson gson = new GsonBuilder().create(); List httpCookies = new ArrayList<>(); Type type = new TypeToken>() { }.getType(); try { httpCookies = gson.fromJson(sourceValue, type); for (HttpCookie cookie : httpCookies) { store.add(URI.create(cookie.getDomain()), cookie); } } catch (Exception e) { logger.error("Wrong news page cookies", e); } } private void saveToPrefs() { List httpCookies = store.getCookies(); Gson gson = new GsonBuilder().create(); String destValue = gson.toJson(httpCookies); PreferencesDialog.saveValue(PreferencesDialog.KEY_NEWS_PAGE_COOKIES, destValue); } @Override public void run() { saveToPrefs(); } @Override public void add(URI uri, HttpCookie cookie) { store.add(uri, cookie); } @Override public List get(URI uri) { return store.get(uri); } @Override public List getCookies() { return store.getCookies(); } @Override public List getURIs() { return store.getURIs(); } @Override public boolean remove(URI uri, HttpCookie cookie) { return store.remove(uri, cookie); } @Override public boolean removeAll() { return store.removeAll(); } } private void createWebView() { // init web engine and events // https://docs.oracle.com/javafx/2/swing/swing-fx-interoperability.htm Platform.runLater(new Runnable() { @Override public void run() { WebView view = new WebView(); engine = view.getEngine(); engine.setUserAgent(engine.getUserAgent() + " XMage/" + clientVersion.toString(false)); view.contextMenuEnabledProperty().setValue(false); CookieManager cookieManager = new CookieManager(new PersistentCookieStore(), CookiePolicy.ACCEPT_ALL); CookieHandler.setDefault(cookieManager); // on error engine.getLoadWorker().exceptionProperty().addListener(new ChangeListener() { public void changed(ObservableValue o, Throwable old, final Throwable value) { if (engine.getLoadWorker().getState() == Worker.State.FAILED) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { logger.error("Can't load news page: " + (value != null ? value.getMessage() : "null"), value); } }); } } }); // on completed engine.getLoadWorker().stateProperty().addListener(new ChangeListener() { public void changed(ObservableValue ov, Worker.State oldState, Worker.State newState) { if (newState == Worker.State.SUCCEEDED) { // 1. replace urls with custom click processing // all classes from org.w3c.dom org.w3c.dom.events.EventListener listener = new EventListener() { public void handleEvent(org.w3c.dom.events.Event ev) { String href = ((org.w3c.dom.Element) ev.getTarget()).getAttribute("href"); ev.preventDefault(); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); try { URI uri = new URI(href); desktop.browse(uri); } catch (IOException | URISyntaxException ex) { // do nothing } } } }); } }; org.w3c.dom.Document doc = engine.getDocument(); org.w3c.dom.NodeList listA = doc.getElementsByTagName("a"); for (int i = 0; i < listA.getLength(); i++) { ((org.w3c.dom.events.EventTarget) listA.item(i)).addEventListener("click", listener, false); } // 2. page can be shown SwingUtilities.invokeLater(new Runnable() { @Override public void run() { isPageReady = true; } }); } } }); fxPanel.setScene(new Scene(view)); } }); } public void loadURL(final String url) { Platform.runLater(new Runnable() { @Override public void run() { String tmp = toURL(url); if (url == null) { tmp = toURL("http://" + url); } engine.load(tmp); } }); } private static String toURL(String str) { try { return new URL(str).toExternalForm(); } catch (MalformedURLException exception) { return null; } } private void onCancel() { this.hideDialog(); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { buttonCancel = new javax.swing.JButton(); buttonRefresh = new javax.swing.JButton(); panelData = new javax.swing.JPanel(); buttonCancel.setText("Close"); buttonCancel.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { buttonCancelActionPerformed(evt); } }); buttonRefresh.setText("Refresh"); buttonRefresh.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { buttonRefreshActionPerformed(evt); } }); panelData.setLayout(new java.awt.BorderLayout()); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(panelData, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() .addComponent(buttonRefresh, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(panelData, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(buttonRefresh, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap()) ); pack(); }// //GEN-END:initComponents private void buttonCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonCancelActionPerformed onCancel(); }//GEN-LAST:event_buttonCancelActionPerformed private void buttonRefreshActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_buttonRefreshActionPerformed loadURL(WHATS_NEW_PAGE); }//GEN-LAST:event_buttonRefreshActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton buttonCancel; private javax.swing.JButton buttonRefresh; private javax.swing.JPanel panelData; // End of variables declaration//GEN-END:variables }