Kick, ban and set groups from the Android client sheet, with a ban list
The client sheet offers kicking from the channel or the server (asking to confirm, with an optional reason), banning with a reason and duration, and the client's server groups and channel group. The server sheet opens the ban list: search, own bans only, and adding, editing and deleting bans, with the reason presets the desktop keeps. Kick and ban powers, like move power, never come with the needed permissions, so core now asks for all four in one piped permget. The ban permission checks, a ban's summary, the duration units, the group choices for a client and the kick reason limit move into core, and Swing uses them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
package com.ts3client.ui;
|
||||
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.net.ban.BanUnit;
|
||||
|
||||
import javax.swing.Box;
|
||||
import javax.swing.BoxLayout;
|
||||
import javax.swing.DefaultListCellRenderer;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
@@ -18,16 +21,8 @@ import java.awt.Component;
|
||||
*/
|
||||
final class BanDurationPicker extends JPanel {
|
||||
|
||||
/**
|
||||
* The units offered, with the seconds each one is worth. The last entry is the
|
||||
* permanent ban, which has no length and so takes no amount.
|
||||
*/
|
||||
private static final String[] UNIT_NAMES = {"seconds", "minutes", "hours", "days", "permanent"};
|
||||
private static final long[] UNIT_SECONDS = {1, 60, 3600, 86400, 0};
|
||||
private static final int PERMANENT = UNIT_NAMES.length - 1;
|
||||
|
||||
private final JSpinner amount = new JSpinner(new SpinnerNumberModel(30, 1, 999_999_999, 1));
|
||||
private final JComboBox<String> unit = new JComboBox<>(UNIT_NAMES);
|
||||
private final JComboBox<BanUnit> unit = new JComboBox<>(BanUnit.values());
|
||||
private final JButton max = new JButton("Max");
|
||||
|
||||
/**
|
||||
@@ -38,7 +33,14 @@ final class BanDurationPicker extends JPanel {
|
||||
setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
setOpaque(false);
|
||||
|
||||
unit.setSelectedIndex(1); // minutes
|
||||
unit.setSelectedItem(BanUnit.MINUTES);
|
||||
unit.setRenderer(new DefaultListCellRenderer() {
|
||||
@Override
|
||||
public Component getListCellRendererComponent(JList<?> list, Object value, int index,
|
||||
boolean selected, boolean focused) {
|
||||
return super.getListCellRendererComponent(list, ((BanUnit) value).label(), index, selected, focused);
|
||||
}
|
||||
});
|
||||
unit.addActionListener(e -> amount.setEnabled(!isPermanent()));
|
||||
max.setToolTipText("Set duration to maximum value");
|
||||
max.addActionListener(e -> setSeconds(maxSeconds));
|
||||
@@ -53,25 +55,23 @@ final class BanDurationPicker extends JPanel {
|
||||
}
|
||||
|
||||
boolean isPermanent() {
|
||||
return UNIT_SECONDS[unit.getSelectedIndex()] == 0;
|
||||
return unit() == BanUnit.PERMANENT;
|
||||
}
|
||||
|
||||
private BanUnit unit() {
|
||||
return (BanUnit) unit.getSelectedItem();
|
||||
}
|
||||
|
||||
/** The ban's length in seconds, or 0 for a permanent ban. */
|
||||
long getSeconds() {
|
||||
if (isPermanent()) return 0;
|
||||
return ((Number) amount.getValue()).longValue() * UNIT_SECONDS[unit.getSelectedIndex()];
|
||||
return unit().toSeconds(((Number) amount.getValue()).longValue());
|
||||
}
|
||||
|
||||
/** Shows a length in the largest unit that divides it evenly; 0 is permanent. */
|
||||
void setSeconds(long seconds) {
|
||||
if (seconds <= 0) {
|
||||
unit.setSelectedIndex(PERMANENT);
|
||||
return;
|
||||
}
|
||||
int i = PERMANENT - 1;
|
||||
while (i > 0 && seconds % UNIT_SECONDS[i] != 0) i--;
|
||||
unit.setSelectedIndex(i);
|
||||
amount.setValue((int) Math.min(seconds / UNIT_SECONDS[i], 999_999_999));
|
||||
BanUnit fit = BanUnit.largestFor(seconds);
|
||||
unit.setSelectedItem(fit);
|
||||
if (fit != BanUnit.PERMANENT) amount.setValue((int) Math.min(fit.amountOf(seconds), 999_999_999));
|
||||
}
|
||||
|
||||
/** Called back whenever the length changes, however it changes. */
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.config.BanReasonPresets;
|
||||
import com.ts3client.config.Settings;
|
||||
import com.ts3client.net.ServerModel;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
import com.ts3client.net.ban.BanEntry;
|
||||
import com.ts3client.net.ban.BanRule;
|
||||
@@ -422,33 +421,25 @@ final class BanListDialog extends JDialog {
|
||||
|
||||
// ---- permissions ----
|
||||
|
||||
/**
|
||||
* Whether we hold a permission. Until the server has named its permissions nothing can
|
||||
* be resolved, so everything is offered and the server is left to refuse.
|
||||
*/
|
||||
private boolean can(String permission) {
|
||||
ServerModel model = conn.getModel();
|
||||
return !model.hasPermissionNames() || model.selfPermissionValue(permission) > 0;
|
||||
return conn.getModel().permits(permission);
|
||||
}
|
||||
|
||||
private boolean canDelete(List<BanEntry> bans) {
|
||||
if (can("b_client_ban_delete")) return true;
|
||||
if (!can("b_client_ban_delete_own")) return false;
|
||||
String self = conn.getSelfUniqueId();
|
||||
return bans.stream().allMatch(b -> b.createdBy(self));
|
||||
return conn.getModel().canDeleteBans(bans, conn.getSelfUniqueId());
|
||||
}
|
||||
|
||||
private void updateButtons() {
|
||||
List<BanEntry> selected = selectedBans();
|
||||
addButton.setEnabled(can("b_client_ban_create"));
|
||||
removeButton.setEnabled(!selected.isEmpty() && canDelete(selected));
|
||||
editButton.setEnabled(selected.size() == 1 && canDelete(selected) && can("b_client_ban_create"));
|
||||
editButton.setEnabled(selected.size() == 1 && conn.getModel().canEditBan(selected.get(0), conn.getSelfUniqueId()));
|
||||
}
|
||||
|
||||
// ---- actions ----
|
||||
|
||||
private long maxBanSeconds() {
|
||||
return conn.getModel().selfPermissionValue("i_client_ban_max_bantime");
|
||||
return conn.getModel().maxBanSeconds();
|
||||
}
|
||||
|
||||
private void addBan() {
|
||||
@@ -558,7 +549,7 @@ final class BanListDialog extends JDialog {
|
||||
String text(BanEntry ban, Column column) {
|
||||
switch (column) {
|
||||
case TARGETS:
|
||||
return targets(ban);
|
||||
return ban.targets();
|
||||
case NAME:
|
||||
return ban.name;
|
||||
case IP:
|
||||
@@ -581,18 +572,6 @@ final class BanListDialog extends JDialog {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** The official summary column: {@code name=…, ip=…, uid=… (last nickname), mytsid=…}. */
|
||||
private String targets(BanEntry ban) {
|
||||
List<String> parts = new ArrayList<>(4);
|
||||
if (!ban.name.isEmpty()) parts.add("name=" + ban.name);
|
||||
if (!ban.ip.isEmpty()) parts.add("ip=" + ban.ip);
|
||||
if (!ban.uid.isEmpty()) {
|
||||
parts.add("uid=" + ban.uid + (ban.lastNickname.isEmpty() ? "" : " (" + ban.lastNickname + ")"));
|
||||
}
|
||||
if (!ban.mytsid.isEmpty()) parts.add("mytsid=" + ban.mytsid);
|
||||
return String.join(", ", parts);
|
||||
}
|
||||
}
|
||||
|
||||
private static String date(long epochSeconds) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ts3client.contacts.ContactType;
|
||||
import com.ts3client.net.ChannelNode;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.GroupChoice;
|
||||
import com.ts3client.net.ServerModel;
|
||||
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
@@ -128,12 +129,12 @@ final class ClientMenu {
|
||||
menu.add(dialog);
|
||||
menu.addSeparator();
|
||||
|
||||
for (Group g : model.allServerGroups()) {
|
||||
boolean assigned = client.inServerGroup(g.id);
|
||||
if (!model.canToggleGroup(g, assigned)) continue;
|
||||
for (GroupChoice choice : model.serverGroupChoices(client)) {
|
||||
if (!choice.allowed()) continue;
|
||||
Group g = choice.group();
|
||||
JCheckBoxMenuItem item = new JCheckBoxMenuItem(g.name, groupIcons.iconOf(g));
|
||||
item.setSelected(assigned);
|
||||
item.addActionListener(a -> actions.setClientServerGroup(client, g, !assigned));
|
||||
item.setSelected(choice.assigned());
|
||||
item.addActionListener(a -> actions.setClientServerGroup(client, g, !choice.assigned()));
|
||||
menu.add(item);
|
||||
}
|
||||
return menu;
|
||||
@@ -144,12 +145,11 @@ final class ClientMenu {
|
||||
JMenu menu = new JMenu("Set Channel Group");
|
||||
menu.setIcon(Icons.of("PERMISSIONS_CHANNEL_GROUPS"));
|
||||
int defaultGroupId = model.defaultChannelGroupId();
|
||||
for (Group g : model.allChannelGroups()) {
|
||||
if (g.id == defaultGroupId) continue;
|
||||
boolean assigned = client.channelGroupId == g.id;
|
||||
if (!model.canToggleGroup(g, assigned)) continue;
|
||||
for (GroupChoice choice : model.channelGroupChoices(client)) {
|
||||
Group g = choice.group();
|
||||
if (g.id == defaultGroupId || !choice.allowed()) continue;
|
||||
JCheckBoxMenuItem item = new JCheckBoxMenuItem(g.name, groupIcons.iconOf(g));
|
||||
item.setSelected(assigned);
|
||||
item.setSelected(choice.assigned());
|
||||
item.addActionListener(a -> actions.setClientChannelGroup(client, g));
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,6 @@ import java.awt.Dimension;
|
||||
*/
|
||||
final class ReasonDialog {
|
||||
|
||||
/** The longest reason the protocol carries with a kick. */
|
||||
static final int KICK_REASON_LIMIT = 40;
|
||||
|
||||
private ReasonDialog() {
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.formdev.flatlaf.util.ScaledEmptyBorder;
|
||||
import com.formdev.flatlaf.util.UIScale;
|
||||
import com.ts3client.net.ClientEntry;
|
||||
import com.ts3client.net.Group;
|
||||
import com.ts3client.net.GroupChoice;
|
||||
import com.ts3client.net.TeamspeakConnection;
|
||||
|
||||
import javax.swing.BoxLayout;
|
||||
@@ -19,7 +20,6 @@ import javax.swing.SwingConstants;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Dimension;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -82,19 +82,15 @@ final class ServerGroupsDialog extends JDialog {
|
||||
}
|
||||
header.setText(client.nickname);
|
||||
|
||||
List<Group> groups = conn.getModel().allServerGroups();
|
||||
|
||||
list.removeAll();
|
||||
for (Group g : groups) {
|
||||
boolean assigned = client.inServerGroup(g.id);
|
||||
boolean allowed = conn.getModel().canToggleGroup(g, assigned);
|
||||
|
||||
for (GroupChoice choice : conn.getModel().serverGroupChoices(client)) {
|
||||
Group g = choice.group();
|
||||
ImageIcon icon = groupIcons.iconOf(g);
|
||||
JCheckBox box = new JCheckBox(g.name, allowed ? icon : grayscale(icon));
|
||||
box.setSelected(assigned);
|
||||
box.setEnabled(allowed);
|
||||
JCheckBox box = new JCheckBox(g.name, choice.allowed() ? icon : grayscale(icon));
|
||||
box.setSelected(choice.assigned());
|
||||
box.setEnabled(choice.allowed());
|
||||
box.setAlignmentX(0f);
|
||||
if (allowed) {
|
||||
if (choice.allowed()) {
|
||||
box.addActionListener(a -> actions.setClientServerGroup(client, g, box.isSelected()));
|
||||
}
|
||||
list.add(box);
|
||||
|
||||
@@ -155,14 +155,14 @@ final class ServerTabTreeActions implements ServerTreePanel.Actions {
|
||||
private String kickReason(String title, ClientEntry client) {
|
||||
if (!conn.isConnected()) return null;
|
||||
return ReasonDialog.prompt(host, title, "Reason for kicking " + client.nickname + ":",
|
||||
ReasonDialog.KICK_REASON_LIMIT);
|
||||
TeamspeakConnection.KICK_REASON_LIMIT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void banClient(ClientEntry client) {
|
||||
if (!conn.isConnected()) return;
|
||||
BanDialog dialog = new BanDialog(host, client.nickname,
|
||||
conn.getModel().selfPermissionValue("i_client_ban_max_bantime"));
|
||||
conn.getModel().maxBanSeconds());
|
||||
dialog.setVisible(true);
|
||||
if (dialog.isConfirmed()) conn.banClient(client.id, dialog.getSeconds(), dialog.getReason());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user