package ctbrec.ui.sites.camsoda; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.FormatStyle; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.GlobalThreadPool; import ctbrec.Model; import ctbrec.recorder.Recorder; import ctbrec.sites.camsoda.Camsoda; import ctbrec.ui.AutosizeAlert; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.SiteUiFactory; import ctbrec.ui.tabs.TabSelectionListener; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.concurrent.Task; import javafx.geometry.Insets; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; import javafx.scene.control.Tab; import javafx.scene.control.TitledPane; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.BorderPane; import javafx.scene.layout.GridPane; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import okhttp3.Request; import okhttp3.Response; public class CamsodaShowsTab extends Tab implements TabSelectionListener { private static final Logger LOG = LoggerFactory.getLogger(CamsodaShowsTab.class); private Camsoda camsoda; private Recorder recorder; private GridPane showList; public CamsodaShowsTab(Camsoda camsoda, Recorder recorder) { this.camsoda = camsoda; this.recorder = recorder; createGui(); } private void createGui() { showList = new GridPane(); showList.setPadding(new Insets(5)); showList.setHgap(5); showList.setVgap(5); var progressIndicator = new ProgressIndicator(); progressIndicator.setPrefSize(100, 100); setContent(progressIndicator); setClosable(false); setText("Shows"); } @Override public void selected() { Task> task = new Task>() { @Override protected List call() throws Exception { return loadShows(); } @Override protected void done() { super.done(); Platform.runLater(() -> { try { List boxes = get(); showList.getChildren().clear(); var index = 0; for (ShowBox showBox : boxes) { showList.add(showBox, index % 2, index++ / 2); GridPane.setMargin(showBox, new Insets(20, 20, 0, 20)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); LOG.error("Couldn't load upcoming camsoda shows", e); } catch (Exception e) { LOG.error("Couldn't load upcoming camsoda shows", e); } setContent(new ScrollPane(showList)); }); } }; GlobalThreadPool.submit(task); } private List loadShows() throws IOException { String url = camsoda.getBaseUrl() + "/api/v1/user/model_shows"; Request req = new Request.Builder().url(url).build(); try(var response = camsoda.getHttpClient().execute(req)) { if (response.isSuccessful()) { var json = new JSONObject(response.body().string()); if (json.optInt("success") == 1) { List boxes = new ArrayList<>(); var results = json.getJSONArray("results"); for (var i = 0; i < results.length(); i++) { var result = results.getJSONObject(i); var modelUrl = camsoda.getBaseUrl() + result.getString("url"); var name = modelUrl.substring(modelUrl.lastIndexOf('/') + 1); var model = camsoda.createModel(name); ZonedDateTime startTime = parseUtcTime(result.getString("start")); ZonedDateTime endTime = parseUtcTime(result.getString("end")); boxes.add(new ShowBox(model, startTime, endTime)); } return boxes; } else { LOG.error("Couldn't load upcoming camsoda shows. Unexpected response: {}", json); showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server"); } } else { showErrorDialog("Oh no!", "Couldn't load upcoming CamSoda shows", "Got an unexpected response from server"); LOG.error("Couldn't load upcoming camsoda shows: {} {}", response.code(), response.message()); } } return Collections.emptyList(); } private ZonedDateTime parseUtcTime(String string) { var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; var ta = formatter.parse(string.replace(" UTC", "")); var instant = Instant.from(ta); return ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); } @Override public void deselected() { // nothing to do } private void showErrorDialog(String title, String head, String msg) { Platform.runLater(() -> { Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene()); alert.setTitle(title); alert.setHeaderText(head); alert.setContentText(msg); alert.showAndWait(); }); } private class ShowBox extends TitledPane { BorderPane root = new BorderPane(); int thumbSize = 200; public ShowBox(Model model, ZonedDateTime startTime, ZonedDateTime endTime) { setText(model.getName()); setPrefHeight(268); setContent(root); var thumb = new ImageView(); thumb.setPreserveRatio(true); thumb.setFitHeight(thumbSize); loadImage(model, thumb); root.setLeft(new ProgressIndicator()); BorderPane.setMargin(thumb, new Insets(10, 30, 10, 10)); var formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM, FormatStyle.MEDIUM); var grid = new GridPane(); grid.add(createLabel("Start", true), 0, 0); grid.add(createLabel(formatter.format(startTime), false), 1, 0); grid.add(createLabel("End", true), 0, 1); grid.add(createLabel(formatter.format(endTime), false), 1, 1); var recordButton = new Button("Record Model"); recordButton.setTooltip(new Tooltip(recordButton.getText())); recordButton.setOnAction(evt -> recordModel(model)); grid.add(recordButton, 1, 2); GridPane.setMargin(recordButton, new Insets(10)); var follow = new Button("Follow"); follow.setTooltip(new Tooltip(follow.getText())); follow.setOnAction(evt -> follow(model)); grid.add(follow, 1, 3); GridPane.setMargin(follow, new Insets(10)); var openInBrowser = new Button("Open in Browser"); openInBrowser.setTooltip(new Tooltip(openInBrowser.getText())); openInBrowser.setOnAction(evt -> DesktopIntegration.open(model.getUrl())); grid.add(openInBrowser, 1, 4); GridPane.setMargin(openInBrowser, new Insets(10)); root.setCenter(grid); loadImage(model, thumb); recordButton.minWidthProperty().bind(openInBrowser.widthProperty()); follow.minWidthProperty().bind(openInBrowser.widthProperty()); } private void follow(Model model) { setCursor(Cursor.WAIT); GlobalThreadPool.submit(() -> { try { SiteUiFactory.getUi(model.getSite()).login(); model.follow(); } catch (Exception e) { LOG.error("Couldn't follow model {}", model, e); showErrorDialog("Oh no!", "Couldn't follow model", e.getMessage()); } finally { Platform.runLater(() -> setCursor(Cursor.DEFAULT)); } }); } private void recordModel(Model model) { setCursor(Cursor.WAIT); GlobalThreadPool.submit(() -> { try { recorder.addModel(model); } catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException | IOException e) { showErrorDialog("Oh no!", "Couldn't add model to the recorder", "Recorder error: " + e.getMessage()); } finally { Platform.runLater(() -> setCursor(Cursor.DEFAULT)); } }); } private void loadImage(Model model, ImageView thumb) { GlobalThreadPool.submit(() -> { try { String url = camsoda.getBaseUrl() + "/api/v1/user/" + model.getName(); var detailRequest = new Request.Builder().url(url).build(); try (Response resp = camsoda.getHttpClient().execute(detailRequest)) { if (resp.isSuccessful()) { parseImageUrl(resp.body().string()).ifPresent(imageUrl -> updateImageView(thumb, imageUrl)); } } } catch (Exception e) { LOG.error("Couldn't load model details", e); } }); } private void updateImageView(ImageView view, String imageUrl) { Platform.runLater(() -> { var img = new Image(imageUrl, 1000, thumbSize, true, true, true); img.progressProperty().addListener((ChangeListener) (observable, oldValue, newValue) -> { if (newValue.doubleValue() == 1.0) { view.setImage(img); root.setLeft(view); } }); }); } private Optional parseImageUrl(String body) { var json = new JSONObject(body); if (json.optBoolean("status") && json.has("user")) { var user = json.getJSONObject("user"); if (user.has("settings")) { var settings = user.getJSONObject("settings"); String imageUrl; if (Objects.equals(System.getenv("CTBREC_DEV"), "1")) { imageUrl = getClass().getResource("/image_not_found.png").toString(); } else { if (settings.has("offline_picture")) { imageUrl = settings.getString("offline_picture"); } else { imageUrl = "https:" + user.getString("thumb"); } } return Optional.of(imageUrl); } } return Optional.empty(); } private Node createLabel(String string, boolean bold) { var label = new Label(string); label.setPadding(new Insets(10)); var def = Font.getDefault(); label.setFont(Font.font(def.getFamily(), bold ? FontWeight.BOLD : FontWeight.NORMAL, 16)); return label; } } }