package ctbrec.ui.sites.myfreecams; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ctbrec.Config; import ctbrec.Model; import ctbrec.StringUtil; import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCamsModel; import ctbrec.sites.mfc.SessionState; import ctbrec.ui.DesktopIntegration; import ctbrec.ui.TabSelectionListener; import ctbrec.ui.action.FollowAction; import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.controls.SearchBox; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.CheckMenuItem; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.SelectionMode; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.ContextMenuEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.util.Duration; public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private static final transient Logger LOG = LoggerFactory.getLogger(MyFreeCamsTableTab.class); private ScrollPane scrollPane = new ScrollPane(); private TableView table = new TableView(); private ObservableList filteredModels = FXCollections.observableArrayList(); private ObservableList observableModels = FXCollections.observableArrayList(); private TableUpdateService updateService; private MyFreeCams mfc; private ReentrantLock lock = new ReentrantLock(); private SearchBox filterInput; private Label count = new Label("models"); private List> columns = new ArrayList<>(); private ContextMenu popup; public MyFreeCamsTableTab(MyFreeCams mfc) { this.mfc = mfc; setText("Tabular"); setClosable(false); initUpdateService(); createGui(); restoreState(); filter(filterInput.getText()); } private void initUpdateService() { updateService = new TableUpdateService(mfc); updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(1))); updateService.setOnSucceeded(this::onSuccess); updateService.setOnFailed((event) -> { LOG.info("Couldn't update MyFreeCams model table", event.getSource().getException()); }); } private void onSuccess(WorkerStateEvent evt) { Collection sessionStates = updateService.getValue(); if (sessionStates == null) { return; } lock.lock(); try { for (SessionState updatedModel : sessionStates) { ModelTableRow row = new ModelTableRow(updatedModel); int index = observableModels.indexOf(row); if (index == -1) { observableModels.add(row); } else { observableModels.get(index).update(updatedModel); } } for (Iterator iterator = observableModels.iterator(); iterator.hasNext();) { ModelTableRow model = iterator.next(); boolean found = false; for (SessionState sessionState : sessionStates) { if(Objects.equals(sessionState.getUid(), model.uid)) { found = true; break; } } if(!found) { iterator.remove(); } } } finally { lock.unlock(); } filteredModels.clear(); filter(filterInput.getText()); table.sort(); } private void createGui() { BorderPane layout = new BorderPane(); layout.setPadding(new Insets(5, 10, 10, 10)); filterInput = new SearchBox(false); filterInput.setPromptText("Filter"); filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> { String filter = filterInput.getText(); Config.getInstance().getSettings().mfcModelsTableFilter = filter; lock.lock(); try { filter(filter); } finally { lock.unlock(); } }); filterInput.getStyleClass().remove("search-box-icon"); HBox.setHgrow(filterInput, Priority.ALWAYS); Button columnSelection = new Button("⚙"); //Button columnSelection = new Button("⩩"); columnSelection.setOnAction(this::showColumnSelection); HBox topBar = new HBox(5); topBar.getChildren().addAll(filterInput, count, columnSelection); count.prefHeightProperty().bind(filterInput.heightProperty()); count.setAlignment(Pos.CENTER); layout.setTop(topBar); BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0)); table.setItems(observableModels); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSortOrder().addListener(createSortOrderChangedListener()); table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { popup = createContextMenu(); if (popup != null) { popup.show(table, event.getScreenX(), event.getScreenY()); } event.consume(); }); table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { if (popup != null) { popup.hide(); } }); int idx = 0; TableColumn name = createTableColumn("Name", 200, idx++); name.setCellValueFactory(cdf -> cdf.getValue().nameProperty()); addTableColumnIfEnabled(name); TableColumn state = createTableColumn("State", 130, idx++); state.setCellValueFactory(cdf -> cdf.getValue().stateProperty()); addTableColumnIfEnabled(state); TableColumn camscore = createTableColumn("Score", 75, idx++); camscore.setCellValueFactory(cdf -> cdf.getValue().camScoreProperty()); addTableColumnIfEnabled(camscore); // this is always 0, use https://api.myfreecams.com/missmfc and https://api.myfreecams.com/missmfc/online // TableColumn missMfc = createTableColumn("Miss MFC", 75, idx++); // missMfc.setCellValueFactory(cdf -> { // Integer mmfc = Optional.ofNullable(cdf.getValue().getM()).map(m -> m.getMissmfc()).orElse(-1); // return new SimpleIntegerProperty(mmfc); // }); // addTableColumnIfEnabled(missMfc); TableColumn newModel = createTableColumn("New", 60, idx++); newModel.setCellValueFactory(cdf -> cdf.getValue().newModelProperty()); addTableColumnIfEnabled(newModel); TableColumn ethnic = createTableColumn("Ethnicity", 130, idx++); ethnic.setCellValueFactory(cdf -> cdf.getValue().ethnicityProperty()); addTableColumnIfEnabled(ethnic); TableColumn country = createTableColumn("Country", 160, idx++); country.setCellValueFactory(cdf -> cdf.getValue().countryProperty()); addTableColumnIfEnabled(country); TableColumn continent = createTableColumn("Continent", 100, idx++); continent.setCellValueFactory(cdf -> cdf.getValue().continentProperty()); addTableColumnIfEnabled(continent); TableColumn occupation = createTableColumn("Occupation", 160, idx++); occupation.setCellValueFactory(cdf -> cdf.getValue().occupationProperty()); addTableColumnIfEnabled(occupation); TableColumn tags = createTableColumn("Tags", 300, idx++); tags.setCellValueFactory(cdf -> cdf.getValue().tagsProperty()); addTableColumnIfEnabled(tags); TableColumn blurp = createTableColumn("Blurp", 300, idx++); blurp.setCellValueFactory(cdf -> cdf.getValue().blurpProperty()); addTableColumnIfEnabled(blurp); TableColumn topic = createTableColumn("Topic", 600, idx++); topic.setCellValueFactory(cdf -> cdf.getValue().topicProperty()); addTableColumnIfEnabled(topic); scrollPane.setFitToHeight(true); scrollPane.setFitToWidth(true); scrollPane.setContent(table); scrollPane.setStyle("-fx-background-color: -fx-background"); layout.setCenter(scrollPane); setContent(layout); } private ContextMenu createContextMenu() { ObservableList selectedStates = table.getSelectionModel().getSelectedItems(); if (selectedStates.isEmpty()) { return null; } List selectedModels = new ArrayList<>(); for (ModelTableRow sessionState : selectedStates) { if(sessionState.name.get() != null) { MyFreeCamsModel model = mfc.createModel(sessionState.name.get()); mfc.getClient().update(model); selectedModels.add(model); } } MenuItem copyUrl = new MenuItem("Copy URL"); copyUrl.setOnAction((e) -> { Model selected = selectedModels.get(0); final Clipboard clipboard = Clipboard.getSystemClipboard(); final ClipboardContent content = new ClipboardContent(); content.putString(selected.getUrl()); clipboard.setContent(content); }); MenuItem startRecording = new MenuItem("Start Recording"); startRecording.setOnAction((e) -> startRecording(selectedModels)); MenuItem openInBrowser = new MenuItem("Open in Browser"); openInBrowser.setOnAction((e) -> DesktopIntegration.open(selectedModels.get(0).getUrl())); MenuItem openInPlayer = new MenuItem("Open in Player"); openInPlayer.setOnAction((e) -> openInPlayer(selectedModels.get(0))); MenuItem follow = new MenuItem("Follow"); follow.setOnAction((e) -> new FollowAction(getTabPane(), selectedModels).execute()); ContextMenu menu = new ContextMenu(); menu.getItems().addAll(startRecording, copyUrl, openInPlayer, openInBrowser, follow); if (selectedModels.size() > 1) { copyUrl.setDisable(true); openInPlayer.setDisable(true); openInBrowser.setDisable(true); } return menu; } private void startRecording(List selectedModels) { new StartRecordingAction(getTabPane(), selectedModels, mfc.getRecorder()).execute(); } private void openInPlayer(Model selectedModel) { new PlayAction(getTabPane(), selectedModel).execute(); } private void addTableColumnIfEnabled(TableColumn tc) { if(isColumnEnabled(tc)) { table.getColumns().add(tc); } } private void filter(String filter) { lock.lock(); try { if (StringUtil.isBlank(filter)) { observableModels.addAll(filteredModels); filteredModels.clear(); return; } String[] tokens = filter.split(" "); observableModels.addAll(filteredModels); filteredModels.clear(); for (int i = 0; i < table.getItems().size(); i++) { StringBuilder sb = new StringBuilder(); for (TableColumn tc : table.getColumns()) { String cellData = tc.getCellData(i).toString(); sb.append(cellData).append(' '); } String searchText = sb.toString(); boolean tokensMissing = false; for (String token : tokens) { if(!searchText.toLowerCase().contains(token.toLowerCase())) { tokensMissing = true; break; } } if(tokensMissing) { ModelTableRow sessionState = table.getItems().get(i); filteredModels.add(sessionState); } } observableModels.removeAll(filteredModels); } finally { lock.unlock(); int filtered = filteredModels.size(); int showing = observableModels.size(); int total = showing + filtered; count.setText(showing + "/" + total); } } private void showColumnSelection(ActionEvent evt) { ContextMenu menu = new ContextMenu(); for (TableColumn tc : columns) { CheckMenuItem item = new CheckMenuItem(tc.getText()); item.setSelected(isColumnEnabled(tc)); menu.getItems().add(item); item.setOnAction(e -> { if(item.isSelected()) { Config.getInstance().getSettings().mfcDisabledModelsTableColumns.remove(tc.getText()); for (int i = table.getColumns().size()-1; i>=0; i--) { TableColumn other = table.getColumns().get(i); int idx = (int) tc.getUserData(); int otherIdx = (int) other.getUserData(); if(otherIdx < idx) { table.getColumns().add(i+1, tc); break; } } } else { Config.getInstance().getSettings().mfcDisabledModelsTableColumns.add(tc.getText()); table.getColumns().remove(tc); } }); } Button src = (Button) evt.getSource(); Point2D location = src.localToScreen(src.getTranslateX(), src.getTranslateY()); menu.show(getTabPane().getScene().getWindow(), location.getX(), location.getY() + src.getHeight() + 5); } private boolean isColumnEnabled(TableColumn tc) { return !Config.getInstance().getSettings().mfcDisabledModelsTableColumns.contains(tc.getText()); } private TableColumn createTableColumn(String text, int width, int idx) { TableColumn tc = new TableColumn<>(text); tc.setPrefWidth(width); tc.sortTypeProperty().addListener((obs, o, n) -> saveState()); tc.widthProperty().addListener((obs, o, n) -> saveState()); tc.setUserData(idx); columns.add(tc); return tc; } @Override public void selected() { if(updateService != null) { State s = updateService.getState(); if (s != State.SCHEDULED && s != State.RUNNING) { updateService.reset(); updateService.restart(); } } } @Override public void deselected() { if(updateService != null) { updateService.cancel(); } } private void saveState() { if (!table.getSortOrder().isEmpty()) { TableColumn col = table.getSortOrder().get(0); Config.getInstance().getSettings().mfcModelsTableSortColumn = col.getText(); Config.getInstance().getSettings().mfcModelsTableSortType = col.getSortType().toString(); } double[] columnWidths = new double[table.getColumns().size()]; for (int i = 0; i < columnWidths.length; i++) { columnWidths[i] = table.getColumns().get(i).getWidth(); } Config.getInstance().getSettings().mfcModelsTableColumnWidths = columnWidths; }; private void restoreState() { String sortCol = Config.getInstance().getSettings().mfcModelsTableSortColumn; if (StringUtil.isNotBlank(sortCol)) { for (TableColumn col : table.getColumns()) { if (Objects.equals(sortCol, col.getText())) { col.setSortType(SortType.valueOf(Config.getInstance().getSettings().mfcModelsTableSortType)); table.getSortOrder().clear(); table.getSortOrder().add(col); break; } } } double[] columnWidths = Config.getInstance().getSettings().mfcModelsTableColumnWidths; if (columnWidths != null && columnWidths.length == table.getColumns().size()) { for (int i = 0; i < columnWidths.length; i++) { table.getColumns().get(i).setPrefWidth(columnWidths[i]); } } filterInput.setText(Config.getInstance().getSettings().mfcModelsTableFilter); } private ListChangeListener> createSortOrderChangedListener() { return new ListChangeListener>() { @Override public void onChanged(Change> c) { saveState(); } }; } private static class ModelTableRow { private Integer uid; private StringProperty name = new SimpleStringProperty(); private StringProperty state = new SimpleStringProperty(); private DoubleProperty camScore = new SimpleDoubleProperty(); private StringProperty newModel = new SimpleStringProperty(); private StringProperty ethnic = new SimpleStringProperty(); private StringProperty country = new SimpleStringProperty(); private StringProperty continent = new SimpleStringProperty(); private StringProperty occupation = new SimpleStringProperty(); private StringProperty tags = new SimpleStringProperty(); private StringProperty blurp = new SimpleStringProperty(); private StringProperty topic = new SimpleStringProperty(); public ModelTableRow(SessionState st) { update(st); } public void update(SessionState st) { uid = st.getUid(); name.set(Optional.ofNullable(st.getNm()).orElse("n/a")); state.set(Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString()).orElse("n/a")); camScore.set(Optional.ofNullable(st.getM()).map(m -> m.getCamscore()).orElse(0d)); Integer nu = Optional.ofNullable(st.getM()).map(m -> m.getNewModel()).orElse(0); newModel.set(nu == 1 ? "new" : ""); ethnic.set(Optional.ofNullable(st.getU()).map(u -> u.getEthnic()).orElse("n/a")); country.set(Optional.ofNullable(st.getU()).map(u -> u.getCountry()).orElse("n/a")); continent.set(Optional.ofNullable(st.getM()).map(m -> m.getContinent()).orElse("n/a")); occupation.set(Optional.ofNullable(st.getU()).map(u -> u.getOccupation()).orElse("n/a")); Set tagSet = Optional.ofNullable(st.getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); if(tagSet.isEmpty()) { tags.set(""); } else { StringBuilder sb = new StringBuilder(); for (String t : tagSet) { sb.append(t).append(',').append(' '); } tags.set(sb.substring(0, sb.length()-2)); } blurp.set(Optional.ofNullable(st.getU()).map(u -> u.getBlurb()).orElse("n/a")); String tpc = Optional.ofNullable(st.getM()).map(m -> m.getTopic()).orElse("n/a"); try { tpc = URLDecoder.decode(tpc, "utf-8"); } catch (UnsupportedEncodingException e) { LOG.warn("Couldn't url decode topic", e); } topic.set(tpc); } public StringProperty nameProperty() { return name; }; public StringProperty stateProperty() { return state; }; public DoubleProperty camScoreProperty() { return camScore; }; public StringProperty newModelProperty() { return newModel; }; public StringProperty ethnicityProperty() { return ethnic; }; public StringProperty countryProperty() { return country; }; public StringProperty continentProperty() { return continent; }; public StringProperty occupationProperty() { return occupation; }; public StringProperty tagsProperty() { return tags; }; public StringProperty blurpProperty() { return blurp; }; public StringProperty topicProperty() { return topic; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((uid == null) ? 0 : uid.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; ModelTableRow other = (ModelTableRow) obj; if (uid == null) { if (other.uid != null) return false; } else if (!uid.equals(other.uid)) return false; return true; }; } }