diff --git a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java index 6ce0cdde..e07c6755 100644 --- a/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/client/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -338,7 +338,8 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } void initializeUpdateService() { - updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); + int refreshRate = Config.getInstance().getSettings().overviewUpdateIntervalInSecs; + updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(refreshRate))); updateService.setOnSucceeded((event) -> onSuccess()); updateService.setOnFailed((event) -> onFail(event)); } diff --git a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java index 68c797b3..30034c4f 100644 --- a/client/src/main/java/ctbrec/ui/settings/SettingsTab.java +++ b/client/src/main/java/ctbrec/ui/settings/SettingsTab.java @@ -57,6 +57,7 @@ public class SettingsTab extends Tab implements TabSelectionListener { private TextField server; private TextField port; private TextField onlineCheckIntervalInSecs; + private TextField overviewUpdateIntervalInSecs; private TextField leaveSpaceOnDevice; private TextField minimumLengthInSecs; private CheckBox loadResolution; @@ -451,17 +452,6 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(chooseStreamQuality, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(chooseStreamQuality, 1, row++); - l = new Label("Update thumbnails"); - layout.add(l, 0, row); - updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails); - updateThumbnails.setOnAction((e) -> { - Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected(); - saveConfig(); - }); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); - layout.add(updateThumbnails, 1, row++); - l = new Label("Enable live previews (experimental)"); layout.add(l, 0, row); livePreviews.setSelected(Config.getInstance().getSettings().livePreviews); @@ -474,6 +464,40 @@ public class SettingsTab extends Tab implements TabSelectionListener { GridPane.setMargin(livePreviews, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); layout.add(livePreviews, 1, row++); + Tooltip tt = new Tooltip("The overviews will still be updated, but the thumbnails won't be changed. This is useful for less powerful systems."); + l = new Label("Update thumbnails"); + l.setTooltip(tt); + layout.add(l, 0, row); + updateThumbnails.setTooltip(tt); + updateThumbnails.setSelected(Config.getInstance().getSettings().updateThumbnails); + updateThumbnails.setOnAction((e) -> { + Config.getInstance().getSettings().updateThumbnails = updateThumbnails.isSelected(); + saveConfig(); + }); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(updateThumbnails, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(updateThumbnails, 1, row++); + + tt = new Tooltip("Update the thumbnail overviews every x seconds"); + l = new Label("Update overview interval (seconds)"); + l.setTooltip(tt); + layout.add(l, 0, row); + overviewUpdateIntervalInSecs = new TextField(Integer.toString(Config.getInstance().getSettings().overviewUpdateIntervalInSecs)); + overviewUpdateIntervalInSecs.setTooltip(tt); + overviewUpdateIntervalInSecs.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + overviewUpdateIntervalInSecs.setText(newValue.replaceAll("[^\\d]", "")); + } + if(!overviewUpdateIntervalInSecs.getText().isEmpty()) { + Config.getInstance().getSettings().overviewUpdateIntervalInSecs = Integer.parseInt(overviewUpdateIntervalInSecs.getText()); + saveConfig(); + showRestartRequired(); + } + }); + GridPane.setMargin(l, new Insets(3, 0, 0, 0)); + GridPane.setMargin(overviewUpdateIntervalInSecs, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + layout.add(overviewUpdateIntervalInSecs, 1, row++); + l = new Label("Start Tab"); layout.add(l, 0, row); startTab = new ComboBox<>(); @@ -482,15 +506,16 @@ public class SettingsTab extends Tab implements TabSelectionListener { saveConfig(); }); layout.add(startTab, 1, row++); - GridPane.setMargin(l, new Insets(3, 0, 0, 0)); - GridPane.setMargin(startTab, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(l, new Insets(0, 0, 0, 0)); + GridPane.setMargin(startTab, new Insets(0, 0, 0, CHECKBOX_MARGIN)); + overviewUpdateIntervalInSecs.maxWidthProperty().bind(startTab.widthProperty()); l = new Label("Colors (Base / Accent)"); layout.add(l, 0, row); ColorSettingsPane colorSettingsPane = new ColorSettingsPane(this); layout.add(colorSettingsPane, 1, row++); GridPane.setMargin(l, new Insets(0, 0, 0, 0)); - GridPane.setMargin(colorSettingsPane, new Insets(CHECKBOX_MARGIN, 0, 0, CHECKBOX_MARGIN)); + GridPane.setMargin(colorSettingsPane, new Insets(0, 0, 0, CHECKBOX_MARGIN)); TitledPane general = new TitledPane("General", layout); general.setCollapsible(false); diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsFriendsTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsFriendsTab.java index 5b2e2a31..004dfec5 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsFriendsTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsFriendsTab.java @@ -1,8 +1,6 @@ package ctbrec.ui.sites.myfreecams; import static ctbrec.ui.sites.myfreecams.FriendsUpdateService.Mode.*; -import java.util.concurrent.TimeUnit; - import ctbrec.sites.mfc.MyFreeCams; import ctbrec.ui.FollowedTab; import ctbrec.ui.ThumbOverviewTab; @@ -13,12 +11,10 @@ import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; -import javafx.util.Duration; public class MyFreeCamsFriendsTab extends ThumbOverviewTab implements FollowedTab { public MyFreeCamsFriendsTab(MyFreeCams mfc) { super("Friends", new FriendsUpdateService(mfc), mfc); - updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); } @Override diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java index 68847ed1..998006e2 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTabProvider.java @@ -2,7 +2,6 @@ package ctbrec.ui.sites.myfreecams; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.TimeUnit; import ctbrec.recorder.Recorder; import ctbrec.sites.mfc.MyFreeCams; @@ -11,7 +10,6 @@ import ctbrec.ui.TabProvider; import ctbrec.ui.ThumbOverviewTab; import javafx.scene.Scene; import javafx.scene.control.Tab; -import javafx.util.Duration; public class MyFreeCamsTabProvider extends TabProvider { private Recorder recorder; @@ -30,7 +28,6 @@ public class MyFreeCamsTabProvider extends TabProvider { PaginatedScheduledService updateService = new OnlineCamsUpdateService(); ThumbOverviewTab online = new ThumbOverviewTab("Online", updateService, myFreeCams); online.setRecorder(recorder); - updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(online); friends = new MyFreeCamsFriendsTab(myFreeCams); @@ -40,13 +37,11 @@ public class MyFreeCamsTabProvider extends TabProvider { updateService = new HDCamsUpdateService(); ThumbOverviewTab hd = new ThumbOverviewTab("HD", updateService, myFreeCams); hd.setRecorder(recorder); - updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(hd); updateService = new PopularModelService(); ThumbOverviewTab pop = new ThumbOverviewTab("Most Popular", updateService, myFreeCams); pop.setRecorder(recorder); - updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(10))); tabs.add(pop); MyFreeCamsTableTab table = new MyFreeCamsTableTab(myFreeCams); diff --git a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java index 2c5fda0f..c9456ff0 100644 --- a/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java +++ b/client/src/main/java/ctbrec/ui/sites/myfreecams/MyFreeCamsTableTab.java @@ -1,6 +1,13 @@ package ctbrec.ui.sites.myfreecams; + +import static java.nio.file.StandardOpenOption.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -12,6 +19,8 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; +import org.json.JSONArray; +import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +37,7 @@ import ctbrec.ui.action.PlayAction; import ctbrec.ui.action.StartRecordingAction; import ctbrec.ui.controls.SearchBox; import javafx.beans.property.DoubleProperty; +import javafx.beans.property.Property; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; @@ -51,6 +61,7 @@ import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.SortType; import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.scene.input.ContextMenuEvent; @@ -58,6 +69,7 @@ import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.stage.FileChooser; import javafx.util.Duration; public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { @@ -73,13 +85,15 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { private Label count = new Label("models"); private List> columns = new ArrayList<>(); private ContextMenu popup; + private long lastJsonWrite = 0; public MyFreeCamsTableTab(MyFreeCams mfc) { this.mfc = mfc; setText("Tabular"); setClosable(false); - initUpdateService(); createGui(); + loadData(); + initUpdateService(); restoreState(); filter(filterInput.getText()); } @@ -131,6 +145,12 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { filteredModels.clear(); filter(filterInput.getText()); table.sort(); + + long now = System.currentTimeMillis(); + if( (now - lastJsonWrite) > TimeUnit.SECONDS.toMillis(30)) { + lastJsonWrite = now; + saveData(); + } } private void createGui() { @@ -151,11 +171,15 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { }); filterInput.getStyleClass().remove("search-box-icon"); HBox.setHgrow(filterInput, Priority.ALWAYS); + Button export = new Button("⬇"); + export.setOnAction(this::export); + export.setTooltip(new Tooltip("Export data")); + Button columnSelection = new Button("⚙"); - //Button columnSelection = new Button("⩩"); columnSelection.setOnAction(this::showColumnSelection); + columnSelection.setTooltip(new Tooltip("Select columns")); HBox topBar = new HBox(5); - topBar.getChildren().addAll(filterInput, count, columnSelection); + topBar.getChildren().addAll(filterInput, count, export, columnSelection); count.prefHeightProperty().bind(filterInput.heightProperty()); count.setAlignment(Pos.CENTER); layout.setTop(topBar); @@ -312,8 +336,11 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { 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(' '); + Object cellData = tc.getCellData(i); + if(cellData != null) { + String content = cellData.toString(); + sb.append(content).append(' '); + } } String searchText = sb.toString(); @@ -339,6 +366,43 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { } } + private void export(ActionEvent evt) { + FileChooser chooser = new FileChooser(); + File file = chooser.showSaveDialog(getTabPane().getScene().getWindow()); + if(file != null) { + try(FileOutputStream fout = new FileOutputStream(file)) { + PrintStream ps = new PrintStream(fout); + List union = new ArrayList<>(); + union.addAll(filteredModels); + union.addAll(observableModels); + ps.println("\"uid\",\"blurp\",\"camScore\",\"continent\",\"country\",\"ethnic\",\"name\",\"new\",\"occupation\",\"state\",\"tags\",\"topic\""); + for (ModelTableRow row : union) { + ps.print("\"" + row.uid + "\""); ps.print(','); + ps.print(escape(row.blurp)); ps.print(','); + ps.print(escape(row.camScore)); ps.print(','); + ps.print(escape(row.continent)); ps.print(','); + ps.print(escape(row.country)); ps.print(','); + ps.print(escape(row.ethnic)); ps.print(','); + ps.print(escape(row.name)); ps.print(','); + ps.print(escape(row.newModel)); ps.print(','); + ps.print(escape(row.occupation)); ps.print(','); + ps.print(escape(row.state)); ps.print(','); + ps.print(escape(row.tags)); ps.print(','); + ps.print(escape(row.topic)); + ps.println(); + } + } catch (Exception e) { + LOG.debug("Couldn't write mfc models table data: {}", e.getMessage()); + e.printStackTrace(); + } + } + } + + private String escape(Property prop) { + String value = prop.getValue() != null ? prop.getValue().toString() : ""; + return "\"" + value.replaceAll("\"", "\"\"") + "\""; + } + private void showColumnSelection(ActionEvent evt) { ContextMenu menu = new ContextMenu(); for (TableColumn tc : columns) { @@ -398,6 +462,68 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { if(updateService != null) { updateService.cancel(); } + saveData(); + } + + private void saveData() { + try { + List union = new ArrayList<>(); + union.addAll(filteredModels); + union.addAll(observableModels); + JSONArray data = new JSONArray(); + for (ModelTableRow row : union) { + JSONObject model = new JSONObject(); + model.put("uid", row.uid); + model.put("blurp", row.blurp.get()); + model.put("camScore", row.camScore.get()); + model.put("continent", row.continent.get()); + model.put("country", row.country.get()); + model.put("ethnic", row.ethnic.get()); + model.put("name", row.name.get()); + model.put("newModel", row.newModel.get()); + model.put("occupation", row.occupation.get()); + model.put("state", row.state.get()); + model.put("tags", row.tags.get()); + model.put("topic", row.topic.get()); + data.put(model); + } + File file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); + Files.write(file.toPath(), data.toString(2).getBytes("utf-8"), CREATE, WRITE); + } catch (Exception e) { + LOG.debug("Couldn't write mfc models table data: {}", e.getMessage()); + } + } + + private void loadData() { + try { + File file = new File(Config.getInstance().getConfigDir(), "mfc-models.json"); + if(!file.exists()) { + return; + } + String json = new String(Files.readAllBytes(file.toPath()), "utf-8"); + JSONArray data = new JSONArray(json); + for (int i = 0; i < data.length(); i++) { + try { + ModelTableRow row = new ModelTableRow(); + JSONObject model = data.getJSONObject(i); + row.uid = model.getInt("uid"); + row.blurp.set(model.optString("blurp")); + row.camScore.set(model.optDouble("camScore")); + row.continent.set(model.optString("continent")); + row.country.set(model.optString("country")); + row.ethnic.set(model.optString("ethnic")); + row.name.set(model.optString("name")); + row.newModel.set(model.optString("newModel")); + row.occupation.set(model.optString("occupation")); + row.state.set(model.optString("state")); + row.tags.set(model.optString("tags")); + row.topic.set(model.optString("topic")); + observableModels.add(row); + } catch (Exception e) {} + } + } catch (Exception e) { + LOG.debug("Couldn't read mfc models table data: {}", e.getMessage()); + } } private void saveState() { @@ -463,28 +589,31 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { update(st); } + private ModelTableRow() { + } + 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")); + setProperty(name, Optional.ofNullable(st.getNm())); + setProperty(state, Optional.ofNullable(st.getVs()).map(vs -> ctbrec.sites.mfc.State.of(vs).toString())); + setProperty(camScore, Optional.ofNullable(st.getM()).map(m -> m.getCamscore())); + Optional isNew = Optional.ofNullable(st.getM()).map(m -> m.getNewModel()); + if(isNew.isPresent()) { + newModel.set(isNew.get() == 1 ? "new" : ""); + } + setProperty(ethnic, Optional.ofNullable(st.getU()).map(u -> u.getEthnic())); + setProperty(country, Optional.ofNullable(st.getU()).map(u -> u.getCountry())); + setProperty(continent, Optional.ofNullable(st.getM()).map(m -> m.getContinent())); + setProperty(occupation, Optional.ofNullable(st.getU()).map(u -> u.getOccupation())); Set tagSet = Optional.ofNullable(st.getM()).map(m -> m.getTags()).orElse(Collections.emptySet()); - if(tagSet.isEmpty()) { - tags.set(""); - } else { + if(!tagSet.isEmpty()) { 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")); + setProperty(blurp, Optional.ofNullable(st.getU()).map(u -> u.getBlurb())); String tpc = Optional.ofNullable(st.getM()).map(m -> m.getTopic()).orElse("n/a"); try { tpc = URLDecoder.decode(tpc, "utf-8"); @@ -494,6 +623,12 @@ public class MyFreeCamsTableTab extends Tab implements TabSelectionListener { topic.set(tpc); } + private void setProperty(Property prop, Optional value) { + if(value.isPresent() && !Objects.equals(value.get(), prop.getValue())) { + prop.setValue(value.get()); + } + } + public StringProperty nameProperty() { return name; }; diff --git a/common/src/main/java/ctbrec/Settings.java b/common/src/main/java/ctbrec/Settings.java index c96cf985..5aedbdb0 100644 --- a/common/src/main/java/ctbrec/Settings.java +++ b/common/src/main/java/ctbrec/Settings.java @@ -92,6 +92,7 @@ public class Settings { public String colorBase = "#FFFFFF"; public String colorAccent = "#FFFFFF"; public int onlineCheckIntervalInSecs = 60; + public int overviewUpdateIntervalInSecs = 10; public String recordedModelsSortColumn = ""; public String recordedModelsSortType = ""; public double[] recordedModelsColumnWidths = new double[0]; diff --git a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java index 36bd9019..7c567fc6 100644 --- a/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java +++ b/common/src/main/java/ctbrec/sites/camsoda/CamsodaModel.java @@ -81,6 +81,9 @@ public class CamsodaModel extends AbstractModel { case "offline": onlineState = OFFLINE; break; + case "connected": + onlineState = AWAY; + break; case "private": onlineState = PRIVATE; break;