Add a Permissions menu with privilege keys: list, create, remove and use
Permissions > Use Privilege Key redeems a key (tokenuse). Permissions > Privilege Keys opens a per-tab window listing the server's keys with their group, channel, creation date and description; keys can be created, removed, copied and reloaded. The create dialog takes a server group, or a channel group in a chosen channel, plus a description, offers only groups we have the power to add members to, and shows each new key for copying. tokenlist and tokenadd answer with notifytokenlist / notifytokenadd ahead of the command's completion, so core collects them the way it does the ban list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -283,6 +283,21 @@ public final class MainFrame extends JFrame implements ServerTabPane.Listener {
|
||||
if (selected != null) selected.showBanList(banReasonPresets());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canListPrivilegeKeys() {
|
||||
return selected != null && selected.session().canListPrivilegeKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowPrivilegeKeys() {
|
||||
if (selected != null) selected.showPrivilegeKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUsePrivilegeKey() {
|
||||
if (selected != null) selected.usePrivilegeKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowSettings() {
|
||||
showSettings();
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* The main window's menu bar: Connections, Bookmarks, Self, Tools and Help.
|
||||
* The main window's menu bar: Connections, Bookmarks, Self, Permissions, Tools and Help.
|
||||
* Talks to {@link MainFrame} only through {@link Listener}; the away/commander
|
||||
* item states are pushed in by {@link #refresh}, mirroring how {@link MainToolbar}
|
||||
* is kept in sync.
|
||||
@@ -66,6 +66,13 @@ final class MainMenuBar extends JMenuBar {
|
||||
|
||||
void onShowBanList();
|
||||
|
||||
/** Whether the current server lets us list its privilege keys; asked as the Permissions menu opens. */
|
||||
boolean canListPrivilegeKeys();
|
||||
|
||||
void onShowPrivilegeKeys();
|
||||
|
||||
void onUsePrivilegeKey();
|
||||
|
||||
void onShowSettings();
|
||||
|
||||
void onShowAbout();
|
||||
@@ -88,6 +95,8 @@ final class MainMenuBar extends JMenuBar {
|
||||
private JMenuItem avatarItem;
|
||||
private JMenuItem deleteAvatarItem;
|
||||
private JMenuItem banListItem;
|
||||
private JMenuItem privilegeKeysItem;
|
||||
private JMenuItem usePrivilegeKeyItem;
|
||||
|
||||
MainMenuBar(Bookmarks bookmarks, Listener listener) {
|
||||
this.bookmarks = bookmarks;
|
||||
@@ -171,6 +180,28 @@ final class MainMenuBar extends JMenuBar {
|
||||
}
|
||||
});
|
||||
|
||||
JMenu permissions = new JMenu("Permissions");
|
||||
privilegeKeysItem = new JMenuItem("Privilege Keys", Icons.of("TOKEN"));
|
||||
privilegeKeysItem.addActionListener(e -> listener.onShowPrivilegeKeys());
|
||||
usePrivilegeKeyItem = new JMenuItem("Use Privilege Key", Icons.of("TOKEN_USE"));
|
||||
usePrivilegeKeyItem.addActionListener(e -> listener.onUsePrivilegeKey());
|
||||
permissions.add(privilegeKeysItem);
|
||||
permissions.add(usePrivilegeKeyItem);
|
||||
permissions.addMenuListener(new MenuListener() {
|
||||
@Override
|
||||
public void menuSelected(MenuEvent e) {
|
||||
privilegeKeysItem.setVisible(!privilegeKeysItem.isEnabled() || listener.canListPrivilegeKeys());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuDeselected(MenuEvent e) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuCanceled(MenuEvent e) {
|
||||
}
|
||||
});
|
||||
|
||||
JMenu tools = new JMenu("Tools");
|
||||
JMenuItem identitiesItem = new JMenuItem("Identities…", Icons.of("IDENTITY_MANAGER"));
|
||||
identitiesItem.addActionListener(e -> listener.onShowIdentities());
|
||||
@@ -214,6 +245,7 @@ final class MainMenuBar extends JMenuBar {
|
||||
add(connections);
|
||||
add(bookmarksMenu);
|
||||
add(self);
|
||||
add(permissions);
|
||||
add(tools);
|
||||
add(help);
|
||||
}
|
||||
@@ -226,6 +258,8 @@ final class MainMenuBar extends JMenuBar {
|
||||
avatarItem.setEnabled(connected);
|
||||
deleteAvatarItem.setEnabled(connected);
|
||||
banListItem.setEnabled(connected);
|
||||
privilegeKeysItem.setEnabled(connected);
|
||||
usePrivilegeKeyItem.setEnabled(connected);
|
||||
awayItem.setSelected(away);
|
||||
commanderItem.setSelected(commander);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
|
||||
import javax.swing.Box;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.GridBagConstraints;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.Window;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
|
||||
/**
|
||||
* Creates privilege keys, as the official client's "Create Privilege Key" dialog: a server
|
||||
* group, or a channel group in a chosen channel, and an optional description. Each key
|
||||
* created is shown for copying, and the dialog stays open for the next one.
|
||||
*/
|
||||
final class PrivilegeKeyCreateDialog extends JDialog {
|
||||
|
||||
private final TeamspeakConnection conn;
|
||||
private final Runnable onCreated;
|
||||
|
||||
private final JRadioButton serverGroup = new JRadioButton("Server Group", true);
|
||||
private final JRadioButton channelGroup = new JRadioButton("Channel Group");
|
||||
private final JComboBox<Group> groupBox = new JComboBox<>();
|
||||
private final JComboBox<ChannelNode> channelBox = new JComboBox<>();
|
||||
private final JTextField descriptionField = new JTextField(28);
|
||||
private final JTextField keyField = new JTextField(28);
|
||||
private final JButton copy = new JButton("Copy", Icons.of("COPY"));
|
||||
private final JButton create = new JButton("Create");
|
||||
|
||||
/** @param onCreated runs on the UI thread after each key is created */
|
||||
PrivilegeKeyCreateDialog(Window owner, TeamspeakConnection conn, GroupIcons groupIcons, Runnable onCreated) {
|
||||
super(owner, "Create Privilege Key", ModalityType.APPLICATION_MODAL);
|
||||
this.conn = conn;
|
||||
this.onCreated = onCreated;
|
||||
|
||||
ButtonGroup type = new ButtonGroup();
|
||||
type.add(serverGroup);
|
||||
type.add(channelGroup);
|
||||
serverGroup.addActionListener(e -> typeChanged());
|
||||
channelGroup.addActionListener(e -> typeChanged());
|
||||
JPanel typeRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
|
||||
typeRow.add(serverGroup);
|
||||
typeRow.add(Box.createHorizontalStrut(UIScale.scale(12)));
|
||||
typeRow.add(channelGroup);
|
||||
|
||||
groupBox.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focused) {
|
||||
super.getListCellRendererComponent(list, value, index, selected, focused);
|
||||
if (value instanceof Group g) {
|
||||
setText(g.name);
|
||||
setIcon(groupIcons.iconOf(g));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
});
|
||||
groupBox.addActionListener(e -> updateCreate());
|
||||
ServerModel model = conn.getModel();
|
||||
channelBox.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focused) {
|
||||
super.getListCellRendererComponent(list, value, index, selected, focused);
|
||||
if (value instanceof ChannelNode c) setText(model.channelPath(c.id));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
for (ChannelNode c : model.channelsInTreeOrder()) channelBox.addItem(c);
|
||||
channelBox.setToolTipText("When creating a privilege key for a channel group, specify the channel here");
|
||||
descriptionField.setToolTipText("Optional description for this privilege key");
|
||||
descriptionField.addActionListener(e -> create());
|
||||
|
||||
keyField.setEditable(false);
|
||||
copy.setEnabled(false);
|
||||
copy.addActionListener(e -> Toolkit.getDefaultToolkit().getSystemClipboard()
|
||||
.setContents(new StringSelection(keyField.getText()), null));
|
||||
JPanel keyRow = new JPanel(new BorderLayout(4, 0));
|
||||
keyRow.add(keyField, BorderLayout.CENTER);
|
||||
keyRow.add(copy, BorderLayout.EAST);
|
||||
|
||||
FormPanel form = new FormPanel();
|
||||
GridBagConstraints c = FormPanel.gbc();
|
||||
FormPanel.addRow(form, c, 0, new JLabel("Group Type:"), typeRow);
|
||||
FormPanel.addRow(form, c, 1, new JLabel("Group:"), groupBox);
|
||||
FormPanel.addRow(form, c, 2, new JLabel("Channel:"), channelBox);
|
||||
FormPanel.addRow(form, c, 3, new JLabel("Description:"), descriptionField);
|
||||
FormPanel.addRow(form, c, 4, new JLabel("Privilege Key:"), keyRow);
|
||||
|
||||
create.addActionListener(e -> create());
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dispose());
|
||||
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
|
||||
buttons.setBorder(new ScaledEmptyBorder(0, 8, 4, 8));
|
||||
buttons.add(create);
|
||||
buttons.add(close);
|
||||
getRootPane().setDefaultButton(create);
|
||||
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
getContentPane().add(form, BorderLayout.CENTER);
|
||||
getContentPane().add(buttons, BorderLayout.SOUTH);
|
||||
|
||||
typeChanged();
|
||||
Dialogs.closeOnEscape(this);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
pack();
|
||||
setResizable(false);
|
||||
setLocationRelativeTo(owner);
|
||||
}
|
||||
|
||||
private boolean forChannelGroup() {
|
||||
return channelGroup.isSelected();
|
||||
}
|
||||
|
||||
private void typeChanged() {
|
||||
groupBox.removeAllItems();
|
||||
for (Group g : conn.getModel().privilegeKeyGroups(forChannelGroup())) groupBox.addItem(g);
|
||||
channelBox.setEnabled(forChannelGroup());
|
||||
updateCreate();
|
||||
}
|
||||
|
||||
private void updateCreate() {
|
||||
create.setEnabled(groupBox.getSelectedItem() != null
|
||||
&& (!forChannelGroup() || channelBox.getSelectedItem() != null));
|
||||
}
|
||||
|
||||
private void create() {
|
||||
if (!create.isEnabled()) return;
|
||||
Group group = (Group) groupBox.getSelectedItem();
|
||||
ChannelNode channel = (ChannelNode) channelBox.getSelectedItem();
|
||||
create.setEnabled(false);
|
||||
conn.addPrivilegeKey(forChannelGroup(), group.id, channel == null ? 0 : channel.id,
|
||||
descriptionField.getText().trim(), (key, error) -> SwingUtilities.invokeLater(() -> {
|
||||
if (!isDisplayable()) return;
|
||||
updateCreate();
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to add Privilege Key: " + error,
|
||||
getTitle(), JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
keyField.setText(key);
|
||||
keyField.selectAll();
|
||||
keyField.requestFocusInWindow();
|
||||
copy.setEnabled(true);
|
||||
onCreated.run();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.PrivilegeKey;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
|
||||
import javax.swing.AbstractAction;
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Toolkit;
|
||||
import java.awt.datatransfer.StringSelection;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* The server's privilege keys, as the official client's Permissions > Privilege Keys
|
||||
* window: Create and Remove above the list, Reload below it.
|
||||
*
|
||||
* <p>One per server tab and modeless, like the ban list; the tab closes it when the
|
||||
* connection goes away.
|
||||
*/
|
||||
final class PrivilegeKeysDialog extends JDialog {
|
||||
|
||||
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
private enum Column {
|
||||
KEY("Privilege Key", 320),
|
||||
TYPE("Type", 110),
|
||||
GROUP("Group", 140),
|
||||
CHANNEL("Channel", 140),
|
||||
CREATED("Created", 150),
|
||||
DESCRIPTION("Description", 180);
|
||||
|
||||
final String title;
|
||||
final int width;
|
||||
|
||||
Column(String title, int width) {
|
||||
this.title = title;
|
||||
this.width = width;
|
||||
}
|
||||
}
|
||||
|
||||
private final TeamspeakConnection conn;
|
||||
private final GroupIcons groupIcons;
|
||||
|
||||
private final KeyTableModel tableModel = new KeyTableModel();
|
||||
private final JTable table = new JTable(tableModel);
|
||||
private final JButton createButton = new JButton("Create", Icons.of("ADD"));
|
||||
private final JButton removeButton = new JButton("Remove", Icons.of("DELETE"));
|
||||
private final JLabel status = new JLabel(" ");
|
||||
|
||||
private boolean loading;
|
||||
|
||||
PrivilegeKeysDialog(Frame owner, TeamspeakConnection conn, GroupIcons groupIcons) {
|
||||
super(owner, "Privilege Keys", false);
|
||||
this.conn = conn;
|
||||
this.groupIcons = groupIcons;
|
||||
|
||||
JPanel content = new JPanel(new BorderLayout(0, UIScale.scale(6)));
|
||||
content.setBackground(Theme.windowBg());
|
||||
content.setBorder(new ScaledEmptyBorder(10, 12, 10, 12));
|
||||
content.add(buildToolbar(), BorderLayout.NORTH);
|
||||
content.add(buildTable(), BorderLayout.CENTER);
|
||||
content.add(buildBottom(), BorderLayout.SOUTH);
|
||||
setContentPane(content);
|
||||
bindKeys();
|
||||
|
||||
serverNameChanged();
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setPreferredSize(UIScale.scale(new Dimension(1040, 400)));
|
||||
pack();
|
||||
setLocationRelativeTo(owner);
|
||||
reload();
|
||||
}
|
||||
|
||||
// ---- construction ----
|
||||
|
||||
private JPanel buildToolbar() {
|
||||
createButton.setToolTipText("<html>Create new privilege key<br>(Ctrl+N)");
|
||||
removeButton.setToolTipText("<html>Remove selected privilege keys<br>(Delete)");
|
||||
createButton.addActionListener(e -> createKey());
|
||||
removeButton.addActionListener(e -> removeKeys());
|
||||
|
||||
JPanel bar = new JPanel();
|
||||
bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS));
|
||||
bar.setOpaque(false);
|
||||
bar.add(createButton);
|
||||
bar.add(Box.createHorizontalStrut(UIScale.scale(6)));
|
||||
bar.add(removeButton);
|
||||
bar.add(Box.createHorizontalGlue());
|
||||
return bar;
|
||||
}
|
||||
|
||||
private JScrollPane buildTable() {
|
||||
table.setFont(Theme.uiFont());
|
||||
table.setRowHeight(UIScale.scale(20));
|
||||
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||
table.setFillsViewportHeight(true);
|
||||
table.setRowSorter(new TableRowSorter<>(tableModel));
|
||||
table.getSelectionModel().addListSelectionListener(e -> updateButtons());
|
||||
table.setComponentPopupMenu(tablePopup());
|
||||
table.getTableHeader().setReorderingAllowed(true);
|
||||
KeyCellRenderer renderer = new KeyCellRenderer();
|
||||
for (Column c : Column.values()) {
|
||||
table.getColumnModel().getColumn(c.ordinal()).setPreferredWidth(UIScale.scale(c.width));
|
||||
table.getColumnModel().getColumn(c.ordinal()).setCellRenderer(renderer);
|
||||
}
|
||||
|
||||
JScrollPane scroll = new JScrollPane(table);
|
||||
scroll.getViewport().setBackground(Theme.treeBg());
|
||||
return scroll;
|
||||
}
|
||||
|
||||
private JPanel buildBottom() {
|
||||
JButton reload = new JButton("Reload", Icons.of("REFRESH"));
|
||||
reload.setToolTipText("<html>Reload privilege keys<br>(F5)");
|
||||
reload.addActionListener(e -> reload());
|
||||
JButton close = new JButton("Close");
|
||||
close.addActionListener(e -> dispose());
|
||||
|
||||
JPanel row = new JPanel(new BorderLayout());
|
||||
row.setOpaque(false);
|
||||
row.add(reload, BorderLayout.WEST);
|
||||
row.add(close, BorderLayout.EAST);
|
||||
|
||||
JPanel bottom = new JPanel(new BorderLayout(0, UIScale.scale(8)));
|
||||
bottom.setOpaque(false);
|
||||
bottom.add(status, BorderLayout.NORTH);
|
||||
bottom.add(row, BorderLayout.SOUTH);
|
||||
status.setForeground(Theme.chatEvent());
|
||||
return bottom;
|
||||
}
|
||||
|
||||
private JPopupMenu tablePopup() {
|
||||
JPopupMenu menu = new JPopupMenu();
|
||||
menu.add(item("Create", "ADD", this::createKey));
|
||||
menu.add(item("Remove", "DELETE", this::removeKeys));
|
||||
menu.add(item("Copy", "COPY", this::copyKeys));
|
||||
menu.addSeparator();
|
||||
menu.add(item("Reload", "REFRESH", this::reload));
|
||||
return menu;
|
||||
}
|
||||
|
||||
private static JMenuItem item(String text, String icon, Runnable action) {
|
||||
JMenuItem item = new JMenuItem(text, Icons.of(icon));
|
||||
item.addActionListener(e -> action.run());
|
||||
return item;
|
||||
}
|
||||
|
||||
private void bindKeys() {
|
||||
bind(getRootPane(), JComponent.WHEN_IN_FOCUSED_WINDOW, KeyStroke.getKeyStroke("control N"), this::createKey);
|
||||
bind(getRootPane(), JComponent.WHEN_IN_FOCUSED_WINDOW, KeyStroke.getKeyStroke("F5"), this::reload);
|
||||
bind(getRootPane(), JComponent.WHEN_IN_FOCUSED_WINDOW, KeyStroke.getKeyStroke("control W"), this::dispose);
|
||||
bind(table, JComponent.WHEN_FOCUSED, KeyStroke.getKeyStroke("control C"), this::copyKeys);
|
||||
bind(table, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
|
||||
this::removeKeys);
|
||||
Dialogs.closeOnEscape(this);
|
||||
}
|
||||
|
||||
private static void bind(JComponent target, int condition, KeyStroke key, Runnable action) {
|
||||
String name = "ts3-privilegekeys-" + key;
|
||||
target.getInputMap(condition).put(key, name);
|
||||
target.getActionMap().put(name, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
action.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- data ----
|
||||
|
||||
/** Follows the server's name, as the ban list's title does. */
|
||||
void serverNameChanged() {
|
||||
String name = conn.getModel().getServerName();
|
||||
setTitle(name == null || name.isBlank() ? "Privilege Keys" : "Privilege Keys - " + name);
|
||||
}
|
||||
|
||||
void connectionLost() {
|
||||
dispose();
|
||||
}
|
||||
|
||||
private void reload() {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
status.setText("Loading…");
|
||||
conn.requestPrivilegeKeys((keys, error) -> SwingUtilities.invokeLater(() -> {
|
||||
loading = false;
|
||||
if (!isDisplayable()) return;
|
||||
if (error != null) {
|
||||
tableModel.setKeys(List.of());
|
||||
status.setText("Failed to request Privilege Key list: " + error);
|
||||
} else {
|
||||
tableModel.setKeys(keys);
|
||||
status.setText(keys.size() == 1 ? "1 privilege key" : keys.size() + " privilege keys");
|
||||
}
|
||||
updateButtons();
|
||||
}));
|
||||
}
|
||||
|
||||
private List<PrivilegeKey> selectedKeys() {
|
||||
List<PrivilegeKey> selected = new ArrayList<>();
|
||||
for (int row : table.getSelectedRows()) selected.add(tableModel.get(table.convertRowIndexToModel(row)));
|
||||
return selected;
|
||||
}
|
||||
|
||||
private void updateButtons() {
|
||||
ServerModel model = conn.getModel();
|
||||
createButton.setEnabled(model.permits("b_virtualserver_token_add"));
|
||||
removeButton.setEnabled(!selectedKeys().isEmpty() && model.permits("b_virtualserver_token_delete"));
|
||||
}
|
||||
|
||||
// ---- actions ----
|
||||
|
||||
private void createKey() {
|
||||
if (!createButton.isEnabled()) return;
|
||||
new PrivilegeKeyCreateDialog(this, conn, groupIcons, this::reload).setVisible(true);
|
||||
}
|
||||
|
||||
private void removeKeys() {
|
||||
if (!removeButton.isEnabled()) return;
|
||||
int answer = JOptionPane.showConfirmDialog(this, "Do you really want to delete the selected privilege key(s)?",
|
||||
"Delete Privilege Keys", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
|
||||
if (answer != JOptionPane.YES_OPTION) return;
|
||||
List<String> tokens = selectedKeys().stream().map(PrivilegeKey::token).collect(Collectors.toList());
|
||||
conn.deletePrivilegeKeys(tokens, error -> SwingUtilities.invokeLater(() -> {
|
||||
if (!isDisplayable()) return;
|
||||
if (error != null) {
|
||||
JOptionPane.showMessageDialog(this, "Failed to delete Privilege Key: " + error, getTitle(),
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
reload();
|
||||
}));
|
||||
}
|
||||
|
||||
private void copyKeys() {
|
||||
List<PrivilegeKey> keys = selectedKeys();
|
||||
if (keys.isEmpty()) return;
|
||||
String text = keys.stream().map(PrivilegeKey::token).collect(Collectors.joining("\n"));
|
||||
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(text), null);
|
||||
}
|
||||
|
||||
// ---- table ----
|
||||
|
||||
private Group groupOf(PrivilegeKey key) {
|
||||
ServerModel model = conn.getModel();
|
||||
return key.channelGroup() ? model.channelGroup(key.groupId()) : model.serverGroup(key.groupId());
|
||||
}
|
||||
|
||||
private final class KeyTableModel extends AbstractTableModel {
|
||||
|
||||
private List<PrivilegeKey> keys = List.of();
|
||||
|
||||
void setKeys(List<PrivilegeKey> keys) {
|
||||
this.keys = new ArrayList<>(keys);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
PrivilegeKey get(int row) {
|
||||
return keys.get(row);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return keys.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return Column.values().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return Column.values()[column].title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int column) {
|
||||
return Column.values()[column] == Column.CREATED ? Long.class : String.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int row, int column) {
|
||||
PrivilegeKey key = keys.get(row);
|
||||
if (Column.values()[column] == Column.CREATED) return key.createdEpochSeconds();
|
||||
return text(key, Column.values()[column]);
|
||||
}
|
||||
|
||||
String text(PrivilegeKey key, Column column) {
|
||||
switch (column) {
|
||||
case KEY:
|
||||
return key.token();
|
||||
case TYPE:
|
||||
return key.channelGroup() ? "Channel Group" : "Server Group";
|
||||
case GROUP:
|
||||
Group group = groupOf(key);
|
||||
return group != null ? group.name : Integer.toString(key.groupId());
|
||||
case CHANNEL:
|
||||
if (!key.channelGroup()) return "";
|
||||
String path = conn.getModel().channelPath(key.channelId());
|
||||
return path.isEmpty() ? Integer.toString(key.channelId()) : path;
|
||||
case CREATED:
|
||||
return key.createdEpochSeconds() <= 0 ? "" : DATE.format(
|
||||
Instant.ofEpochSecond(key.createdEpochSeconds()).atZone(ZoneId.systemDefault()));
|
||||
case DESCRIPTION:
|
||||
return key.description();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows each column's text, with the group's icon beside its name. */
|
||||
private final class KeyCellRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable t, Object value, boolean selected,
|
||||
boolean focused, int row, int column) {
|
||||
PrivilegeKey key = tableModel.get(t.convertRowIndexToModel(row));
|
||||
Column c = Column.values()[t.convertColumnIndexToModel(column)];
|
||||
super.getTableCellRendererComponent(t, tableModel.text(key, c), selected, focused, row, column);
|
||||
setIcon(c == Column.GROUP ? groupIcons.iconOf(groupOf(key)) : null);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,9 @@ import com.ts3client.sound.SoundNotifier;
|
||||
import com.ts3client.text.TsLink;
|
||||
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSplitPane;
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ComponentAdapter;
|
||||
import java.awt.event.ComponentEvent;
|
||||
@@ -52,6 +54,7 @@ final class ServerTab {
|
||||
|
||||
/** The one open ban list window for this server, if any. */
|
||||
private BanListDialog banList;
|
||||
private PrivilegeKeysDialog privilegeKeys;
|
||||
|
||||
ServerTab(MainFrame host, Settings settings, IdentityStore identities, AudioBackend audio,
|
||||
SoundNotifier sounds, ContactStore contacts, PictureDownloads pictures) {
|
||||
@@ -187,11 +190,39 @@ final class ServerTab {
|
||||
banList.toFront();
|
||||
}
|
||||
|
||||
/** The server's name changed or the connection went; the ban list window follows. */
|
||||
void banListServerChanged(boolean connected) {
|
||||
if (banList == null) return;
|
||||
if (connected) banList.serverNameChanged();
|
||||
else banList.connectionLost();
|
||||
/** Asks for a privilege key and redeems it on this server. */
|
||||
void usePrivilegeKey() {
|
||||
if (!session.isConnected()) return;
|
||||
String key = (String) JOptionPane.showInputDialog(host, "Privilege Key:", "Use Privilege Key",
|
||||
JOptionPane.PLAIN_MESSAGE, Icons.of("TOKEN_USE"), null, null);
|
||||
if (key == null || key.isBlank()) return;
|
||||
conn.usePrivilegeKey(key.trim(), error -> {
|
||||
if (error == null) return;
|
||||
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(host, error,
|
||||
"Error using Privilege Key", JOptionPane.ERROR_MESSAGE));
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens the server's privilege keys, or brings the already open window forward. */
|
||||
void showPrivilegeKeys() {
|
||||
if (!session.isConnected()) return;
|
||||
if (privilegeKeys == null || !privilegeKeys.isDisplayable()) {
|
||||
privilegeKeys = new PrivilegeKeysDialog(host, conn, groupIcons);
|
||||
}
|
||||
privilegeKeys.setVisible(true);
|
||||
privilegeKeys.toFront();
|
||||
}
|
||||
|
||||
/** The server's name changed or the connection went; the ban list and privilege keys windows follow. */
|
||||
void serverWindowsChanged(boolean connected) {
|
||||
if (banList != null) {
|
||||
if (connected) banList.serverNameChanged();
|
||||
else banList.connectionLost();
|
||||
}
|
||||
if (privilegeKeys != null) {
|
||||
if (connected) privilegeKeys.serverNameChanged();
|
||||
else privilegeKeys.connectionLost();
|
||||
}
|
||||
}
|
||||
|
||||
/** The contact list changed: names, colours and per-client playback follow. */
|
||||
|
||||
@@ -50,7 +50,7 @@ final class ServerTabConnectionEvents implements ServerSession.Listener {
|
||||
@Override
|
||||
public void onSessionChanged() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (session.isConnected()) tab.banListServerChanged(true);
|
||||
if (session.isConnected()) tab.serverWindowsChanged(true);
|
||||
host.tabUpdated(tab);
|
||||
});
|
||||
}
|
||||
@@ -97,7 +97,7 @@ final class ServerTabConnectionEvents implements ServerSession.Listener {
|
||||
chatPanel.closeNoteTabs();
|
||||
chatPanel.setServerName(null);
|
||||
chatPanel.setChannelName(null);
|
||||
tab.banListServerChanged(false);
|
||||
tab.serverWindowsChanged(false);
|
||||
host.tabDisconnected(tab);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user