forked from j62/ctbrec
1
0
Fork 0

Playing around with notifications

This commit is contained in:
0xboobface 2018-11-28 23:24:06 +01:00
parent 1e51298f41
commit 4150a2911b
18 changed files with 1116 additions and 7 deletions

View File

@ -10,19 +10,21 @@ import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.common.eventbus.AsyncEventBus; import com.google.common.eventbus.Subscribe;
import com.google.common.eventbus.EventBus;
import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types; import com.squareup.moshi.Types;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.EventBusHolder;
import ctbrec.Model;
import ctbrec.StringUtil; import ctbrec.StringUtil;
import ctbrec.Version; import ctbrec.Version;
import ctbrec.io.HttpClient; import ctbrec.io.HttpClient;
@ -35,6 +37,7 @@ import ctbrec.sites.cam4.Cam4;
import ctbrec.sites.camsoda.Camsoda; import ctbrec.sites.camsoda.Camsoda;
import ctbrec.sites.chaturbate.Chaturbate; import ctbrec.sites.chaturbate.Chaturbate;
import ctbrec.sites.mfc.MyFreeCams; import ctbrec.sites.mfc.MyFreeCams;
import eu.hansolo.enzo.notification.Notification;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.HostServices; import javafx.application.HostServices;
import javafx.application.Platform; import javafx.application.Platform;
@ -45,6 +48,7 @@ import javafx.scene.control.Alert;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import javafx.scene.control.TabPane; import javafx.scene.control.TabPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.Stage; import javafx.stage.Stage;
import okhttp3.Request; import okhttp3.Request;
@ -54,17 +58,19 @@ public class CamrecApplication extends Application {
static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class); static final transient Logger LOG = LoggerFactory.getLogger(CamrecApplication.class);
private Stage primaryStage;
private Config config; private Config config;
private Recorder recorder; private Recorder recorder;
static HostServices hostServices; static HostServices hostServices;
private SettingsTab settingsTab; private SettingsTab settingsTab;
private TabPane rootPane = new TabPane(); private TabPane rootPane = new TabPane();
static EventBus bus;
private List<Site> sites = new ArrayList<>(); private List<Site> sites = new ArrayList<>();
public static HttpClient httpClient; public static HttpClient httpClient;
private Notification.Notifier notifier;
@Override @Override
public void start(Stage primaryStage) throws Exception { public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
logEnvironment(); logEnvironment();
sites.add(new BongaCams()); sites.add(new BongaCams());
sites.add(new Cam4()); sites.add(new Cam4());
@ -73,7 +79,6 @@ public class CamrecApplication extends Application {
sites.add(new MyFreeCams()); sites.add(new MyFreeCams());
loadConfig(); loadConfig();
createHttpClient(); createHttpClient();
bus = new AsyncEventBus(Executors.newSingleThreadExecutor());
hostServices = getHostServices(); hostServices = getHostServices();
createRecorder(); createRecorder();
for (Site site : sites) { for (Site site : sites) {
@ -88,6 +93,14 @@ public class CamrecApplication extends Application {
} }
createGui(primaryStage); createGui(primaryStage);
checkForUpdates(); checkForUpdates();
new Thread(() -> {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
Platform.runLater(() -> registerAlertSystem());
}).start();
} }
private void logEnvironment() { private void logEnvironment() {
@ -197,6 +210,33 @@ public class CamrecApplication extends Application {
}); });
} }
private void registerAlertSystem() {
Notification.Notifier.setNotificationOwner(primaryStage);
notifier = Notification.Notifier.INSTANCE;
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void modelEvent(Map<String, Object> e) {
try {
if (Objects.equals("model.status", e.get("event"))) {
String status = (String) e.get("status");
Model model = (Model) e.get("model");
LOG.debug("Alert: {} is {}", model.getName(), status);
if (Objects.equals("online", status)) {
Platform.runLater(() -> {
AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3");
clip.play();
Notification notification = new Notification("Model Online", model.getName() + " is now online");
notifier.notify(notification);
});
}
}
} catch (Exception e1) {
e1.printStackTrace();
}
}
});
}
private void writeColorSchemeStyleSheet(Stage primaryStage) { private void writeColorSchemeStyleSheet(Stage primaryStage) {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css"); File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) { try(FileOutputStream fos = new FileOutputStream(colorCss)) {

View File

@ -25,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.EventBusHolder;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.recorder.Recorder; import ctbrec.recorder.Recorder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
@ -468,7 +469,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener {
Map<String, Object> event = new HashMap<>(); Map<String, Object> event = new HashMap<>();
event.put("event", "tokens.sent"); event.put("event", "tokens.sent");
event.put("amount", tokens); event.put("amount", tokens);
CamrecApplication.bus.post(event); EventBusHolder.BUS.post(event);
} catch (Exception e1) { } catch (Exception e1) {
showError("Couldn't send tip", "An error occured while sending tip:", e1); showError("Couldn't send tip", "An error occured while sending tip:", e1);
} }

View File

@ -9,6 +9,7 @@ import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
import ctbrec.EventBusHolder;
import ctbrec.sites.Site; import ctbrec.sites.Site;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -24,7 +25,7 @@ public class TokenLabel extends Label {
public TokenLabel(Site site) { public TokenLabel(Site site) {
this.site = site; this.site = site;
setText("Tokens: loading…"); setText("Tokens: loading…");
CamrecApplication.bus.register(new Object() { EventBusHolder.BUS.register(new Object() {
@Subscribe @Subscribe
public void tokensUpdates(Map<String, Object> e) { public void tokensUpdates(Map<String, Object> e) {
if (Objects.equals("tokens", e.get("event"))) { if (Objects.equals("tokens", e.get("event"))) {

View File

@ -0,0 +1,369 @@
/*
* Copyright (c) 2013 by Gerrit Grunwald
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.hansolo.enzo.notification;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Popup;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
/**
* Created by
* User: hansolo
* Date: 01.07.13
* Time: 07:10
*/
public class Notification {
public static final Image INFO_ICON = new Image(Notifier.class.getResourceAsStream("info.png"));
public static final Image WARNING_ICON = new Image(Notifier.class.getResourceAsStream("warning.png"));
public static final Image SUCCESS_ICON = new Image(Notifier.class.getResourceAsStream("success.png"));
public static final Image ERROR_ICON = new Image(Notifier.class.getResourceAsStream("error.png"));
public final String TITLE;
public final String MESSAGE;
public final Image IMAGE;
// ******************** Constructors **************************************
public Notification(final String TITLE, final String MESSAGE) {
this(TITLE, MESSAGE, null);
}
public Notification(final String MESSAGE, final Image IMAGE) {
this("", MESSAGE, IMAGE);
}
public Notification(final String TITLE, final String MESSAGE, final Image IMAGE) {
this.TITLE = TITLE;
this.MESSAGE = MESSAGE;
this.IMAGE = IMAGE;
}
// ******************** Inner Classes *************************************
public enum Notifier {
INSTANCE;
private static final double ICON_WIDTH = 24;
private static final double ICON_HEIGHT = 24;
private static double width = 300;
private static double height = 80;
private static double offsetX = 0;
private static double offsetY = 25;
private static double spacingY = 5;
private static Pos popupLocation = Pos.TOP_RIGHT;
private static Stage stageRef = null;
private Duration popupLifetime;
private Stage stage;
private Scene scene;
private ObservableList<Popup> popups;
// ******************** Constructor ***************************************
private Notifier() {
init();
initGraphics();
}
// ******************** Initialization ************************************
private void init() {
popupLifetime = Duration.millis(5000);
popups = FXCollections.observableArrayList();
}
private void initGraphics() {
scene = new Scene(new Region());
scene.setFill(null);
scene.getStylesheets().add(getClass().getResource("notifier.css").toExternalForm());
stage = new Stage();
stage.initStyle(StageStyle.TRANSPARENT);
stage.setScene(scene);
}
// ******************** Methods *******************************************
/**
* @param STAGE_REF The Notification will be positioned relative to the given Stage.<br>
* If null then the Notification will be positioned relative to the primary Screen.
* @param POPUP_LOCATION The default is TOP_RIGHT of primary Screen.
*/
public static void setPopupLocation(final Stage STAGE_REF, final Pos POPUP_LOCATION) {
if (null != STAGE_REF) {
INSTANCE.stage.initOwner(STAGE_REF);
Notifier.stageRef = STAGE_REF;
}
Notifier.popupLocation = POPUP_LOCATION;
}
/**
* Sets the Notification's owner stage so that when the owner
* stage is closed Notifications will be shut down as well.<br>
* This is only needed if <code>setPopupLocation</code> is called
* <u>without</u> a stage reference.
* @param OWNER
*/
public static void setNotificationOwner(final Stage OWNER) {
INSTANCE.stage.initOwner(OWNER);
}
/**
* @param OFFSET_X The horizontal shift required.
* <br> The default is 0 px.
*/
public static void setOffsetX(final double OFFSET_X) {
Notifier.offsetX = OFFSET_X;
}
/**
* @param OFFSET_Y The vertical shift required.
* <br> The default is 25 px.
*/
public static void setOffsetY(final double OFFSET_Y) {
Notifier.offsetY = OFFSET_Y;
}
/**
* @param WIDTH The default is 300 px.
*/
public static void setWidth(final double WIDTH) {
Notifier.width = WIDTH;
}
/**
* @param HEIGHT The default is 80 px.
*/
public static void setHeight(final double HEIGHT) {
Notifier.height = HEIGHT;
}
/**
* @param SPACING_Y The spacing between multiple Notifications.
* <br> The default is 5 px.
*/
public static void setSpacingY(final double SPACING_Y) {
Notifier.spacingY = SPACING_Y;
}
public void stop() {
popups.clear();
stage.close();
}
/**
* Returns the Duration that the notification will stay on screen before it
* will fade out.
* @return the Duration the popup notification will stay on screen
*/
public Duration getPopupLifetime() {
return popupLifetime;
}
/**
* Defines the Duration that the popup notification will stay on screen before it
* will fade out. The parameter is limited to values between 2 and 20 seconds.
* @param POPUP_LIFETIME
*/
public void setPopupLifetime(final Duration POPUP_LIFETIME) {
popupLifetime = Duration.millis(clamp(2000, 20000, POPUP_LIFETIME.toMillis()));
}
/**
* Show the given Notification on the screen
* @param NOTIFICATION
*/
public void notify(final Notification NOTIFICATION) {
preOrder();
showPopup(NOTIFICATION);
}
/**
* Show a Notification with the given parameters on the screen
* @param TITLE
* @param MESSAGE
* @param IMAGE
*/
public void notify(final String TITLE, final String MESSAGE, final Image IMAGE) {
notify(new Notification(TITLE, MESSAGE, IMAGE));
}
/**
* Show a Notification with the given title and message and an Info icon
* @param TITLE
* @param MESSAGE
*/
public void notifyInfo(final String TITLE, final String MESSAGE) {
notify(new Notification(TITLE, MESSAGE, Notification.INFO_ICON));
}
/**
* Show a Notification with the given title and message and a Warning icon
* @param TITLE
* @param MESSAGE
*/
public void notifyWarning(final String TITLE, final String MESSAGE) {
notify(new Notification(TITLE, MESSAGE, Notification.WARNING_ICON));
}
/**
* Show a Notification with the given title and message and a Checkmark icon
* @param TITLE
* @param MESSAGE
*/
public void notifySuccess(final String TITLE, final String MESSAGE) {
notify(new Notification(TITLE, MESSAGE, Notification.SUCCESS_ICON));
}
/**
* Show a Notification with the given title and message and an Error icon
* @param TITLE
* @param MESSAGE
*/
public void notifyError(final String TITLE, final String MESSAGE) {
notify(new Notification(TITLE, MESSAGE, Notification.ERROR_ICON));
}
/**
* Makes sure that the given VALUE is within the range of MIN to MAX
* @param MIN
* @param MAX
* @param VALUE
* @return
*/
private double clamp(final double MIN, final double MAX, final double VALUE) {
if (VALUE < MIN) return MIN;
if (VALUE > MAX) return MAX;
return VALUE;
}
/**
* Reorder the popup Notifications on screen so that the latest Notification will stay on top
*/
private void preOrder() {
if (popups.isEmpty()) return;
for (int i = 0 ; i < popups.size() ; i++) {
switch (popupLocation) {
case TOP_LEFT: case TOP_CENTER: case TOP_RIGHT: popups.get(i).setY(popups.get(i).getY() + height + spacingY); break;
default: popups.get( i ).setY( popups.get( i ).getY() - height - spacingY);
}
}
}
/**
* Creates and shows a popup with the data from the given Notification object
* @param NOTIFICATION
*/
private void showPopup(final Notification NOTIFICATION) {
Label title = new Label(NOTIFICATION.TITLE);
title.getStyleClass().add("title");
ImageView icon = new ImageView(NOTIFICATION.IMAGE);
icon.setFitWidth(ICON_WIDTH);
icon.setFitHeight(ICON_HEIGHT);
Label message = new Label(NOTIFICATION.MESSAGE, icon);
message.getStyleClass().add("message");
VBox popupLayout = new VBox();
popupLayout.setSpacing(10);
popupLayout.setPadding(new Insets(10, 10, 10, 10));
popupLayout.getChildren().addAll(title, message);
StackPane popupContent = new StackPane();
popupContent.setPrefSize(width, height);
popupContent.getStyleClass().add("notification");
popupContent.getChildren().addAll(popupLayout);
final Popup POPUP = new Popup();
POPUP.setX( getX() );
POPUP.setY( getY() );
System.out.println(POPUP.getX() + "," + POPUP.getY());
POPUP.getContent().add(popupContent);
popups.add(POPUP);
// Add a timeline for popup fade out
KeyValue fadeOutBegin = new KeyValue(POPUP.opacityProperty(), 1.0);
KeyValue fadeOutEnd = new KeyValue(POPUP.opacityProperty(), 0.0);
KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin);
KeyFrame kfEnd = new KeyFrame(Duration.millis(500), fadeOutEnd);
Timeline timeline = new Timeline(kfBegin, kfEnd);
timeline.setDelay(popupLifetime);
timeline.setOnFinished(actionEvent -> Platform.runLater(() -> {
POPUP.hide();
popups.remove(POPUP);
}));
// Move popup to the right during fade out
//POPUP.opacityProperty().addListener((observableValue, oldOpacity, opacity) -> popup.setX(popup.getX() + (1.0 - opacity.doubleValue()) * popup.getWidth()) );
if (stage.isShowing()) {
stage.toFront();
} else {
stage.show();
}
POPUP.show(stage);
timeline.play();
}
private double getX() {
if (null == stageRef) return calcX( 0.0, Screen.getPrimary().getBounds().getWidth() );
return calcX(stageRef.getX(), stageRef.getWidth());
}
private double getY() {
if (null == stageRef) return calcY( 0.0, Screen.getPrimary().getBounds().getHeight() );
return calcY(stageRef.getY(), stageRef.getHeight());
}
private double calcX(final double LEFT, final double TOTAL_WIDTH) {
switch (popupLocation) {
case TOP_LEFT : case CENTER_LEFT : case BOTTOM_LEFT : return LEFT + offsetX;
case TOP_CENTER: case CENTER : case BOTTOM_CENTER: return LEFT + (TOTAL_WIDTH - width) * 0.5 - offsetX;
case TOP_RIGHT : case CENTER_RIGHT: case BOTTOM_RIGHT : return LEFT + TOTAL_WIDTH - width - offsetX;
default: return 0.0;
}
}
private double calcY(final double TOP, final double TOTAL_HEIGHT ) {
switch (popupLocation) {
case TOP_LEFT : case TOP_CENTER : case TOP_RIGHT : return TOP + offsetY;
case CENTER_LEFT: case CENTER : case CENTER_RIGHT: return TOP + (TOTAL_HEIGHT- height)/2 - offsetY;
case BOTTOM_LEFT: case BOTTOM_CENTER: case BOTTOM_RIGHT: return TOP + TOTAL_HEIGHT - height - offsetY;
default: return 0.0;
}
}
}
}

View File

@ -0,0 +1,4 @@
Enzo
====
A repo that contains custom controls for JavaFX 8 (current version is hosted on bitbucket)

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2013 by Gerrit Grunwald
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.root {
-fx-background-color: transparent;
-fx-fill : transparent;
}
.notification {
-fx-background-color : -fx-background;
-fx-background-radius: 5;
-fx-effect : innershadow(two-pass-box, rgba(255, 255, 255, 0.6), 5.0, 0.25, 0, 0);
-foreground-color : -fx-base;
-icon-color : -fx-base;
}
.notification .title {
-fx-font-size : 1.083333em;
-fx-font-weight: bold;
-fx-text-fill : -foreground-color;
}
.notification .message {
-fx-font-size : 1.0em;
-fx-content-display : left;
-fx-graphic-text-gap: 10;
-fx-text-fill : -foreground-color;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

View File

@ -0,0 +1,19 @@
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.media.AudioClip;
import javafx.stage.Stage;
public class AudioTest extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
AudioClip clip = new AudioClip("file:///tmp/Oxygen-Im-Highlight-Msg.mp3");
clip.cycleCountProperty().set(3);
clip.play();
Platform.exit();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@ -0,0 +1,44 @@
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import javafx.stage.Stage;
public class HlsTest extends Application {
// media = new Media("http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8");
private static final String MEDIA_URL = "http://localhost:3202/hls/sun_shine_baby/2018-11-28_20-43/playlist.m3u8";
private Media media;
private MediaPlayer mediaPlayer;
private MediaControl mediaControl;
public Parent createContent() {
media = new Media(MEDIA_URL);
mediaPlayer = new MediaPlayer(media);
mediaPlayer.setOnError(()-> {
mediaPlayer.getError().printStackTrace(System.err);
});
mediaControl = new MediaControl(mediaPlayer);
mediaControl.setMinSize(480, 280);
mediaControl.setPrefSize(480, 280);
mediaControl.setMaxSize(480, 280);
return mediaControl;
}
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(createContent()));
primaryStage.show();
}
@Override
public void stop() {
mediaPlayer.stop();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@ -0,0 +1,355 @@
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.media.MediaPlayer;
import javafx.scene.media.MediaView;
import javafx.stage.Stage;
import javafx.util.Duration;
public class MediaControl extends BorderPane {
private MediaPlayer mp;
private MediaView mediaView;
private final boolean repeat = false;
private boolean stopRequested = false;
private boolean atEndOfMedia = false;
private Duration duration;
private Slider timeSlider;
private Label playTime;
private Slider volumeSlider;
private HBox mediaBar;
private Pane mvPane;
private Stage newStage;
private boolean fullScreen = false;
@Override
protected void layoutChildren() {
if (mediaView != null && getBottom() != null) {
mediaView.setFitWidth(getWidth());
mediaView.setFitHeight(getHeight() - getBottom().prefHeight(-1));
}
super.layoutChildren();
if (mediaView != null && getCenter() != null) {
mediaView.setTranslateX((((Pane) getCenter()).getWidth() - mediaView.prefWidth(-1)) / 2);
mediaView.setTranslateY((((Pane) getCenter()).getHeight() - mediaView.prefHeight(-1)) / 2);
}
}
@Override
protected double computeMinWidth(double height) {
return mediaBar.prefWidth(-1);
}
@Override
protected double computeMinHeight(double width) {
return 200;
}
@Override
protected double computePrefWidth(double height) {
return Math.max(mp.getMedia().getWidth(), mediaBar.prefWidth(height));
}
@Override
protected double computePrefHeight(double width) {
return mp.getMedia().getHeight() + mediaBar.prefHeight(width);
}
@Override
protected double computeMaxWidth(double height) {
return Double.MAX_VALUE;
}
@Override
protected double computeMaxHeight(double width) {
return Double.MAX_VALUE;
}
public MediaControl(final MediaPlayer mp) {
this.mp = mp;
setStyle("-fx-background-color: #bfc2c7;"); // TODO: Use css file
mediaView = new MediaView(mp);
mvPane = new Pane();
mvPane.getChildren().add(mediaView);
mvPane.setStyle("-fx-background-color: black;"); // TODO: Use css file
setCenter(mvPane);
mediaBar = new HBox(5.0);
mediaBar.setPadding(new Insets(5, 10, 5, 10));
mediaBar.setAlignment(Pos.CENTER_LEFT);
BorderPane.setAlignment(mediaBar, Pos.CENTER);
final Button playButton = new Button();
playButton.setMinWidth(Control.USE_PREF_SIZE);
String PLAY = "playbutton.png";
String PAUSE = "pausebutton.png";
Image PlayButton = new Image(getClass().getResourceAsStream(PLAY));
Image PauseButton = new Image(getClass().getResourceAsStream(PAUSE));
ImageView imageViewPlay = new ImageView(PlayButton);
ImageView imageViewPause = new ImageView(PauseButton);
playButton.setGraphic(imageViewPlay);
playButton.setOnAction((ActionEvent e) -> {
updateValues();
MediaPlayer.Status status = mp.getStatus();
if (status == MediaPlayer.Status.UNKNOWN || status == MediaPlayer.Status.HALTED) {
// don't do anything in these states
return;
}
if (status == MediaPlayer.Status.PAUSED || status == MediaPlayer.Status.READY || status == MediaPlayer.Status.STOPPED) {
// rewind the movie if we're sitting at the end
if (atEndOfMedia) {
mp.seek(mp.getStartTime());
atEndOfMedia = false;
playButton.setGraphic(imageViewPlay);
// playButton.setText(">");
updateValues();
}
mp.play();
playButton.setGraphic(imageViewPause);
// playButton.setText("||");
} else {
mp.pause();
}
});
ReadOnlyObjectProperty<Duration> time = mp.currentTimeProperty();
time.addListener((ObservableValue<? extends Duration> observable, Duration oldValue, Duration newValue) -> {
//updateValues();
});
mp.setOnPlaying(() -> {
if (stopRequested) {
mp.pause();
stopRequested = false;
} else {
playButton.setGraphic(imageViewPause);
// playButton.setText("||");
}
});
mp.setOnPaused(() -> {
playButton.setGraphic(imageViewPlay);
// playButton.setText("||");
});
mp.setOnReady(() -> {
duration = mp.getMedia().getDuration();
updateValues();
});
mp.setCycleCount(repeat ? MediaPlayer.INDEFINITE : 1);
mp.setOnEndOfMedia(() -> {
if (!repeat) {
playButton.setGraphic(imageViewPlay);
// playButton.setText(">");
stopRequested = true;
atEndOfMedia = true;
}
});
mediaBar.getChildren().add(playButton);
// Time label
Label timeLabel = new Label("Time");
timeLabel.setMinWidth(Control.USE_PREF_SIZE);
mediaBar.getChildren().add(timeLabel);
// Time slider
timeSlider = new Slider();
timeSlider.setMinWidth(30);
timeSlider.setMaxWidth(Double.MAX_VALUE);
HBox.setHgrow(timeSlider, Priority.ALWAYS);
DoubleProperty timeValue = timeSlider.valueProperty();
timeValue.addListener((ObservableValue<? extends Number> observable, Number old, Number now) -> {
if (timeSlider.isValueChanging()) {
// multiply duration by percentage calculated by slider position
if (duration != null) {
System.out.println(timeSlider.getValue() + "%");
mp.seek(duration.multiply(timeSlider.getValue() / 100.0));
}
updateValues();
} else if (Math.abs(now.doubleValue() - old.doubleValue()) > 1.5) {
// multiply duration by percentage calculated by slider position
System.out.println(timeSlider.getValue() + "%");
if (duration != null) {
mp.seek(duration.multiply(timeSlider.getValue() / 100.0));
}
}
});
mediaBar.getChildren().add(timeSlider);
// Play label
playTime = new Label();
playTime.setMinWidth(Control.USE_PREF_SIZE);
mediaBar.getChildren().add(playTime);
// Fullscreen button
Button buttonFullScreen = new Button("Full Screen");
buttonFullScreen.setMinWidth(Control.USE_PREF_SIZE);
buttonFullScreen.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
if (!fullScreen) {
newStage = new Stage();
ReadOnlyBooleanProperty full = newStage.fullScreenProperty();
full.addListener((ObservableValue<? extends Boolean> ov, Boolean old, Boolean now) -> {
onFullScreen();
});
final BorderPane borderPane = new BorderPane() {
@Override
protected void layoutChildren() {
if (mediaView != null && getBottom() != null) {
mediaView.setFitWidth(getWidth());
double height = getHeight() - getBottom().prefHeight(-1);
mediaView.setFitHeight(height);
}
super.layoutChildren();
if (mediaView != null) {
final Pane center = (Pane) getCenter();
if (center != null) { // if smaller pane has content
double width = center.getWidth() - mediaView.prefWidth(-1);
double height = center.getHeight() - mediaView.prefHeight(-1);
double xval = width / 2.0;
double yval = height / 2.0;
mediaView.setTranslateX(xval);
mediaView.setTranslateY(yval);
}
}
}
};
setCenter(null);
setBottom(null);
borderPane.setCenter(mvPane);
borderPane.setBottom(mediaBar);
Scene newScene = new Scene(borderPane);
newStage.setScene(newScene);
// Workaround for disposing stage when exit fullscreen
newStage.setX(-100000);
newStage.setY(-100000);
newStage.setFullScreen(true);
fullScreen = true;
newStage.show();
} else {
// toggle FullScreen
fullScreen = false;
newStage.setFullScreen(false);
}
}
});
mediaBar.getChildren().add(buttonFullScreen);
// Volume label
Label volumeLabel = new Label("Vol");
volumeLabel.setMinWidth(Control.USE_PREF_SIZE);
mediaBar.getChildren().add(volumeLabel);
// Volume slider
volumeSlider = new Slider();
volumeSlider.setPrefWidth(70);
volumeSlider.setMinWidth(30);
volumeSlider.setMaxWidth(Region.USE_PREF_SIZE);
volumeSlider.valueProperty().addListener((Observable ov) -> {
});
final DoubleProperty volume = volumeSlider.valueProperty();
volume.addListener((ObservableValue<? extends Number> observable, Number old, Number now) -> {
mp.setVolume(volumeSlider.getValue() / 100.0);
});
mediaBar.getChildren().add(volumeSlider);
setBottom(mediaBar);
}
protected void onFullScreen() {
if (!newStage.isFullScreen()) {
fullScreen = false;
BorderPane smallBP = (BorderPane) newStage.getScene().getRoot();
smallBP.setCenter(null);
setCenter(mvPane);
smallBP.setBottom(null);
setBottom(mediaBar);
Platform.runLater(() -> {
newStage.close();
});
}
}
protected void updateValues() {
if (playTime != null && timeSlider != null && volumeSlider != null && duration != null) {
Platform.runLater(() -> {
Duration now = mp.getCurrentTime();
playTime.setText(formatTime(now, duration));
timeSlider.setDisable(duration.isUnknown());
if (!timeSlider.isDisabled() && duration.greaterThan(Duration.ZERO) && !timeSlider.isValueChanging()) {
final double value = now.divide(duration).toMillis() * 100.0;
timeSlider.setValue(value);
}
if (!volumeSlider.isValueChanging()) {
final int value = (int) Math.round(mp.getVolume() * 100);
volumeSlider.setValue(value);
}
});
}
}
private String formatTime(Duration elapsed, Duration duration) {
int intElapsed = (int) Math.floor(elapsed.toSeconds());
int elapsedHours = intElapsed / (60 * 60);
if (elapsedHours > 0) {
intElapsed -= elapsedHours * 60 * 60;
}
int elapsedMinutes = intElapsed / 60;
int elapsedSeconds = intElapsed - elapsedHours * 60 * 60 - elapsedMinutes * 60;
if (duration.greaterThan(Duration.ZERO)) {
int intDuration = (int) Math.floor(duration.toSeconds());
int durationHours = intDuration / (60 * 60);
if (durationHours > 0) {
intDuration -= durationHours * 60 * 60;
}
int durationMinutes = intDuration / 60;
int durationSeconds = intDuration - durationHours * 60 * 60 - durationMinutes * 60;
if (durationHours > 0) {
return String.format("%d:%02d:%02d/%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds, durationHours, durationMinutes,
durationSeconds);
} else {
return String.format("%02d:%02d/%02d:%02d", elapsedMinutes, elapsedSeconds, durationMinutes, durationSeconds);
}
} else {
if (elapsedHours > 0) {
return String.format("%d:%02d:%02d", elapsedHours, elapsedMinutes, elapsedSeconds);
} else {
return String.format("%02d:%02d", elapsedMinutes, elapsedSeconds);
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

View File

@ -0,0 +1,10 @@
package ctbrec;
import java.util.concurrent.Executors;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
public class EventBusHolder {
public static final EventBus BUS = new AsyncEventBus(Executors.newSingleThreadExecutor());
}

View File

@ -6,7 +6,9 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.time.Instant; import java.time.Instant;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import org.json.JSONObject; import org.json.JSONObject;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -16,6 +18,7 @@ import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi; import com.squareup.moshi.Moshi;
import ctbrec.Config; import ctbrec.Config;
import ctbrec.EventBusHolder;
import ctbrec.Hmac; import ctbrec.Hmac;
import ctbrec.Model; import ctbrec.Model;
import ctbrec.Recording; import ctbrec.Recording;
@ -95,6 +98,7 @@ public class RemoteRecorder implements Recorder {
models.add(model); models.add(model);
} else if ("stop".equals(action)) { } else if ("stop".equals(action)) {
models.remove(model); models.remove(model);
onlineModels.remove(model);
} }
} else { } else {
throw new HttpException(response.code(), response.message()); throw new HttpException(response.code(), response.message());
@ -231,6 +235,7 @@ public class RemoteRecorder implements Recorder {
if (response.isSuccessful()) { if (response.isSuccessful()) {
ModelListResponse resp = modelListResponseAdapter.fromJson(json); ModelListResponse resp = modelListResponseAdapter.fromJson(json);
if (resp.status.equals("success")) { if (resp.status.equals("success")) {
List<Model> previouslyOnline = onlineModels;
onlineModels = resp.models; onlineModels = resp.models;
for (Model model : models) { for (Model model : models) {
for (Site site : sites) { for (Site site : sites) {
@ -239,6 +244,25 @@ public class RemoteRecorder implements Recorder {
} }
} }
} }
for (Model prev : previouslyOnline) {
if(!onlineModels.contains(prev)) {
Map<String, Object> evt = new HashMap<>();
evt.put("event", "model.status");
evt.put("status", "offline");
evt.put("model", prev);
EventBusHolder.BUS.post(evt);
}
}
for (Model model : onlineModels) {
if(!previouslyOnline.contains(model)) {
Map<String, Object> evt = new HashMap<>();
evt.put("event", "model.status");
evt.put("status", "online");
evt.put("model", model);
EventBusHolder.BUS.post(evt);
}
}
} else { } else {
LOG.error("Server returned error: {} - {}", resp.status, resp.msg); LOG.error("Server returned error: {} - {}", resp.status, resp.msg);
} }