package com.ts3client.ui; import com.ts3client.net.ChannelNode; import com.ts3client.net.TeamspeakConnection; import com.ts3client.net.filetransfer.FileTransfer; import com.ts3client.net.filetransfer.RemoteFile; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.table.AbstractTableModel; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Frame; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.File; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; /** * Browses a channel's file repository and drives uploads and downloads. The table * lists the current directory; the toolbar navigates and acts on the selection; the * strip at the bottom shows live progress for each active transfer. * *

All server calls run on background threads so the dialog never blocks the EDT; * results are marshalled back with {@link SwingUtilities#invokeLater}. */ public final class FileBrowserDialog extends JDialog { private final TeamspeakConnection conn; private final int channelId; private final String channelPassword; private final FileTableModel tableModel = new FileTableModel(); private final JTable table = new JTable(tableModel); private final JLabel pathLabel = new JLabel("/"); private final JButton upButton = new JButton("Up", Icons.of("FILE_UP")); private final JButton downloadButton = new JButton("Download", Icons.of("DOWNLOAD")); private final JButton deleteButton = new JButton("Delete", Icons.of("DELETE")); private final JPanel transfersPanel = new JPanel(); private volatile String currentPath = "/"; public FileBrowserDialog(Frame owner, TeamspeakConnection conn, ChannelNode channel) { super(owner, "Files — " + channel.name, false); this.conn = conn; this.channelId = channel.id; this.channelPassword = ""; JPanel content = new JPanel(new BorderLayout(0, 8)); content.setBackground(Theme.WINDOW_BG); content.setBorder(BorderFactory.createEmptyBorder(10, 12, 10, 12)); content.add(buildToolbar(), BorderLayout.NORTH); content.add(buildTable(), BorderLayout.CENTER); content.add(buildTransfers(), BorderLayout.SOUTH); setContentPane(content); setPreferredSize(new Dimension(640, 480)); pack(); setLocationRelativeTo(owner); refresh(); } // ---- construction ---- private JPanel buildToolbar() { JPanel bar = new JPanel(); bar.setLayout(new BoxLayout(bar, BoxLayout.X_AXIS)); bar.setBackground(Theme.WINDOW_BG); upButton.addActionListener(e -> navigateUp()); JButton refresh = new JButton("Refresh", Icons.of("FILE_REFRESH")); refresh.addActionListener(e -> refresh()); JButton mkdir = new JButton("New folder", Icons.of("ADD_FOLDER")); mkdir.addActionListener(e -> createDirectory()); JButton upload = new JButton("Upload…", Icons.of("UPLOAD")); upload.addActionListener(e -> chooseUpload()); downloadButton.addActionListener(e -> downloadSelected()); deleteButton.addActionListener(e -> deleteSelected()); bar.add(upButton); bar.add(Box.createHorizontalStrut(6)); bar.add(refresh); bar.add(Box.createHorizontalStrut(16)); bar.add(pathLabel); bar.add(Box.createHorizontalGlue()); bar.add(mkdir); bar.add(Box.createHorizontalStrut(6)); bar.add(upload); bar.add(Box.createHorizontalStrut(6)); bar.add(downloadButton); bar.add(Box.createHorizontalStrut(6)); bar.add(deleteButton); pathLabel.setFont(Theme.UI_FONT); pathLabel.setForeground(Theme.TREE_TEXT); return bar; } private JScrollPane buildTable() { table.setFont(Theme.UI_FONT); table.setRowHeight(20); table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); table.setFillsViewportHeight(true); table.getColumnModel().getColumn(0).setPreferredWidth(300); table.getColumnModel().getColumn(0).setCellRenderer(new NameRenderer()); table.getColumnModel().getColumn(1).setPreferredWidth(90); table.getColumnModel().getColumn(2).setPreferredWidth(70); table.getColumnModel().getColumn(3).setPreferredWidth(150); table.getSelectionModel().addListSelectionListener(e -> updateButtons()); table.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { int row = table.rowAtPoint(e.getPoint()); if (row >= 0) openEntry(tableModel.get(row)); } } }); JScrollPane scroll = new JScrollPane(table); scroll.getViewport().setBackground(Theme.TREE_BG); return scroll; } private JScrollPane buildTransfers() { transfersPanel.setLayout(new BoxLayout(transfersPanel, BoxLayout.Y_AXIS)); transfersPanel.setBackground(Theme.WINDOW_BG); JScrollPane scroll = new JScrollPane(transfersPanel); scroll.setBorder(BorderFactory.createTitledBorder("Transfers")); scroll.setPreferredSize(new Dimension(10, 120)); scroll.getViewport().setBackground(Theme.WINDOW_BG); return scroll; } // ---- navigation ---- private void navigateUp() { String path = currentPath; if (path.equals("/")) return; String trimmed = path.substring(0, path.length() - 1); // drop trailing slash int slash = trimmed.lastIndexOf('/'); currentPath = slash <= 0 ? "/" : trimmed.substring(0, slash + 1); refresh(); } private void openEntry(RemoteFile file) { if (file == null) return; if (file.isDirectory()) { currentPath = currentPath + file.getName() + "/"; refresh(); } else { download(file); } } /** Reloads the current directory listing from the server. */ private void refresh() { pathLabel.setText(currentPath); upButton.setEnabled(!currentPath.equals("/")); final String path = currentPath; new Thread(() -> { try { List files = conn.listFiles(channelId, channelPassword, path); Collections.sort(files, DIRECTORIES_FIRST); SwingUtilities.invokeLater(() -> { if (path.equals(currentPath)) tableModel.setFiles(files); updateButtons(); }); } catch (Exception ex) { SwingUtilities.invokeLater(() -> { if (path.equals(currentPath)) tableModel.setFiles(new ArrayList<>()); error("Could not list files", ex); }); } }, "ts3j-ft-list").start(); } private void updateButtons() { boolean hasSelection = table.getSelectedRowCount() > 0; deleteButton.setEnabled(hasSelection); boolean fileSelected = false; for (int row : table.getSelectedRows()) { if (!tableModel.get(row).isDirectory()) { fileSelected = true; break; } } downloadButton.setEnabled(fileSelected); } // ---- actions ---- private void downloadSelected() { List files = new ArrayList<>(); for (int row : table.getSelectedRows()) { RemoteFile f = tableModel.get(row); if (!f.isDirectory()) files.add(f); } if (files.isEmpty()) return; if (files.size() == 1) { download(files.get(0)); return; } // Multiple files: pick a target directory once. JFileChooser chooser = new JFileChooser(); chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); chooser.setDialogTitle("Download " + files.size() + " files to…"); if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; File dir = chooser.getSelectedFile(); for (RemoteFile f : files) { startDownload(f, new File(dir, f.getName())); } } private void download(RemoteFile file) { JFileChooser chooser = new JFileChooser(); chooser.setSelectedFile(new File(file.getName())); chooser.setDialogTitle("Download " + file.getName()); if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return; startDownload(file, chooser.getSelectedFile()); } private void startDownload(RemoteFile file, File target) { TransferRow row = new TransferRow(); FileTransfer transfer = conn.downloadFile(channelId, channelPassword, file.getFullPath(), target, t -> SwingUtilities.invokeLater(() -> row.update(t))); row.attach(transfer); addTransferRow(row); } private void chooseUpload() { JFileChooser chooser = new JFileChooser(); chooser.setMultiSelectionEnabled(true); chooser.setDialogTitle("Upload to " + currentPath); if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return; for (File source : chooser.getSelectedFiles()) { if (!source.isFile()) continue; String remotePath = currentPath + source.getName(); TransferRow row = new TransferRow(); FileTransfer transfer = conn.uploadFile(channelId, channelPassword, remotePath, source, true, t -> SwingUtilities.invokeLater(() -> { row.update(t); if (t.getState() == FileTransfer.State.COMPLETED) refresh(); })); row.attach(transfer); addTransferRow(row); } } private void createDirectory() { String name = JOptionPane.showInputDialog(this, "New folder name:", "New folder", JOptionPane.PLAIN_MESSAGE); if (name == null || name.trim().isEmpty()) return; final String dirPath = currentPath + name.trim(); new Thread(() -> { try { conn.createDirectory(channelId, channelPassword, dirPath); SwingUtilities.invokeLater(this::refresh); } catch (Exception ex) { SwingUtilities.invokeLater(() -> error("Could not create folder", ex)); } }, "ts3j-ft-mkdir").start(); } private void deleteSelected() { List files = new ArrayList<>(); for (int row : table.getSelectedRows()) files.add(tableModel.get(row)); if (files.isEmpty()) return; int choice = JOptionPane.showConfirmDialog(this, "Delete " + (files.size() == 1 ? "\"" + files.get(0).getName() + "\"" : files.size() + " items") + "?", "Delete", JOptionPane.OK_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); if (choice != JOptionPane.OK_OPTION) return; new Thread(() -> { Exception failure = null; for (RemoteFile f : files) { try { conn.deleteFile(channelId, channelPassword, f.getFullPath()); } catch (Exception ex) { failure = ex; } } final Exception shown = failure; SwingUtilities.invokeLater(() -> { refresh(); if (shown != null) error("Could not delete some items", shown); }); }, "ts3j-ft-delete").start(); } private void addTransferRow(TransferRow row) { transfersPanel.add(row); transfersPanel.revalidate(); transfersPanel.repaint(); } private void error(String title, Exception ex) { JOptionPane.showMessageDialog(this, rootMessage(ex), title, JOptionPane.ERROR_MESSAGE); } // ---- one transfer's row ---- private final class TransferRow extends JPanel { private final JLabel label = new JLabel(); private final JProgressBar bar = new JProgressBar(0, 1000); private final JButton action = new JButton("Cancel"); private FileTransfer transfer; TransferRow() { setLayout(new BorderLayout(8, 0)); setBackground(Theme.WINDOW_BG); setBorder(BorderFactory.createEmptyBorder(3, 2, 3, 2)); setMaximumSize(new Dimension(Integer.MAX_VALUE, 44)); label.setFont(Theme.UI_FONT); bar.setStringPainted(true); add(label, BorderLayout.NORTH); add(bar, BorderLayout.CENTER); add(action, BorderLayout.EAST); } void attach(FileTransfer t) { this.transfer = t; action.addActionListener(e -> { if (transfer.isDone()) { transfersPanel.remove(this); transfersPanel.revalidate(); transfersPanel.repaint(); } else { transfer.cancel(); } }); update(t); } void update(FileTransfer t) { String arrow = t.getDirection() == FileTransfer.Direction.UPLOAD ? "↑" : "↓"; String head = arrow + " " + t.getRemoteName(); switch (t.getState()) { case PENDING: label.setText(head + " — starting…"); break; case ACTIVE: label.setText(head + " — " + formatBytes(t.getTransferredBytes()) + " / " + formatBytes(t.getTotalBytes()) + " (" + formatRate(t.getAverageSpeed()) + ")"); break; case COMPLETED: label.setText(head + " — done (" + formatBytes(t.getTotalBytes()) + ")"); break; case CANCELLED: label.setText(head + " — cancelled"); break; case FAILED: label.setText(head + " — failed: " + orEmpty(t.getErrorMessage())); break; default: break; } int permille = (int) Math.round(t.getProgress() * 1000); bar.setValue(permille); bar.setString(t.getTotalBytes() > 0 ? (permille / 10) + "%" : ""); if (t.isDone()) { bar.setValue(t.getState() == FileTransfer.State.COMPLETED ? 1000 : bar.getValue()); action.setText("Clear"); } } } // ---- table model ---- private static final Comparator DIRECTORIES_FIRST = (a, b) -> { if (a.isDirectory() != b.isDirectory()) return a.isDirectory() ? -1 : 1; return a.getName().compareToIgnoreCase(b.getName()); }; private static final class FileTableModel extends AbstractTableModel { private static final String[] COLUMNS = {"Name", "Size", "Type", "Modified"}; private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm"); private List files = new ArrayList<>(); void setFiles(List files) { this.files = files; fireTableDataChanged(); } RemoteFile get(int row) { return files.get(row); } @Override public int getRowCount() { return files.size(); } @Override public int getColumnCount() { return COLUMNS.length; } @Override public String getColumnName(int column) { return COLUMNS[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { RemoteFile f = files.get(rowIndex); switch (columnIndex) { case 0: return f.getName(); case 1: return f.isDirectory() ? "" : formatBytes(f.getSize()); case 2: return f.isDirectory() ? "Folder" : "File"; case 3: long sec = f.getLastModifiedEpochSec(); return sec > 0 ? dateFormat.format(new Date(sec * 1000)) : ""; default: return ""; } } } /** Marks folders and files with the icon pack's own glyphs. */ private final class NameRenderer extends javax.swing.table.DefaultTableCellRenderer { @Override public java.awt.Component getTableCellRendererComponent(JTable t, Object value, boolean selected, boolean focus, int row, int column) { super.getTableCellRendererComponent(t, value, selected, focus, row, column); boolean directory = row >= 0 && row < tableModel.getRowCount() && tableModel.get(row).isDirectory(); setIcon(Icons.of(directory ? "FOLDER" : "DEFAULT")); return this; } } // ---- formatting ---- private static String formatBytes(long bytes) { if (bytes < 0) return "—"; if (bytes < 1024) return bytes + " B"; double kib = bytes / 1024.0; if (kib < 1024) return new DecimalFormat("0.0").format(kib) + " KiB"; double mib = kib / 1024.0; if (mib < 1024) return new DecimalFormat("0.00").format(mib) + " MiB"; return new DecimalFormat("0.00").format(mib / 1024.0) + " GiB"; } private static String formatRate(double bytesPerSecond) { if (bytesPerSecond <= 0) return "—"; return formatBytes(Math.round(bytesPerSecond)) + "/s"; } private static String orEmpty(String s) { return s == null ? "" : s; } private static String rootMessage(Throwable t) { Throwable r = t; while (r.getCause() != null && r.getCause() != r) r = r.getCause(); String m = r.getMessage(); return m != null ? m : r.getClass().getSimpleName(); } }