mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-12 16:47:31 -05:00
#829 Integrates JMBE Creator with new user preferences editor for auto-downloading and compiling the JMBE library from within sdrtrunk. (#844)
Co-authored-by: Denny <denny@denny-desktop>
This commit is contained in:
@@ -38,6 +38,8 @@ import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -68,12 +70,14 @@ public class JavaFxWindowManager extends Application
|
||||
public static final String USER_PREFERENCES_EDITOR = "preferences";
|
||||
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
|
||||
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
|
||||
public static final String STAGE_MONITOR_KEY_JMBE_EDITOR = "jmbe.editor";
|
||||
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
|
||||
public static final String STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR = "user.preferences";
|
||||
|
||||
private JFXPanel mJFXPanel;
|
||||
private ChannelMapEditor mChannelMapEditor;
|
||||
private IconManager mIconManager;
|
||||
private JmbeEditor mJmbeEditor;
|
||||
private PlaylistEditor mPlaylistEditor;
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
@@ -81,6 +85,7 @@ public class JavaFxWindowManager extends Application
|
||||
|
||||
private Stage mChannelMapStage;
|
||||
private Stage mIconManagerStage;
|
||||
private Stage mJmbeEditorStage;
|
||||
private Stage mPlaylistStage;
|
||||
private Stage mUserPreferencesStage;
|
||||
|
||||
@@ -205,6 +210,56 @@ public class JavaFxWindowManager extends Application
|
||||
return mIconManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a JMBE s editor request
|
||||
*/
|
||||
@Subscribe
|
||||
public void process(final JmbeEditorRequest request)
|
||||
{
|
||||
if(request.isCloseEditorRequest())
|
||||
{
|
||||
execute(() -> {
|
||||
getJmbeEditorStage().hide();
|
||||
mJmbeEditorStage = null;
|
||||
mJmbeEditor = null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
execute(() -> {
|
||||
getJmbeEditorStage().show();
|
||||
getJmbeEditorStage().requestFocus();
|
||||
getJmbeEditorStage().toFront();
|
||||
getJmbeEditor().process(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public Stage getJmbeEditorStage()
|
||||
{
|
||||
if(mJmbeEditorStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getJmbeEditor(), 650, 650);
|
||||
mJmbeEditorStage = new Stage();
|
||||
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
|
||||
mJmbeEditorStage.setScene(scene);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mJmbeEditorStage, STAGE_MONITOR_KEY_JMBE_EDITOR);
|
||||
}
|
||||
|
||||
return mJmbeEditorStage;
|
||||
}
|
||||
|
||||
public JmbeEditor getJmbeEditor()
|
||||
{
|
||||
if(mJmbeEditor == null)
|
||||
{
|
||||
mJmbeEditor = new JmbeEditor(mUserPreferences);
|
||||
}
|
||||
|
||||
return mJmbeEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy construct and access the playlist editor
|
||||
*/
|
||||
|
||||
@@ -243,7 +243,7 @@ public class PlaylistEditor extends BorderPane
|
||||
{
|
||||
if(mChannelEditor == null)
|
||||
{
|
||||
mChannelEditor = new ChannelEditor(mPlaylistManager);
|
||||
mChannelEditor = new ChannelEditor(mPlaylistManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mChannelEditor;
|
||||
|
||||
@@ -30,6 +30,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -60,9 +61,9 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
*/
|
||||
public AMConfigurationEditor(PlaylistManager playlistManager)
|
||||
public AMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getRecordPane());
|
||||
|
||||
@@ -25,13 +25,17 @@ package io.github.dsheirer.gui.playlist.channel;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelException;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -48,6 +52,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
@@ -78,6 +83,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
|
||||
private Button mPlayButton;
|
||||
private TextField mSystemField;
|
||||
@@ -97,9 +103,10 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
private IconNode mStopGraphicNode;
|
||||
private ChannelProcessingMonitor mChannelProcessingMonitor = new ChannelProcessingMonitor();
|
||||
|
||||
public ChannelConfigurationEditor(PlaylistManager playlistManager)
|
||||
public ChannelConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
@@ -317,6 +324,45 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
});
|
||||
}
|
||||
|
||||
if(requiresJmbeLibrarySetup() &&
|
||||
mUserPreferences.getJmbeLibraryPreference().getAlertIfMissingLibraryRequired() &&
|
||||
!getItem().processingProperty().get())
|
||||
{
|
||||
String content = "The decoder for this channel configuration requires the (optional) JMBE " +
|
||||
"library to produce audio and the JMBE library is not currently setup. Do you want to " +
|
||||
"setup the JMBE library?";
|
||||
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION, content, ButtonType.YES, ButtonType.NO);
|
||||
alert.setTitle("JMBE Library");
|
||||
alert.setHeaderText("Setup JMBE Library?");
|
||||
|
||||
Label label = new Label(content);
|
||||
label.setMaxWidth(Double.MAX_VALUE);
|
||||
label.setMaxHeight(Double.MAX_VALUE);
|
||||
label.getStyleClass().add("content");
|
||||
label.setWrapText(true);
|
||||
|
||||
CheckBox checkBox = new CheckBox("Don't Show This Again");
|
||||
checkBox.setOnAction(event1 -> {
|
||||
boolean dontShowAgain = checkBox.isSelected();
|
||||
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(!dontShowAgain);
|
||||
});
|
||||
|
||||
VBox contentBox = new VBox();
|
||||
contentBox.setPrefWidth(360);
|
||||
contentBox.setSpacing(10);
|
||||
contentBox.getChildren().addAll(label, checkBox);
|
||||
alert.getDialogPane().setContent(contentBox);
|
||||
alert.initOwner(getPlayButton().getScene().getWindow());
|
||||
Optional<ButtonType> optionalButtonType = alert.showAndWait();
|
||||
|
||||
if(optionalButtonType.isPresent() && optionalButtonType.get() == ButtonType.YES)
|
||||
{
|
||||
MyEventBus.getEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.JMBE_LIBRARY));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!getItem().processingProperty().get())
|
||||
{
|
||||
ThreadPool.SCHEDULED.execute(() -> {
|
||||
@@ -366,6 +412,17 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
return mPlayButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the decoder type for the channel configuration requires the JMBE library and if the
|
||||
* application is not currently setup for the JMBE library.
|
||||
*/
|
||||
private boolean requiresJmbeLibrarySetup()
|
||||
{
|
||||
return getItem() != null &&
|
||||
getItem().getDecodeConfiguration().getDecoderType().providesMBEAudioFrames() &&
|
||||
!mUserPreferences.getJmbeLibraryPreference().hasJmbeLibraryPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the text and graphic of the channel play button to reflect the playing state of the channel
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,7 @@ package io.github.dsheirer.gui.playlist.channel;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -44,26 +45,27 @@ public class ChannelConfigurationEditorFactory
|
||||
* @param playlistManager for the editor
|
||||
* @return constructed editor
|
||||
*/
|
||||
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager)
|
||||
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
|
||||
UserPreferences userPreferences)
|
||||
{
|
||||
switch(decoderType)
|
||||
{
|
||||
case AM:
|
||||
return new AMConfigurationEditor(playlistManager);
|
||||
return new AMConfigurationEditor(playlistManager, userPreferences);
|
||||
case NBFM:
|
||||
return new NBFMConfigurationEditor(playlistManager);
|
||||
return new NBFMConfigurationEditor(playlistManager, userPreferences);
|
||||
case LTR_NET:
|
||||
return new LTRNetConfigurationEditor(playlistManager);
|
||||
return new LTRNetConfigurationEditor(playlistManager, userPreferences);
|
||||
case LTR:
|
||||
return new LTRConfigurationEditor(playlistManager);
|
||||
return new LTRConfigurationEditor(playlistManager, userPreferences);
|
||||
case MPT1327:
|
||||
return new MPT1327ConfigurationEditor(playlistManager);
|
||||
return new MPT1327ConfigurationEditor(playlistManager, userPreferences);
|
||||
case PASSPORT:
|
||||
return new PassportConfigurationEditor(playlistManager);
|
||||
return new PassportConfigurationEditor(playlistManager, userPreferences);
|
||||
case P25_PHASE1:
|
||||
return new P25P1ConfigurationEditor(playlistManager);
|
||||
return new P25P1ConfigurationEditor(playlistManager, userPreferences);
|
||||
case P25_PHASE2:
|
||||
return new P25P2ConfigurationEditor(playlistManager);
|
||||
return new P25P2ConfigurationEditor(playlistManager, userPreferences);
|
||||
default:
|
||||
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
|
||||
{
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.module.decode.DecoderFactory;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import javafx.application.Platform;
|
||||
@@ -79,6 +80,7 @@ public class ChannelEditor extends SplitPane
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private TableView<Channel> mChannelTableView;
|
||||
private Label mPlaceholderLabel;
|
||||
private MenuButton mNewButton;
|
||||
@@ -101,10 +103,11 @@ public class ChannelEditor extends SplitPane
|
||||
* Constructs an instance
|
||||
* @param playlistManager containing playlists and channel configurations
|
||||
*/
|
||||
public ChannelEditor(PlaylistManager playlistManager)
|
||||
public ChannelEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager);
|
||||
mUserPreferences = userPreferences;
|
||||
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, userPreferences);
|
||||
|
||||
HBox channelsBox = new HBox();
|
||||
channelsBox.setSpacing(10.0);
|
||||
@@ -200,7 +203,8 @@ public class ChannelEditor extends SplitPane
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager);
|
||||
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
|
||||
mUserPreferences);
|
||||
|
||||
if(editor != null)
|
||||
{
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -74,10 +75,11 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public LTRConfigurationEditor(PlaylistManager playlistManager)
|
||||
public LTRConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -74,10 +75,11 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public LTRNetConfigurationEditor(PlaylistManager playlistManager)
|
||||
public LTRNetConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
@@ -36,6 +36,7 @@ import io.github.dsheirer.module.decode.mpt1327.DecodeConfigMPT1327;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -84,10 +85,11 @@ public class MPT1327ConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public MPT1327ConfigurationEditor(PlaylistManager playlistManager)
|
||||
public MPT1327ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
|
||||
@@ -33,6 +33,7 @@ import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -73,10 +74,11 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public NBFMConfigurationEditor(PlaylistManager playlistManager)
|
||||
public NBFMConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -80,10 +81,11 @@ public class P25P1ConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public P25P1ConfigurationEditor(PlaylistManager playlistManager)
|
||||
public P25P1ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
|
||||
@@ -35,6 +35,7 @@ import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameter
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -70,10 +71,11 @@ public class P25P2ConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public P25P2ConfigurationEditor(PlaylistManager playlistManager)
|
||||
public P25P2ConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
|
||||
@@ -34,6 +34,7 @@ import io.github.dsheirer.module.decode.passport.DecodeConfigPassport;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
@@ -66,10 +67,11 @@ public class PassportConfigurationEditor extends ChannelConfigurationEditor
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager
|
||||
* @param userPreferences
|
||||
*/
|
||||
public PassportConfigurationEditor(PlaylistManager playlistManager)
|
||||
public PassportConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import javafx.scene.control.TitledPane;
|
||||
@@ -45,9 +46,9 @@ public class UnknownConfigurationEditor extends ChannelConfigurationEditor
|
||||
* Constructs an instance
|
||||
* @param aliasModel
|
||||
*/
|
||||
public UnknownConfigurationEditor(PlaylistManager playlistManager)
|
||||
public UnknownConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
{
|
||||
super(playlistManager);
|
||||
super(playlistManager, userPreferences);
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
}
|
||||
|
||||
|
||||
@@ -22,17 +22,28 @@ package io.github.dsheirer.gui.preference.decoder;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.jmbe.JmbeCreator;
|
||||
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
||||
import io.github.dsheirer.jmbe.github.GitHub;
|
||||
import io.github.dsheirer.jmbe.github.Release;
|
||||
import io.github.dsheirer.jmbe.github.Version;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
@@ -45,27 +56,64 @@ import java.nio.file.Path;
|
||||
/**
|
||||
* Preference settings for decoders
|
||||
*/
|
||||
public class JmbeLibraryPreferenceEditor extends HBox
|
||||
public class JmbeLibraryPreferenceEditor extends VBox
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(JmbeLibraryPreferenceEditor.class);
|
||||
|
||||
private static final String PATH_NOT_SET = "(not set)";
|
||||
private JmbeLibraryPreference mJmbeLibraryPreference;
|
||||
private static final String CREATE_LIBRARY = "Create Library";
|
||||
private static final String CHECK_FOR_UPDATE = "Check For Library Update";
|
||||
private static final String LIBRARY_NOT_SETUP = "None. Click Select or Create Library button to setup.";
|
||||
|
||||
private UserPreferences mUserPreferences;
|
||||
private GridPane mEditorPane;
|
||||
private Label mJmbeLibraryLabel;
|
||||
private Button mPathToJmbeLibraryButton;
|
||||
private Button mPathToJmbeLibraryResetButton;
|
||||
private Label mJmbeVersionLabel;
|
||||
private Label mPathToJmbeLibraryLabel;
|
||||
private Button mSelectButton;
|
||||
private Button mResetButton;
|
||||
private Button mCreateButton;
|
||||
private HBox mButtonsBox;
|
||||
private CheckBox mAlertUserWhenMissingCheckBox;
|
||||
|
||||
public JmbeLibraryPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mJmbeLibraryPreference = userPreferences.getJmbeLibraryPreference();
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
//Register to receive directory preference update notifications so we can update the path labels
|
||||
MyEventBus.getEventBus().register(this);
|
||||
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
getChildren().add(getEditorPane());
|
||||
setPadding(new Insets(10,10,10,10));
|
||||
setSpacing(10);
|
||||
getChildren().addAll(getEditorPane(), getButtonsBox(), getAlertUserWhenMissingCheckBox());
|
||||
}
|
||||
|
||||
private CheckBox getAlertUserWhenMissingCheckBox()
|
||||
{
|
||||
if(mAlertUserWhenMissingCheckBox == null)
|
||||
{
|
||||
mAlertUserWhenMissingCheckBox = new CheckBox("Alert when decoder requires missing JMBE library");
|
||||
mAlertUserWhenMissingCheckBox.setSelected(mUserPreferences.getJmbeLibraryPreference()
|
||||
.getAlertIfMissingLibraryRequired());
|
||||
mAlertUserWhenMissingCheckBox.setOnAction(event -> {
|
||||
boolean alert = mAlertUserWhenMissingCheckBox.isSelected();
|
||||
mUserPreferences.getJmbeLibraryPreference().setAlertIfMissingLibraryRequired(alert);
|
||||
});
|
||||
}
|
||||
|
||||
return mAlertUserWhenMissingCheckBox;
|
||||
}
|
||||
|
||||
private HBox getButtonsBox()
|
||||
{
|
||||
if(mButtonsBox == null)
|
||||
{
|
||||
mButtonsBox = new HBox();
|
||||
mButtonsBox.setSpacing(10);
|
||||
mButtonsBox.getChildren().addAll(getCreateButton(), getSelectButton(), getResetButton());
|
||||
}
|
||||
|
||||
return mButtonsBox;
|
||||
}
|
||||
|
||||
private GridPane getEditorPane()
|
||||
@@ -73,85 +121,206 @@ public class JmbeLibraryPreferenceEditor extends HBox
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(10);
|
||||
|
||||
int row = 0;
|
||||
|
||||
GridPane.setMargin(getJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
|
||||
mEditorPane.add(getJmbeLibraryLabel(), 0, row);
|
||||
mEditorPane.add(getJmbeLibraryLabel(), 0, row, 2, 1);
|
||||
|
||||
Label versionLabel = new Label("Current Version:");
|
||||
GridPane.setHalignment(versionLabel, HPos.RIGHT);
|
||||
mEditorPane.add(versionLabel, 0, ++row);
|
||||
|
||||
mEditorPane.add(getJmbeVersionLabel(), 1, row);
|
||||
|
||||
Label fileLabel = new Label("File:");
|
||||
GridPane.setHalignment(fileLabel, HPos.RIGHT);
|
||||
mEditorPane.add(fileLabel, 0, ++row);
|
||||
|
||||
GridPane.setMargin(getPathToJmbeLibraryLabel(), new Insets(0, 10, 0, 0));
|
||||
mEditorPane.add(getPathToJmbeLibraryLabel(), 1, row);
|
||||
|
||||
GridPane.setMargin(getPathToJmbeLibraryButton(), new Insets(2, 10, 2, 0));
|
||||
mEditorPane.add(getPathToJmbeLibraryButton(), 2, row);
|
||||
|
||||
GridPane.setMargin(getPathToJmbeLibraryResetButton(), new Insets(2, 0, 2, 0));
|
||||
mEditorPane.add(getPathToJmbeLibraryResetButton(), 3, row++);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private Button getCreateButton()
|
||||
{
|
||||
if(mCreateButton == null)
|
||||
{
|
||||
mCreateButton = new Button();
|
||||
mCreateButton.setText(mUserPreferences.getJmbeLibraryPreference()
|
||||
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
|
||||
mCreateButton.setOnAction(event -> checkForUpdatedLibrary());
|
||||
}
|
||||
|
||||
return mCreateButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for availability of an updated JMBE library
|
||||
*/
|
||||
private void checkForUpdatedLibrary()
|
||||
{
|
||||
ThreadPool.SCHEDULED.execute(() -> {
|
||||
try
|
||||
{
|
||||
Version current = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
|
||||
final Release release = GitHub.getLatestRelease(JmbeCreator.GITHUB_JMBE_RELEASES_URL);
|
||||
|
||||
mLog.info("Checking for JMBE Library Updates ...");
|
||||
mLog.info("Current: " + (current != null ? current.toString() : "empty"));
|
||||
|
||||
final boolean canUpdate = (release != null) && ((current == null) ||
|
||||
(release.getVersion().compareTo(current) > 0));
|
||||
|
||||
if(release != null)
|
||||
{
|
||||
mLog.info("Available: " + release.getVersion().toString());
|
||||
}
|
||||
|
||||
if(canUpdate)
|
||||
{
|
||||
mLog.info("JMBE Library update is available");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("No JMBE library update is available at this time");
|
||||
}
|
||||
|
||||
Platform.runLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
if(canUpdate)
|
||||
{
|
||||
String content = "JMBE library version " + release.getVersion().toString() +
|
||||
" is available. Would you like to download the latest source code and " +
|
||||
"create an updated JMBE library?";
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, content, ButtonType.YES, ButtonType.NO);
|
||||
alert.setResizable(true);
|
||||
|
||||
//Workaroud for dialog sizing issue on Windows
|
||||
alert.getDialogPane().setMinHeight(Region.USE_PREF_SIZE);
|
||||
|
||||
alert.setTitle("JMBE Library Update Check");
|
||||
alert.setHeaderText("Update is available");
|
||||
alert.initOwner(getCreateButton().getScene().getWindow());
|
||||
alert.showAndWait().ifPresent(buttonType -> {
|
||||
if(buttonType == ButtonType.YES)
|
||||
{
|
||||
mLog.info("Posting JMBE editor request to event bus");
|
||||
MyEventBus.getEventBus().post(new JmbeEditorRequest(release));
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION,
|
||||
"No JMBE library update is available.", ButtonType.OK);
|
||||
alert.setTitle("JMBE Library Update Check");
|
||||
if(release != null)
|
||||
{
|
||||
alert.setHeaderText("No update available");
|
||||
}
|
||||
else
|
||||
{
|
||||
alert.setHeaderText("Unable to determine latest release version. Check network connection");
|
||||
}
|
||||
alert.initOwner(getCreateButton().getScene().getWindow());
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error during JavaFX portion of create library");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error during create library", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Label getJmbeLibraryLabel()
|
||||
{
|
||||
if(mJmbeLibraryLabel == null)
|
||||
{
|
||||
mJmbeLibraryLabel = new Label("JMBE Audio Library:");
|
||||
mJmbeLibraryLabel = new Label("JMBE Audio Library");
|
||||
}
|
||||
|
||||
return mJmbeLibraryLabel;
|
||||
}
|
||||
|
||||
private Button getPathToJmbeLibraryButton()
|
||||
private Label getJmbeVersionLabel()
|
||||
{
|
||||
if(mPathToJmbeLibraryButton == null)
|
||||
if(mJmbeVersionLabel == null)
|
||||
{
|
||||
mPathToJmbeLibraryButton = new Button("Change...");
|
||||
mPathToJmbeLibraryButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
mJmbeVersionLabel = new Label();
|
||||
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
|
||||
|
||||
if(version != null)
|
||||
{
|
||||
mJmbeVersionLabel.setText(version.toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
mJmbeVersionLabel.setText(LIBRARY_NOT_SETUP);
|
||||
}
|
||||
}
|
||||
|
||||
return mJmbeVersionLabel;
|
||||
}
|
||||
|
||||
private Button getSelectButton()
|
||||
{
|
||||
if(mSelectButton == null)
|
||||
{
|
||||
mSelectButton = new Button("Select...");
|
||||
mSelectButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Select JMBE Audio Library Location");
|
||||
Stage stage = (Stage)getPathToJmbeLibraryButton().getScene().getWindow();
|
||||
fileChooser.setInitialDirectory(mUserPreferences.getDirectoryPreference().getDefaultJmbeDirectory().toFile());
|
||||
Stage stage = (Stage)getSelectButton().getScene().getWindow();
|
||||
File selected = fileChooser.showOpenDialog(stage);
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
mJmbeLibraryPreference.setPathJmbeLibrary(selected.toPath());
|
||||
mUserPreferences.getJmbeLibraryPreference().setPathJmbeLibrary(selected.toPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mPathToJmbeLibraryButton;
|
||||
return mSelectButton;
|
||||
}
|
||||
|
||||
private Button getPathToJmbeLibraryResetButton()
|
||||
private Button getResetButton()
|
||||
{
|
||||
if(mPathToJmbeLibraryResetButton == null)
|
||||
if(mResetButton == null)
|
||||
{
|
||||
mPathToJmbeLibraryResetButton = new Button("Reset");
|
||||
mPathToJmbeLibraryResetButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
mJmbeLibraryPreference.resetPathJmbeLibrary();
|
||||
}
|
||||
});
|
||||
mResetButton = new Button("Reset");
|
||||
mResetButton.setOnAction(event -> mUserPreferences.getJmbeLibraryPreference().resetPathJmbeLibrary());
|
||||
}
|
||||
|
||||
return mPathToJmbeLibraryResetButton;
|
||||
return mResetButton;
|
||||
}
|
||||
|
||||
private Label getPathToJmbeLibraryLabel()
|
||||
{
|
||||
if(mPathToJmbeLibraryLabel == null)
|
||||
{
|
||||
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
|
||||
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
|
||||
mPathToJmbeLibraryLabel = new Label(path != null ? path.toString() : PATH_NOT_SET);
|
||||
}
|
||||
|
||||
@@ -163,8 +332,14 @@ public class JmbeLibraryPreferenceEditor extends HBox
|
||||
{
|
||||
if(preferenceType != null && preferenceType == PreferenceType.JMBE_LIBRARY)
|
||||
{
|
||||
Path path = mJmbeLibraryPreference.getPathJmbeLibrary();
|
||||
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
|
||||
getPathToJmbeLibraryLabel().setText(path != null ? path.toString() : PATH_NOT_SET);
|
||||
Version version = mUserPreferences.getJmbeLibraryPreference().getCurrentVersion();
|
||||
getJmbeVersionLabel().setText(version != null ? version.toString() : LIBRARY_NOT_SETUP);
|
||||
getCreateButton().setText(mUserPreferences.getJmbeLibraryPreference()
|
||||
.getPathJmbeLibrary() != null ? CHECK_FOR_UPDATE : CREATE_LIBRARY);
|
||||
getAlertUserWhenMissingCheckBox().setSelected(mUserPreferences.getJmbeLibraryPreference()
|
||||
.getAlertIfMissingLibraryRequired());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,11 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
private Button mResetEventLogsButton;
|
||||
private Label mEventLogsPathLabel;
|
||||
|
||||
private Label mJmbeLabel;
|
||||
private Button mChangeJmbeButton;
|
||||
private Button mResetJmbeButton;
|
||||
private Label mJmbePathLabel;
|
||||
|
||||
private Label mPlaylistLabel;
|
||||
private Button mChangePlaylistButton;
|
||||
private Button mResetPlaylistButton;
|
||||
@@ -157,6 +162,19 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
mEditorPane.add(getResetEventLogsButton(), 3, row++);
|
||||
|
||||
|
||||
GridPane.setMargin(getJmbeLabel(), new Insets(0, 10, 0, 0));
|
||||
mEditorPane.add(getJmbeLabel(), 0, row);
|
||||
|
||||
GridPane.setMargin(getJmbePathLabel(), new Insets(0, 10, 0, 0));
|
||||
mEditorPane.add(getJmbePathLabel(), 1, row);
|
||||
|
||||
GridPane.setMargin(getChangeJmbeButton(), new Insets(2, 10, 2, 0));
|
||||
mEditorPane.add(getChangeJmbeButton(), 2, row);
|
||||
|
||||
GridPane.setMargin(getResetJmbeButton(), new Insets(2, 0, 2, 0));
|
||||
mEditorPane.add(getResetJmbeButton(), 3, row++);
|
||||
|
||||
|
||||
GridPane.setMargin(getPlaylistLabel(), new Insets(0, 10, 0, 0));
|
||||
mEditorPane.add(getPlaylistLabel(), 0, row);
|
||||
|
||||
@@ -407,6 +425,71 @@ public class DirectoryPreferenceEditor extends HBox
|
||||
return mEventLogsPathLabel;
|
||||
}
|
||||
|
||||
private Label getJmbeLabel()
|
||||
{
|
||||
if(mJmbeLabel == null)
|
||||
{
|
||||
mJmbeLabel = new Label("JMBE Libraries");
|
||||
}
|
||||
|
||||
return mJmbeLabel;
|
||||
}
|
||||
|
||||
private Button getChangeJmbeButton()
|
||||
{
|
||||
if(mChangeJmbeButton == null)
|
||||
{
|
||||
mChangeJmbeButton = new Button("Change...");
|
||||
mChangeJmbeButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
directoryChooser.setTitle("Select JMBE Folder");
|
||||
directoryChooser.setInitialDirectory(mDirectoryPreference.getDirectoryJmbe().toFile());
|
||||
Stage stage = (Stage)getChangeJmbeButton().getScene().getWindow();
|
||||
File selected = directoryChooser.showDialog(stage);
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
mDirectoryPreference.setDirectoryJmbe(selected.toPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mChangeJmbeButton;
|
||||
}
|
||||
|
||||
private Button getResetJmbeButton()
|
||||
{
|
||||
if(mResetJmbeButton == null)
|
||||
{
|
||||
mResetJmbeButton = new Button("Reset");
|
||||
mResetJmbeButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
mDirectoryPreference.resetDirectoryJmbe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mResetJmbeButton;
|
||||
}
|
||||
|
||||
private Label getJmbePathLabel()
|
||||
{
|
||||
if(mJmbePathLabel == null)
|
||||
{
|
||||
mJmbePathLabel = new Label(mDirectoryPreference.getDirectoryJmbe().toString());
|
||||
}
|
||||
|
||||
return mJmbePathLabel;
|
||||
}
|
||||
|
||||
private Label getPlaylistLabel()
|
||||
{
|
||||
if(mPlaylistLabel == null)
|
||||
|
||||
Reference in New Issue
Block a user